From 5e7dcb3d627fef25551d23ecadf79796774237f1 Mon Sep 17 00:00:00 2001 From: Filipe Varela Date: Wed, 27 May 2026 14:05:06 -0500 Subject: [PATCH 01/21] Components: add BoundedLayout layout primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a shared `BoundedLayout` wrapper to `@automattic/jetpack-components` with two MSD-aligned presets — `compact` (660px) and `wide` (1344px) — so products can keep visual alignment across Overview / Settings / Dashboard screens. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/changelog/add-bounded-layout | 4 ++ .../layout/bounded-layout/index.tsx | 51 +++++++++++++++++++ .../layout/bounded-layout/style.module.scss | 19 +++++++ projects/js-packages/components/index.ts | 5 ++ 4 files changed, 79 insertions(+) create mode 100644 projects/js-packages/components/changelog/add-bounded-layout create mode 100644 projects/js-packages/components/components/layout/bounded-layout/index.tsx create mode 100644 projects/js-packages/components/components/layout/bounded-layout/style.module.scss diff --git a/projects/js-packages/components/changelog/add-bounded-layout b/projects/js-packages/components/changelog/add-bounded-layout new file mode 100644 index 000000000000..ace7371d2eb8 --- /dev/null +++ b/projects/js-packages/components/changelog/add-bounded-layout @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add `BoundedLayout` — a shared content-width wrapper with `compact` (660px) and `wide` (1344px) presets, mirroring MSD layout conventions so products can stay visually aligned across Overview/Settings/Dashboard screens. diff --git a/projects/js-packages/components/components/layout/bounded-layout/index.tsx b/projects/js-packages/components/components/layout/bounded-layout/index.tsx new file mode 100644 index 000000000000..4e45b6babee8 --- /dev/null +++ b/projects/js-packages/components/components/layout/bounded-layout/index.tsx @@ -0,0 +1,51 @@ +import clsx from 'clsx'; +import { createElement } from 'react'; +import styles from './style.module.scss'; +import type { FC, ReactElement, ReactNode } from 'react'; + +export type BoundedLayoutWidth = 'compact' | 'wide'; + +export type BoundedLayoutProps = { + /** + * Preset max-width token. `compact` (660px) for settings-style single + * columns; `wide` (1344px) for dashboard grids. Mirrors MSD conventions. + */ + width?: BoundedLayoutWidth; + + /** + * Tag name for the rendered element. Defaults to `div`. + */ + tagName?: string; + + /** + * Additional className to merge onto the wrapper. + */ + className?: string; + + children?: ReactNode; +}; + +/** + * Centers its children and caps the content area at a standardized Jetpack + * product max-width. Use it to keep Overview / Dashboard / Settings screens + * visually consistent across products. + * + * @param {BoundedLayoutProps} props - Component properties. + * @return {ReactElement} BoundedLayout component. + */ +const BoundedLayout: FC< BoundedLayoutProps > = ( { + width = 'wide', + tagName = 'div', + className, + children, +} ) => { + return createElement( + tagName, + { + className: clsx( styles.bounds, styles[ width ], className ), + }, + children + ); +}; + +export default BoundedLayout; diff --git a/projects/js-packages/components/components/layout/bounded-layout/style.module.scss b/projects/js-packages/components/components/layout/bounded-layout/style.module.scss new file mode 100644 index 000000000000..487e9f26b2ea --- /dev/null +++ b/projects/js-packages/components/components/layout/bounded-layout/style.module.scss @@ -0,0 +1,19 @@ +// Standardized content-area widths shared across Jetpack products. Mirrors +// MSD (Multi-site Dashboard) layout tokens so admin surfaces stay aligned. +// `compact` is for focused forms and settings; `wide` is for dashboards. +.bounds { + --jp-bounded-layout-width: 1344px; + + box-sizing: border-box; + width: 100%; + max-width: var(--jp-bounded-layout-width); + margin-inline: auto; +} + +.compact { + --jp-bounded-layout-width: 660px; +} + +.wide { + --jp-bounded-layout-width: 1344px; +} diff --git a/projects/js-packages/components/index.ts b/projects/js-packages/components/index.ts index 6a2bac5bb5b0..1d8ec995388d 100644 --- a/projects/js-packages/components/index.ts +++ b/projects/js-packages/components/index.ts @@ -40,6 +40,11 @@ export { default as DecorativeCard } from './components/decorative-card/index.ts export { default as Col } from './components/layout/col/index.tsx'; export { default as Testimonials } from './components/testimonials/index.tsx'; export { default as Container } from './components/layout/container/index.tsx'; +export { default as BoundedLayout } from './components/layout/bounded-layout/index.tsx'; +export type { + BoundedLayoutProps, + BoundedLayoutWidth, +} from './components/layout/bounded-layout/index.tsx'; export { default as useBreakpointMatch } from './components/layout/use-breakpoint-match/index.ts'; export { default as CopyToClipboard } from './components/copy-to-clipboard/index.tsx'; export * from './components/icons/index.tsx'; From 4a58e53879af92022dabb26ba2c73f1d590935df Mon Sep 17 00:00:00 2001 From: Filipe Varela Date: Wed, 27 May 2026 14:19:57 -0500 Subject: [PATCH 02/21] Jetpack SEO: introduce package foundation and Site visibility Overview Adds a new `automattic/jetpack-seo` package that mounts a React admin at `wp-admin/admin.php?page=jetpack-seo`. Menu item gates on the `seo-tools` module being active. This PR is the first of a staged series introducing the Jetpack SEO product; it ships only the package scaffold and the Overview screen's Site visibility card. Follow-up PRs add the Settings, Per-post SEO (Content), and AI tabs, along with their REST writers and respective Overview cards. New package - PHP: `Initializer` (admin menu registration + asset enqueue, gated on `seo-tools`), `REST_Controller` exposing `/jetpack-seo/v1/overview` only. - React SPA: `createHashRouter` + `RouterProvider`, `AdminPage` from `@automattic/jetpack-components`, TanStack Query for server state. - Single screen: Overview with a Site visibility card (search-engines allowed, sitemap active/inactive, SEO tools active/inactive). All other Overview cards land with the PRs that own their underlying features. Jetpack plugin wiring - `class.jetpack.php`: load the new package in `late_initialization()`. - `composer.json`: require `automattic/jetpack-seo`. Co-Authored-By: Angela Blake Co-Authored-By: Claude Opus 4.7 (1M context) --- pnpm-lock.yaml | 1152 +++++++++++++++++ projects/packages/seo/.gitignore | 5 + projects/packages/seo/.phpcs.dir.xml | 24 + projects/packages/seo/CHANGELOG.md | 11 + projects/packages/seo/README.md | 32 + projects/packages/seo/_inc/admin.tsx | 31 + projects/packages/seo/_inc/constants.ts | 5 + .../packages/seo/_inc/data/overview-types.ts | 12 + projects/packages/seo/_inc/data/types.ts | 5 + .../packages/seo/_inc/data/use-overview.ts | 11 + .../seo/_inc/data/use-simple-mutation.ts | 46 + .../seo/_inc/data/use-simple-query.ts | 28 + .../seo/_inc/header-actions-context.tsx | 45 + projects/packages/seo/_inc/providers.tsx | 24 + .../seo/_inc/screens/overview/index.tsx | 45 + .../screens/overview/site-visibility-card.tsx | 63 + .../seo/_inc/screens/overview/status-dot.tsx | 24 + .../_inc/screens/overview/style.module.scss | 76 ++ projects/packages/seo/_inc/shell.tsx | 50 + projects/packages/seo/_inc/style.module.scss | 47 + projects/packages/seo/_inc/types.ts | 14 + projects/packages/seo/babel.config.js | 10 + .../seo/changelog/add-initial-scaffold | 4 + projects/packages/seo/composer.json | 79 ++ projects/packages/seo/global.d.ts | 11 + projects/packages/seo/package.json | 69 + projects/packages/seo/phpunit.11.xml.dist | 34 + .../packages/seo/src/class-initializer.php | 240 ++++ .../seo/src/class-rest-controller.php | 88 ++ projects/packages/seo/tests/.phpcs.dir.xml | 4 + .../seo/tests/php/InitializerTest.php | 32 + projects/packages/seo/tests/php/bootstrap.php | 12 + projects/packages/seo/tsconfig.json | 4 + projects/packages/seo/webpack.config.js | 47 + .../changelog/add-seo-package-scaffold | 4 + projects/plugins/jetpack/class.jetpack.php | 2 + projects/plugins/jetpack/composer.json | 1 + projects/plugins/jetpack/composer.lock | 77 +- 38 files changed, 2467 insertions(+), 1 deletion(-) create mode 100644 projects/packages/seo/.gitignore create mode 100644 projects/packages/seo/.phpcs.dir.xml create mode 100644 projects/packages/seo/CHANGELOG.md create mode 100644 projects/packages/seo/README.md create mode 100644 projects/packages/seo/_inc/admin.tsx create mode 100644 projects/packages/seo/_inc/constants.ts create mode 100644 projects/packages/seo/_inc/data/overview-types.ts create mode 100644 projects/packages/seo/_inc/data/types.ts create mode 100644 projects/packages/seo/_inc/data/use-overview.ts create mode 100644 projects/packages/seo/_inc/data/use-simple-mutation.ts create mode 100644 projects/packages/seo/_inc/data/use-simple-query.ts create mode 100644 projects/packages/seo/_inc/header-actions-context.tsx create mode 100644 projects/packages/seo/_inc/providers.tsx create mode 100644 projects/packages/seo/_inc/screens/overview/index.tsx create mode 100644 projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx create mode 100644 projects/packages/seo/_inc/screens/overview/status-dot.tsx create mode 100644 projects/packages/seo/_inc/screens/overview/style.module.scss create mode 100644 projects/packages/seo/_inc/shell.tsx create mode 100644 projects/packages/seo/_inc/style.module.scss create mode 100644 projects/packages/seo/_inc/types.ts create mode 100644 projects/packages/seo/babel.config.js create mode 100644 projects/packages/seo/changelog/add-initial-scaffold create mode 100644 projects/packages/seo/composer.json create mode 100644 projects/packages/seo/global.d.ts create mode 100644 projects/packages/seo/package.json create mode 100644 projects/packages/seo/phpunit.11.xml.dist create mode 100644 projects/packages/seo/src/class-initializer.php create mode 100644 projects/packages/seo/src/class-rest-controller.php create mode 100644 projects/packages/seo/tests/.phpcs.dir.xml create mode 100644 projects/packages/seo/tests/php/InitializerTest.php create mode 100644 projects/packages/seo/tests/php/bootstrap.php create mode 100644 projects/packages/seo/tsconfig.json create mode 100644 projects/packages/seo/webpack.config.js create mode 100644 projects/plugins/jetpack/changelog/add-seo-package-scaffold diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e593660e50f2..73968daea8ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4520,6 +4520,121 @@ importers: specifier: 6.0.1 version: 6.0.1(webpack@5.105.2) + projects/packages/seo: + dependencies: + '@automattic/babel-plugin-replace-textdomain': + specifier: workspace:* + version: link:../../js-packages/babel-plugin-replace-textdomain + '@automattic/jetpack-base-styles': + specifier: workspace:* + version: link:../../js-packages/base-styles + '@automattic/jetpack-components': + specifier: workspace:* + version: link:../../js-packages/components + '@automattic/jetpack-connection': + specifier: workspace:* + version: link:../../js-packages/connection + '@automattic/jetpack-script-data': + specifier: workspace:* + version: link:../../js-packages/script-data + '@automattic/social-previews': + specifier: workspace:* + version: link:../../js-packages/social-previews + '@tanstack/react-query': + specifier: 5.90.8 + version: 5.90.8(react@18.3.1) + '@wordpress/api-fetch': + specifier: 7.44.0 + version: 7.44.0 + '@wordpress/components': + specifier: 32.6.0 + version: 32.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/compose': + specifier: 7.44.0 + version: 7.44.0(react@18.3.1) + '@wordpress/data': + specifier: 10.44.0 + version: 10.44.0(react@18.3.1) + '@wordpress/dataviews': + specifier: 14.0.0 + version: 14.0.0(@types/react@18.3.28)(react@18.3.1) + '@wordpress/element': + specifier: 6.44.0 + version: 6.44.0 + '@wordpress/i18n': + specifier: 6.17.0 + version: 6.17.0 + '@wordpress/icons': + specifier: 12.1.0 + version: 12.1.0(react@18.3.1) + '@wordpress/ui': + specifier: 0.10.0 + version: 0.10.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/url': + specifier: 4.44.0 + version: 4.44.0 + clsx: + specifier: 2.1.1 + version: 2.1.1 + react-router: + specifier: 7.12.0 + version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@automattic/jetpack-webpack-config': + specifier: workspace:* + version: link:../../js-packages/webpack-config + '@babel/core': + specifier: 7.29.0 + version: 7.29.0 + '@babel/preset-env': + specifier: 7.29.2 + version: 7.29.2(@babel/core@7.29.0) + '@testing-library/dom': + specifier: 10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: 6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/jest': + specifier: 30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.12.0 + version: 24.12.3 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260225.1 + version: 7.0.0-dev.20260225.1 + jest: + specifier: 30.3.0 + version: 30.3.0(@types/node@24.12.3) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + sass-embedded: + specifier: 1.97.3 + version: 1.97.3 + sass-loader: + specifier: 16.0.5 + version: 16.0.5(sass-embedded@1.97.3)(webpack@5.105.2) + typescript: + specifier: 5.9.3 + version: 5.9.3 + webpack: + specifier: 5.105.2 + version: 5.105.2(webpack-cli@6.0.1) + webpack-cli: + specifier: 6.0.1 + version: 6.0.1(webpack@5.105.2) + projects/packages/stats-admin: {} projects/packages/transport-helper: {} @@ -8130,10 +8245,23 @@ packages: resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} + '@jest/console@30.3.0': + resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/console@30.4.1': resolution: {integrity: sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/core@30.3.0': + resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/core@30.4.2': resolution: {integrity: sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -8143,6 +8271,10 @@ packages: node-notifier: optional: true + '@jest/diff-sequences@30.3.0': + resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/diff-sequences@30.4.0': resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -8157,18 +8289,34 @@ packages: canvas: optional: true + '@jest/environment@30.3.0': + resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@30.4.1': resolution: {integrity: sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect-utils@30.3.0': + resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect-utils@30.4.1': resolution: {integrity: sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect@30.3.0': + resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect@30.4.1': resolution: {integrity: sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/fake-timers@30.3.0': + resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/fake-timers@30.4.1': resolution: {integrity: sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -8177,14 +8325,31 @@ packages: resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/globals@30.3.0': + resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/globals@30.4.1': resolution: {integrity: sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/pattern@30.4.0': resolution: {integrity: sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/reporters@30.3.0': + resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/reporters@30.4.1': resolution: {integrity: sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -8194,10 +8359,18 @@ packages: node-notifier: optional: true + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/schemas@30.4.1': resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/snapshot-utils@30.3.0': + resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/snapshot-utils@30.4.1': resolution: {integrity: sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -8206,18 +8379,34 @@ packages: resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-result@30.3.0': + resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-result@30.4.1': resolution: {integrity: sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-sequencer@30.3.0': + resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-sequencer@30.4.1': resolution: {integrity: sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/transform@30.3.0': + resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/transform@30.4.1': resolution: {integrity: sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@30.3.0': + resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@30.4.1': resolution: {integrity: sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -10662,6 +10851,10 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/api-fetch@7.44.0': + resolution: {integrity: sha512-KZP5Y0AzUVPRbwCsp2MUNEjIyYPJdaa7ojzYyc/IVlaAlbXVdd0Ofk8UDf4l8PjtXkyyPs9pX9sFy5iNcrF2cQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/api-fetch@7.46.0': resolution: {integrity: sha512-QOxuHSUXMzLat3Y90+0HNUDPSlBUK53r4mQ4m7f4/OKaWRRZU5jzvDBJyj52dEST7yJ1eZtuqUkEwK2T1MEBfQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10684,6 +10877,10 @@ packages: resolution: {integrity: sha512-HpjX32OkbSpNZkhVo2WdQuP1MkpVg24hVaq7uM5whDdYR88pSc5bfhJ1cNsWagYJQvuYFBf+YIBSvxref4ojXA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/base-styles@6.20.0': + resolution: {integrity: sha512-Dsug4Zxz2xOFtK6CGThKYXwCqC9Yztw2STKQzwztrX4yW+o6iDbzkxpcwdDhsaVJs0Jt9A4LmJpZPh+pUozzLA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/base-styles@8.0.0': resolution: {integrity: sha512-livtgwnvBm7xbpm/gaBxwtdZm3KCXq210UNsr48WA8TGfi/OfZ4oOzk4Mp4/ZHsq2baaXzhZ0iXjyR7oyaOTsw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10753,6 +10950,13 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/components@32.6.0': + resolution: {integrity: sha512-MpOr0mGTkKDRjxK5LKm86Uoj9p9Z6KkrvhkNVi5zVKCftyHVMK+tun7wL2Qn/JZVLbxZpB1kW5sJ5aMf3T2ToA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + '@wordpress/components@33.1.0': resolution: {integrity: sha512-5nFqe2pk7ePIhJhz+nDNS8r1az5hIJrUycuYJzmL3KL9hYgDknAzJDHb6IUNlVcNDPgLUuxzC780YlVG5Bi0LQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10760,6 +10964,12 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/compose@7.44.0': + resolution: {integrity: sha512-NlMSR+sqEkHppjUM3irJhB0PLaWYoAgWFa7BL6xb94ciWxr4C5CIB0pSCXW8B0WNBPgS7q/xCeJGKGSfLkBgIQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/compose@7.46.0': resolution: {integrity: sha512-6Yv9Wb6tlA4JYU9bdWWuIWpTTzBAVA1zrYu1GY9x2/mCOckk9iLcEEfbKULxdjwwcMo3SKqvyby4f6kEUw/Wsw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10779,12 +10989,24 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/data@10.44.0': + resolution: {integrity: sha512-NMOJ3sDAT+ZSKm5iMvL3JVstNxDdvW9rYbzMKYzyfXbfAi9zdlNfN3Pc/0ozsUfDwhn336mA/Wu9EBNc0P+Ajw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/data@10.46.0': resolution: {integrity: sha512-vxOO2IEn+29eue9Pq7Mzsq1SipMAg0Rp0Oztz9LsgWQIF9yyylGlP3yHnFjEmJ4MonGSjzvpArlc7jWwkzutKg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 + '@wordpress/dataviews@14.0.0': + resolution: {integrity: sha512-DTfJiwNL+Yt60P4BmLvYLjpd/QeOmJJry/DRdMqcPyt6w98rRFwr32hcbn0FwVkADJXmW8l2Y2vczf01StnZJw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/dataviews@14.3.0': resolution: {integrity: sha512-2fFSgyatDldjPb2gO+vLDwkbI2Jw+8zd/O0/BwLftQ5QhrrRtAqECFp+eYzcQ8Onh8OMhxq0n7tsaIHE/jWqJQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10834,6 +11056,10 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/element@6.44.0': + resolution: {integrity: sha512-kVCRSwGMPFu7oBcAzN0VzwFQw3mwctUb/TEHkGeG5An1Uus6olruGJyvFwkHNtO9WRCdTXXunUaSk0CIA9+Wig==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/element@6.46.0': resolution: {integrity: sha512-hjnrqZi0cZVdkmN0xQavKfSQJYAkb9pVSnDPpuX65OLxeD9/EWkIXvFzBb+nH8c4NzKKSqQU96XCTQrH37OCIA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10903,6 +11129,11 @@ packages: resolution: {integrity: sha512-YJ/V9R2p4lwYkhc9/bQrXxoX0rNDtt1WQGInKAxRWqF1w1gYQk0iWiwGcNnahnFofwK2LJSVf4/jYFjJrS/sPw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/i18n@6.17.0': + resolution: {integrity: sha512-v1SLBweg7CRzQ+5+WSC1U93i8h9d3AoB0YBvMsd6gWI5vO8Zh4YKlEMexvrHQC++WN83egwqux84fWEdeU0MUA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + hasBin: true + '@wordpress/i18n@6.19.0': resolution: {integrity: sha512-hRXd2E0SF9OQf22ZZWw7Ny/o+Q9u8jINiF1p0bF+rnSDKQUgoStihak6YiazWVRiIEYwctzotKXlt0HePJelXA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10914,6 +11145,18 @@ packages: peerDependencies: react: ^18 + '@wordpress/icons@12.1.0': + resolution: {integrity: sha512-JOEVd94kZQsGYyLhjq1edfaMOTPON/7qUDuzT74uSwSCJ6OiHf3yJHfxMlLOMoh12dQshWPciLVLagkYLCldag==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + + '@wordpress/icons@12.2.0': + resolution: {integrity: sha512-Fiw7bmfHDNPjTdCrBF23/9K0VN/GUi73d2ZPZaeWdXhTmIX62T9KYvb1c+WnlBkX7GpXgJO6Q8mypQCY9mw5SQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/icons@13.1.0': resolution: {integrity: sha512-KMZAeYghsLs6e5wKMZ3/Ynrsuu5yZt2gAlMHmZSkWJKQFld++Pz/pEj8nDCJ79z/zx9FO7q4teG49vHHvVosjQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11100,6 +11343,17 @@ packages: resolution: {integrity: sha512-g7lTru47e4VonYFwvQRwX8Cj+o3n7p0SW1YXgwoIRP9cc7ce5Ipb2X+zqrnT81SN/wFqzH3mZDVNRvksqKQakw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/theme@0.10.0': + resolution: {integrity: sha512-U8CaRvGzeQtFfGQFsKarcbzPEH+jfXJmpOlIpt4bq2goW9CgeWFlDC29p0oyzoMn1Ga9hX+c8ay3nUgSbhmSSA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + stylelint: '>=16.8.2' + peerDependenciesMeta: + stylelint: + optional: true + '@wordpress/theme@0.13.0': resolution: {integrity: sha512-4Lasso3BPej43c7e+eO+YN/fl/mcg/Q9+nclp1FmV6xdWFiUXvfwAOsEeNQQ/5s5mw5aCgseK3//qX5gydhfUA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11115,6 +11369,13 @@ packages: resolution: {integrity: sha512-g9UytUCFcLnj8LWNHFUK0c53FeokTEXDlZ3C3VrpDnxq0jC0BnNj0uJCAmbzfehg23LWI2O5xnQzmpAJ9ldAKg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/ui@0.10.0': + resolution: {integrity: sha512-HnS8/yCxcgpoVOw0ssiKjFa0WfGbC3BDYeDaitE9iLPOUtk1YxuuljKcXt21T6BZvtV1f/8SKhIUOqTwDMWo2Q==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + '@wordpress/ui@0.13.0': resolution: {integrity: sha512-NSP/Hh6X3qbN0B7KsWFGZfmiYp28NiVZnxu8uJSspZs9mzVP+qKC9yOgIxPYIjFuGDrXJ6QK9wL3soRXkJMG0w==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} @@ -11133,6 +11394,10 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/url@4.44.0': + resolution: {integrity: sha512-kWalXttgtRwFy4szBPX9dJcqHErRC0V9JuZ7uxdrxxdXl6WNv+lx8SYpLx12q3Zk6zNIw73M8E5wHON7eyXZZw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/url@4.46.0': resolution: {integrity: sha512-LGja+dYBzaNkkPSE5ddPgk03M66wadUheuhyOLTu4uLQU2UmipN9qQgI4VAnZLrnXs7dqb4fJ8f0AuNmshHpbg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11469,6 +11734,12 @@ packages: react-native-b4a: optional: true + babel-jest@30.3.0: + resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + babel-jest@30.4.1: resolution: {integrity: sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -11486,6 +11757,10 @@ packages: resolution: {integrity: sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==} engines: {node: '>=18'} + babel-plugin-jest-hoist@30.3.0: + resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + babel-plugin-jest-hoist@30.4.0: resolution: {integrity: sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -11533,6 +11808,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 + babel-preset-jest@30.3.0: + resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + babel-preset-jest@30.4.0: resolution: {integrity: sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -12323,6 +12604,9 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} @@ -13105,6 +13389,10 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + expect@30.3.0: + resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + expect@30.4.1: resolution: {integrity: sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -14102,14 +14390,32 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + jest-changed-files@30.3.0: + resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-changed-files@30.4.1: resolution: {integrity: sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-circus@30.3.0: + resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-circus@30.4.2: resolution: {integrity: sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-cli@30.3.0: + resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jest-cli@30.4.2: resolution: {integrity: sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -14120,6 +14426,21 @@ packages: node-notifier: optional: true + jest-config@30.3.0: + resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + jest-config@30.4.2: resolution: {integrity: sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -14135,18 +14456,34 @@ packages: ts-node: optional: true + jest-diff@30.3.0: + resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-diff@30.4.1: resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@30.2.0: + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@30.4.0: resolution: {integrity: sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-each@30.3.0: + resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-each@30.4.1: resolution: {integrity: sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-environment-node@30.3.0: + resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-environment-node@30.4.1: resolution: {integrity: sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -14161,22 +14498,42 @@ packages: jest: optional: true + jest-haste-map@30.3.0: + resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-haste-map@30.4.1: resolution: {integrity: sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-leak-detector@30.3.0: + resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-leak-detector@30.4.1: resolution: {integrity: sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-matcher-utils@30.3.0: + resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-matcher-utils@30.4.1: resolution: {integrity: sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@30.3.0: + resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@30.4.1: resolution: {integrity: sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@30.3.0: + resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@30.4.1: resolution: {integrity: sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -14190,38 +14547,74 @@ packages: jest-resolve: optional: true + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-regex-util@30.4.0: resolution: {integrity: sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve-dependencies@30.3.0: + resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve-dependencies@30.4.2: resolution: {integrity: sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve@30.3.0: + resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve@30.4.1: resolution: {integrity: sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runner@30.3.0: + resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runner@30.4.2: resolution: {integrity: sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runtime@30.3.0: + resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runtime@30.4.2: resolution: {integrity: sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-snapshot@30.3.0: + resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-snapshot@30.4.1: resolution: {integrity: sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-util@30.3.0: + resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-util@30.4.1: resolution: {integrity: sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@30.3.0: + resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@30.4.1: resolution: {integrity: sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-watcher@30.3.0: + resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-watcher@30.4.1: resolution: {integrity: sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -14230,10 +14623,24 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + jest-worker@30.3.0: + resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-worker@30.4.1: resolution: {integrity: sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest@30.3.0: + resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jest@30.4.2: resolution: {integrity: sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -15832,6 +16239,10 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-format@30.4.1: resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -17727,6 +18138,11 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -20261,6 +20677,15 @@ snapshots: '@istanbuljs/schema@0.1.6': {} + '@jest/console@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + chalk: 4.1.2 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + slash: 3.0.0 + '@jest/console@30.4.1': dependencies: '@jest/types': 30.4.1 @@ -20270,6 +20695,41 @@ snapshots: jest-util: 30.4.1 slash: 3.0.0 + '@jest/core@30.3.0': + dependencies: + '@jest/console': 30.3.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.4.0 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.3.0 + jest-config: 30.3.0(@types/node@24.12.3) + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-resolve-dependencies: 30.3.0 + jest-runner: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + jest-watcher: 30.3.0 + pretty-format: 30.3.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + '@jest/core@30.4.2': dependencies: '@jest/console': 30.4.1 @@ -20306,6 +20766,8 @@ snapshots: - supports-color - ts-node + '@jest/diff-sequences@30.3.0': {} + '@jest/diff-sequences@30.4.0': {} '@jest/environment-jsdom-abstract@30.4.1(jsdom@27.4.0)': @@ -20319,6 +20781,13 @@ snapshots: jest-util: 30.4.1 jsdom: 27.4.0 + '@jest/environment@30.3.0': + dependencies: + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + jest-mock: 30.3.0 + '@jest/environment@30.4.1': dependencies: '@jest/fake-timers': 30.4.1 @@ -20326,10 +20795,21 @@ snapshots: '@types/node': 24.12.3 jest-mock: 30.4.1 + '@jest/expect-utils@30.3.0': + dependencies: + '@jest/get-type': 30.1.0 + '@jest/expect-utils@30.4.1': dependencies: '@jest/get-type': 30.1.0 + '@jest/expect@30.3.0': + dependencies: + expect: 30.3.0 + jest-snapshot: 30.3.0 + transitivePeerDependencies: + - supports-color + '@jest/expect@30.4.1': dependencies: expect: 30.4.1 @@ -20337,6 +20817,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/fake-timers@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@sinonjs/fake-timers': 15.4.0 + '@types/node': 24.12.3 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + '@jest/fake-timers@30.4.1': dependencies: '@jest/types': 30.4.1 @@ -20348,6 +20837,15 @@ snapshots: '@jest/get-type@30.1.0': {} + '@jest/globals@30.3.0': + dependencies: + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/types': 30.3.0 + jest-mock: 30.3.0 + transitivePeerDependencies: + - supports-color + '@jest/globals@30.4.1': dependencies: '@jest/environment': 30.4.1 @@ -20357,11 +20855,44 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 24.12.3 + jest-regex-util: 30.0.1 + '@jest/pattern@30.4.0': dependencies: '@types/node': 24.12.3 jest-regex-util: 30.4.0 + '@jest/reporters@30.3.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 24.12.3 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 13.0.6 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + jest-worker: 30.3.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + '@jest/reporters@30.4.1': dependencies: '@bcoe/v8-coverage': 0.2.3 @@ -20390,10 +20921,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.49 + '@jest/schemas@30.4.1': dependencies: '@sinclair/typebox': 0.34.49 + '@jest/snapshot-utils@30.3.0': + dependencies: + '@jest/types': 30.3.0 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + '@jest/snapshot-utils@30.4.1': dependencies: '@jest/types': 30.4.1 @@ -20407,6 +20949,13 @@ snapshots: callsites: 3.1.0 graceful-fs: 4.2.11 + '@jest/test-result@30.3.0': + dependencies: + '@jest/console': 30.3.0 + '@jest/types': 30.3.0 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + '@jest/test-result@30.4.1': dependencies: '@jest/console': 30.4.1 @@ -20414,6 +20963,13 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.3 + '@jest/test-sequencer@30.3.0': + dependencies: + '@jest/test-result': 30.3.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + slash: 3.0.0 + '@jest/test-sequencer@30.4.1': dependencies: '@jest/test-result': 30.4.1 @@ -20421,6 +20977,25 @@ snapshots: jest-haste-map: 30.4.1 slash: 3.0.0 + '@jest/transform@30.3.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 8.0.0 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + '@jest/transform@30.4.1': dependencies: '@babel/core': 7.29.0 @@ -20440,6 +21015,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/types@30.3.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.12.3 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jest/types@30.4.1': dependencies: '@jest/pattern': 30.4.0 @@ -23423,6 +24008,11 @@ snapshots: react: 18.3.1 uuid: 14.0.0 + '@wordpress/api-fetch@7.44.0': + dependencies: + '@wordpress/i18n': 6.19.0 + '@wordpress/url': 4.46.0 + '@wordpress/api-fetch@7.46.0': dependencies: '@wordpress/i18n': 6.19.0 @@ -23452,6 +24042,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@wordpress/base-styles@6.20.0': {} + '@wordpress/base-styles@8.0.0': {} '@wordpress/blob@4.46.0': {} @@ -24097,6 +24689,63 @@ snapshots: - '@types/react-dom' - supports-color + '@wordpress/components@32.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@date-fns/utc': 2.1.1 + '@emotion/cache': 11.14.0 + '@emotion/css': 11.13.5 + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/serialize': 1.3.3 + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@emotion/utils': 1.4.2 + '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/gradient-parser': 1.1.0 + '@types/highlight-words-core': 1.2.1 + '@types/react': 18.3.28 + '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/a11y': 4.46.0 + '@wordpress/base-styles': 6.20.0 + '@wordpress/compose': 7.46.0(react@18.3.1) + '@wordpress/date': 5.46.0 + '@wordpress/deprecated': 4.46.0 + '@wordpress/dom': 4.46.0 + '@wordpress/element': 6.46.0 + '@wordpress/escape-html': 3.46.0 + '@wordpress/hooks': 4.46.0 + '@wordpress/html-entities': 4.46.0 + '@wordpress/i18n': 6.19.0 + '@wordpress/icons': 12.2.0(react@18.3.1) + '@wordpress/is-shallow-equal': 5.46.0 + '@wordpress/keycodes': 4.46.0 + '@wordpress/primitives': 4.46.0(react@18.3.1) + '@wordpress/private-apis': 1.46.0 + '@wordpress/rich-text': 7.46.0(react@18.3.1) + '@wordpress/warning': 3.46.0 + change-case: 4.1.2 + clsx: 2.1.1 + colord: 2.9.3 + csstype: 3.2.3 + date-fns: 3.6.0 + deepmerge: 4.3.1 + fast-deep-equal: 3.1.3 + framer-motion: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + gradient-parser: 1.1.1 + highlight-words-core: 1.2.3 + is-plain-object: 5.0.0 + memize: 2.1.1 + path-to-regexp: 6.3.0 + re-resizable: 6.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-colorful: 5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-day-picker: 9.14.0(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + remove-accents: 0.5.0 + uuid: 9.0.1(patch_hash=87a713b75995ed86c0ecd0cadfd1b9f85092ca16fdff7132ff98b62fc3cf2db0) + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - supports-color + '@wordpress/components@33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -24155,6 +24804,21 @@ snapshots: - '@emotion/is-prop-valid' - supports-color + '@wordpress/compose@7.44.0(react@18.3.1)': + dependencies: + '@types/mousetrap': 1.6.15 + '@wordpress/deprecated': 4.46.0 + '@wordpress/dom': 4.46.0 + '@wordpress/element': 6.46.0 + '@wordpress/is-shallow-equal': 5.46.0 + '@wordpress/keycodes': 4.46.0 + '@wordpress/priority-queue': 3.46.0 + '@wordpress/undo-manager': 1.46.0 + change-case: 4.1.2 + mousetrap: 1.6.5 + react: 18.3.1 + use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/compose@7.46.0(react@18.3.1)': dependencies: '@types/mousetrap': 1.6.15 @@ -24273,6 +24937,24 @@ snapshots: '@wordpress/deprecated': 4.46.0 react: 18.3.1 + '@wordpress/data@10.44.0(react@18.3.1)': + dependencies: + '@wordpress/compose': 7.46.0(react@18.3.1) + '@wordpress/deprecated': 4.46.0 + '@wordpress/element': 6.46.0 + '@wordpress/is-shallow-equal': 5.46.0 + '@wordpress/priority-queue': 3.46.0 + '@wordpress/private-apis': 1.46.0 + '@wordpress/redux-routine': 5.46.0(redux@5.0.1) + deepmerge: 4.3.1 + equivalent-key-map: 0.2.2 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + react: 18.3.1 + redux: 5.0.1 + rememo: 4.0.2 + use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/data@10.46.0(react@18.3.1)': dependencies: '@wordpress/compose': 7.46.0(react@18.3.1) @@ -24291,6 +24973,57 @@ snapshots: rememo: 4.0.2 use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/dataviews@14.0.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@date-fns/tz': 1.4.1 + '@wordpress/base-styles': 6.20.0 + '@wordpress/components': 32.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/compose': 7.46.0(react@18.3.1) + '@wordpress/data': 10.46.0(react@18.3.1) + '@wordpress/deprecated': 4.46.0 + '@wordpress/element': 6.46.0 + '@wordpress/i18n': 6.19.0 + '@wordpress/icons': 12.1.0(react@18.3.1) + '@wordpress/keycodes': 4.46.0 + '@wordpress/primitives': 4.46.0(react@18.3.1) + '@wordpress/private-apis': 1.46.0 + '@wordpress/ui': 0.10.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/warning': 3.46.0 + clsx: 2.1.1 + react: 18.3.1 + remove-accents: 0.5.0 + optionalDependencies: + '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@emotion/cache': 11.14.0 + '@emotion/css': 11.13.5 + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@emotion/utils': 1.4.2 + '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/date': 5.46.0 + '@wordpress/hooks': 4.46.0 + change-case: 4.1.2 + colord: 2.9.3 + date-fns: 4.1.0 + deepmerge: 4.3.1 + fast-deep-equal: 3.1.3 + framer-motion: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + highlight-words-core: 1.2.3 + is-plain-object: 5.0.0 + memize: 2.1.1 + react-colorful: 5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-day-picker: 9.14.0(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + use-memo-one: 1.1.3(react@18.3.1) + uuid: 9.0.1(patch_hash=87a713b75995ed86c0ecd0cadfd1b9f85092ca16fdff7132ff98b62fc3cf2db0) + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - stylelint + - supports-color + '@wordpress/dataviews@14.3.0(@types/react@18.3.28)(react@18.3.1)': dependencies: '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -24764,6 +25497,16 @@ snapshots: - stylelint - supports-color + '@wordpress/element@6.44.0': + dependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@wordpress/escape-html': 3.46.0 + change-case: 4.1.2 + is-plain-object: 5.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@wordpress/element@6.46.0': dependencies: '@types/react': 18.3.28 @@ -25067,6 +25810,14 @@ snapshots: '@wordpress/html-entities@4.46.0': {} + '@wordpress/i18n@6.17.0': + dependencies: + '@tannin/sprintf': 1.3.3 + '@wordpress/hooks': 4.46.0 + gettext-parser: 1.4.0 + memize: 2.1.1 + tannin: 1.2.0 + '@wordpress/i18n@6.19.0': dependencies: '@tannin/sprintf': 1.3.3 @@ -25082,6 +25833,20 @@ snapshots: '@wordpress/primitives': 4.46.0(react@18.3.1) react: 18.3.1 + '@wordpress/icons@12.1.0(react@18.3.1)': + dependencies: + '@wordpress/element': 6.46.0 + '@wordpress/primitives': 4.46.0(react@18.3.1) + change-case: 4.1.2 + react: 18.3.1 + + '@wordpress/icons@12.2.0(react@18.3.1)': + dependencies: + '@wordpress/element': 6.46.0 + '@wordpress/primitives': 4.46.0(react@18.3.1) + change-case: 4.1.2 + react: 18.3.1 + '@wordpress/icons@13.1.0(react@18.3.1)': dependencies: '@wordpress/element': 6.46.0 @@ -25775,6 +26540,15 @@ snapshots: y-protocols: 1.0.7(yjs@13.6.29) yjs: 13.6.29 + '@wordpress/theme@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@wordpress/element': 6.46.0 + '@wordpress/private-apis': 1.46.0 + colorjs.io: 0.6.1 + memize: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/element': 6.46.0 @@ -25799,6 +26573,28 @@ snapshots: '@wordpress/token-list@3.46.0': {} + '@wordpress/ui@0.10.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@date-fns/tz': 1.4.1 + '@wordpress/a11y': 4.46.0 + '@wordpress/compose': 7.46.0(react@18.3.1) + '@wordpress/element': 6.46.0 + '@wordpress/i18n': 6.19.0 + '@wordpress/icons': 12.1.0(react@18.3.1) + '@wordpress/keycodes': 4.46.0 + '@wordpress/primitives': 4.46.0(react@18.3.1) + '@wordpress/private-apis': 1.46.0 + '@wordpress/theme': 0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: 2.1.1 + date-fns: 4.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.4.0 + transitivePeerDependencies: + - '@types/react' + - stylelint + '@wordpress/ui@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -25867,6 +26663,10 @@ snapshots: - '@emotion/is-prop-valid' - supports-color + '@wordpress/url@4.44.0': + dependencies: + remove-accents: 0.5.0 + '@wordpress/url@4.46.0': dependencies: remove-accents: 0.5.0 @@ -26320,6 +27120,19 @@ snapshots: b4a@1.8.1: {} + babel-jest@30.3.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 8.0.0 + babel-preset-jest: 30.3.0(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-jest@30.4.1(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -26349,6 +27162,10 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-jest-hoist@30.3.0: + dependencies: + '@types/babel__core': 7.20.5 + babel-plugin-jest-hoist@30.4.0: dependencies: '@types/babel__core': 7.20.5 @@ -26428,6 +27245,12 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + babel-preset-jest@30.3.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + babel-preset-jest@30.4.0(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -27285,6 +28108,8 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 + date-fns@3.6.0: {} + date-fns@4.1.0: {} debounce@1.2.1: {} @@ -28187,6 +29012,15 @@ snapshots: expect-type@1.3.0: {} + expect@30.3.0: + dependencies: + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + expect@30.4.1: dependencies: '@jest/expect-utils': 30.4.1 @@ -29237,12 +30071,44 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jest-changed-files@30.3.0: + dependencies: + execa: 5.1.1 + jest-util: 30.3.0 + p-limit: 3.1.0 + jest-changed-files@30.4.1: dependencies: execa: 5.1.1 jest-util: 30.4.1 p-limit: 3.1.0 + jest-circus@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2 + is-generator-fn: 2.1.0 + jest-each: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + p-limit: 3.1.0 + pretty-format: 30.3.0 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-circus@30.4.2: dependencies: '@jest/environment': 30.4.1 @@ -29269,6 +30135,25 @@ snapshots: - babel-plugin-macros - supports-color + jest-cli@30.3.0(@types/node@24.12.3): + dependencies: + '@jest/core': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.3.0(@types/node@24.12.3) + jest-util: 30.3.0 + jest-validate: 30.3.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jest-cli@30.4.2: dependencies: '@jest/core': 30.4.2 @@ -29307,6 +30192,37 @@ snapshots: - supports-color - ts-node + jest-config@30.3.0(@types/node@24.12.3): + dependencies: + '@babel/core': 7.29.0 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 4.4.0 + deepmerge: 4.3.1 + glob: 13.0.6 + graceful-fs: 4.2.11 + jest-circus: 30.3.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-runner: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + parse-json: 5.2.0 + pretty-format: 30.3.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.12.3 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@30.4.2: dependencies: '@babel/core': 7.29.0 @@ -29367,6 +30283,13 @@ snapshots: - babel-plugin-macros - supports-color + jest-diff@30.3.0: + dependencies: + '@jest/diff-sequences': 30.3.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.3.0 + jest-diff@30.4.1: dependencies: '@jest/diff-sequences': 30.4.0 @@ -29374,10 +30297,22 @@ snapshots: chalk: 4.1.2 pretty-format: 30.4.1 + jest-docblock@30.2.0: + dependencies: + detect-newline: 3.1.0 + jest-docblock@30.4.0: dependencies: detect-newline: 3.1.0 + jest-each@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + jest-util: 30.3.0 + pretty-format: 30.3.0 + jest-each@30.4.1: dependencies: '@jest/get-type': 30.1.0 @@ -29386,6 +30321,16 @@ snapshots: jest-util: 30.4.1 pretty-format: 30.4.1 + jest-environment-node@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + jest-mock: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + jest-environment-node@30.4.1: dependencies: '@jest/environment': 30.4.1 @@ -29403,6 +30348,21 @@ snapshots: optionalDependencies: jest: 30.4.2 + jest-haste-map@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + jest-worker: 30.3.0 + picomatch: 4.0.4 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + jest-haste-map@30.4.1: dependencies: '@jest/types': 30.4.1 @@ -29418,11 +30378,23 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + jest-leak-detector@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.3.0 + jest-leak-detector@30.4.1: dependencies: '@jest/get-type': 30.1.0 pretty-format: 30.4.1 + jest-matcher-utils@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.3.0 + pretty-format: 30.3.0 + jest-matcher-utils@30.4.1: dependencies: '@jest/get-type': 30.1.0 @@ -29430,6 +30402,18 @@ snapshots: jest-diff: 30.4.1 pretty-format: 30.4.1 + jest-message-util@30.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 30.3.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + pretty-format: 30.3.0 + slash: 3.0.0 + stack-utils: 2.0.6 + jest-message-util@30.4.1: dependencies: '@babel/code-frame': 7.29.0 @@ -29443,18 +30427,37 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 + jest-mock@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + jest-util: 30.3.0 + jest-mock@30.4.1: dependencies: '@jest/types': 30.4.1 '@types/node': 24.12.3 jest-util: 30.4.1 + jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): + optionalDependencies: + jest-resolve: 30.3.0 + jest-pnp-resolver@1.2.3(jest-resolve@30.4.1): optionalDependencies: jest-resolve: 30.4.1 + jest-regex-util@30.0.1: {} + jest-regex-util@30.4.0: {} + jest-resolve-dependencies@30.3.0: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.3.0 + transitivePeerDependencies: + - supports-color + jest-resolve-dependencies@30.4.2: dependencies: jest-regex-util: 30.4.0 @@ -29462,6 +30465,17 @@ snapshots: transitivePeerDependencies: - supports-color + jest-resolve@30.3.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0) + jest-util: 30.3.0 + jest-validate: 30.3.0 + slash: 3.0.0 + unrs-resolver: 1.11.1 + jest-resolve@30.4.1: dependencies: chalk: 4.1.2 @@ -29473,6 +30487,33 @@ snapshots: slash: 3.0.0 unrs-resolver: 1.11.1 + jest-runner@30.3.0: + dependencies: + '@jest/console': 30.3.0 + '@jest/environment': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-haste-map: 30.3.0 + jest-leak-detector: 30.3.0 + jest-message-util: 30.3.0 + jest-resolve: 30.3.0 + jest-runtime: 30.3.0 + jest-util: 30.3.0 + jest-watcher: 30.3.0 + jest-worker: 30.3.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + jest-runner@30.4.2: dependencies: '@jest/console': 30.4.1 @@ -29500,6 +30541,33 @@ snapshots: transitivePeerDependencies: - supports-color + jest-runtime@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/globals': 30.3.0 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + chalk: 4.1.2 + cjs-module-lexer: 2.2.0 + collect-v8-coverage: 1.0.3 + glob: 13.0.6 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + jest-runtime@30.4.2: dependencies: '@jest/environment': 30.4.1 @@ -29527,6 +30595,32 @@ snapshots: transitivePeerDependencies: - supports-color + jest-snapshot@30.3.0: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + chalk: 4.1.2 + expect: 30.3.0 + graceful-fs: 4.2.11 + jest-diff: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + pretty-format: 30.3.0 + semver: 7.7.3 + synckit: 0.11.12 + transitivePeerDependencies: + - supports-color + jest-snapshot@30.4.1: dependencies: '@babel/core': 7.29.0 @@ -29553,6 +30647,15 @@ snapshots: transitivePeerDependencies: - supports-color + jest-util@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + jest-util@30.4.1: dependencies: '@jest/types': 30.4.1 @@ -29562,6 +30665,15 @@ snapshots: graceful-fs: 4.2.11 picomatch: 4.0.4 + jest-validate@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.3.0 + jest-validate@30.4.1: dependencies: '@jest/get-type': 30.1.0 @@ -29571,6 +30683,17 @@ snapshots: leven: 3.1.0 pretty-format: 30.4.1 + jest-watcher@30.3.0: + dependencies: + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.3 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.3.0 + string-length: 4.0.2 + jest-watcher@30.4.1: dependencies: '@jest/test-result': 30.4.1 @@ -29588,6 +30711,14 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest-worker@30.3.0: + dependencies: + '@types/node': 24.12.3 + '@ungap/structured-clone': 1.3.1 + jest-util: 30.3.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jest-worker@30.4.1: dependencies: '@types/node': 24.12.3 @@ -29596,6 +30727,19 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@30.3.0(@types/node@24.12.3): + dependencies: + '@jest/core': 30.3.0 + '@jest/types': 30.3.0 + import-local: 3.2.0 + jest-cli: 30.3.0(@types/node@24.12.3) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jest@30.4.2: dependencies: '@jest/core': 30.4.2 @@ -31422,6 +32566,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + pretty-format@30.4.1: dependencies: '@jest/schemas': 30.4.1 @@ -33625,6 +34775,8 @@ snapshots: uuid@8.3.2(patch_hash=87a713b75995ed86c0ecd0cadfd1b9f85092ca16fdff7132ff98b62fc3cf2db0): {} + uuid@9.0.1(patch_hash=87a713b75995ed86c0ecd0cadfd1b9f85092ca16fdff7132ff98b62fc3cf2db0): {} + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 diff --git a/projects/packages/seo/.gitignore b/projects/packages/seo/.gitignore new file mode 100644 index 000000000000..77b928272c4a --- /dev/null +++ b/projects/packages/seo/.gitignore @@ -0,0 +1,5 @@ +vendor/ +node_modules/ +build/ +.cache/ +wordpress diff --git a/projects/packages/seo/.phpcs.dir.xml b/projects/packages/seo/.phpcs.dir.xml new file mode 100644 index 000000000000..466f89239b46 --- /dev/null +++ b/projects/packages/seo/.phpcs.dir.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/packages/seo/CHANGELOG.md b/projects/packages/seo/CHANGELOG.md new file mode 100644 index 000000000000..073c86d86d23 --- /dev/null +++ b/projects/packages/seo/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0-alpha] - unreleased + +### Added + +- Initial package scaffold. Introduces `Automattic\Jetpack\SEO\Initializer` which registers the `admin.php?page=jetpack-seo` visibility command center on every site type via `Admin_Menu::add_menu()`. diff --git a/projects/packages/seo/README.md b/projects/packages/seo/README.md new file mode 100644 index 000000000000..296e089cc02c --- /dev/null +++ b/projects/packages/seo/README.md @@ -0,0 +1,32 @@ +# Jetpack SEO + +The visibility command center for WordPress sites in the agentic web — a unified wp-admin screen that consolidates SEO, sitemaps, AI discoverability, and site verification settings across all site types (self-hosted, Atomic/WoW, Simple). + +## What this package provides + +- A standalone wp-admin page registered at `admin.php?page=jetpack-seo` +- REST API under the `jetpack-seo/v1` namespace +- llms.txt generation and AI crawler management (free for all plans) +- Multi-surface SERP preview component shared between the admin screen, post-list popovers, and the block editor sidebar +- Expanded JSON-LD schema emitters (Article, Organization, FAQPage, HowTo, LocalBusiness) + +## Architecture + +Follows the pattern established by `projects/packages/my-jetpack/`: + +- **PHP:** `Automattic\Jetpack\SEO\Initializer` registers the admin menu via `Admin_Menu::add_menu()` and enqueues the built React bundle +- **React:** `_inc/admin.tsx` mounts a `HashRouter` SPA using `@wordpress/components`, `@wordpress/dataviews`, `@tanstack/react-query`, and `@automattic/social-previews` +- **Data:** TanStack Query wrappers (`useSimpleQuery` / `useSimpleMutation`) consume the REST API + +## Development + +```bash +# Build once +pnpm run build + +# Watch mode +pnpm run watch + +# Tests +pnpm run test +``` diff --git a/projects/packages/seo/_inc/admin.tsx b/projects/packages/seo/_inc/admin.tsx new file mode 100644 index 000000000000..2dcb548cce4b --- /dev/null +++ b/projects/packages/seo/_inc/admin.tsx @@ -0,0 +1,31 @@ +import { createRoot } from '@wordpress/element'; +import { createHashRouter, RouterProvider } from 'react-router'; +import { JetpackSeoRoutes } from './constants'; +import Providers from './providers'; +import OverviewScreen from './screens/overview'; +import Shell from './shell'; +import './style.module.scss'; + +// Data router (`createHashRouter` + `RouterProvider`) rather than declarative +// ``, so future screens can use `useBlocker` for unsaved-changes +// guards. +const router = createHashRouter( [ + { + element: , + children: [ + { path: JetpackSeoRoutes.Overview, element: }, + { path: '*', element: }, + ], + }, +] ); + +const App = () => ( + + + +); + +const container = document.getElementById( 'jetpack-seo-root' ); +if ( container ) { + createRoot( container ).render( ); +} diff --git a/projects/packages/seo/_inc/constants.ts b/projects/packages/seo/_inc/constants.ts new file mode 100644 index 000000000000..a61ff18d1f59 --- /dev/null +++ b/projects/packages/seo/_inc/constants.ts @@ -0,0 +1,5 @@ +export const JetpackSeoRoutes = { + Overview: '/', +} as const; + +export const REST_NAMESPACE = 'jetpack-seo/v1'; diff --git a/projects/packages/seo/_inc/data/overview-types.ts b/projects/packages/seo/_inc/data/overview-types.ts new file mode 100644 index 000000000000..386e53523715 --- /dev/null +++ b/projects/packages/seo/_inc/data/overview-types.ts @@ -0,0 +1,12 @@ +export interface OverviewResponse { + site_visibility: { + search_engines_visible: boolean; + sitemap_active: boolean; + sitemap_url: string; + seo_tools_active: boolean; + front_page_description: string; + }; + plan: { + seo_enabled_for_site: boolean; + }; +} diff --git a/projects/packages/seo/_inc/data/types.ts b/projects/packages/seo/_inc/data/types.ts new file mode 100644 index 000000000000..462080599c3e --- /dev/null +++ b/projects/packages/seo/_inc/data/types.ts @@ -0,0 +1,5 @@ +export interface WP_Error { + code: string; + message: string; + data?: { status?: number } & Record< string, unknown >; +} diff --git a/projects/packages/seo/_inc/data/use-overview.ts b/projects/packages/seo/_inc/data/use-overview.ts new file mode 100644 index 000000000000..fd865d94db2b --- /dev/null +++ b/projects/packages/seo/_inc/data/use-overview.ts @@ -0,0 +1,11 @@ +import { REST_NAMESPACE } from '../constants'; +import useSimpleQuery from './use-simple-query'; +import type { OverviewResponse } from './overview-types'; + +const useOverview = () => + useSimpleQuery< OverviewResponse >( { + name: 'jetpack-seo-overview', + query: { path: `/${ REST_NAMESPACE }/overview` }, + } ); + +export default useOverview; diff --git a/projects/packages/seo/_inc/data/use-simple-mutation.ts b/projects/packages/seo/_inc/data/use-simple-mutation.ts new file mode 100644 index 000000000000..22b995c113cb --- /dev/null +++ b/projects/packages/seo/_inc/data/use-simple-mutation.ts @@ -0,0 +1,46 @@ +/* eslint-disable jsdoc/require-returns, jsdoc/require-param */ + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import apiFetch from '@wordpress/api-fetch'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import type { APIFetchOptions } from '@wordpress/api-fetch'; + +type QueryParams< T, V > = { + name: string; + query: APIFetchOptions< true >; + invalidates?: string[]; + options?: Pick< UseMutationOptions< T, Error, V >, 'onSuccess' | 'onError' >; +}; + +/** + * Thin TanStack Query wrapper for wp-api write requests. + * + * `invalidates` lists query names to invalidate on success — e.g. mutating + * /settings should invalidate the cached /overview response so the home + * base re-renders with fresh numbers. + */ +const useSimpleMutation = < T = unknown, V = unknown >( { + name, + query, + invalidates = [], + options, +}: QueryParams< T, V > ) => { + const queryClient = useQueryClient(); + return useMutation< T, Error, V >( { + mutationKey: [ name ], + mutationFn: ( variables: V ) => + apiFetch< T >( { + ...query, + data: ( variables ?? undefined ) as unknown as Record< string, unknown >, + } ), + onSuccess: ( data, variables, context ) => { + invalidates.forEach( key => { + queryClient.invalidateQueries( { queryKey: [ key ] } ); + } ); + options?.onSuccess?.( data, variables, context ); + }, + onError: options?.onError, + } ); +}; + +export default useSimpleMutation; diff --git a/projects/packages/seo/_inc/data/use-simple-query.ts b/projects/packages/seo/_inc/data/use-simple-query.ts new file mode 100644 index 000000000000..8f941592bb88 --- /dev/null +++ b/projects/packages/seo/_inc/data/use-simple-query.ts @@ -0,0 +1,28 @@ +/* eslint-disable jsdoc/require-returns, jsdoc/require-param */ + +import { useQuery } from '@tanstack/react-query'; +import apiFetch from '@wordpress/api-fetch'; +import type { WP_Error } from './types'; +import type { UseQueryOptions } from '@tanstack/react-query'; +import type { APIFetchOptions } from '@wordpress/api-fetch'; + +type QueryParams< T > = { + name: string; + query: APIFetchOptions< true >; + options?: Pick< UseQueryOptions< T, WP_Error >, 'enabled' | 'gcTime' | 'refetchOnMount' >; +}; + +/** + * Thin TanStack Query wrapper for wp-api GET requests. Keeps the Overview, + * Content, and Discoverability screens free of repetitive fetching boilerplate. + */ +const useSimpleQuery = < T >( { name, query, options }: QueryParams< T > ) => + useQuery< T, WP_Error >( { + queryKey: [ name, query ], + queryFn: () => apiFetch< T >( query ), + refetchOnWindowFocus: false, + refetchIntervalInBackground: false, + ...options, + } ); + +export default useSimpleQuery; diff --git a/projects/packages/seo/_inc/header-actions-context.tsx b/projects/packages/seo/_inc/header-actions-context.tsx new file mode 100644 index 000000000000..7f4b857c525d --- /dev/null +++ b/projects/packages/seo/_inc/header-actions-context.tsx @@ -0,0 +1,45 @@ +/* eslint-disable jsdoc/require-returns, jsdoc/require-param, jsdoc/escape-inline-tags */ + +import { createContext, useCallback, useContext, useState } from 'react'; +import type { FC, ReactNode } from 'react'; + +interface HeaderActionsContextValue { + actions: ReactNode; + setActions: ( actions: ReactNode ) => void; +} + +const HeaderActionsContext = createContext< HeaderActionsContextValue >( { + actions: null, + setActions: () => {}, +} ); + +/** + * Provider owned by Shell. Lets routed screens inject buttons into the + * AdminPage header's `actions` slot without Shell knowing about them ahead + * of time. Mirrors the Slot/Fill pattern @wordpress/components uses for the + * plugin editor sidebars, but implemented as React context for simplicity. + */ +export const HeaderActionsProvider: FC< { children: ReactNode } > = ( { children } ) => { + const [ actions, setActions ] = useState< ReactNode >( null ); + return ( + + { children } + + ); +}; + +/** + * Read the actions Shell should render in AdminPage's header. + */ +export const useHeaderActions = (): ReactNode => useContext( HeaderActionsContext ).actions; + +/** + * Register a render function for the header actions slot. Pass `null` (or + * omit this hook) to clear the slot. Accepts a stable setter function — if + * you call `setHeaderActions` with a ReactNode that changes identity each + * render, wrap in `useMemo` to avoid churn. + */ +export const useSetHeaderActions = (): ( ( actions: ReactNode ) => void ) => { + const { setActions } = useContext( HeaderActionsContext ); + return useCallback( ( next: ReactNode ) => setActions( next ), [ setActions ] ); +}; diff --git a/projects/packages/seo/_inc/providers.tsx b/projects/packages/seo/_inc/providers.tsx new file mode 100644 index 000000000000..7eda5b7069d9 --- /dev/null +++ b/projects/packages/seo/_inc/providers.tsx @@ -0,0 +1,24 @@ +import { ThemeProvider } from '@automattic/jetpack-components'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { FC, ReactNode } from 'react'; + +interface ProvidersProps { + children: ReactNode; +} + +const queryClient = new QueryClient( { + defaultOptions: { + queries: { + staleTime: 30_000, + refetchOnWindowFocus: false, + }, + }, +} ); + +const Providers: FC< ProvidersProps > = ( { children } ) => ( + + { children } + +); + +export default Providers; diff --git a/projects/packages/seo/_inc/screens/overview/index.tsx b/projects/packages/seo/_inc/screens/overview/index.tsx new file mode 100644 index 000000000000..e74a441a2f2a --- /dev/null +++ b/projects/packages/seo/_inc/screens/overview/index.tsx @@ -0,0 +1,45 @@ +import { Notice, Spinner } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import useOverview from '../../data/use-overview'; +import SiteVisibilityCard from './site-visibility-card'; +import styles from './style.module.scss'; +import type { FC } from 'react'; + +const OverviewScreen: FC = () => { + const { data, isLoading, isError, error } = useOverview(); + + if ( isLoading ) { + return ( +
+ + { __( 'Loading site visibility…', 'jetpack-seo' ) } +
+ ); + } + + if ( isError || ! data ) { + return ( + + { error?.message ?? __( 'Unable to load overview.', 'jetpack-seo' ) } + + ); + } + + return ( + <> + { ! data.plan.seo_enabled_for_site && ( + + { __( + 'SEO tools are not enabled on this site. Some cards reflect the underlying WordPress options only.', + 'jetpack-seo' + ) } + + ) } +
+ +
+ + ); +}; + +export default OverviewScreen; diff --git a/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx b/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx new file mode 100644 index 000000000000..dcd317c0397d --- /dev/null +++ b/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx @@ -0,0 +1,63 @@ +import { Button, ExternalLink } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { Card } from '@wordpress/ui'; +import { NavLink } from 'react-router'; +import { JetpackSeoRoutes } from '../../constants'; +import StatusDot from './status-dot'; +import styles from './style.module.scss'; +import type { OverviewResponse } from '../../data/overview-types'; +import type { FC } from 'react'; + +interface Props { + data: OverviewResponse[ 'site_visibility' ]; +} + +const SiteVisibilityCard: FC< Props > = ( { data } ) => { + const visibilityStatus = data.search_engines_visible ? 'ok' : 'err'; + const visibilityLabel = data.search_engines_visible + ? __( 'Search engines allowed', 'jetpack-seo' ) + : __( 'Search engines blocked', 'jetpack-seo' ); + + return ( + + + { __( 'Site visibility', 'jetpack-seo' ) } + + +
+ +
+
+ + { data.sitemap_active && ( + { __( 'View', 'jetpack-seo' ) } + ) } +
+
+ +
+
+ +
+
+
+ ); +}; + +export default SiteVisibilityCard; diff --git a/projects/packages/seo/_inc/screens/overview/status-dot.tsx b/projects/packages/seo/_inc/screens/overview/status-dot.tsx new file mode 100644 index 000000000000..723f8b0108b8 --- /dev/null +++ b/projects/packages/seo/_inc/screens/overview/status-dot.tsx @@ -0,0 +1,24 @@ +import clsx from 'clsx'; +import styles from './style.module.scss'; +import type { FC } from 'react'; + +interface Props { + status: 'ok' | 'warn' | 'err'; + label?: string; +} + +const StatusDot: FC< Props > = ( { status, label } ) => ( + + +); + +export default StatusDot; diff --git a/projects/packages/seo/_inc/screens/overview/style.module.scss b/projects/packages/seo/_inc/screens/overview/style.module.scss new file mode 100644 index 000000000000..ba3312b5e9e3 --- /dev/null +++ b/projects/packages/seo/_inc/screens/overview/style.module.scss @@ -0,0 +1,76 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 16px; + align-items: stretch; +} + +// Make each Overview card a flex column so its body stretches and its footer +// (the card's action button) can be pinned to the bottom of the card via +// `margin-top: auto`. Combined with the grid's default stretch alignment, +// every card ends up the same height and every button sits on the same row. +// We target the last child of each Card.Root (which is Card.Content) since +// @wordpress/ui uses hashed CSS-module class names that aren't stable. +.grid > * { + display: flex; + flex-direction: column; + height: 100%; +} + +.grid > * > *:last-child { + display: flex; + flex-direction: column; + flex: 1 1 auto; +} + +.statRow { + display: flex; + justify-content: space-between; + padding: 4px 0; + + & + & { + border-top: 1px solid #f0f0f0; + } +} + +.statValue { + font-variant-numeric: tabular-nums; + font-weight: 600; +} + +.statusDot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; + vertical-align: middle; +} + +.statusOk { + background: #4ab866; +} + +.statusWarn { + background: #dba617; +} + +.statusErr { + background: #cc1818; +} + +// `margin-top: auto` pushes the footer row to the bottom of its flex column +// parent — the Card.Content. Every Overview card ends up with its button +// aligned horizontally across the row. +.cardFooter { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: auto; + padding-top: 16px; +} + +.loading { + padding: 16px 0; + color: #757575; +} diff --git a/projects/packages/seo/_inc/shell.tsx b/projects/packages/seo/_inc/shell.tsx new file mode 100644 index 000000000000..7a11f22315ab --- /dev/null +++ b/projects/packages/seo/_inc/shell.tsx @@ -0,0 +1,50 @@ +/* eslint-disable jsdoc/require-returns */ + +import { AdminPage } from '@automattic/jetpack-components'; +import { __ } from '@wordpress/i18n'; +import { Outlet } from 'react-router'; +import { HeaderActionsProvider, useHeaderActions } from './header-actions-context'; +import styles from './style.module.scss'; +import type { FC } from 'react'; + +/** + * The chrome inside the HeaderActionsProvider. Split so the AdminPage + * render can read header actions from context. + */ +const ShellChrome: FC = () => { + const headerActions = useHeaderActions(); + + return ( + +
+ +
+
+ ); +}; + +/** + * Top-level chrome for the Jetpack SEO admin page. + * + * Header: `AdminPage` from `@automattic/jetpack-components` (shared with + * Boost, Social, Protect, Backup, Forms). + * + * Routed screens inject header buttons (Save, etc.) via the + * `HeaderActionsProvider` — see `header-actions-context.tsx`. + */ +const Shell: FC = () => ( + + + +); + +export default Shell; diff --git a/projects/packages/seo/_inc/style.module.scss b/projects/packages/seo/_inc/style.module.scss new file mode 100644 index 000000000000..137fb3c969e0 --- /dev/null +++ b/projects/packages/seo/_inc/style.module.scss @@ -0,0 +1,47 @@ +.placeholder { + padding: 48px; + text-align: center; + color: #757575; +} + +// Override AdminPage's default `--jp-white` background with the admin-ui +// neutral-surface token (#fcfcfc). Same approach as +// `_inc/client/components/settings-nav-tabs/style.scss` — double-class the +// selector for specificity over AdminPage's own `.background` rule. +.neutralBg.neutralBg { + + &:global(.background), + :global(.admin-ui-page), + :global(.admin-ui-page__content) { + background-color: var(--wpds-color-bg-surface-neutral, #fcfcfc); + } +} + +// Padding around the routed content on Overview and Settings. The admin-ui +// Page renders children unwrapped (no `.admin-ui-page__content` exists unless +// `hasPadding` is passed, which jetpack-components' AdminPage doesn't do), so +// Shell wraps the Outlet in this container and we pad it here. +.paddedContent { + padding: var(--wpds-dimension-padding-2xl, 24px); +} + +// Content tab hosts a full-height DataViews. No extra padding so it can fill +// the region edge-to-edge. +.fullBleed { + padding: 0; +} + +// The admin-ui `Page` component renders the tabs prop between the header and +// the content container (not inside the header like Forms' own Page does). +// Wrap them so they visually continue the header: same white background, same +// horizontal padding (matching admin-ui's `--wpds-dimension-padding-2xl` = 24px +// on `.admin-ui-page__header`), and a soft divider at the bottom. This keeps +// the tabs visually anchored to the header and makes their starting edge line +// up with the page title. +.tabsBar { + background: var(--wpds-color-bg-surface-neutral-strong, #fff); + padding-inline: var(--wpds-dimension-padding-2xl, 24px); + border-bottom: + var(--wpds-border-width-xs, 1px) solid + var(--wpds-color-stroke-surface-neutral-weak, #e4e4e4); +} diff --git a/projects/packages/seo/_inc/types.ts b/projects/packages/seo/_inc/types.ts new file mode 100644 index 000000000000..b84641898720 --- /dev/null +++ b/projects/packages/seo/_inc/types.ts @@ -0,0 +1,14 @@ +export interface JetpackSeoInitialState { + adminUrl: string; + seoAdminUrl: string; + siteUrl: string; + siteSuffix: string; + blogId: number | null; + isSiteConnected: boolean; + isUserConnected: boolean; + isAtomic: boolean; + isSimple: boolean; + userIsAdmin: boolean; + seoEnabled: boolean; + packageVersion: string; +} diff --git a/projects/packages/seo/babel.config.js b/projects/packages/seo/babel.config.js new file mode 100644 index 000000000000..4d19ae7042d2 --- /dev/null +++ b/projects/packages/seo/babel.config.js @@ -0,0 +1,10 @@ +const config = { + presets: [ + [ + '@automattic/jetpack-webpack-config/babel/preset', + { pluginReplaceTextdomain: { textdomain: 'jetpack-seo' } }, + ], + ], +}; + +module.exports = config; diff --git a/projects/packages/seo/changelog/add-initial-scaffold b/projects/packages/seo/changelog/add-initial-scaffold new file mode 100644 index 000000000000..182b13b9e50b --- /dev/null +++ b/projects/packages/seo/changelog/add-initial-scaffold @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Initial package scaffold. Adds the Jetpack SEO admin page at admin.php?page=jetpack-seo with an Overview screen and a Site visibility card. Foundation for additional tabs and cards in follow-up PRs. diff --git a/projects/packages/seo/composer.json b/projects/packages/seo/composer.json new file mode 100644 index 000000000000..e06114f2748a --- /dev/null +++ b/projects/packages/seo/composer.json @@ -0,0 +1,79 @@ +{ + "name": "automattic/jetpack-seo", + "description": "Jetpack SEO — the visibility command center for WordPress sites in the agentic web.", + "type": "jetpack-library", + "license": "GPL-2.0-or-later", + "require": { + "php": ">=7.2", + "automattic/jetpack-admin-ui": "@dev", + "automattic/jetpack-assets": "@dev", + "automattic/jetpack-connection": "@dev", + "automattic/jetpack-constants": "@dev", + "automattic/jetpack-status": "@dev" + }, + "require-dev": { + "yoast/phpunit-polyfills": "^4.0.0", + "automattic/jetpack-changelogger": "@dev", + "automattic/jetpack-test-environment": "@dev", + "automattic/phpunit-select-config": "@dev" + }, + "suggest": { + "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "scripts": { + "phpunit": [ + "phpunit-select-config phpunit.#.xml.dist --colors=always" + ], + "test-php": [ + "@composer phpunit" + ], + "test-js": [ + "pnpm run test" + ], + "build-development": [ + "pnpm run build" + ], + "build-production": [ + "NODE_ENV=production pnpm run build" + ], + "watch": [ + "Composer\\Config::disableProcessTimeout", + "pnpm run watch" + ] + }, + "repositories": [ + { + "type": "path", + "url": "../../packages/*", + "options": { + "monorepo": true + } + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "extra": { + "autotagger": true, + "mirror-repo": "Automattic/jetpack-seo", + "textdomain": "jetpack-seo", + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-seo/compare/${old}...${new}" + }, + "branch-alias": { + "dev-trunk": "0.1.x-dev" + }, + "version-constants": { + "::PACKAGE_VERSION": "src/class-initializer.php" + } + }, + "config": { + "allow-plugins": { + "roots/wordpress-core-installer": true + } + } +} diff --git a/projects/packages/seo/global.d.ts b/projects/packages/seo/global.d.ts new file mode 100644 index 000000000000..8ad674407036 --- /dev/null +++ b/projects/packages/seo/global.d.ts @@ -0,0 +1,11 @@ +declare module '*.module.scss'; +declare module '*.scss'; +declare module '*.svg'; + +interface Window { + jetpackSeoInitialState?: import('./_inc/types').JetpackSeoInitialState; + jetpackSeoRest?: { + apiRoot: string; + apiNonce: string; + }; +} diff --git a/projects/packages/seo/package.json b/projects/packages/seo/package.json new file mode 100644 index 000000000000..2f3d780a9e53 --- /dev/null +++ b/projects/packages/seo/package.json @@ -0,0 +1,69 @@ +{ + "name": "@automattic/jetpack-seo", + "version": "0.1.0-alpha", + "private": true, + "description": "Jetpack SEO — the visibility command center for WordPress sites in the agentic web.", + "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/seo/#readme", + "bugs": { + "url": "https://github.com/Automattic/jetpack/labels/[Package] SEO" + }, + "repository": { + "type": "git", + "url": "https://github.com/Automattic/jetpack.git", + "directory": "projects/packages/seo" + }, + "license": "GPL-2.0-or-later", + "author": "Automattic", + "sideEffects": [ + "*.css", + "*.scss" + ], + "scripts": { + "build": "pnpm run clean && pnpm run build-client", + "build-client": "pnpm webpack --config webpack.config.js", + "clean": "rm -rf build/", + "typecheck": "tsgo --noEmit", + "watch": "pnpm run build && pnpm webpack watch" + }, + "dependencies": { + "@automattic/babel-plugin-replace-textdomain": "workspace:*", + "@automattic/jetpack-base-styles": "workspace:*", + "@automattic/jetpack-components": "workspace:*", + "@automattic/jetpack-connection": "workspace:*", + "@automattic/jetpack-script-data": "workspace:*", + "@automattic/social-previews": "workspace:*", + "@tanstack/react-query": "5.90.8", + "@wordpress/api-fetch": "7.44.0", + "@wordpress/components": "32.6.0", + "@wordpress/compose": "7.44.0", + "@wordpress/data": "10.44.0", + "@wordpress/dataviews": "14.0.0", + "@wordpress/element": "6.44.0", + "@wordpress/i18n": "6.17.0", + "@wordpress/icons": "12.1.0", + "@wordpress/ui": "0.10.0", + "@wordpress/url": "4.44.0", + "clsx": "2.1.1", + "react-router": "7.12.0" + }, + "devDependencies": { + "@automattic/jetpack-webpack-config": "workspace:*", + "@babel/core": "7.29.0", + "@babel/preset-env": "7.29.2", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", + "@types/jest": "30.0.0", + "@types/node": "^24.12.0", + "@types/react": "18.3.28", + "@typescript/native-preview": "7.0.0-dev.20260225.1", + "jest": "30.3.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "sass-embedded": "1.97.3", + "sass-loader": "16.0.5", + "typescript": "5.9.3", + "webpack": "5.105.2", + "webpack-cli": "6.0.1" + } +} diff --git a/projects/packages/seo/phpunit.11.xml.dist b/projects/packages/seo/phpunit.11.xml.dist new file mode 100644 index 000000000000..24f0f1e361af --- /dev/null +++ b/projects/packages/seo/phpunit.11.xml.dist @@ -0,0 +1,34 @@ + + + + + tests/php + + + + + + src + + + + + diff --git a/projects/packages/seo/src/class-initializer.php b/projects/packages/seo/src/class-initializer.php new file mode 100644 index 000000000000..e9db1147427e --- /dev/null +++ b/projects/packages/seo/src/class-initializer.php @@ -0,0 +1,240 @@ + Settings is hidden. + * + * Gated on the `seo-tools` Jetpack module being active — if the user + * has the module turned off the menu item disappears entirely. + * + * @return void + */ + public static function add_menu_item() { + if ( ! self::is_seo_tools_module_active() ) { + return; + } + + $page_suffix = Admin_Menu::add_menu( + __( 'SEO', 'jetpack-seo' ), + __( 'SEO', 'jetpack-seo' ), + 'manage_options', + self::MENU_SLUG, + array( __CLASS__, 'admin_page' ), + 2 + ); + + if ( $page_suffix ) { + add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) ); + } + } + + /** + * Whether the `seo-tools` Jetpack module is currently active. + * + * @return bool + */ + private static function is_seo_tools_module_active() { + if ( ! class_exists( 'Automattic\\Jetpack\\Modules' ) ) { + return false; + } + return ( new Modules() )->is_active( 'seo-tools' ); + } + + /** + * Runs on `load-{$page_suffix}` — right before the admin page renders. + * + * @return void + */ + public static function admin_init() { + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) ); + } + + /** + * Render the admin page container. + * + * The React app mounts into `#jetpack-seo-root`. + * + * @return void + */ + public static function admin_page() { + echo '
'; + } + + /** + * Enqueue the built JS bundle and localize the initial state. + * + * @return void + */ + public static function enqueue_scripts() { + Assets::register_script( + 'jetpack-seo-app', + '../build/index.js', + __FILE__, + array( + 'enqueue' => true, + 'in_footer' => true, + 'textdomain' => 'jetpack-seo', + ) + ); + + wp_localize_script( + 'jetpack-seo-app', + 'jetpackSeoInitialState', + self::get_initial_state() + ); + + wp_localize_script( + 'jetpack-seo-app', + 'jetpackSeoRest', + array( + 'apiRoot' => esc_url_raw( rest_url() ), + 'apiNonce' => wp_create_nonce( 'wp_rest' ), + ) + ); + + Connection_Initial_State::render_script( 'jetpack-seo-app' ); + } + + /** + * Build the initial state passed to the React app. + * + * Kept intentionally small — the app reads live data via REST. Only + * values that affect the first paint live here. + * + * @return array + */ + private static function get_initial_state() { + $connection = new Connection_Manager(); + $status = new Status(); + + return array( + 'adminUrl' => esc_url( admin_url() ), + 'seoAdminUrl' => admin_url( 'admin.php?page=' . self::MENU_SLUG ), + 'siteUrl' => esc_url( get_site_url() ), + 'siteSuffix' => $status->get_site_suffix(), + 'blogId' => Connection_Manager::get_site_id( true ), + 'isSiteConnected' => $connection->is_connected(), + 'isUserConnected' => $connection->is_user_connected(), + 'isAtomic' => ( new Status_Host() )->is_woa_site(), + 'isSimple' => ( new Status_Host() )->is_wpcom_simple(), + 'userIsAdmin' => current_user_can( 'manage_options' ), + 'seoEnabled' => self::is_seo_enabled(), + 'packageVersion' => self::PACKAGE_VERSION, + ); + } + + /** + * Whether Jetpack's seo-tools module is enabled on this site. + * + * @return bool + */ + private static function is_seo_enabled() { + if ( class_exists( 'Jetpack_SEO_Utils' ) ) { + return (bool) Jetpack_SEO_Utils::is_enabled_jetpack_seo(); + } + return false; + } + + /** + * Register REST endpoints for the jetpack-seo/v1 namespace. + * + * @return void + */ + public static function register_rest_endpoints() { + REST_Controller::register_routes(); + } + + /** + * 301-redirect legacy SEO surfaces to the unified screen. + * + * The old routes are: + * - admin.php?page=jetpack#/traffic + * - admin.php?page=jetpack&tab=seo + * + * Only hash fragments arrive client-side, so the hash-based redirect + * happens from a tiny script enqueued on the main Jetpack settings + * page — the server-side path handles the ?tab= variant. + * + * @return void + */ + public static function maybe_redirect_legacy_surfaces() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; + if ( 'jetpack' === $page && in_array( $tab, array( 'seo', 'traffic' ), true ) ) { + wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG ), 301 ); + exit; + } + } +} diff --git a/projects/packages/seo/src/class-rest-controller.php b/projects/packages/seo/src/class-rest-controller.php new file mode 100644 index 000000000000..b94e9cebf2ec --- /dev/null +++ b/projects/packages/seo/src/class-rest-controller.php @@ -0,0 +1,88 @@ + WP_REST_Server::READABLE, + 'callback' => array( __CLASS__, 'get_overview' ), + 'permission_callback' => array( __CLASS__, 'permissions_check' ), + ) + ); + } + + /** + * Default permission gate: site administrators only. + * + * @return bool + */ + public static function permissions_check() { + return current_user_can( 'manage_options' ); + } + + /** + * GET /jetpack-seo/v1/overview. + * + * Returns the aggregated state the Overview screen renders. Kept in a + * single endpoint so the dashboard loads with one request. Later PRs + * extend the response shape with additional cards (content health, + * AI discoverability, site verification). + * + * @param WP_REST_Request $request REST request. + * @return WP_REST_Response + */ + public static function get_overview( WP_REST_Request $request ) { + unset( $request ); + + $modules = new Modules(); + $seo_enabled = class_exists( 'Jetpack_SEO_Utils' ) && Jetpack_SEO_Utils::is_enabled_jetpack_seo(); + $seo_tools_active = $modules->is_active( 'seo-tools' ); + $sitemaps_active = (bool) get_option( 'jetpack_seo_sitemap_enabled', false ); + $front_page_desc = $seo_enabled ? Jetpack_SEO_Utils::get_front_page_meta_description() : ''; + + return new WP_REST_Response( + array( + 'site_visibility' => array( + 'search_engines_visible' => (int) get_option( 'blog_public', 1 ) === 1, + 'sitemap_active' => $sitemaps_active, + 'sitemap_url' => home_url( '/sitemap.xml' ), + 'seo_tools_active' => $seo_tools_active, + 'front_page_description' => $front_page_desc, + ), + 'plan' => array( + 'seo_enabled_for_site' => $seo_enabled, + ), + ) + ); + } +} diff --git a/projects/packages/seo/tests/.phpcs.dir.xml b/projects/packages/seo/tests/.phpcs.dir.xml new file mode 100644 index 000000000000..46951fe77b37 --- /dev/null +++ b/projects/packages/seo/tests/.phpcs.dir.xml @@ -0,0 +1,4 @@ + + + + diff --git a/projects/packages/seo/tests/php/InitializerTest.php b/projects/packages/seo/tests/php/InitializerTest.php new file mode 100644 index 000000000000..a1191f5b9bb3 --- /dev/null +++ b/projects/packages/seo/tests/php/InitializerTest.php @@ -0,0 +1,32 @@ +assertSame( 'jetpack-seo', Initializer::MENU_SLUG ); + } + + /** + * The package version constant is defined and non-empty. + */ + public function test_package_version_constant_is_defined() { + $this->assertNotEmpty( Initializer::PACKAGE_VERSION ); + } +} diff --git a/projects/packages/seo/tests/php/bootstrap.php b/projects/packages/seo/tests/php/bootstrap.php new file mode 100644 index 000000000000..778fcdea63ce --- /dev/null +++ b/projects/packages/seo/tests/php/bootstrap.php @@ -0,0 +1,12 @@ +=7.2" + }, + "require-dev": { + "automattic/jetpack-changelogger": "@dev", + "automattic/jetpack-test-environment": "@dev", + "automattic/phpunit-select-config": "@dev", + "yoast/phpunit-polyfills": "^4.0.0" + }, + "suggest": { + "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." + }, + "type": "jetpack-library", + "extra": { + "autotagger": true, + "mirror-repo": "Automattic/jetpack-seo", + "textdomain": "jetpack-seo", + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-seo/compare/${old}...${new}" + }, + "branch-alias": { + "dev-trunk": "0.1.x-dev" + }, + "version-constants": { + "::PACKAGE_VERSION": "src/class-initializer.php" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "scripts": { + "phpunit": [ + "phpunit-select-config phpunit.#.xml.dist --colors=always" + ], + "test-php": [ + "@composer phpunit" + ], + "test-js": [ + "pnpm run test" + ], + "build-development": [ + "pnpm run build" + ], + "build-production": [ + "NODE_ENV=production pnpm run build" + ], + "watch": [ + "Composer\\Config::disableProcessTimeout", + "pnpm run watch" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "Jetpack SEO — the visibility command center for WordPress sites in the agentic web.", + "transport-options": { + "relative": true + } + }, { "name": "automattic/jetpack-stats", "version": "dev-trunk", @@ -5803,6 +5877,7 @@ "automattic/jetpack-roles": 20, "automattic/jetpack-scan-page": 20, "automattic/jetpack-search": 20, + "automattic/jetpack-seo": 20, "automattic/jetpack-stats": 20, "automattic/jetpack-stats-admin": 20, "automattic/jetpack-status": 20, From 71a062f4572f96986baeac00ec44fb934cd23ce6 Mon Sep 17 00:00:00 2001 From: Angela Blake Date: Wed, 27 May 2026 15:05:30 -0500 Subject: [PATCH 03/21] SEO foundation: fix typecheck, project structure, and changelog format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI feedback from the initial push surfaced four classes of issues: - Typecheck failures from the partial scope: - Remove the unused "Manage settings" button from SiteVisibilityCard (referenced the trimmed JetpackSeoRoutes.Settings constant). The button returns in PR #3 when the Settings tab lands as its deep-link target. - Update useSimpleMutation onSuccess to TanStack Query v5's 4-arg form (data, variables, onMutateResult, context) — v5 added onMutateResult between variables and context. - Project-structure expectations for a new package: - Add .phan/config.php so static analysis recognises the package's PHP. - Add changelog/.gitkeep so the changelog directory survives release. - Changelog format: - Reset CHANGELOG.md to the keep-a-changelog header only (no pre-release `[0.1.0-alpha] - unreleased` block — pending entries live in changelog/). - Change the Jetpack-plugin changelog Type from `added` to `enhancement` (plugin/jetpack uses major|enhancement|compat|bugfix|other, not the keep-a-changelog vocabulary the packages use). Co-Authored-By: Filipe Varela Co-Authored-By: Claude Opus 4.7 (1M context) --- projects/packages/seo/.phan/config.php | 13 +++++++++++++ projects/packages/seo/CHANGELOG.md | 9 ++------- .../packages/seo/_inc/data/use-simple-mutation.ts | 4 ++-- .../_inc/screens/overview/site-visibility-card.tsx | 9 +-------- projects/packages/seo/changelog/.gitkeep | 0 .../jetpack/changelog/add-seo-package-scaffold | 2 +- 6 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 projects/packages/seo/.phan/config.php create mode 100644 projects/packages/seo/changelog/.gitkeep diff --git a/projects/packages/seo/.phan/config.php b/projects/packages/seo/.phan/config.php new file mode 100644 index 000000000000..334ac14a8d8c --- /dev/null +++ b/projects/packages/seo/.phan/config.php @@ -0,0 +1,13 @@ +( { ...query, data: ( variables ?? undefined ) as unknown as Record< string, unknown >, } ), - onSuccess: ( data, variables, context ) => { + onSuccess: ( data, variables, onMutateResult, context ) => { invalidates.forEach( key => { queryClient.invalidateQueries( { queryKey: [ key ] } ); } ); - options?.onSuccess?.( data, variables, context ); + options?.onSuccess?.( data, variables, onMutateResult, context ); }, onError: options?.onError, } ); diff --git a/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx b/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx index dcd317c0397d..2af4859f7aa2 100644 --- a/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx +++ b/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx @@ -1,8 +1,6 @@ -import { Button, ExternalLink } from '@wordpress/components'; +import { ExternalLink } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { Card } from '@wordpress/ui'; -import { NavLink } from 'react-router'; -import { JetpackSeoRoutes } from '../../constants'; import StatusDot from './status-dot'; import styles from './style.module.scss'; import type { OverviewResponse } from '../../data/overview-types'; @@ -50,11 +48,6 @@ const SiteVisibilityCard: FC< Props > = ( { data } ) => { } /> -
- -
); diff --git a/projects/packages/seo/changelog/.gitkeep b/projects/packages/seo/changelog/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/projects/plugins/jetpack/changelog/add-seo-package-scaffold b/projects/plugins/jetpack/changelog/add-seo-package-scaffold index 29a7b50376a9..1e975d42f37a 100644 --- a/projects/plugins/jetpack/changelog/add-seo-package-scaffold +++ b/projects/plugins/jetpack/changelog/add-seo-package-scaffold @@ -1,4 +1,4 @@ Significance: minor -Type: added +Type: enhancement SEO: scaffold the new jetpack-seo package and mount its admin page at admin.php?page=jetpack-seo, reachable on every site type. Initial release ships the Overview screen with a Site visibility card. From 01fbd45167f1fff338a264bcdf6c2e9acbcab820 Mon Sep 17 00:00:00 2001 From: Angela Blake Date: Wed, 27 May 2026 15:20:02 -0500 Subject: [PATCH 04/21] SEO foundation: address Phan + scope-leftover issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI surfaced two more issues on the foundation PR: - Initializer still called `LLMS_Txt::init()`, `AI_Crawlers::init()`, and `Schema_Builder::init()` — those classes live in later PRs (#4 and #6). Remove the calls; they return when their classes land. - Initializer registered `maybe_redirect_legacy_surfaces` on `admin_init`, which would have 301-redirected `?page=jetpack&tab=seo|traffic` to the new admin page. But the legacy Traffic page is still live on trunk in this PR's window, so redirecting now strands users mid-task. Remove the hook + the method entirely; the redirect lands in PR #5b alongside the discoverability banner and the actual deletion of the legacy surfaces. - Phan can't see `Jetpack_SEO_Utils` (lives in plugins/jetpack, not this package). The runtime-safety `class_exists` checks already guard the calls; add `@phan-suppress-next-line PhanUndeclaredClassMethod` so static analysis stops flagging them. Co-Authored-By: Filipe Varela Co-Authored-By: Claude Opus 4.7 (1M context) --- .../packages/seo/src/class-initializer.php | 30 +------------------ .../seo/src/class-rest-controller.php | 6 ++-- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/projects/packages/seo/src/class-initializer.php b/projects/packages/seo/src/class-initializer.php index e9db1147427e..c905dc4b63bc 100644 --- a/projects/packages/seo/src/class-initializer.php +++ b/projects/packages/seo/src/class-initializer.php @@ -52,13 +52,8 @@ public static function init() { Connection_Rest_Authentication::init(); - LLMS_Txt::init(); - AI_Crawlers::init(); - Schema_Builder::init(); - add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) ); add_action( 'admin_menu', array( __CLASS__, 'add_menu_item' ) ); - add_action( 'admin_init', array( __CLASS__, 'maybe_redirect_legacy_surfaces' ) ); /** * Fires after the Jetpack SEO package is initialized. @@ -200,6 +195,7 @@ private static function get_initial_state() { */ private static function is_seo_enabled() { if ( class_exists( 'Jetpack_SEO_Utils' ) ) { + // @phan-suppress-next-line PhanUndeclaredClassMethod -- Class lives in plugins/jetpack and is guarded by class_exists. return (bool) Jetpack_SEO_Utils::is_enabled_jetpack_seo(); } return false; @@ -213,28 +209,4 @@ private static function is_seo_enabled() { public static function register_rest_endpoints() { REST_Controller::register_routes(); } - - /** - * 301-redirect legacy SEO surfaces to the unified screen. - * - * The old routes are: - * - admin.php?page=jetpack#/traffic - * - admin.php?page=jetpack&tab=seo - * - * Only hash fragments arrive client-side, so the hash-based redirect - * happens from a tiny script enqueued on the main Jetpack settings - * page — the server-side path handles the ?tab= variant. - * - * @return void - */ - public static function maybe_redirect_legacy_surfaces() { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; - if ( 'jetpack' === $page && in_array( $tab, array( 'seo', 'traffic' ), true ) ) { - wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG ), 301 ); - exit; - } - } } diff --git a/projects/packages/seo/src/class-rest-controller.php b/projects/packages/seo/src/class-rest-controller.php index b94e9cebf2ec..69d9dd3b3e0d 100644 --- a/projects/packages/seo/src/class-rest-controller.php +++ b/projects/packages/seo/src/class-rest-controller.php @@ -64,11 +64,13 @@ public static function permissions_check() { public static function get_overview( WP_REST_Request $request ) { unset( $request ); - $modules = new Modules(); + $modules = new Modules(); + // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Utils lives in plugins/jetpack and is guarded by class_exists. $seo_enabled = class_exists( 'Jetpack_SEO_Utils' ) && Jetpack_SEO_Utils::is_enabled_jetpack_seo(); $seo_tools_active = $modules->is_active( 'seo-tools' ); $sitemaps_active = (bool) get_option( 'jetpack_seo_sitemap_enabled', false ); - $front_page_desc = $seo_enabled ? Jetpack_SEO_Utils::get_front_page_meta_description() : ''; + // @phan-suppress-next-line PhanUndeclaredClassMethod -- Same as above; only invoked when class_exists. + $front_page_desc = $seo_enabled ? Jetpack_SEO_Utils::get_front_page_meta_description() : ''; return new WP_REST_Response( array( From 61fc712061df66d99f0f0da79b9016c74d79d554 Mon Sep 17 00:00:00 2001 From: Angela Blake Date: Thu, 28 May 2026 15:41:15 -0500 Subject: [PATCH 05/21] SEO foundation: opt into shared admin-page-layout mixin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the SEO admin page's reliance on admin-ui's internal `.admin-ui-page*` classes — which are hashed in the standalone `@wordpress/admin-ui` build and only literal in some WP core bundles — in favor of the stable `.jp-admin-page__page` hook that `` forwards onto admin-ui's `` and the shared `jetpack-admin-page-layout-wp-build` mixin from `@automattic/jetpack-base-styles`. This also delivers the sticky `JetpackFooter` behavior previously proposed as a one-off in ``'s own SCSS — the shared mixin already pins the footer to the bottom of the viewport via the stable selector chain, which is the same path Newsletter / Protect / Backup / Search / VideoPress take. See CGastrell's review on #49201. Per-page opt-in lives in a new non-module `_inc/admin-page-layout.scss` (matching the Newsletter pattern), so the mixin's body-class scope stays out of the CSS-modules file. Co-Authored-By: Claude Opus 4.7 (1M context) --- projects/packages/seo/_inc/admin-page-layout.scss | 9 +++++++++ projects/packages/seo/_inc/admin.tsx | 1 + projects/packages/seo/_inc/style.module.scss | 11 ++++++----- .../seo/changelog/use-shared-admin-page-layout | 4 ++++ 4 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 projects/packages/seo/_inc/admin-page-layout.scss create mode 100644 projects/packages/seo/changelog/use-shared-admin-page-layout diff --git a/projects/packages/seo/_inc/admin-page-layout.scss b/projects/packages/seo/_inc/admin-page-layout.scss new file mode 100644 index 000000000000..d7f2b762bfe7 --- /dev/null +++ b/projects/packages/seo/_inc/admin-page-layout.scss @@ -0,0 +1,9 @@ +@use "@automattic/jetpack-base-styles/admin-page-layout" as *; + +// `-wp-build` variant: the SEO admin page mounts `@wordpress/admin-ui` 2.x +// components through ``, so we layer that variant's defensive +// resets (breadcrumb list/anchor normalization) on top of the shared layout. +body.jetpack_page_jetpack-seo { + + @include jetpack-admin-page-layout-wp-build; +} diff --git a/projects/packages/seo/_inc/admin.tsx b/projects/packages/seo/_inc/admin.tsx index 2dcb548cce4b..db31f502f9f4 100644 --- a/projects/packages/seo/_inc/admin.tsx +++ b/projects/packages/seo/_inc/admin.tsx @@ -4,6 +4,7 @@ import { JetpackSeoRoutes } from './constants'; import Providers from './providers'; import OverviewScreen from './screens/overview'; import Shell from './shell'; +import './admin-page-layout.scss'; import './style.module.scss'; // Data router (`createHashRouter` + `RouterProvider`) rather than declarative diff --git a/projects/packages/seo/_inc/style.module.scss b/projects/packages/seo/_inc/style.module.scss index 137fb3c969e0..57191ef7e80a 100644 --- a/projects/packages/seo/_inc/style.module.scss +++ b/projects/packages/seo/_inc/style.module.scss @@ -5,14 +5,15 @@ } // Override AdminPage's default `--jp-white` background with the admin-ui -// neutral-surface token (#fcfcfc). Same approach as -// `_inc/client/components/settings-nav-tabs/style.scss` — double-class the -// selector for specificity over AdminPage's own `.background` rule. +// neutral-surface token (#fcfcfc). Targets the stable `.jp-admin-page__page` +// hook (passed via `className` from ``) rather than admin-ui's +// internal `.admin-ui-page*` classes, which are hashed in the standalone +// admin-ui build and only literal in some WP core bundles. Double-class on +// `.neutralBg` for specificity over AdminPage's own `.background` rule. .neutralBg.neutralBg { &:global(.background), - :global(.admin-ui-page), - :global(.admin-ui-page__content) { + :global(.jp-admin-page__page) { background-color: var(--wpds-color-bg-surface-neutral, #fcfcfc); } } diff --git a/projects/packages/seo/changelog/use-shared-admin-page-layout b/projects/packages/seo/changelog/use-shared-admin-page-layout new file mode 100644 index 000000000000..2cddd2a374b1 --- /dev/null +++ b/projects/packages/seo/changelog/use-shared-admin-page-layout @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Layout: opt into the shared `jetpack-admin-page-layout-wp-build` mixin so the SEO admin page uses the stable `.jp-admin-page__page` hook instead of admin-ui's internal `.admin-ui-page*` classes. From 2fb97bb0e2c13a081218decff0f409bc4d4e8167 Mon Sep 17 00:00:00 2001 From: Angela Blake Date: Thu, 28 May 2026 15:55:00 -0500 Subject: [PATCH 06/21] SEO foundation: ship PHPUnit + jest configs across the CI matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first PR push only included `phpunit.11.xml.dist`, so CI's PHP test matrix (PHP 7.4–8.5) failed at config-read because PHPUnit 8/9/12 had no config to select. Adds `phpunit.8.xml.dist`, `phpunit.9.xml.dist`, and `phpunit.12.xml.dist`, mirrored verbatim from `packages/ip`'s set. Adds a `test` script + a `tests/jest.config.js` so the JS-tests CI job doesn't error with `Missing script: test` either. Uses `--passWithNoTests` so the package can ship before it grows JS tests, and inherits the shared `jetpack-js-tools/jest/config.base.js` for when it does (asset stub for `.scss` imports, ts/jsx transform, JSDOM environment). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../seo/changelog/add-test-infrastructure | 4 +++ projects/packages/seo/package.json | 2 ++ projects/packages/seo/phpunit.12.xml.dist | 36 +++++++++++++++++++ projects/packages/seo/phpunit.8.xml.dist | 17 +++++++++ projects/packages/seo/phpunit.9.xml.dist | 17 +++++++++ projects/packages/seo/tests/jest.config.js | 7 ++++ 6 files changed, 83 insertions(+) create mode 100644 projects/packages/seo/changelog/add-test-infrastructure create mode 100644 projects/packages/seo/phpunit.12.xml.dist create mode 100644 projects/packages/seo/phpunit.8.xml.dist create mode 100644 projects/packages/seo/phpunit.9.xml.dist create mode 100644 projects/packages/seo/tests/jest.config.js diff --git a/projects/packages/seo/changelog/add-test-infrastructure b/projects/packages/seo/changelog/add-test-infrastructure new file mode 100644 index 000000000000..1df42f590656 --- /dev/null +++ b/projects/packages/seo/changelog/add-test-infrastructure @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Tests: ship PHPUnit configs for versions 8/9/12 in addition to 11, add the `test`/`test-coverage` jest scripts, and add `tests/jest.config.js` so CI can run the package's test job across the PHP test matrix. diff --git a/projects/packages/seo/package.json b/projects/packages/seo/package.json index 2f3d780a9e53..7878636e5340 100644 --- a/projects/packages/seo/package.json +++ b/projects/packages/seo/package.json @@ -22,6 +22,8 @@ "build": "pnpm run clean && pnpm run build-client", "build-client": "pnpm webpack --config webpack.config.js", "clean": "rm -rf build/", + "test": "jest --config=tests/jest.config.js --passWithNoTests", + "test-coverage": "pnpm run test --coverage", "typecheck": "tsgo --noEmit", "watch": "pnpm run build && pnpm webpack watch" }, diff --git a/projects/packages/seo/phpunit.12.xml.dist b/projects/packages/seo/phpunit.12.xml.dist new file mode 100644 index 000000000000..f7418373829b --- /dev/null +++ b/projects/packages/seo/phpunit.12.xml.dist @@ -0,0 +1,36 @@ + + + + + tests/php + + + + + + + + src + + + + + diff --git a/projects/packages/seo/phpunit.8.xml.dist b/projects/packages/seo/phpunit.8.xml.dist new file mode 100644 index 000000000000..3965963c485e --- /dev/null +++ b/projects/packages/seo/phpunit.8.xml.dist @@ -0,0 +1,17 @@ + + + + + tests/php + + + diff --git a/projects/packages/seo/phpunit.9.xml.dist b/projects/packages/seo/phpunit.9.xml.dist new file mode 100644 index 000000000000..3965963c485e --- /dev/null +++ b/projects/packages/seo/phpunit.9.xml.dist @@ -0,0 +1,17 @@ + + + + + tests/php + + + diff --git a/projects/packages/seo/tests/jest.config.js b/projects/packages/seo/tests/jest.config.js new file mode 100644 index 000000000000..b5ceacda1f7e --- /dev/null +++ b/projects/packages/seo/tests/jest.config.js @@ -0,0 +1,7 @@ +const path = require( 'path' ); +const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' ); + +module.exports = { + ...baseConfig, + rootDir: path.join( __dirname, '..' ), +}; From baa4d8c69bd3e1f1e6bff3add5ae68cb93551043 Mon Sep 17 00:00:00 2001 From: Angela Blake Date: Thu, 28 May 2026 16:32:32 -0500 Subject: [PATCH 07/21] SEO foundation: add .gitattributes so build/ ships with the package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without `.gitattributes`, the production-publish pipeline that vendors `automattic/jetpack-seo` into the Jetpack plugin's `jetpack_vendor/` respects `.gitignore` and drops the webpack-built `build/` directory. The PHP source files come along (they're not gitignored), so the SEO admin menu item appears and the mount `
` renders — but the enqueued `build/index.js` URL 404s, leaving the admin page visually blank. Mirrors Newsletter's `.gitattributes` (the canonical wp-build package): - `/build/** production-include` overrides gitignore for build output - `*.tsx`/`*.ts`/`*.scss` under `_inc/` and `src/` are `production-exclude` since webpack has already compiled them into `build/`. Confirmed against the 404 on `wp-content/plugins/jetpack-dev/jetpack_vendor/automattic/jetpack-seo/build/index.js` in JetpackBeta on `sweetly-partial-gazelle.jurassic.ninja`. Co-Authored-By: Claude Opus 4.7 (1M context) --- projects/packages/seo/.gitattributes | 16 ++++++++++++++++ .../packages/seo/changelog/add-gitattributes | 4 ++++ 2 files changed, 20 insertions(+) create mode 100644 projects/packages/seo/.gitattributes create mode 100644 projects/packages/seo/changelog/add-gitattributes diff --git a/projects/packages/seo/.gitattributes b/projects/packages/seo/.gitattributes new file mode 100644 index 000000000000..a52ee46f6eb8 --- /dev/null +++ b/projects/packages/seo/.gitattributes @@ -0,0 +1,16 @@ +# Files to include in the mirror repo, but excluded via gitignore +# Remember to end all directories with `/**` to properly tag every file. +/build/** production-include + +# Files to exclude from the mirror repo, but included in the monorepo. +# (see also entries in the monorepo root .gitattributes) +# Remember to end all directories with `/**` to properly tag every file. +global.d.ts production-exclude +_inc/**/*.scss production-exclude +_inc/**/*.tsx production-exclude +_inc/**/*.ts production-exclude +_inc/**/*.jsx production-exclude +src/**/*.scss production-exclude +src/**/*.tsx production-exclude +src/**/*.ts production-exclude +src/**/*.jsx production-exclude diff --git a/projects/packages/seo/changelog/add-gitattributes b/projects/packages/seo/changelog/add-gitattributes new file mode 100644 index 000000000000..13ac33edd8c5 --- /dev/null +++ b/projects/packages/seo/changelog/add-gitattributes @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Packaging: add `.gitattributes` so the webpack-built `build/` directory is included when the package is vendored into the Jetpack plugin distributable. Source `.tsx`/`.ts`/`.scss` files are excluded since webpack already compiles them into `build/`. From 88243c30641c67b680a2f5c1e54e1364dc972588 Mon Sep 17 00:00:00 2001 From: Angela Blake Date: Fri, 29 May 2026 14:38:51 -0500 Subject: [PATCH 08/21] Jetpack SEO: migrate Overview to the shared data-sync layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on #49203 — replace hand-rolled data plumbing with Jetpack's established patterns. Data layer: - Register a read-only `overview` data-sync entry (namespace `jetpack_seo`) via Data_Sync_Readonly + Schema, bootstrapped onto the page with attach_to_plugin() so the app hydrates from window.jetpack_seo without a round-trip. - use-overview -> useDataSync; overview-types -> Zod OverviewSchema; providers -> DataSyncProvider. - Delete class-rest-controller.php, use-simple-query.ts and the unused use-simple-mutation.ts; drop the dead jetpackSeoRest bootstrap and the now-unused @wordpress/api-fetch and @tanstack/react-query deps. Module gating: - Move the seo-tools module check up into Initializer::init() so no routes, menu, or assets register when the module is off (matches how other Jetpack modules gate), removing the redundant per-method check. Co-Authored-By: Claude Opus 4.8 (1M context) --- pnpm-lock.yaml | 21 +--- projects/packages/seo/_inc/constants.ts | 5 +- .../packages/seo/_inc/data/overview-types.ts | 32 +++-- .../packages/seo/_inc/data/use-overview.ts | 19 +-- .../seo/_inc/data/use-simple-mutation.ts | 46 ------- .../seo/_inc/data/use-simple-query.ts | 28 ----- projects/packages/seo/_inc/providers.tsx | 15 +-- projects/packages/seo/changelog/use-data-sync | 4 + projects/packages/seo/composer.json | 4 +- projects/packages/seo/global.d.ts | 4 - projects/packages/seo/package.json | 6 +- .../packages/seo/src/class-initializer.php | 112 +++++++++++++++--- .../seo/src/class-rest-controller.php | 90 -------------- 13 files changed, 149 insertions(+), 237 deletions(-) delete mode 100644 projects/packages/seo/_inc/data/use-simple-mutation.ts delete mode 100644 projects/packages/seo/_inc/data/use-simple-query.ts create mode 100644 projects/packages/seo/changelog/use-data-sync delete mode 100644 projects/packages/seo/src/class-rest-controller.php diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73968daea8ea..ba4eaf46397a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4534,18 +4534,15 @@ importers: '@automattic/jetpack-connection': specifier: workspace:* version: link:../../js-packages/connection + '@automattic/jetpack-react-data-sync-client': + specifier: workspace:* + version: link:../../js-packages/react-data-sync-client '@automattic/jetpack-script-data': specifier: workspace:* version: link:../../js-packages/script-data '@automattic/social-previews': specifier: workspace:* version: link:../../js-packages/social-previews - '@tanstack/react-query': - specifier: 5.90.8 - version: 5.90.8(react@18.3.1) - '@wordpress/api-fetch': - specifier: 7.44.0 - version: 7.44.0 '@wordpress/components': specifier: 32.6.0 version: 32.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -4579,6 +4576,9 @@ importers: react-router: specifier: 7.12.0 version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@automattic/jetpack-webpack-config': specifier: workspace:* @@ -10851,10 +10851,6 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/api-fetch@7.44.0': - resolution: {integrity: sha512-KZP5Y0AzUVPRbwCsp2MUNEjIyYPJdaa7ojzYyc/IVlaAlbXVdd0Ofk8UDf4l8PjtXkyyPs9pX9sFy5iNcrF2cQ==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/api-fetch@7.46.0': resolution: {integrity: sha512-QOxuHSUXMzLat3Y90+0HNUDPSlBUK53r4mQ4m7f4/OKaWRRZU5jzvDBJyj52dEST7yJ1eZtuqUkEwK2T1MEBfQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -24008,11 +24004,6 @@ snapshots: react: 18.3.1 uuid: 14.0.0 - '@wordpress/api-fetch@7.44.0': - dependencies: - '@wordpress/i18n': 6.19.0 - '@wordpress/url': 4.46.0 - '@wordpress/api-fetch@7.46.0': dependencies: '@wordpress/i18n': 6.19.0 diff --git a/projects/packages/seo/_inc/constants.ts b/projects/packages/seo/_inc/constants.ts index a61ff18d1f59..8fa23de4aa23 100644 --- a/projects/packages/seo/_inc/constants.ts +++ b/projects/packages/seo/_inc/constants.ts @@ -2,4 +2,7 @@ export const JetpackSeoRoutes = { Overview: '/', } as const; -export const REST_NAMESPACE = 'jetpack-seo/v1'; +// Data-sync namespace — also the name of the bootstrapped window global +// (`window.jetpack_seo`) and must match the server-side +// `Initializer::DATA_SYNC_NAMESPACE`. +export const DATA_SYNC_NAMESPACE = 'jetpack_seo'; diff --git a/projects/packages/seo/_inc/data/overview-types.ts b/projects/packages/seo/_inc/data/overview-types.ts index 386e53523715..1feea4046c06 100644 --- a/projects/packages/seo/_inc/data/overview-types.ts +++ b/projects/packages/seo/_inc/data/overview-types.ts @@ -1,12 +1,20 @@ -export interface OverviewResponse { - site_visibility: { - search_engines_visible: boolean; - sitemap_active: boolean; - sitemap_url: string; - seo_tools_active: boolean; - front_page_description: string; - }; - plan: { - seo_enabled_for_site: boolean; - }; -} +import { z } from 'zod'; + +// Zod schema for the `overview` data-sync entry. `useDataSync` validates the +// server payload against this and `OverviewResponse` is inferred from it, so +// the schema is the single source of truth for the shape. Mirrors the +// server-side `Initializer::overview_schema()` registration. +export const OverviewSchema = z.object( { + site_visibility: z.object( { + search_engines_visible: z.boolean(), + sitemap_active: z.boolean(), + sitemap_url: z.string(), + seo_tools_active: z.boolean(), + front_page_description: z.string(), + } ), + plan: z.object( { + seo_enabled_for_site: z.boolean(), + } ), +} ); + +export type OverviewResponse = z.infer< typeof OverviewSchema >; diff --git a/projects/packages/seo/_inc/data/use-overview.ts b/projects/packages/seo/_inc/data/use-overview.ts index fd865d94db2b..9519a7d9d8c7 100644 --- a/projects/packages/seo/_inc/data/use-overview.ts +++ b/projects/packages/seo/_inc/data/use-overview.ts @@ -1,11 +1,14 @@ -import { REST_NAMESPACE } from '../constants'; -import useSimpleQuery from './use-simple-query'; -import type { OverviewResponse } from './overview-types'; +import { useDataSync } from '@automattic/jetpack-react-data-sync-client'; +import { DATA_SYNC_NAMESPACE } from '../constants'; +import { OverviewSchema } from './overview-types'; -const useOverview = () => - useSimpleQuery< OverviewResponse >( { - name: 'jetpack-seo-overview', - query: { path: `/${ REST_NAMESPACE }/overview` }, - } ); +// Read the aggregated Overview state from the `overview` data-sync entry. The +// value is bootstrapped onto the page (`window.jetpack_seo.overview`) by the +// server, so the first render has data without a round-trip. Read-only entry — +// we only use the query half of the `useDataSync` tuple. +const useOverview = () => { + const [ query ] = useDataSync( DATA_SYNC_NAMESPACE, 'overview', OverviewSchema ); + return query; +}; export default useOverview; diff --git a/projects/packages/seo/_inc/data/use-simple-mutation.ts b/projects/packages/seo/_inc/data/use-simple-mutation.ts deleted file mode 100644 index 8e2b967f8cdf..000000000000 --- a/projects/packages/seo/_inc/data/use-simple-mutation.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable jsdoc/require-returns, jsdoc/require-param */ - -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import apiFetch from '@wordpress/api-fetch'; -import type { UseMutationOptions } from '@tanstack/react-query'; -import type { APIFetchOptions } from '@wordpress/api-fetch'; - -type QueryParams< T, V > = { - name: string; - query: APIFetchOptions< true >; - invalidates?: string[]; - options?: Pick< UseMutationOptions< T, Error, V >, 'onSuccess' | 'onError' >; -}; - -/** - * Thin TanStack Query wrapper for wp-api write requests. - * - * `invalidates` lists query names to invalidate on success — e.g. mutating - * /settings should invalidate the cached /overview response so the home - * base re-renders with fresh numbers. - */ -const useSimpleMutation = < T = unknown, V = unknown >( { - name, - query, - invalidates = [], - options, -}: QueryParams< T, V > ) => { - const queryClient = useQueryClient(); - return useMutation< T, Error, V >( { - mutationKey: [ name ], - mutationFn: ( variables: V ) => - apiFetch< T >( { - ...query, - data: ( variables ?? undefined ) as unknown as Record< string, unknown >, - } ), - onSuccess: ( data, variables, onMutateResult, context ) => { - invalidates.forEach( key => { - queryClient.invalidateQueries( { queryKey: [ key ] } ); - } ); - options?.onSuccess?.( data, variables, onMutateResult, context ); - }, - onError: options?.onError, - } ); -}; - -export default useSimpleMutation; diff --git a/projects/packages/seo/_inc/data/use-simple-query.ts b/projects/packages/seo/_inc/data/use-simple-query.ts deleted file mode 100644 index 8f941592bb88..000000000000 --- a/projects/packages/seo/_inc/data/use-simple-query.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable jsdoc/require-returns, jsdoc/require-param */ - -import { useQuery } from '@tanstack/react-query'; -import apiFetch from '@wordpress/api-fetch'; -import type { WP_Error } from './types'; -import type { UseQueryOptions } from '@tanstack/react-query'; -import type { APIFetchOptions } from '@wordpress/api-fetch'; - -type QueryParams< T > = { - name: string; - query: APIFetchOptions< true >; - options?: Pick< UseQueryOptions< T, WP_Error >, 'enabled' | 'gcTime' | 'refetchOnMount' >; -}; - -/** - * Thin TanStack Query wrapper for wp-api GET requests. Keeps the Overview, - * Content, and Discoverability screens free of repetitive fetching boilerplate. - */ -const useSimpleQuery = < T >( { name, query, options }: QueryParams< T > ) => - useQuery< T, WP_Error >( { - queryKey: [ name, query ], - queryFn: () => apiFetch< T >( query ), - refetchOnWindowFocus: false, - refetchIntervalInBackground: false, - ...options, - } ); - -export default useSimpleQuery; diff --git a/projects/packages/seo/_inc/providers.tsx b/projects/packages/seo/_inc/providers.tsx index 7eda5b7069d9..7d5abb082991 100644 --- a/projects/packages/seo/_inc/providers.tsx +++ b/projects/packages/seo/_inc/providers.tsx @@ -1,23 +1,16 @@ import { ThemeProvider } from '@automattic/jetpack-components'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { DataSyncProvider } from '@automattic/jetpack-react-data-sync-client'; import type { FC, ReactNode } from 'react'; interface ProvidersProps { children: ReactNode; } -const queryClient = new QueryClient( { - defaultOptions: { - queries: { - staleTime: 30_000, - refetchOnWindowFocus: false, - }, - }, -} ); - +// `DataSyncProvider` supplies the TanStack `QueryClient` that `useDataSync` +// relies on — no need for a hand-rolled client here. const Providers: FC< ProvidersProps > = ( { children } ) => ( - { children } + { children } ); diff --git a/projects/packages/seo/changelog/use-data-sync b/projects/packages/seo/changelog/use-data-sync new file mode 100644 index 000000000000..536a20b6685e --- /dev/null +++ b/projects/packages/seo/changelog/use-data-sync @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Use the shared data-sync layer (useDataSync / wp-js-data-sync) for the Overview instead of a custom REST controller and bespoke React Query wrappers. diff --git a/projects/packages/seo/composer.json b/projects/packages/seo/composer.json index e06114f2748a..3dfcd4355223 100644 --- a/projects/packages/seo/composer.json +++ b/projects/packages/seo/composer.json @@ -9,7 +9,9 @@ "automattic/jetpack-assets": "@dev", "automattic/jetpack-connection": "@dev", "automattic/jetpack-constants": "@dev", - "automattic/jetpack-status": "@dev" + "automattic/jetpack-schema": "@dev", + "automattic/jetpack-status": "@dev", + "automattic/jetpack-wp-js-data-sync": "@dev" }, "require-dev": { "yoast/phpunit-polyfills": "^4.0.0", diff --git a/projects/packages/seo/global.d.ts b/projects/packages/seo/global.d.ts index 8ad674407036..113c6c75bcce 100644 --- a/projects/packages/seo/global.d.ts +++ b/projects/packages/seo/global.d.ts @@ -4,8 +4,4 @@ declare module '*.svg'; interface Window { jetpackSeoInitialState?: import('./_inc/types').JetpackSeoInitialState; - jetpackSeoRest?: { - apiRoot: string; - apiNonce: string; - }; } diff --git a/projects/packages/seo/package.json b/projects/packages/seo/package.json index 7878636e5340..a6c6bd830163 100644 --- a/projects/packages/seo/package.json +++ b/projects/packages/seo/package.json @@ -32,10 +32,9 @@ "@automattic/jetpack-base-styles": "workspace:*", "@automattic/jetpack-components": "workspace:*", "@automattic/jetpack-connection": "workspace:*", + "@automattic/jetpack-react-data-sync-client": "workspace:*", "@automattic/jetpack-script-data": "workspace:*", "@automattic/social-previews": "workspace:*", - "@tanstack/react-query": "5.90.8", - "@wordpress/api-fetch": "7.44.0", "@wordpress/components": "32.6.0", "@wordpress/compose": "7.44.0", "@wordpress/data": "10.44.0", @@ -46,7 +45,8 @@ "@wordpress/ui": "0.10.0", "@wordpress/url": "4.44.0", "clsx": "2.1.1", - "react-router": "7.12.0" + "react-router": "7.12.0", + "zod": "3.25.76" }, "devDependencies": { "@automattic/jetpack-webpack-config": "workspace:*", diff --git a/projects/packages/seo/src/class-initializer.php b/projects/packages/seo/src/class-initializer.php index c905dc4b63bc..93a7b23e8aca 100644 --- a/projects/packages/seo/src/class-initializer.php +++ b/projects/packages/seo/src/class-initializer.php @@ -17,8 +17,11 @@ use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication; use Automattic\Jetpack\Modules; +use Automattic\Jetpack\Schema\Schema; use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host as Status_Host; +use Automattic\Jetpack\WP_JS_Data_Sync\Data_Sync; +use Automattic\Jetpack\WP_JS_Data_Sync\Data_Sync_Readonly; use Jetpack_SEO_Utils; /** @@ -38,6 +41,13 @@ class Initializer { */ const MENU_SLUG = 'jetpack-seo'; + /** + * Data-sync namespace. Also the name of the window global the registry + * bootstraps onto the page (`window.jetpack_seo`); must match the JS-side + * `DATA_SYNC_NAMESPACE` constant. + */ + const DATA_SYNC_NAMESPACE = 'jetpack_seo'; + /** * Initialize the package. * @@ -50,9 +60,17 @@ public static function init() { return; } + // Gate the entire SEO surface on the `seo-tools` module being active, + // the same way other Jetpack modules do. When the module is off we + // register nothing — no REST routes, no admin menu, no assets — + // rather than registering everything and hiding the menu downstream. + if ( ! self::is_seo_tools_module_active() ) { + return; + } + Connection_Rest_Authentication::init(); - add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) ); + self::register_data_sync(); add_action( 'admin_menu', array( __CLASS__, 'add_menu_item' ) ); /** @@ -69,16 +87,12 @@ public static function init() { * Uses Admin_Menu so the page is reachable on wp-admin across all * site types, including Atomic/WoW where Jetpack > Settings is hidden. * - * Gated on the `seo-tools` Jetpack module being active — if the user - * has the module turned off the menu item disappears entirely. + * Only registered when the `seo-tools` module is active — `init()` gates + * the whole surface, so by the time this fires the module is guaranteed on. * * @return void */ public static function add_menu_item() { - if ( ! self::is_seo_tools_module_active() ) { - return; - } - $page_suffix = Admin_Menu::add_menu( __( 'SEO', 'jetpack-seo' ), __( 'SEO', 'jetpack-seo' ), @@ -90,6 +104,11 @@ public static function add_menu_item() { if ( $page_suffix ) { add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) ); + // Bootstrap the data-sync registry onto this page so the React app + // hydrates from `window.jetpack_seo` without an extra round-trip. + // `$page_suffix` is the page-render hook, which fires after our + // footer script registers — so the localize attaches cleanly. + Data_Sync::get_instance( self::DATA_SYNC_NAMESPACE )->attach_to_plugin( 'jetpack-seo-app', $page_suffix ); } } @@ -148,14 +167,9 @@ public static function enqueue_scripts() { self::get_initial_state() ); - wp_localize_script( - 'jetpack-seo-app', - 'jetpackSeoRest', - array( - 'apiRoot' => esc_url_raw( rest_url() ), - 'apiNonce' => wp_create_nonce( 'wp_rest' ), - ) - ); + // The REST root + nonce the React app needs are bootstrapped by the + // data-sync registry onto `window.jetpack_seo.rest_api` (see + // Data_Sync::attach_to_plugin in add_menu_item()). Connection_Initial_State::render_script( 'jetpack-seo-app' ); } @@ -202,11 +216,73 @@ private static function is_seo_enabled() { } /** - * Register REST endpoints for the jetpack-seo/v1 namespace. + * Register the package's data-sync entries. + * + * Replaces the hand-written REST controller — the shared data-sync layer + * provides the REST endpoint, nonce handling, schema validation, and the + * page bootstrap. The Overview is a read-only aggregate, so it's a + * `Data_Sync_Readonly` entry backed by {@see self::get_overview_data()}. * * @return void */ - public static function register_rest_endpoints() { - REST_Controller::register_routes(); + public static function register_data_sync() { + Data_Sync::get_instance( self::DATA_SYNC_NAMESPACE )->register( + 'overview', + self::overview_schema(), + new Data_Sync_Readonly( array( __CLASS__, 'get_overview_data' ) ) + ); + } + + /** + * Schema for the `overview` entry. Single source of truth on the server + * side; mirrored by the JS `OverviewSchema` Zod schema. + * + * @return \Automattic\Jetpack\Schema\Parser + */ + private static function overview_schema() { + return Schema::as_assoc_array( + array( + 'site_visibility' => Schema::as_assoc_array( + array( + 'search_engines_visible' => Schema::as_boolean(), + 'sitemap_active' => Schema::as_boolean(), + 'sitemap_url' => Schema::as_string(), + 'seo_tools_active' => Schema::as_boolean(), + 'front_page_description' => Schema::as_string(), + ) + ), + 'plan' => Schema::as_assoc_array( + array( + 'seo_enabled_for_site' => Schema::as_boolean(), + ) + ), + ) + ); + } + + /** + * Build the aggregated Overview state the dashboard renders. + * + * @return array + */ + public static function get_overview_data() { + $modules = new Modules(); + // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Utils lives in plugins/jetpack and is guarded by class_exists. + $seo_enabled = class_exists( 'Jetpack_SEO_Utils' ) && Jetpack_SEO_Utils::is_enabled_jetpack_seo(); + // @phan-suppress-next-line PhanUndeclaredClassMethod -- Same as above; only invoked when class_exists. + $front_page_desc = $seo_enabled ? Jetpack_SEO_Utils::get_front_page_meta_description() : ''; + + return array( + 'site_visibility' => array( + 'search_engines_visible' => (int) get_option( 'blog_public', 1 ) === 1, + 'sitemap_active' => (bool) get_option( 'jetpack_seo_sitemap_enabled', false ), + 'sitemap_url' => home_url( '/sitemap.xml' ), + 'seo_tools_active' => $modules->is_active( 'seo-tools' ), + 'front_page_description' => (string) $front_page_desc, + ), + 'plan' => array( + 'seo_enabled_for_site' => $seo_enabled, + ), + ); } } diff --git a/projects/packages/seo/src/class-rest-controller.php b/projects/packages/seo/src/class-rest-controller.php deleted file mode 100644 index 69d9dd3b3e0d..000000000000 --- a/projects/packages/seo/src/class-rest-controller.php +++ /dev/null @@ -1,90 +0,0 @@ - WP_REST_Server::READABLE, - 'callback' => array( __CLASS__, 'get_overview' ), - 'permission_callback' => array( __CLASS__, 'permissions_check' ), - ) - ); - } - - /** - * Default permission gate: site administrators only. - * - * @return bool - */ - public static function permissions_check() { - return current_user_can( 'manage_options' ); - } - - /** - * GET /jetpack-seo/v1/overview. - * - * Returns the aggregated state the Overview screen renders. Kept in a - * single endpoint so the dashboard loads with one request. Later PRs - * extend the response shape with additional cards (content health, - * AI discoverability, site verification). - * - * @param WP_REST_Request $request REST request. - * @return WP_REST_Response - */ - public static function get_overview( WP_REST_Request $request ) { - unset( $request ); - - $modules = new Modules(); - // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Utils lives in plugins/jetpack and is guarded by class_exists. - $seo_enabled = class_exists( 'Jetpack_SEO_Utils' ) && Jetpack_SEO_Utils::is_enabled_jetpack_seo(); - $seo_tools_active = $modules->is_active( 'seo-tools' ); - $sitemaps_active = (bool) get_option( 'jetpack_seo_sitemap_enabled', false ); - // @phan-suppress-next-line PhanUndeclaredClassMethod -- Same as above; only invoked when class_exists. - $front_page_desc = $seo_enabled ? Jetpack_SEO_Utils::get_front_page_meta_description() : ''; - - return new WP_REST_Response( - array( - 'site_visibility' => array( - 'search_engines_visible' => (int) get_option( 'blog_public', 1 ) === 1, - 'sitemap_active' => $sitemaps_active, - 'sitemap_url' => home_url( '/sitemap.xml' ), - 'seo_tools_active' => $seo_tools_active, - 'front_page_description' => $front_page_desc, - ), - 'plan' => array( - 'seo_enabled_for_site' => $seo_enabled, - ), - ) - ); - } -} From e1a878f26b0c3a8d44bd8e1a82480ffeca9879c3 Mon Sep 17 00:00:00 2001 From: Angela Blake Date: Fri, 29 May 2026 15:56:17 -0500 Subject: [PATCH 09/21] Jetpack SEO: adopt shared components and design tokens on the Overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on #49203 — replace hand-rolled UI with the platform's primitives. - Collapse shell.tsx onto AdminPage (drop the header-actions slot/fill context and the background/padding CSS overrides); delete header-actions-context.tsx and _inc/style.module.scss. - Use @wordpress/ui Notice (compound), Stack, and Link in place of @wordpress/components Notice/ExternalLink and flexbox CSS. - Give StatusDot its own style module using @wordpress/theme color tokens; tokenize remaining colors and spacing. - Extract ternary __() labels to module scope so i18n extraction survives the production minifier. - Remove the unused BoundedLayout from @automattic/jetpack-components. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/changelog/add-bounded-layout | 4 - .../layout/bounded-layout/index.tsx | 51 ------------- .../layout/bounded-layout/style.module.scss | 19 ----- projects/js-packages/components/index.ts | 5 -- projects/packages/seo/_inc/admin.tsx | 1 - .../seo/_inc/header-actions-context.tsx | 45 ----------- .../seo/_inc/screens/overview/index.tsx | 25 ++++--- .../screens/overview/site-visibility-card.tsx | 74 +++++++++---------- .../screens/overview/status-dot.module.scss | 20 +++++ .../seo/_inc/screens/overview/status-dot.tsx | 10 +-- .../_inc/screens/overview/style.module.scss | 62 ++-------------- projects/packages/seo/_inc/shell.tsx | 51 ++++--------- projects/packages/seo/_inc/style.module.scss | 48 ------------ 13 files changed, 96 insertions(+), 319 deletions(-) delete mode 100644 projects/js-packages/components/changelog/add-bounded-layout delete mode 100644 projects/js-packages/components/components/layout/bounded-layout/index.tsx delete mode 100644 projects/js-packages/components/components/layout/bounded-layout/style.module.scss delete mode 100644 projects/packages/seo/_inc/header-actions-context.tsx create mode 100644 projects/packages/seo/_inc/screens/overview/status-dot.module.scss delete mode 100644 projects/packages/seo/_inc/style.module.scss diff --git a/projects/js-packages/components/changelog/add-bounded-layout b/projects/js-packages/components/changelog/add-bounded-layout deleted file mode 100644 index ace7371d2eb8..000000000000 --- a/projects/js-packages/components/changelog/add-bounded-layout +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Add `BoundedLayout` — a shared content-width wrapper with `compact` (660px) and `wide` (1344px) presets, mirroring MSD layout conventions so products can stay visually aligned across Overview/Settings/Dashboard screens. diff --git a/projects/js-packages/components/components/layout/bounded-layout/index.tsx b/projects/js-packages/components/components/layout/bounded-layout/index.tsx deleted file mode 100644 index 4e45b6babee8..000000000000 --- a/projects/js-packages/components/components/layout/bounded-layout/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import clsx from 'clsx'; -import { createElement } from 'react'; -import styles from './style.module.scss'; -import type { FC, ReactElement, ReactNode } from 'react'; - -export type BoundedLayoutWidth = 'compact' | 'wide'; - -export type BoundedLayoutProps = { - /** - * Preset max-width token. `compact` (660px) for settings-style single - * columns; `wide` (1344px) for dashboard grids. Mirrors MSD conventions. - */ - width?: BoundedLayoutWidth; - - /** - * Tag name for the rendered element. Defaults to `div`. - */ - tagName?: string; - - /** - * Additional className to merge onto the wrapper. - */ - className?: string; - - children?: ReactNode; -}; - -/** - * Centers its children and caps the content area at a standardized Jetpack - * product max-width. Use it to keep Overview / Dashboard / Settings screens - * visually consistent across products. - * - * @param {BoundedLayoutProps} props - Component properties. - * @return {ReactElement} BoundedLayout component. - */ -const BoundedLayout: FC< BoundedLayoutProps > = ( { - width = 'wide', - tagName = 'div', - className, - children, -} ) => { - return createElement( - tagName, - { - className: clsx( styles.bounds, styles[ width ], className ), - }, - children - ); -}; - -export default BoundedLayout; diff --git a/projects/js-packages/components/components/layout/bounded-layout/style.module.scss b/projects/js-packages/components/components/layout/bounded-layout/style.module.scss deleted file mode 100644 index 487e9f26b2ea..000000000000 --- a/projects/js-packages/components/components/layout/bounded-layout/style.module.scss +++ /dev/null @@ -1,19 +0,0 @@ -// Standardized content-area widths shared across Jetpack products. Mirrors -// MSD (Multi-site Dashboard) layout tokens so admin surfaces stay aligned. -// `compact` is for focused forms and settings; `wide` is for dashboards. -.bounds { - --jp-bounded-layout-width: 1344px; - - box-sizing: border-box; - width: 100%; - max-width: var(--jp-bounded-layout-width); - margin-inline: auto; -} - -.compact { - --jp-bounded-layout-width: 660px; -} - -.wide { - --jp-bounded-layout-width: 1344px; -} diff --git a/projects/js-packages/components/index.ts b/projects/js-packages/components/index.ts index 1d8ec995388d..6a2bac5bb5b0 100644 --- a/projects/js-packages/components/index.ts +++ b/projects/js-packages/components/index.ts @@ -40,11 +40,6 @@ export { default as DecorativeCard } from './components/decorative-card/index.ts export { default as Col } from './components/layout/col/index.tsx'; export { default as Testimonials } from './components/testimonials/index.tsx'; export { default as Container } from './components/layout/container/index.tsx'; -export { default as BoundedLayout } from './components/layout/bounded-layout/index.tsx'; -export type { - BoundedLayoutProps, - BoundedLayoutWidth, -} from './components/layout/bounded-layout/index.tsx'; export { default as useBreakpointMatch } from './components/layout/use-breakpoint-match/index.ts'; export { default as CopyToClipboard } from './components/copy-to-clipboard/index.tsx'; export * from './components/icons/index.tsx'; diff --git a/projects/packages/seo/_inc/admin.tsx b/projects/packages/seo/_inc/admin.tsx index db31f502f9f4..bd86b807aa7c 100644 --- a/projects/packages/seo/_inc/admin.tsx +++ b/projects/packages/seo/_inc/admin.tsx @@ -5,7 +5,6 @@ import Providers from './providers'; import OverviewScreen from './screens/overview'; import Shell from './shell'; import './admin-page-layout.scss'; -import './style.module.scss'; // Data router (`createHashRouter` + `RouterProvider`) rather than declarative // ``, so future screens can use `useBlocker` for unsaved-changes diff --git a/projects/packages/seo/_inc/header-actions-context.tsx b/projects/packages/seo/_inc/header-actions-context.tsx deleted file mode 100644 index 7f4b857c525d..000000000000 --- a/projects/packages/seo/_inc/header-actions-context.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable jsdoc/require-returns, jsdoc/require-param, jsdoc/escape-inline-tags */ - -import { createContext, useCallback, useContext, useState } from 'react'; -import type { FC, ReactNode } from 'react'; - -interface HeaderActionsContextValue { - actions: ReactNode; - setActions: ( actions: ReactNode ) => void; -} - -const HeaderActionsContext = createContext< HeaderActionsContextValue >( { - actions: null, - setActions: () => {}, -} ); - -/** - * Provider owned by Shell. Lets routed screens inject buttons into the - * AdminPage header's `actions` slot without Shell knowing about them ahead - * of time. Mirrors the Slot/Fill pattern @wordpress/components uses for the - * plugin editor sidebars, but implemented as React context for simplicity. - */ -export const HeaderActionsProvider: FC< { children: ReactNode } > = ( { children } ) => { - const [ actions, setActions ] = useState< ReactNode >( null ); - return ( - - { children } - - ); -}; - -/** - * Read the actions Shell should render in AdminPage's header. - */ -export const useHeaderActions = (): ReactNode => useContext( HeaderActionsContext ).actions; - -/** - * Register a render function for the header actions slot. Pass `null` (or - * omit this hook) to clear the slot. Accepts a stable setter function — if - * you call `setHeaderActions` with a ReactNode that changes identity each - * render, wrap in `useMemo` to avoid churn. - */ -export const useSetHeaderActions = (): ( ( actions: ReactNode ) => void ) => { - const { setActions } = useContext( HeaderActionsContext ); - return useCallback( ( next: ReactNode ) => setActions( next ), [ setActions ] ); -}; diff --git a/projects/packages/seo/_inc/screens/overview/index.tsx b/projects/packages/seo/_inc/screens/overview/index.tsx index e74a441a2f2a..c832e1256906 100644 --- a/projects/packages/seo/_inc/screens/overview/index.tsx +++ b/projects/packages/seo/_inc/screens/overview/index.tsx @@ -1,5 +1,6 @@ -import { Notice, Spinner } from '@wordpress/components'; +import { Spinner } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { Notice } from '@wordpress/ui'; import useOverview from '../../data/use-overview'; import SiteVisibilityCard from './site-visibility-card'; import styles from './style.module.scss'; @@ -19,21 +20,25 @@ const OverviewScreen: FC = () => { if ( isError || ! data ) { return ( - - { error?.message ?? __( 'Unable to load overview.', 'jetpack-seo' ) } - + + + { error?.message ?? __( 'Unable to load overview.', 'jetpack-seo' ) } + + ); } return ( <> { ! data.plan.seo_enabled_for_site && ( - - { __( - 'SEO tools are not enabled on this site. Some cards reflect the underlying WordPress options only.', - 'jetpack-seo' - ) } - + + + { __( + 'SEO tools are not enabled on this site. Some cards reflect the underlying WordPress options only.', + 'jetpack-seo' + ) } + + ) }
diff --git a/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx b/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx index 2af4859f7aa2..d0539f888420 100644 --- a/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx +++ b/projects/packages/seo/_inc/screens/overview/site-visibility-card.tsx @@ -1,8 +1,6 @@ -import { ExternalLink } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { Card } from '@wordpress/ui'; +import { Card, Link, Stack } from '@wordpress/ui'; import StatusDot from './status-dot'; -import styles from './style.module.scss'; import type { OverviewResponse } from '../../data/overview-types'; import type { FC } from 'react'; @@ -10,47 +8,45 @@ interface Props { data: OverviewResponse[ 'site_visibility' ]; } -const SiteVisibilityCard: FC< Props > = ( { data } ) => { - const visibilityStatus = data.search_engines_visible ? 'ok' : 'err'; - const visibilityLabel = data.search_engines_visible - ? __( 'Search engines allowed', 'jetpack-seo' ) - : __( 'Search engines blocked', 'jetpack-seo' ); +// Labels resolved at module scope so the production minifier can't fold an +// adjacent `cond ? __(A) : __(B)` into `__(cond ? A : B)`, which would erase +// the literals from i18n extraction. See feedback_i18n_ternary_minifier_fold. +const searchAllowedLabel = __( 'Search engines allowed', 'jetpack-seo' ); +const searchBlockedLabel = __( 'Search engines blocked', 'jetpack-seo' ); +const sitemapActiveLabel = __( 'Sitemap active', 'jetpack-seo' ); +const sitemapDisabledLabel = __( 'Sitemap disabled', 'jetpack-seo' ); +const seoToolsActiveLabel = __( 'SEO tools active', 'jetpack-seo' ); +const seoToolsInactiveLabel = __( 'SEO tools inactive', 'jetpack-seo' ); - return ( - - - { __( 'Site visibility', 'jetpack-seo' ) } - - -
- -
-
+const SiteVisibilityCard: FC< Props > = ( { data } ) => ( + + + { __( 'Site visibility', 'jetpack-seo' ) } + + + + + { data.sitemap_active && ( - { __( 'View', 'jetpack-seo' ) } + + { __( 'View', 'jetpack-seo' ) } + ) } -
-
- -
-
-
- ); -}; + + + + + +); export default SiteVisibilityCard; diff --git a/projects/packages/seo/_inc/screens/overview/status-dot.module.scss b/projects/packages/seo/_inc/screens/overview/status-dot.module.scss new file mode 100644 index 000000000000..1ce64eeaba3f --- /dev/null +++ b/projects/packages/seo/_inc/screens/overview/status-dot.module.scss @@ -0,0 +1,20 @@ +.dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: var(--wpds-dimension-gap-sm, 8px); + vertical-align: middle; +} + +.ok { + background: var(--wpds-color-fg-content-success, #4ab866); +} + +.warn { + background: var(--wpds-color-fg-content-caution, #dba617); +} + +.err { + background: var(--wpds-color-fg-content-error, #cc1818); +} diff --git a/projects/packages/seo/_inc/screens/overview/status-dot.tsx b/projects/packages/seo/_inc/screens/overview/status-dot.tsx index 723f8b0108b8..33ea88d890c5 100644 --- a/projects/packages/seo/_inc/screens/overview/status-dot.tsx +++ b/projects/packages/seo/_inc/screens/overview/status-dot.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import styles from './style.module.scss'; +import styles from './status-dot.module.scss'; import type { FC } from 'react'; interface Props { @@ -10,10 +10,10 @@ interface Props { const StatusDot: FC< Props > = ( { status, label } ) => (