diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9db70934e007..6320b407a48e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -536,6 +536,9 @@ importers: '@babel/runtime': specifier: ^7 version: 7.28.6 + '@wordpress/admin-ui': + specifier: 1.8.0 + version: 1.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/browserslist-config': specifier: 6.40.0 version: 6.40.0 diff --git a/projects/js-packages/components/changelog/update-normalize-admin-page-headers b/projects/js-packages/components/changelog/update-normalize-admin-page-headers new file mode 100644 index 000000000000..ad02bc7be2d0 --- /dev/null +++ b/projects/js-packages/components/changelog/update-normalize-admin-page-headers @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add AdminHeader component wrapping @wordpress/admin-ui Page for unified admin page headers. diff --git a/projects/js-packages/components/components/admin-header/admin-ui-styles.css b/projects/js-packages/components/components/admin-header/admin-ui-styles.css new file mode 100644 index 000000000000..b9b1bf26cef7 --- /dev/null +++ b/projects/js-packages/components/components/admin-header/admin-ui-styles.css @@ -0,0 +1,8 @@ +/* Proxy file to import @wordpress/admin-ui styles. + * + * Importing the CSS via @import inside a .css file avoids the + * @wordpress/dependency-extraction-webpack-plugin externalization that + * breaks direct JS `import '@wordpress/admin-ui/build-style/style.css'`. + * css-loader resolves @import independently of the externals plugin. + */ +@import "@wordpress/admin-ui/build-style/style.css"; diff --git a/projects/js-packages/components/components/admin-header/index.tsx b/projects/js-packages/components/components/admin-header/index.tsx new file mode 100644 index 000000000000..516729064504 --- /dev/null +++ b/projects/js-packages/components/components/admin-header/index.tsx @@ -0,0 +1,59 @@ +import { Page } from '@wordpress/admin-ui'; +import './admin-ui-styles.css'; +import { + __experimentalHeading as Heading, // eslint-disable-line @wordpress/no-unsafe-wp-apis + __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis +} from '@wordpress/components'; +import JetpackLogo from '../jetpack-logo/index.tsx'; +import type { AdminHeaderProps } from './types.ts'; +import type { FC } from 'react'; + +/** + * Unified admin page header component. + * + * Renders a sticky header with logo, product title, optional subtitle, + * actions, and tabs. Wraps the `@wordpress/admin-ui` Page component. + * + * @param {AdminHeaderProps} props - Component properties. + * @return {ReactNode} AdminHeader component. + */ +const AdminHeader: FC< AdminHeaderProps > = ( { + logo, + title, + subTitle, + actions, + tabs = null, + className, + breadcrumbs = null, + badges = null, +} ) => { + const classes = className; + + // While admin-ui Page has a title prop, it fails to render both the logo and + // text. Internally it tries to accommodate both inside Heading. + // Composing here with Heading as it is on admin-ui Page. + const composedTitle = title ? ( + + { logo || } + + { title } + + + ) : undefined; + + return ( + + { tabs } + + ); +}; + +export default AdminHeader; diff --git a/projects/js-packages/components/components/admin-header/types.ts b/projects/js-packages/components/components/admin-header/types.ts new file mode 100644 index 000000000000..cd2940a4b5b8 --- /dev/null +++ b/projects/js-packages/components/components/admin-header/types.ts @@ -0,0 +1,43 @@ +import type { ReactNode } from 'react'; + +export type AdminHeaderProps = { + /** + * Custom logo element. Defaults to JetpackLogo icon (bolt only). + */ + logo?: ReactNode; + + /** + * Product title displayed next to the logo. + */ + title: string; + + /** + * Optional subtitle displayed below the title row. + */ + subTitle?: string; + + /** + * Optional breadcrumb elements displayed next to the title. + */ + breadcrumbs?: ReactNode; + + /** + * Optional badge elements displayed next to the title. + */ + badges?: ReactNode; + + /** + * Optional action elements (buttons, links) displayed on the right side of the header. + */ + actions?: ReactNode; + + /** + * Optional tab navigation displayed below the title/tagline. + */ + tabs?: ReactNode; + + /** + * Additional CSS class name. + */ + className?: string; +}; diff --git a/projects/js-packages/components/components/admin-page/index.tsx b/projects/js-packages/components/components/admin-page/index.tsx index c543af519dc6..d0bad43689b2 100644 --- a/projects/js-packages/components/components/admin-page/index.tsx +++ b/projects/js-packages/components/components/admin-page/index.tsx @@ -2,6 +2,7 @@ import restApi from '@automattic/jetpack-api'; import { __, sprintf } from '@wordpress/i18n'; import clsx from 'clsx'; import { useEffect, useCallback } from 'react'; +import AdminHeader from '../admin-header/index.tsx'; import JetpackFooter from '../jetpack-footer/index.tsx'; import JetpackLogo from '../jetpack-logo/index.tsx'; import Col from '../layout/col/index.tsx'; @@ -32,6 +33,11 @@ const AdminPage: FC< AdminPageProps > = ( { apiNonce = '', optionalMenuItems, header, + title, + subTitle, + logo, + actions, + tabs, } ) => { useEffect( () => { restApi.setApiRoot( apiRoot ); @@ -62,25 +68,35 @@ const AdminPage: FC< AdminPageProps > = ( { return (
- { showHeader && ( - - - { header ? header : } - { sandboxedDomain && ( - - API Sandboxed - - ) } - - + { showHeader && title ? ( + + ) : ( + showHeader && ( + + + { header ? header : } + { sandboxedDomain && ( + + API Sandboxed + + ) } + + + ) ) } { children } diff --git a/projects/js-packages/components/components/admin-page/types.ts b/projects/js-packages/components/components/admin-page/types.ts index 7e9e6ad59bf3..bec390ae496d 100644 --- a/projects/js-packages/components/components/admin-page/types.ts +++ b/projects/js-packages/components/components/admin-page/types.ts @@ -18,10 +18,37 @@ export type AdminPageProps = { showHeader?: boolean; /** - * Custom header. Optional + * Custom header. Optional. + * @deprecated Use `title` and `subTitle` props instead for the unified header. */ header?: ReactNode; + /** + * Product title displayed in the unified header (e.g. "Social", "Backup"). + * When provided, renders the new AdminHeader instead of the legacy header slot. + */ + title?: string; + + /** + * Optional tagline displayed below the title in the unified header. + */ + subTitle?: string; + + /** + * Custom logo element for the unified header. Defaults to JetpackLogo icon. + */ + logo?: ReactNode; + + /** + * Action elements displayed on the right side of the unified header. + */ + actions?: ReactNode; + + /** + * Tab navigation displayed below the title/tagline in the unified header. + */ + tabs?: ReactNode; + /** * Whether or not to display the Footer */ diff --git a/projects/js-packages/components/index.ts b/projects/js-packages/components/index.ts index 5b2df114305e..7dc51e617bbc 100644 --- a/projects/js-packages/components/index.ts +++ b/projects/js-packages/components/index.ts @@ -34,6 +34,8 @@ export { default as NumberSlider } from './components/number-slider/index.tsx'; export { default as AdminSection } from './components/admin-section/basic/index.tsx'; export { default as AdminSectionHero } from './components/admin-section/hero/index.tsx'; export { default as AdminPage } from './components/admin-page/index.tsx'; +export { default as AdminHeader } from './components/admin-header/index.tsx'; +export type { AdminHeaderProps } from './components/admin-header/types.ts'; export { default as DecorativeCard } from './components/decorative-card/index.tsx'; export { default as Col } from './components/layout/col/index.tsx'; export { default as Testimonials } from './components/testimonials/index.tsx'; diff --git a/projects/js-packages/components/package.json b/projects/js-packages/components/package.json index a349d5d31e40..1713f5b0eeb3 100644 --- a/projects/js-packages/components/package.json +++ b/projects/js-packages/components/package.json @@ -53,6 +53,7 @@ "@automattic/jetpack-script-data": "workspace:*", "@automattic/number-formatters": "workspace:*", "@babel/runtime": "^7", + "@wordpress/admin-ui": "1.8.0", "@wordpress/browserslist-config": "6.40.0", "@wordpress/components": "32.2.0", "@wordpress/compose": "7.40.0", diff --git a/projects/js-packages/components/tools/copy-scss-to-build.mjs b/projects/js-packages/components/tools/copy-scss-to-build.mjs index f2622ba37f5b..505e73b64612 100644 --- a/projects/js-packages/components/tools/copy-scss-to-build.mjs +++ b/projects/js-packages/components/tools/copy-scss-to-build.mjs @@ -50,7 +50,7 @@ async function main() { return; } - for await ( const filePath of fs.glob( '**/*.scss', { + for await ( const filePath of fs.glob( '**/*.{scss,css}', { cwd: packageRoot, exclude: Array.from( IGNORED_DIRS, dir => `**/${ dir }/**` ), } ) ) { diff --git a/projects/packages/publicize/_inc/components/admin-page/index.tsx b/projects/packages/publicize/_inc/components/admin-page/index.tsx index c10b78feb15b..b0ef6e2d3ca1 100644 --- a/projects/packages/publicize/_inc/components/admin-page/index.tsx +++ b/projects/packages/publicize/_inc/components/admin-page/index.tsx @@ -8,6 +8,7 @@ import { } from '@automattic/jetpack-components'; import { useConnection } from '@automattic/jetpack-connection'; import { + getMyJetpackUrl, isJetpackSelfHostedSite, isSimpleSite, siteHasFeature, @@ -15,13 +16,13 @@ import { } from '@automattic/jetpack-script-data'; import { shouldUseInternalLinks } from '@automattic/jetpack-shared-extension-utils'; import { useSelect } from '@wordpress/data'; -import { useState, useCallback } from '@wordpress/element'; +import { createInterpolateElement, useState, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; import { store as socialStore } from '../../social-store'; import { features, getSocialScriptData, hasSocialPaidFeatures } from '../../utils'; import ConnectionScreen from './connection-screen'; import Header from './header'; import InfoSection from './info-section'; -import AdminPageHeader from './page-header'; import './styles.module.scss'; import PricingPage from './pricing-page'; import SupportSection from './support-section'; @@ -65,7 +66,8 @@ export const SocialAdminPage = () => { return ( @@ -78,10 +80,25 @@ export const SocialAdminPage = () => { ); } + const licenseAction = + ! hasSocialPaidFeatures() && isJetpackSite + ? createInterpolateElement( + __( + 'Already have an existing plan or license key? Click here to get started', + 'jetpack-publicize-pkg' + ), + { + a: , + } + ) + : null; + return ( } + title={ __( 'Social', 'jetpack-publicize-pkg' ) } + subTitle={ __( 'Publish once. Share everywhere.', 'jetpack-publicize-pkg' ) } + actions={ licenseAction } showFooter={ isJetpackSite } useInternalLinks={ shouldUseInternalLinks() } > diff --git a/projects/packages/publicize/_inc/components/admin-page/page-header/index.jsx b/projects/packages/publicize/_inc/components/admin-page/page-header/index.jsx deleted file mode 100644 index bbeb0c2ae4f2..000000000000 --- a/projects/packages/publicize/_inc/components/admin-page/page-header/index.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import { getMyJetpackUrl, isJetpackSelfHostedSite } from '@automattic/jetpack-script-data'; -import { createInterpolateElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { hasSocialPaidFeatures } from '../../../utils'; -import Logo from './logo'; -import styles from './styles.module.scss'; - -const AdminPageHeader = () => { - const isJetpackSite = isJetpackSelfHostedSite(); - - return ( -
- - - - - { ! hasSocialPaidFeatures() && isJetpackSite && ( -

- { createInterpolateElement( - __( - 'Already have an existing plan or license key? Click here to get started', - 'jetpack-publicize-pkg' - ), - { - a: , - } - ) } -

- ) } -
- ); -}; - -export default AdminPageHeader; diff --git a/projects/packages/publicize/_inc/components/admin-page/page-header/logo.js b/projects/packages/publicize/_inc/components/admin-page/page-header/logo.js deleted file mode 100644 index fa319c65f09a..000000000000 --- a/projects/packages/publicize/_inc/components/admin-page/page-header/logo.js +++ /dev/null @@ -1,22 +0,0 @@ -const Logo = ( { height = 40 } ) => ( - - - - - - -); - -export default Logo; diff --git a/projects/packages/publicize/_inc/components/admin-page/page-header/styles.module.scss b/projects/packages/publicize/_inc/components/admin-page/page-header/styles.module.scss deleted file mode 100644 index 05a64bb759a4..000000000000 --- a/projects/packages/publicize/_inc/components/admin-page/page-header/styles.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.header { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - gap: calc(var(--horizontal-spacing) * 3); - - .logo { - flex-shrink: 0; - } -} diff --git a/projects/packages/publicize/_inc/components/admin-page/test/page-header.test.jsx b/projects/packages/publicize/_inc/components/admin-page/test/page-header.test.jsx deleted file mode 100644 index f74f6e861369..000000000000 --- a/projects/packages/publicize/_inc/components/admin-page/test/page-header.test.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { clearMockedScriptData, mockScriptData } from '../../../utils/test-utils'; -import AdminPageHeader from '../page-header'; - -describe( 'AdminPageHeader', () => { - beforeEach( () => { - mockScriptData(); - } ); - - afterEach( () => { - clearMockedScriptData(); - } ); - it( 'should show license text when no paid features and is Jetpack site', () => { - render( ); - expect( - screen.getByText( /Already have an existing plan or license key\?/i ) - ).toBeInTheDocument(); - expect( screen.getByRole( 'link' ) ).toHaveAttribute( - 'href', - expect.stringContaining( 'admin.php?page=my-jetpack#/add-license' ) - ); - } ); - - it( 'should not show license text when has paid features', () => { - mockScriptData( { - site: { - plan: { - features: { - active: [ 'social-enhanced-publishing' ], - }, - }, - }, - } ); - render( ); - expect( - screen.queryByText( /Already have an existing plan or license key\?/i ) - ).not.toBeInTheDocument(); - clearMockedScriptData(); - } ); - - it( 'should not show license text when not a Jetpack site', () => { - mockScriptData( { - site: { - host: 'wpcom', - plan: { - features: { - active: [ 'social-enhanced-publishing' ], - }, - }, - }, - } ); - render( ); - expect( - screen.queryByText( /Already have an existing plan or license key\?/i ) - ).not.toBeInTheDocument(); - clearMockedScriptData(); - } ); -} ); diff --git a/projects/packages/publicize/changelog/update-normalize-admin-page-headers b/projects/packages/publicize/changelog/update-normalize-admin-page-headers new file mode 100644 index 000000000000..179dd8abc914 --- /dev/null +++ b/projects/packages/publicize/changelog/update-normalize-admin-page-headers @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Migrate admin page header to use unified AdminHeader component.