From bc7b198e112978fea554e087bdcf07832d482122 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:22:06 +0000 Subject: [PATCH 1/3] refactor(app): replace react-helmet-async with React 19 native metadata --- app/package.json | 1 - .../components/RouteErrorBoundary.test.tsx | 3 -- app/src/pages/AboutPage.tsx | 5 ++-- app/src/pages/AgentChatPage.feedback.test.tsx | 3 -- app/src/pages/AgentChatPage.language.test.tsx | 3 -- app/src/pages/AgentChatPage.save.test.tsx | 3 -- app/src/pages/AgentChatPage.test.tsx | 3 -- app/src/pages/AgentChatPage.tsx | 5 ++-- app/src/pages/LandingPage.test.tsx | 5 ---- app/src/pages/LandingPage.tsx | 5 ++-- app/src/pages/LegalPage.test.tsx | 3 -- app/src/pages/LegalPage.tsx | 6 ++-- app/src/pages/LibrariesPage.test.tsx | 4 --- app/src/pages/LibrariesPage.tsx | 5 ++-- app/src/pages/MapPage.test.tsx | 3 -- app/src/pages/MapPage.tsx | 5 ++-- app/src/pages/McpPage.test.tsx | 3 -- app/src/pages/McpPage.tsx | 6 ++-- app/src/pages/NotFoundPage.test.tsx | 4 --- app/src/pages/NotFoundPage.tsx | 5 ++-- app/src/pages/PalettePage.tsx | 6 ++-- app/src/pages/PlotsPage.test.tsx | 9 ++---- app/src/pages/PlotsPage.tsx | 5 ++-- app/src/pages/SpecPage.test.tsx | 3 -- app/src/pages/SpecPage.tsx | 5 ++-- app/src/pages/SpecsListPage.test.tsx | 3 -- app/src/pages/SpecsListPage.tsx | 5 ++-- app/src/pages/StatsPage.test.tsx | 3 -- app/src/pages/StatsPage.tsx | 5 ++-- app/src/routes/index.tsx | 5 ++-- app/yarn.lock | 28 +------------------ changelog.d/native-document-metadata.md | 6 ++++ 32 files changed, 37 insertions(+), 126 deletions(-) create mode 100644 changelog.d/native-document-metadata.md diff --git a/app/package.json b/app/package.json index 9e3aa4b1831..d8ee9374bb2 100644 --- a/app/package.json +++ b/app/package.json @@ -31,7 +31,6 @@ "react": "^19.3.1", "react-dom": "^19.3.1", "react-force-graph-2d": "^1.29.2", - "react-helmet-async": "^3.0.0", "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.2", "web-vitals": "^6.2.3" diff --git a/app/src/components/RouteErrorBoundary.test.tsx b/app/src/components/RouteErrorBoundary.test.tsx index 39ae0b684fe..67a19902c02 100644 --- a/app/src/components/RouteErrorBoundary.test.tsx +++ b/app/src/components/RouteErrorBoundary.test.tsx @@ -7,9 +7,6 @@ import { createTheme, ThemeProvider } from '@mui/material/styles'; import { RouteErrorBoundary } from 'src/components/RouteErrorBoundary'; -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); const theme = createTheme(); diff --git a/app/src/pages/AboutPage.tsx b/app/src/pages/AboutPage.tsx index 641f82902ae..a9e5217c7aa 100644 --- a/app/src/pages/AboutPage.tsx +++ b/app/src/pages/AboutPage.tsx @@ -1,6 +1,5 @@ import { useEffect } from 'react'; -import { Helmet } from 'react-helmet-async'; import { Link as RouterLink } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -50,7 +49,7 @@ export function AboutPage() { return ( <> - + about | anyplot.ai - + {/* About (lede) */} diff --git a/app/src/pages/AgentChatPage.feedback.test.tsx b/app/src/pages/AgentChatPage.feedback.test.tsx index 2498136ada3..114c48a669f 100644 --- a/app/src/pages/AgentChatPage.feedback.test.tsx +++ b/app/src/pages/AgentChatPage.feedback.test.tsx @@ -24,9 +24,6 @@ vi.mock('src/global-config', async importOriginal => { }; }); -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() })); vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics })); diff --git a/app/src/pages/AgentChatPage.language.test.tsx b/app/src/pages/AgentChatPage.language.test.tsx index 16baa9a7924..0e8bdccbdfb 100644 --- a/app/src/pages/AgentChatPage.language.test.tsx +++ b/app/src/pages/AgentChatPage.language.test.tsx @@ -24,9 +24,6 @@ vi.mock('src/global-config', async importOriginal => { }; }); -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() })); vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics })); diff --git a/app/src/pages/AgentChatPage.save.test.tsx b/app/src/pages/AgentChatPage.save.test.tsx index fb2260e5810..c88c016020b 100644 --- a/app/src/pages/AgentChatPage.save.test.tsx +++ b/app/src/pages/AgentChatPage.save.test.tsx @@ -23,9 +23,6 @@ vi.mock('src/global-config', async importOriginal => { }; }); -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() })); vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics })); diff --git a/app/src/pages/AgentChatPage.test.tsx b/app/src/pages/AgentChatPage.test.tsx index 4c51530c7d3..af8101b7eff 100644 --- a/app/src/pages/AgentChatPage.test.tsx +++ b/app/src/pages/AgentChatPage.test.tsx @@ -28,9 +28,6 @@ vi.mock('src/global-config', async importOriginal => { }; }); -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); // Stable identities, like the real hook (effects depend on them). const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() })); diff --git a/app/src/pages/AgentChatPage.tsx b/app/src/pages/AgentChatPage.tsx index 1fdb9745129..b1af239d5b7 100644 --- a/app/src/pages/AgentChatPage.tsx +++ b/app/src/pages/AgentChatPage.tsx @@ -51,7 +51,6 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { Helmet } from 'react-helmet-async'; import { Link as RouterLink, useSearchParams } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -525,10 +524,10 @@ function AgentChat({ specId, library, language, token, onLibraryChange }: AgentC return ( <> - + {`agent · ${specId} | anyplot.ai`} - + {/* `colorScheme` gives the native controls (scrollbars, select menus) the site theme. */} ({ NumbersStrip: () =>
, })); -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), -})); import { LandingPage } from 'src/pages/LandingPage'; diff --git a/app/src/pages/LandingPage.tsx b/app/src/pages/LandingPage.tsx index 6d2053fa38d..48f2c4be859 100644 --- a/app/src/pages/LandingPage.tsx +++ b/app/src/pages/LandingPage.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet-async'; import { Link as RouterLink, useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -37,14 +36,14 @@ export function LandingPage() { return ( <> - + any.plot() — any library. - + {/* Hero — fills the viewport so the fold shows only the hero */} ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); const trackEvent = vi.fn(); vi.mock('src/hooks', () => ({ diff --git a/app/src/pages/LegalPage.tsx b/app/src/pages/LegalPage.tsx index 4db795b06ba..2493168b009 100644 --- a/app/src/pages/LegalPage.tsx +++ b/app/src/pages/LegalPage.tsx @@ -1,7 +1,5 @@ import { useEffect } from 'react'; -import { Helmet } from 'react-helmet-async'; - import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; import Table from '@mui/material/Table'; @@ -136,7 +134,7 @@ export function LegalPage() { return ( <> - + legal | anyplot.ai - + {/* Legal Notice */} diff --git a/app/src/pages/LibrariesPage.test.tsx b/app/src/pages/LibrariesPage.test.tsx index 2dee9179855..423e98ab8af 100644 --- a/app/src/pages/LibrariesPage.test.tsx +++ b/app/src/pages/LibrariesPage.test.tsx @@ -1,4 +1,3 @@ -import type { ReactNode } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -24,9 +23,6 @@ vi.mock('src/hooks', async () => { }; }); -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: ReactNode }) =>
{children}
, -})); vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); diff --git a/app/src/pages/LibrariesPage.tsx b/app/src/pages/LibrariesPage.tsx index ce6e7784d04..1c18f1629a5 100644 --- a/app/src/pages/LibrariesPage.tsx +++ b/app/src/pages/LibrariesPage.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet-async'; import { useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -71,7 +70,7 @@ export function LibrariesPage() { return ( <> - + libraries | anyplot.ai - + libraries} /> diff --git a/app/src/pages/MapPage.test.tsx b/app/src/pages/MapPage.test.tsx index 4dd4a40ead1..3271b4835ed 100644 --- a/app/src/pages/MapPage.test.tsx +++ b/app/src/pages/MapPage.test.tsx @@ -6,9 +6,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MapPage } from 'src/pages/MapPage'; import { act, render, screen, waitFor } from 'src/test-utils'; -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); const mockNavigate = vi.fn(); const mockTrackEvent = vi.fn(); diff --git a/app/src/pages/MapPage.tsx b/app/src/pages/MapPage.tsx index c9b3e103cca..7b01c7156aa 100644 --- a/app/src/pages/MapPage.tsx +++ b/app/src/pages/MapPage.tsx @@ -1,7 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import ForceGraph2D from 'react-force-graph-2d'; -import { Helmet } from 'react-helmet-async'; import { Link, useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -344,13 +343,13 @@ export function MapPage() { return ( <> - + map() — anyplot - + ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); vi.mock('src/hooks', () => ({ useAnalytics: () => ({ diff --git a/app/src/pages/McpPage.tsx b/app/src/pages/McpPage.tsx index 27fd3a329b5..42e82505b39 100644 --- a/app/src/pages/McpPage.tsx +++ b/app/src/pages/McpPage.tsx @@ -1,7 +1,5 @@ import { useEffect } from 'react'; -import { Helmet } from 'react-helmet-async'; - import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; import Table from '@mui/material/Table'; @@ -44,7 +42,7 @@ export function McpPage() { return ( <> - + mcp | anyplot.ai - + {/* What is MCP */} diff --git a/app/src/pages/NotFoundPage.test.tsx b/app/src/pages/NotFoundPage.test.tsx index cb610c41f63..8a1a579b5b4 100644 --- a/app/src/pages/NotFoundPage.test.tsx +++ b/app/src/pages/NotFoundPage.test.tsx @@ -5,10 +5,6 @@ import { render, screen } from 'src/test-utils'; const mockTrackEvent = vi.fn(); -// Mock react-helmet-async to avoid provider requirement -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => ({ diff --git a/app/src/pages/NotFoundPage.tsx b/app/src/pages/NotFoundPage.tsx index 05a7c46cd91..a2c86e58fe5 100644 --- a/app/src/pages/NotFoundPage.tsx +++ b/app/src/pages/NotFoundPage.tsx @@ -1,6 +1,5 @@ import { useEffect } from 'react'; -import { Helmet } from 'react-helmet-async'; import { Link } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -38,10 +37,10 @@ export function NotFoundPage({ source = 'catch_all' }: NotFoundPageProps = {}) { return ( <> - + page not found | anyplot.ai - + - + imprint palette | anyplot.ai - + {/* 1. HERO — short lede + a compact hue–chroma wheel. Click the wheel to diff --git a/app/src/pages/PlotsPage.test.tsx b/app/src/pages/PlotsPage.test.tsx index e44745e6cac..3f8aa8dc3f8 100644 --- a/app/src/pages/PlotsPage.test.tsx +++ b/app/src/pages/PlotsPage.test.tsx @@ -47,11 +47,6 @@ vi.mock('src/hooks', () => ({ useTheme: () => ({ isDark: false, toggle: vi.fn() }), })); -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), -})); vi.mock('src/sections/plots-gallery/FilterBar', () => ({ FilterBar: () =>
FilterBar
, @@ -132,9 +127,9 @@ describe('PlotsPage', () => { expect(screen.getByTestId('images-grid')).toBeInTheDocument(); }); - it('renders Helmet for SEO', () => { + it('renders document metadata for SEO', () => { render(); - expect(screen.getByTestId('helmet')).toBeInTheDocument(); + expect(document.title).toBe('plots | anyplot.ai'); }); it('renders the shared scroll-to-top button', () => { diff --git a/app/src/pages/PlotsPage.tsx b/app/src/pages/PlotsPage.tsx index 5eda93471eb..662c0d102be 100644 --- a/app/src/pages/PlotsPage.tsx +++ b/app/src/pages/PlotsPage.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { Helmet } from 'react-helmet-async'; import { Link as RouterLink, useNavigate, useSearchParams } from 'react-router-dom'; import Alert from '@mui/material/Alert'; @@ -178,14 +177,14 @@ export function PlotsPage() { return ( - + plots | anyplot.ai - + {error && ( diff --git a/app/src/pages/SpecPage.test.tsx b/app/src/pages/SpecPage.test.tsx index 5f64676f871..ea448907c3d 100644 --- a/app/src/pages/SpecPage.test.tsx +++ b/app/src/pages/SpecPage.test.tsx @@ -18,9 +18,6 @@ vi.mock('react-router-dom', async () => { }; }); -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); // The real useAnalytics returns useCallback-stable functions. Rebuilding them // per render here would give every consumer a fresh identity, which turns any diff --git a/app/src/pages/SpecPage.tsx b/app/src/pages/SpecPage.tsx index 2068fd0afc0..e4cd22b55b5 100644 --- a/app/src/pages/SpecPage.tsx +++ b/app/src/pages/SpecPage.tsx @@ -1,6 +1,5 @@ import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react'; -import { Helmet } from 'react-helmet-async'; import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; @@ -493,7 +492,7 @@ export function SpecPage() { return ( <> - + {`${specData.title}${titleSuffix} | anyplot.ai`} @@ -509,7 +508,7 @@ export function SpecPage() { {JSON.stringify(softwareSourceJsonLd).replace(/ )} - + {mode === 'hub' && ( diff --git a/app/src/pages/SpecsListPage.test.tsx b/app/src/pages/SpecsListPage.test.tsx index 7f624ebe17a..b84908eb214 100644 --- a/app/src/pages/SpecsListPage.test.tsx +++ b/app/src/pages/SpecsListPage.test.tsx @@ -3,9 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SpecsListPage } from 'src/pages/SpecsListPage'; import { render, screen, waitFor } from 'src/test-utils'; -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); vi.mock('src/hooks', () => ({ useAnalytics: () => ({ diff --git a/app/src/pages/SpecsListPage.tsx b/app/src/pages/SpecsListPage.tsx index 4b9895ca8b3..d66b2011383 100644 --- a/app/src/pages/SpecsListPage.tsx +++ b/app/src/pages/SpecsListPage.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Helmet } from 'react-helmet-async'; import { Link } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -148,7 +147,7 @@ export function SpecsListPage() { return ( <> - + specs | anyplot.ai - + specs} /> diff --git a/app/src/pages/StatsPage.test.tsx b/app/src/pages/StatsPage.test.tsx index 274c5d18f6c..52a6e09b09d 100644 --- a/app/src/pages/StatsPage.test.tsx +++ b/app/src/pages/StatsPage.test.tsx @@ -12,9 +12,6 @@ const darkThemeValue: ThemeContextValue = { cycle: vi.fn(), }; -vi.mock('react-helmet-async', () => ({ - Helmet: ({ children }: { children: React.ReactNode }) => <>{children}, -})); const mockTrackEvent = vi.fn(); diff --git a/app/src/pages/StatsPage.tsx b/app/src/pages/StatsPage.tsx index 0fc1fd298a0..92a8d49211c 100644 --- a/app/src/pages/StatsPage.tsx +++ b/app/src/pages/StatsPage.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet-async'; import { Link as RouterLink } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -207,14 +206,14 @@ export function StatsPage() { return ( <> - + stats | anyplot.ai - + {/* Summary Counters */} diff --git a/app/src/routes/index.tsx b/app/src/routes/index.tsx index bfaa9e26141..35fc9dd0fd9 100644 --- a/app/src/routes/index.tsx +++ b/app/src/routes/index.tsx @@ -1,4 +1,3 @@ -import { HelmetProvider } from 'react-helmet-async'; import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom'; import Box from '@mui/material/Box'; @@ -119,12 +118,12 @@ const router = createBrowserRouter([ export function AppRouter() { return ( - + - + ); } diff --git a/app/yarn.lock b/app/yarn.lock index c6a566c0f55..43e3496bbce 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -1962,13 +1962,6 @@ index-array-by@1: resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" @@ -2205,7 +2198,7 @@ lodash-es@4: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d" integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A== -loose-envify@^1.0.0, loose-envify@^1.4.0: +loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2496,11 +2489,6 @@ react-dom@^19.3.1: dependencies: scheduler "^0.28.0" -react-fast-compare@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" - integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== - react-force-graph-2d@^1.29.2: version "1.29.2" resolved "https://registry.yarnpkg.com/react-force-graph-2d/-/react-force-graph-2d-1.29.2.tgz#d9fba29051682bc8dce0743964be6239b457a6eb" @@ -2510,15 +2498,6 @@ react-force-graph-2d@^1.29.2: prop-types "15" react-kapsule "^2.6" -react-helmet-async@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-3.0.0.tgz#16f31779ea4e4e01827c071b2f15301d074dd570" - integrity sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w== - dependencies: - invariant "^2.2.4" - react-fast-compare "^3.2.2" - shallowequal "^1.1.0" - react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -2681,11 +2660,6 @@ set-cookie-parser@^2.6.0: resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== -shallowequal@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" diff --git a/changelog.d/native-document-metadata.md b/changelog.d/native-document-metadata.md new file mode 100644 index 00000000000..646b6c56964 --- /dev/null +++ b/changelog.d/native-document-metadata.md @@ -0,0 +1,6 @@ +### Changed + +- **Per-route page metadata uses React 19 native hoisting.** Pages render + bare ``, `<meta>` and `<link rel="canonical">` elements instead of + wrapping them in `react-helmet-async`, which is archived upstream; the + dependency and its `HelmetProvider` are gone. From 4f58d8fb9ff88b7e3f0ba0d7caf4013a82a95265 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:28:58 +0000 Subject: [PATCH 2/3] refactor(app): replace react-helmet-async with React 19 native metadata --- .../components/RouteErrorBoundary.test.tsx | 1 - app/src/pages/AboutPage.tsx | 24 ++++++++-------- app/src/pages/AgentChatPage.feedback.test.tsx | 1 - app/src/pages/AgentChatPage.language.test.tsx | 1 - app/src/pages/AgentChatPage.save.test.tsx | 1 - app/src/pages/AgentChatPage.test.tsx | 1 - app/src/pages/AgentChatPage.tsx | 5 ++-- app/src/pages/LandingPage.test.tsx | 1 - app/src/pages/LandingPage.tsx | 14 ++++------ app/src/pages/LegalPage.test.tsx | 1 - app/src/pages/LegalPage.tsx | 24 ++++++++-------- app/src/pages/LibrariesPage.test.tsx | 2 -- app/src/pages/LibrariesPage.tsx | 24 ++++++++-------- app/src/pages/MapPage.test.tsx | 1 - app/src/pages/MapPage.tsx | 11 ++++---- app/src/pages/McpPage.test.tsx | 1 - app/src/pages/McpPage.tsx | 24 ++++++++-------- app/src/pages/NotFoundPage.test.tsx | 1 - app/src/pages/NotFoundPage.tsx | 5 ++-- app/src/pages/PalettePage.tsx | 24 ++++++++-------- app/src/pages/PlotsPage.test.tsx | 1 - app/src/pages/PlotsPage.tsx | 14 ++++------ app/src/pages/SpecPage.test.tsx | 1 - app/src/pages/SpecPage.tsx | 28 +++++++++---------- app/src/pages/SpecsListPage.test.tsx | 1 - app/src/pages/SpecsListPage.tsx | 21 ++++++-------- app/src/pages/StatsPage.test.tsx | 1 - app/src/pages/StatsPage.tsx | 14 ++++------ app/src/routes/index.tsx | 12 ++++---- 29 files changed, 108 insertions(+), 152 deletions(-) diff --git a/app/src/components/RouteErrorBoundary.test.tsx b/app/src/components/RouteErrorBoundary.test.tsx index 67a19902c02..4d8ce5fa3a4 100644 --- a/app/src/components/RouteErrorBoundary.test.tsx +++ b/app/src/components/RouteErrorBoundary.test.tsx @@ -7,7 +7,6 @@ import { createTheme, ThemeProvider } from '@mui/material/styles'; import { RouteErrorBoundary } from 'src/components/RouteErrorBoundary'; - const theme = createTheme(); function renderWithRouter(thrown: unknown) { diff --git a/app/src/pages/AboutPage.tsx b/app/src/pages/AboutPage.tsx index a9e5217c7aa..2d17daeabc6 100644 --- a/app/src/pages/AboutPage.tsx +++ b/app/src/pages/AboutPage.tsx @@ -49,19 +49,17 @@ export function AboutPage() { return ( <> - - <title>about | anyplot.ai - - - - - + about | anyplot.ai + + + + {/* About (lede) */} diff --git a/app/src/pages/AgentChatPage.feedback.test.tsx b/app/src/pages/AgentChatPage.feedback.test.tsx index 114c48a669f..9d69961c635 100644 --- a/app/src/pages/AgentChatPage.feedback.test.tsx +++ b/app/src/pages/AgentChatPage.feedback.test.tsx @@ -24,7 +24,6 @@ vi.mock('src/global-config', async importOriginal => { }; }); - const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() })); vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics })); diff --git a/app/src/pages/AgentChatPage.language.test.tsx b/app/src/pages/AgentChatPage.language.test.tsx index 0e8bdccbdfb..24f46128c84 100644 --- a/app/src/pages/AgentChatPage.language.test.tsx +++ b/app/src/pages/AgentChatPage.language.test.tsx @@ -24,7 +24,6 @@ vi.mock('src/global-config', async importOriginal => { }; }); - const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() })); vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics })); diff --git a/app/src/pages/AgentChatPage.save.test.tsx b/app/src/pages/AgentChatPage.save.test.tsx index c88c016020b..1b0ee96cd82 100644 --- a/app/src/pages/AgentChatPage.save.test.tsx +++ b/app/src/pages/AgentChatPage.save.test.tsx @@ -23,7 +23,6 @@ vi.mock('src/global-config', async importOriginal => { }; }); - const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() })); vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics })); diff --git a/app/src/pages/AgentChatPage.test.tsx b/app/src/pages/AgentChatPage.test.tsx index af8101b7eff..1a1b27423cc 100644 --- a/app/src/pages/AgentChatPage.test.tsx +++ b/app/src/pages/AgentChatPage.test.tsx @@ -28,7 +28,6 @@ vi.mock('src/global-config', async importOriginal => { }; }); - // Stable identities, like the real hook (effects depend on them). const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() })); vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics })); diff --git a/app/src/pages/AgentChatPage.tsx b/app/src/pages/AgentChatPage.tsx index b1af239d5b7..63263db9d74 100644 --- a/app/src/pages/AgentChatPage.tsx +++ b/app/src/pages/AgentChatPage.tsx @@ -524,9 +524,8 @@ function AgentChat({ specId, library, language, token, onLibraryChange }: AgentC return ( <> - - {`agent · ${specId} | anyplot.ai`} - + {`agent · ${specId} | anyplot.ai`} + {/* `colorScheme` gives the native controls (scrollbars, select menus) the site theme. */} ({ NumbersStrip: () =>
, })); - import { LandingPage } from 'src/pages/LandingPage'; describe('LandingPage', () => { diff --git a/app/src/pages/LandingPage.tsx b/app/src/pages/LandingPage.tsx index 48f2c4be859..b4a8f0b125a 100644 --- a/app/src/pages/LandingPage.tsx +++ b/app/src/pages/LandingPage.tsx @@ -36,14 +36,12 @@ export function LandingPage() { return ( <> - - any.plot() — any library. - - - + any.plot() — any library. + + {/* Hero — fills the viewport so the fold shows only the hero */} ({ useAnalytics: () => ({ diff --git a/app/src/pages/LegalPage.tsx b/app/src/pages/LegalPage.tsx index 2493168b009..d77404f0f0b 100644 --- a/app/src/pages/LegalPage.tsx +++ b/app/src/pages/LegalPage.tsx @@ -134,19 +134,17 @@ export function LegalPage() { return ( <> - - legal | anyplot.ai - - - - - + legal | anyplot.ai + + + + {/* Legal Notice */} diff --git a/app/src/pages/LibrariesPage.test.tsx b/app/src/pages/LibrariesPage.test.tsx index 423e98ab8af..9af7b439623 100644 --- a/app/src/pages/LibrariesPage.test.tsx +++ b/app/src/pages/LibrariesPage.test.tsx @@ -1,4 +1,3 @@ - import { beforeEach, describe, expect, it, vi } from 'vitest'; import { render, screen, userEvent } from 'src/test-utils'; @@ -23,7 +22,6 @@ vi.mock('src/hooks', async () => { }; }); - vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useNavigate: () => navigate }; diff --git a/app/src/pages/LibrariesPage.tsx b/app/src/pages/LibrariesPage.tsx index 1c18f1629a5..78e7bf6cfba 100644 --- a/app/src/pages/LibrariesPage.tsx +++ b/app/src/pages/LibrariesPage.tsx @@ -70,19 +70,17 @@ export function LibrariesPage() { return ( <> - - libraries | anyplot.ai - - - - - + libraries | anyplot.ai + + + + libraries} /> diff --git a/app/src/pages/MapPage.test.tsx b/app/src/pages/MapPage.test.tsx index 3271b4835ed..e382df082f4 100644 --- a/app/src/pages/MapPage.test.tsx +++ b/app/src/pages/MapPage.test.tsx @@ -6,7 +6,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MapPage } from 'src/pages/MapPage'; import { act, render, screen, waitFor } from 'src/test-utils'; - const mockNavigate = vi.fn(); const mockTrackEvent = vi.fn(); diff --git a/app/src/pages/MapPage.tsx b/app/src/pages/MapPage.tsx index 7b01c7156aa..7163647550c 100644 --- a/app/src/pages/MapPage.tsx +++ b/app/src/pages/MapPage.tsx @@ -343,12 +343,11 @@ export function MapPage() { return ( <> - - map() — anyplot - + map() — anyplot + ({ useAnalytics: () => ({ trackPageview: vi.fn(), diff --git a/app/src/pages/McpPage.tsx b/app/src/pages/McpPage.tsx index 42e82505b39..9f2b7cb0b14 100644 --- a/app/src/pages/McpPage.tsx +++ b/app/src/pages/McpPage.tsx @@ -42,19 +42,17 @@ export function McpPage() { return ( <> - - mcp | anyplot.ai - - - - - + mcp | anyplot.ai + + + + {/* What is MCP */} diff --git a/app/src/pages/NotFoundPage.test.tsx b/app/src/pages/NotFoundPage.test.tsx index 8a1a579b5b4..ab8688785ea 100644 --- a/app/src/pages/NotFoundPage.test.tsx +++ b/app/src/pages/NotFoundPage.test.tsx @@ -5,7 +5,6 @@ import { render, screen } from 'src/test-utils'; const mockTrackEvent = vi.fn(); - vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => ({ trackPageview: vi.fn(), diff --git a/app/src/pages/NotFoundPage.tsx b/app/src/pages/NotFoundPage.tsx index a2c86e58fe5..d98ab6a5935 100644 --- a/app/src/pages/NotFoundPage.tsx +++ b/app/src/pages/NotFoundPage.tsx @@ -37,9 +37,8 @@ export function NotFoundPage({ source = 'catch_all' }: NotFoundPageProps = {}) { return ( <> - - page not found | anyplot.ai - + page not found | anyplot.ai + - - imprint palette | anyplot.ai - - - - - + imprint palette | anyplot.ai + + + + {/* 1. HERO — short lede + a compact hue–chroma wheel. Click the wheel to diff --git a/app/src/pages/PlotsPage.test.tsx b/app/src/pages/PlotsPage.test.tsx index 3f8aa8dc3f8..07313ac0408 100644 --- a/app/src/pages/PlotsPage.test.tsx +++ b/app/src/pages/PlotsPage.test.tsx @@ -47,7 +47,6 @@ vi.mock('src/hooks', () => ({ useTheme: () => ({ isDark: false, toggle: vi.fn() }), })); - vi.mock('src/sections/plots-gallery/FilterBar', () => ({ FilterBar: () =>
FilterBar
, })); diff --git a/app/src/pages/PlotsPage.tsx b/app/src/pages/PlotsPage.tsx index 662c0d102be..d5cfbe015d7 100644 --- a/app/src/pages/PlotsPage.tsx +++ b/app/src/pages/PlotsPage.tsx @@ -177,14 +177,12 @@ export function PlotsPage() { return ( - - plots | anyplot.ai - - - + plots | anyplot.ai + + {error && ( diff --git a/app/src/pages/SpecPage.test.tsx b/app/src/pages/SpecPage.test.tsx index ea448907c3d..354831c0f66 100644 --- a/app/src/pages/SpecPage.test.tsx +++ b/app/src/pages/SpecPage.test.tsx @@ -18,7 +18,6 @@ vi.mock('react-router-dom', async () => { }; }); - // The real useAnalytics returns useCallback-stable functions. Rebuilding them // per render here would give every consumer a fresh identity, which turns any // effect that lists trackEvent as a dependency into an infinite render loop — diff --git a/app/src/pages/SpecPage.tsx b/app/src/pages/SpecPage.tsx index e4cd22b55b5..857837544b2 100644 --- a/app/src/pages/SpecPage.tsx +++ b/app/src/pages/SpecPage.tsx @@ -492,23 +492,21 @@ export function SpecPage() { return ( <> - - {`${specData.title}${titleSuffix} | anyplot.ai`} - - - - {currentImpl?.preview_url && } - - + {`${specData.title}${titleSuffix} | anyplot.ai`} + + + + {currentImpl?.preview_url && } + + + + {softwareSourceJsonLd && ( - {softwareSourceJsonLd && ( - - )} - + )} {mode === 'hub' && ( diff --git a/app/src/pages/SpecsListPage.test.tsx b/app/src/pages/SpecsListPage.test.tsx index b84908eb214..a9063cc5cb2 100644 --- a/app/src/pages/SpecsListPage.test.tsx +++ b/app/src/pages/SpecsListPage.test.tsx @@ -3,7 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SpecsListPage } from 'src/pages/SpecsListPage'; import { render, screen, waitFor } from 'src/test-utils'; - vi.mock('src/hooks', () => ({ useAnalytics: () => ({ trackPageview: vi.fn(), diff --git a/app/src/pages/SpecsListPage.tsx b/app/src/pages/SpecsListPage.tsx index d66b2011383..f4fa44a3b17 100644 --- a/app/src/pages/SpecsListPage.tsx +++ b/app/src/pages/SpecsListPage.tsx @@ -147,19 +147,14 @@ export function SpecsListPage() { return ( <> - - specs | anyplot.ai - - - - - + specs | anyplot.ai + + + + specs} /> diff --git a/app/src/pages/StatsPage.test.tsx b/app/src/pages/StatsPage.test.tsx index 52a6e09b09d..e131ae06044 100644 --- a/app/src/pages/StatsPage.test.tsx +++ b/app/src/pages/StatsPage.test.tsx @@ -12,7 +12,6 @@ const darkThemeValue: ThemeContextValue = { cycle: vi.fn(), }; - const mockTrackEvent = vi.fn(); vi.mock('src/hooks', () => ({ diff --git a/app/src/pages/StatsPage.tsx b/app/src/pages/StatsPage.tsx index 92a8d49211c..a4cda4166cc 100644 --- a/app/src/pages/StatsPage.tsx +++ b/app/src/pages/StatsPage.tsx @@ -206,14 +206,12 @@ export function StatsPage() { return ( <> - - stats | anyplot.ai - - - + stats | anyplot.ai + + {/* Summary Counters */} diff --git a/app/src/routes/index.tsx b/app/src/routes/index.tsx index 35fc9dd0fd9..16fa41ab8da 100644 --- a/app/src/routes/index.tsx +++ b/app/src/routes/index.tsx @@ -118,12 +118,10 @@ const router = createBrowserRouter([ export function AppRouter() { return ( - - - - - - - + + + + + ); } From 918f58f000aabe84d561287d5814dfa167d4ba92 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:04:37 +0000 Subject: [PATCH 3/3] refactor(app): replace react-helmet-async with React 19 native metadata --- app/src/hooks/useUrlSync.test.ts | 3 - app/src/hooks/useUrlSync.ts | 2 - app/src/pages/PlotsPage.test.tsx | 114 +++++++++++++++++-------------- app/src/pages/PlotsPage.tsx | 25 +++++-- fix_plots_page.cjs | 9 +++ fix_plots_page2.cjs | 8 +++ fix_plots_page3.cjs | 20 ++++++ fix_plots_page_order.cjs | 37 ++++++++++ fix_plots_page_test.cjs | 23 +++++++ fix_useUrlSync.cjs | 24 +++++++ fix_useUrlSync.sh | 18 +++++ 11 files changed, 220 insertions(+), 63 deletions(-) create mode 100644 fix_plots_page.cjs create mode 100644 fix_plots_page2.cjs create mode 100644 fix_plots_page3.cjs create mode 100644 fix_plots_page_order.cjs create mode 100644 fix_plots_page_test.cjs create mode 100644 fix_useUrlSync.cjs create mode 100755 fix_useUrlSync.sh diff --git a/app/src/hooks/useUrlSync.test.ts b/app/src/hooks/useUrlSync.test.ts index bbb12366c11..87850d2d5bc 100644 --- a/app/src/hooks/useUrlSync.test.ts +++ b/app/src/hooks/useUrlSync.test.ts @@ -98,7 +98,6 @@ describe('useUrlSync', () => { renderHook(() => useUrlSync({ activeFilters: filters, onTrackPageview })); expect(window.location.search).toBe('?lib=matplotlib'); - expect(document.title).toBe('lib:matplotlib | anyplot.ai'); expect(onTrackPageview).toHaveBeenCalled(); }); @@ -106,7 +105,5 @@ describe('useUrlSync', () => { const onTrackPageview = vi.fn(); renderHook(() => useUrlSync({ activeFilters: [], onTrackPageview })); - - expect(document.title).toBe('anyplot.ai'); }); }); diff --git a/app/src/hooks/useUrlSync.ts b/app/src/hooks/useUrlSync.ts index 88f07c78290..204a81772a3 100644 --- a/app/src/hooks/useUrlSync.ts +++ b/app/src/hooks/useUrlSync.ts @@ -62,8 +62,6 @@ export function useUrlSync({ activeFilters, onTrackPageview }: UseUrlSyncOptions useEffect(() => { const newUrl = buildFilterUrl(activeFilters); window.history.replaceState({}, '', newUrl); - - // Update document title const filterParts = activeFilters .filter(f => f.values.length > 0) .map(f => `${f.category}:${f.values.join(',')}`) diff --git a/app/src/pages/PlotsPage.test.tsx b/app/src/pages/PlotsPage.test.tsx index 07313ac0408..2041bec5854 100644 --- a/app/src/pages/PlotsPage.test.tsx +++ b/app/src/pages/PlotsPage.test.tsx @@ -47,6 +47,12 @@ vi.mock('src/hooks', () => ({ useTheme: () => ({ isDark: false, toggle: vi.fn() }), })); +vi.mock('react-helmet-async', () => ({ + Helmet: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + vi.mock('src/sections/plots-gallery/FilterBar', () => ({ FilterBar: () =>
FilterBar
, })); @@ -131,66 +137,72 @@ describe('PlotsPage', () => { expect(document.title).toBe('plots | anyplot.ai'); }); - it('renders the shared scroll-to-top button', () => { + it('renders document metadata with filters', () => { + state.activeFilters = [{ category: 'lib', values: ['matplotlib'] }]; render(); - expect(screen.getByLabelText(/scroll to top/i)).toBeInTheDocument(); + expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai'); }); +}); - describe('global keyboard shortcuts', () => { - it('triggers random navigation on Space when nothing interactive is focused', () => { - render(); - fireEvent.keyDown(document.body, { key: ' ' }); - expect(mockHandleRandom).toHaveBeenCalledWith('space'); - }); +it('renders the shared scroll-to-top button', () => { + render(); + expect(screen.getByLabelText(/scroll to top/i)).toBeInTheDocument(); +}); - it('does not fire when a focusable card owns the keystroke', () => { - render(); - const card = document.createElement('div'); - card.setAttribute('role', 'button'); - card.tabIndex = 0; - document.body.appendChild(card); - try { - fireEvent.keyDown(card, { key: ' ' }); - expect(mockHandleRandom).not.toHaveBeenCalled(); - } finally { - card.remove(); - } - }); +describe('global keyboard shortcuts', () => { + it('triggers random navigation on Space when nothing interactive is focused', () => { + render(); + fireEvent.keyDown(document.body, { key: ' ' }); + expect(mockHandleRandom).toHaveBeenCalledWith('space'); + }); - it('does not fire when a native button owns the keystroke', () => { - render(); - const button = document.createElement('button'); - document.body.appendChild(button); - try { - fireEvent.keyDown(button, { key: ' ' }); - expect(mockHandleRandom).not.toHaveBeenCalled(); - } finally { - button.remove(); - } - }); + it('does not fire when a focusable card owns the keystroke', () => { + render(); + const card = document.createElement('div'); + card.setAttribute('role', 'button'); + card.tabIndex = 0; + document.body.appendChild(card); + try { + fireEvent.keyDown(card, { key: ' ' }); + expect(mockHandleRandom).not.toHaveBeenCalled(); + } finally { + card.remove(); + } }); - describe('scrollRestoration', () => { - const original = history.scrollRestoration; + it('does not fire when a native button owns the keystroke', () => { + render(); + const button = document.createElement('button'); + document.body.appendChild(button); + try { + fireEvent.keyDown(button, { key: ' ' }); + expect(mockHandleRandom).not.toHaveBeenCalled(); + } finally { + button.remove(); + } + }); +}); - afterEach(() => { - history.scrollRestoration = original; - }); +describe('scrollRestoration', () => { + const original = history.scrollRestoration; - it("sets scrollRestoration to 'manual' on mount and restores the previous value on unmount", () => { - history.scrollRestoration = 'auto'; - const { unmount } = render(); - expect(history.scrollRestoration).toBe('manual'); - unmount(); - expect(history.scrollRestoration).toBe('auto'); - }); + afterEach(() => { + history.scrollRestoration = original; + }); - it('restores a non-default previous value on unmount instead of forcing auto', () => { - history.scrollRestoration = 'manual'; - const { unmount } = render(); - expect(history.scrollRestoration).toBe('manual'); - unmount(); - expect(history.scrollRestoration).toBe('manual'); - }); + it("sets scrollRestoration to 'manual' on mount and restores the previous value on unmount", () => { + history.scrollRestoration = 'auto'; + const { unmount } = render(); + expect(history.scrollRestoration).toBe('manual'); + unmount(); + expect(history.scrollRestoration).toBe('auto'); + }); + + it('restores a non-default previous value on unmount instead of forcing auto', () => { + history.scrollRestoration = 'manual'; + const { unmount } = render(); + expect(history.scrollRestoration).toBe('manual'); + unmount(); + expect(history.scrollRestoration).toBe('manual'); }); }); diff --git a/app/src/pages/PlotsPage.tsx b/app/src/pages/PlotsPage.tsx index d5cfbe015d7..e655bd11b38 100644 --- a/app/src/pages/PlotsPage.tsx +++ b/app/src/pages/PlotsPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Link as RouterLink, useNavigate, useSearchParams } from 'react-router-dom'; @@ -59,6 +59,15 @@ export function PlotsPage() { onTrackEvent: trackEvent, }); + const filterParts = useMemo(() => { + return activeFilters + .filter(f => f.values.length > 0) + .map(f => `${f.category}:${f.values.join(',')}`) + .join(' '); + }, [activeFilters]); + + const pageTitle = filterParts ? `${filterParts} | plots | anyplot.ai` : 'plots | anyplot.ai'; + const { loadMoreRef } = useInfiniteScroll({ allImages, displayedImages, @@ -177,12 +186,14 @@ export function PlotsPage() { return ( - plots | anyplot.ai - - + <> + {pageTitle} + + + {error && ( diff --git a/fix_plots_page.cjs b/fix_plots_page.cjs new file mode 100644 index 00000000000..98688b35cbc --- /dev/null +++ b/fix_plots_page.cjs @@ -0,0 +1,9 @@ +const fs = require('fs'); + +const pageFile = 'app/src/pages/PlotsPage.tsx'; +let content = fs.readFileSync(pageFile, 'utf8'); + +// The reason useMemo is unused is because I am using it, but it might be imported twice or something? +// Let's print out the first 20 lines to see. +const lines = content.split('\n'); +console.log(lines.slice(0, 20).join('\n')); diff --git a/fix_plots_page2.cjs b/fix_plots_page2.cjs new file mode 100644 index 00000000000..57669af1090 --- /dev/null +++ b/fix_plots_page2.cjs @@ -0,0 +1,8 @@ +const fs = require('fs'); + +const pageFile = 'app/src/pages/PlotsPage.tsx'; +let content = fs.readFileSync(pageFile, 'utf8'); + +// I inserted it at the end of the file, around useFilterState, where is that? Let's check lines 30-60 +const lines = content.split('\n'); +console.log(lines.slice(20, 70).join('\n')); diff --git a/fix_plots_page3.cjs b/fix_plots_page3.cjs new file mode 100644 index 00000000000..a64df09a71d --- /dev/null +++ b/fix_plots_page3.cjs @@ -0,0 +1,20 @@ +const fs = require('fs'); + +const pageFile = 'app/src/pages/PlotsPage.tsx'; +let content = fs.readFileSync(pageFile, 'utf8'); + +// I mistakenly replaced `clearFilters,\n } = useFilterState(specsData, librariesData, languagesData);` +// but it looks like `clearFilters` isn't there, or the hook signature changed. +// Let's insert the useMemo logic right after `setHasMore,` on line 43. +content = content.replace(" const { loadMoreRef } = useInfiniteScroll({", ` const filterParts = useMemo(() => { + return activeFilters + .filter(f => f.values.length > 0) + .map(f => \`\${f.category}:\${f.values.join(',')}\`) + .join(' '); + }, [activeFilters]); + + const pageTitle = filterParts ? \`\${filterParts} | plots | anyplot.ai\` : 'plots | anyplot.ai'; + + const { loadMoreRef } = useInfiniteScroll({`); + +fs.writeFileSync(pageFile, content); diff --git a/fix_plots_page_order.cjs b/fix_plots_page_order.cjs new file mode 100644 index 00000000000..a1677d13f0e --- /dev/null +++ b/fix_plots_page_order.cjs @@ -0,0 +1,37 @@ +const fs = require('fs'); + +const pageFile = 'app/src/pages/PlotsPage.tsx'; +let content = fs.readFileSync(pageFile, 'utf8'); + +// Fix the import order for useMemo and useState +content = content.replace("import { useCallback, useEffect, useRef, useState, useMemo } from 'react';", "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"); + +// Move filterParts computation after activeFilters is declared +// activeFilters is declared at line: +// const { +// activeFilters, +// ... +// clearFilters, +// } = useFilterState(specsData, librariesData, languagesData); + +const filterLogic = ` + const filterParts = useMemo(() => { + return activeFilters + .filter(f => f.values.length > 0) + .map(f => \`\${f.category}:\${f.values.join(',')}\`) + .join(' '); + }, [activeFilters]); + + const pageTitle = filterParts ? \`\${filterParts} | plots | anyplot.ai\` : 'plots | anyplot.ai'; +`; + +// Remove it from its current place +content = content.replace(/\s*const filterParts = useMemo\(\(\) => \{[\s\S]*?\}, \[activeFilters\]\);\s*const pageTitle = filterParts \? `\$\{filterParts\} \| plots \| anyplot\.ai` : 'plots \| anyplot\.ai';/, ""); + +// Insert after clearFilters +content = content.replace(/\s*clearFilters,\s*\} = useFilterState\(specsData, librariesData, languagesData\);/, (match) => { + return `${match}\n\n${filterLogic}`; +}); + + +fs.writeFileSync(pageFile, content); diff --git a/fix_plots_page_test.cjs b/fix_plots_page_test.cjs new file mode 100644 index 00000000000..b69f08baead --- /dev/null +++ b/fix_plots_page_test.cjs @@ -0,0 +1,23 @@ +const fs = require('fs'); + +const testFile = 'app/src/pages/PlotsPage.test.tsx'; +let testContent = fs.readFileSync(testFile, 'utf8'); + +// I accidentally duplicated the render and expect logic outside the test block or something! +// Look at the grep output: +// 140: it('renders document metadata with filters', () => { +// 141- state.activeFilters = [{ category: 'lib', values: ['matplotlib'] }]; +// 142- render(); +// 143- expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai'); +// 144- }); +// 145- +// 146- render(); +// 147- expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai'); +// 148-}); + +testContent = testContent.replace( + /it\('renders document metadata with filters', \(\) => \{\n\s*state\.activeFilters = \[\{ category: 'lib', values: \['matplotlib'\] \}\];\n\s*render\(\);\n\s*expect\(document\.title\)\.toBe\('lib:matplotlib \| plots \| anyplot\.ai'\);\n\s*\}\);\n\n\s*render\(\);\n\s*expect\(document\.title\)\.toBe\('lib:matplotlib \| plots \| anyplot\.ai'\);\n\}\);/, + "it('renders document metadata with filters', () => {\n state.activeFilters = [{ category: 'lib', values: ['matplotlib'] }];\n render();\n expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai');\n });\n});" +); + +fs.writeFileSync(testFile, testContent); diff --git a/fix_useUrlSync.cjs b/fix_useUrlSync.cjs new file mode 100644 index 00000000000..8e8d83abf52 --- /dev/null +++ b/fix_useUrlSync.cjs @@ -0,0 +1,24 @@ +const fs = require('fs'); + +// 1. Update useUrlSync.ts +const file = "app/src/hooks/useUrlSync.ts"; +let content = fs.readFileSync(file, 'utf8'); + +content = content.replace(/\/\*\*[\s\S]*?Hook to synchronize active filters with URL and updates document title\.[\s\S]*?\*\//, "/**\n * Hook to synchronize active filters with URL.\n */"); + +const titleRegex = /\s*const filterParts = activeFilters[\s\S]*?if \(filterParts\) \{[\s\S]*?document\.title = `\$\{filterParts\} \| anyplot\.ai`;[\s\S]*?\} else \{[\s\S]*?document\.title = 'anyplot\.ai';[\s\S]*?\}/; +content = content.replace(titleRegex, ""); + +content = content.replace(/\s*\/\/ Update document title/, ""); + +fs.writeFileSync(file, content); + +// 1b. Update useUrlSync.test.ts +const file_test = "app/src/hooks/useUrlSync.test.ts"; +let content_test = fs.readFileSync(file_test, 'utf8'); + +content_test = content_test.replace("updates URL and title", "updates URL"); +content_test = content_test.replace(/\s*expect\(document\.title\)\.toBe\('lib:matplotlib \| anyplot\.ai'\);/, ""); +content_test = content_test.replace(/\s*expect\(document\.title\)\.toBe\('anyplot\.ai'\);/, ""); + +fs.writeFileSync(file_test, content_test); diff --git a/fix_useUrlSync.sh b/fix_useUrlSync.sh new file mode 100755 index 00000000000..41d1485ef70 --- /dev/null +++ b/fix_useUrlSync.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# 1. Update useUrlSync.ts +file="app/src/hooks/useUrlSync.ts" + +sed -i '/const filterParts = activeFilters/,/document.title =/d' $file +sed -i "/document.title = 'anyplot.ai'/d" $file +sed -i "/if (filterParts) {/d" $file +sed -i "/} else {/d" $file +sed -i "/}/d" $file +sed -i "s/and updates document title//" $file +sed -i "s/\/\/ Update document title//g" $file + +# 1b. Update useUrlSync.test.ts +file_test="app/src/hooks/useUrlSync.test.ts" +sed -i "s/updates URL and title/updates URL/" $file_test +sed -i "/expect(document.title).toBe('lib:matplotlib | anyplot.ai');/d" $file_test +sed -i "/expect(document.title).toBe('anyplot.ai');/d" $file_test