newspack-components 4.5.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/DEVELOPMENT.md +1 -1
  3. package/dist/cjs/accordion/index.js +43 -26
  4. package/dist/cjs/accordion/index.test.js +80 -0
  5. package/dist/cjs/accordion/style.scss +25 -21
  6. package/dist/cjs/action-card/style.scss +0 -22
  7. package/dist/cjs/breadcrumbs/index.js +80 -0
  8. package/dist/cjs/breadcrumbs/index.test.js +109 -0
  9. package/dist/cjs/breadcrumbs/style.scss +41 -0
  10. package/dist/cjs/card/core-card.js +8 -4
  11. package/dist/cjs/card/index.js +1 -1
  12. package/dist/cjs/card/style-core.scss +14 -0
  13. package/dist/cjs/card/style.scss +0 -9
  14. package/dist/cjs/card-feature/index.js +25 -13
  15. package/dist/cjs/card-settings-group/index.js +3 -0
  16. package/dist/cjs/confirm-dialog/index.js +34 -8
  17. package/dist/cjs/confirm-dialog/index.test.js +173 -0
  18. package/dist/cjs/divider/style.scss +4 -2
  19. package/dist/cjs/grid/style.scss +5 -0
  20. package/dist/cjs/handoff/index.js +8 -5
  21. package/dist/cjs/index.js +34 -1
  22. package/dist/cjs/integration-icon/index.js +40 -0
  23. package/dist/cjs/integration-icon/style.scss +42 -0
  24. package/dist/cjs/integration-icons/index.js +21 -2
  25. package/dist/cjs/modal/style.scss +4 -0
  26. package/dist/cjs/page/index.js +121 -0
  27. package/dist/cjs/page/index.test.js +69 -0
  28. package/dist/cjs/page/style.scss +86 -0
  29. package/dist/cjs/route-match.js +59 -0
  30. package/dist/cjs/route-match.test.js +77 -0
  31. package/dist/cjs/section-header/index.js +8 -1
  32. package/dist/cjs/section-header/style.scss +11 -11
  33. package/dist/cjs/select-control/GroupedSelectControl.js +1 -1
  34. package/dist/cjs/select-control/style.scss +4 -0
  35. package/dist/cjs/style.scss +1 -4
  36. package/dist/cjs/tabbed-navigation/index.js +188 -40
  37. package/dist/cjs/tabbed-navigation/index.test.js +299 -0
  38. package/dist/cjs/tabbed-navigation/style.scss +77 -57
  39. package/dist/cjs/with-wizard/index.js +34 -45
  40. package/dist/cjs/with-wizard-screen/index.js +82 -41
  41. package/dist/cjs/with-wizard-screen/index.test.js +50 -0
  42. package/dist/cjs/with-wizard-screen/style.scss +15 -118
  43. package/dist/cjs/wizard/breadcrumbs-select.js +70 -0
  44. package/dist/cjs/wizard/breadcrumbs-select.test.js +200 -0
  45. package/dist/cjs/wizard/components/WizardError.js +8 -4
  46. package/dist/cjs/wizard/index.js +162 -141
  47. package/dist/esm/accordion/index.js +42 -26
  48. package/dist/esm/accordion/index.test.js +79 -0
  49. package/dist/esm/accordion/style.scss +25 -21
  50. package/dist/esm/action-card/style.scss +0 -22
  51. package/dist/esm/breadcrumbs/index.js +75 -0
  52. package/dist/esm/breadcrumbs/index.test.js +105 -0
  53. package/dist/esm/breadcrumbs/style.scss +41 -0
  54. package/dist/esm/card/core-card.js +8 -4
  55. package/dist/esm/card/index.js +1 -1
  56. package/dist/esm/card/style-core.scss +14 -0
  57. package/dist/esm/card/style.scss +0 -9
  58. package/dist/esm/card-feature/index.js +25 -13
  59. package/dist/esm/card-settings-group/index.js +3 -0
  60. package/dist/esm/confirm-dialog/index.js +34 -8
  61. package/dist/esm/confirm-dialog/index.test.js +173 -0
  62. package/dist/esm/divider/style.scss +4 -2
  63. package/dist/esm/grid/style.scss +5 -0
  64. package/dist/esm/handoff/index.js +7 -5
  65. package/dist/esm/index.js +5 -1
  66. package/dist/esm/integration-icon/index.js +34 -0
  67. package/dist/esm/integration-icon/style.scss +42 -0
  68. package/dist/esm/integration-icons/index.js +22 -1
  69. package/dist/esm/modal/style.scss +4 -0
  70. package/dist/esm/page/index.js +113 -0
  71. package/dist/esm/page/index.test.js +65 -0
  72. package/dist/esm/page/style.scss +86 -0
  73. package/dist/esm/route-match.js +51 -0
  74. package/dist/esm/route-match.test.js +74 -0
  75. package/dist/esm/section-header/index.js +8 -1
  76. package/dist/esm/section-header/style.scss +11 -11
  77. package/dist/esm/select-control/GroupedSelectControl.js +1 -1
  78. package/dist/esm/select-control/style.scss +4 -0
  79. package/dist/esm/style.scss +1 -4
  80. package/dist/esm/tabbed-navigation/index.js +192 -38
  81. package/dist/esm/tabbed-navigation/index.test.js +296 -0
  82. package/dist/esm/tabbed-navigation/style.scss +77 -57
  83. package/dist/esm/with-wizard/index.js +33 -45
  84. package/dist/esm/with-wizard-screen/index.js +78 -41
  85. package/dist/esm/with-wizard-screen/index.test.js +46 -0
  86. package/dist/esm/with-wizard-screen/style.scss +15 -118
  87. package/dist/esm/wizard/breadcrumbs-select.js +63 -0
  88. package/dist/esm/wizard/breadcrumbs-select.test.js +197 -0
  89. package/dist/esm/wizard/components/WizardError.js +7 -5
  90. package/dist/esm/wizard/index.js +165 -144
  91. package/package.json +10 -2
  92. package/src/accordion/index.js +27 -15
  93. package/src/accordion/index.test.js +62 -0
  94. package/src/accordion/style.scss +25 -21
  95. package/src/action-card/style.scss +0 -22
  96. package/src/breadcrumbs/index.js +63 -0
  97. package/src/breadcrumbs/index.test.js +59 -0
  98. package/src/breadcrumbs/style.scss +41 -0
  99. package/src/card/core-card.js +11 -6
  100. package/src/card/index.js +0 -1
  101. package/src/card/style-core.scss +14 -0
  102. package/src/card/style.scss +0 -9
  103. package/src/card-feature/README.md +3 -3
  104. package/src/card-feature/index.tsx +37 -21
  105. package/src/card-settings-group/index.tsx +4 -0
  106. package/src/card-sortable-list/index.tsx +1 -1
  107. package/src/confirm-dialog/index.test.js +142 -0
  108. package/src/confirm-dialog/index.tsx +34 -7
  109. package/src/divider/style.scss +4 -2
  110. package/src/grid/style.scss +5 -0
  111. package/src/handoff/index.js +4 -3
  112. package/src/index.js +5 -1
  113. package/src/integration-icon/index.tsx +35 -0
  114. package/src/integration-icon/style.scss +42 -0
  115. package/src/integration-icons/index.js +23 -0
  116. package/src/modal/style.scss +4 -0
  117. package/src/page/index.js +89 -0
  118. package/src/page/index.test.js +43 -0
  119. package/src/page/style.scss +86 -0
  120. package/src/route-match.js +47 -0
  121. package/src/route-match.test.js +50 -0
  122. package/src/section-header/index.js +7 -3
  123. package/src/section-header/style.scss +11 -11
  124. package/src/select-control/GroupedSelectControl.js +1 -1
  125. package/src/select-control/style.scss +4 -0
  126. package/src/style.scss +1 -4
  127. package/src/tabbed-navigation/index.js +181 -34
  128. package/src/tabbed-navigation/index.test.js +228 -0
  129. package/src/tabbed-navigation/style.scss +77 -57
  130. package/src/with-wizard/index.js +18 -39
  131. package/src/with-wizard-screen/index.js +70 -35
  132. package/src/with-wizard-screen/index.test.js +36 -0
  133. package/src/with-wizard-screen/style.scss +15 -118
  134. package/src/wizard/breadcrumbs-select.js +51 -0
  135. package/src/wizard/breadcrumbs-select.test.js +124 -0
  136. package/src/wizard/components/WizardError.js +4 -3
  137. package/src/wizard/index.js +160 -146
  138. package/tsconfig.json +7 -0
@@ -34,7 +34,7 @@
34
34
  // Variant
35
35
 
36
36
  &--variant-default {
37
- background-color: wp-colors.$gray-300;
37
+ background-color: var( --wpds-color-stroke-surface-neutral, #{wp-colors.$gray-300} );
38
38
  }
39
39
 
40
40
  &--variant-primary {
@@ -46,6 +46,8 @@
46
46
  }
47
47
 
48
48
  &--variant-tertiary {
49
- background-color: wp-colors.$gray-100;
49
+ // Fallback for consumers whose bundle doesn't carry the wpds token sheet
50
+ // (it ships with the Page header's stylesheet).
51
+ background-color: var( --wpds-color-stroke-surface-neutral-weak, #{wp-colors.$gray-100} );
50
52
  }
51
53
  }
@@ -61,6 +61,11 @@
61
61
  grid-row-gap: 16px;
62
62
  }
63
63
 
64
+ // Row Gap 24
65
+ &__row-gap-24 {
66
+ grid-row-gap: 24px;
67
+ }
68
+
64
69
  // Row Gap 32
65
70
  &__row-gap-32 {
66
71
  grid-row-gap: 32px;
@@ -7,12 +7,13 @@
7
7
  */
8
8
  import apiFetch from '@wordpress/api-fetch';
9
9
  import { Component, Fragment } from '@wordpress/element';
10
+ import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
10
11
  import { __ } from '@wordpress/i18n';
11
12
 
12
13
  /**
13
14
  * Internal dependencies.
14
15
  */
15
- import { Button, Card, Modal, Waiting } from '../';
16
+ import { Button, Modal, Waiting } from '../';
16
17
 
17
18
  /**
18
19
  * External dependencies.
@@ -168,14 +169,14 @@ class Handoff extends Component {
168
169
  { showModal && (
169
170
  <Modal title={ modalTitle } onRequestClose={ () => this.setState( { showModal: false } ) }>
170
171
  <p>{ modalBody }</p>
171
- <Card buttonsCard noBorder className="justify-end">
172
+ <HStack justify="flex-end" spacing={ 4 } wrap className="newspack-modal__footer">
172
173
  <Button variant="secondary" onClick={ () => this.setState( { showModal: false } ) }>
173
174
  { dismissModalButton }
174
175
  </Button>
175
176
  <Button variant="primary" onClick={ goTo }>
176
177
  { primaryModalButton }
177
178
  </Button>
178
- </Card>
179
+ </HStack>
179
180
  </Modal>
180
181
  ) }
181
182
  </Fragment>
package/src/index.js CHANGED
@@ -1,9 +1,10 @@
1
- export { default as Accordion } from './accordion';
1
+ export { default as Accordion, AccordionPanel } from './accordion';
2
2
  export { default as ActionCard } from './action-card';
3
3
  export { default as AutocompleteTokenField } from './autocomplete-tokenfield';
4
4
  export { default as AutocompleteWithSuggestions } from './autocomplete-with-suggestions';
5
5
  export { default as AutocompleteWithLatestPosts } from './autocomplete-with-latest-posts';
6
6
  export { default as Badge } from './badge';
7
+ export { default as Breadcrumbs } from './breadcrumbs';
7
8
  export { default as Button } from './button';
8
9
  export { default as BoxContrast } from './box-contrast';
9
10
  export { default as Card } from './card';
@@ -11,6 +12,7 @@ export { default as CardFeature } from './card-feature';
11
12
  export { default as CardForm } from './card-form';
12
13
  export { default as CardSettingsGroup } from './card-settings-group';
13
14
  export { default as CardSortableList } from './card-sortable-list';
15
+ export { default as IntegrationIcon } from './integration-icon';
14
16
  export { default as CategoryAutocomplete } from './category-autocomplete';
15
17
  export { default as ColorPicker } from './color-picker';
16
18
  export { default as ConfirmDialog } from './confirm-dialog';
@@ -28,6 +30,7 @@ export { default as Grid } from './grid';
28
30
  export { default as Modal } from './modal';
29
31
  export { default as NewspackIcon } from './newspack-icon';
30
32
  export { default as Notice } from './notice';
33
+ export { default as Page } from './page';
31
34
  export { default as PluginInstaller } from './plugin-installer';
32
35
  export { default as PluginSettings } from './plugin-settings';
33
36
  export { default as PluginToggle } from './plugin-toggle';
@@ -54,6 +57,7 @@ export { default as useConfirmDialog } from './hooks/use-confirm-dialog';
54
57
  export { default as useUnsavedChangesDialog } from './hooks/use-unsaved-changes-dialog';
55
58
  import * as integrationIcons from './integration-icons';
56
59
  export { integrationIcons };
60
+ export { espProviderOrder } from './integration-icons';
57
61
  export { default as utils } from './utils';
58
62
 
59
63
  import './style.scss';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { espProviderIcons } from '../integration-icons';
10
+ import './style.scss';
11
+
12
+ /**
13
+ * The single source for how an ESP provider is shown: brand mark plus badge
14
+ * background. A new provider needs an icon file, an `espProviderIcons` entry,
15
+ * and a background rule in this component's stylesheet.
16
+ */
17
+ const IntegrationIcon = ( { provider, className }: { provider: string; className?: string } ) => {
18
+ const icons: Record< string, React.ReactNode > = espProviderIcons;
19
+ const node = Object.prototype.hasOwnProperty.call( espProviderIcons, provider ) ? icons[ provider ] : null;
20
+ if ( ! node ) {
21
+ return null;
22
+ }
23
+ // Decorative: the provider is always named in adjacent visible text (the card
24
+ // title), so hide the mark from assistive tech to avoid duplicate announcements.
25
+ return (
26
+ <span
27
+ aria-hidden="true"
28
+ className={ classnames( 'newspack-integration-icon', `newspack-integration-icon--${ provider.replace( /_/g, '-' ) }`, className ) }
29
+ >
30
+ { node }
31
+ </span>
32
+ );
33
+ };
34
+
35
+ export default IntegrationIcon;
@@ -0,0 +1,42 @@
1
+ @use "~@wordpress/base-styles/colors" as wp-colors;
2
+ @use "~@wordpress/base-styles/variables" as wp-vars;
3
+
4
+ .newspack-integration-icon {
5
+ align-items: center;
6
+ border-radius: wp-vars.$radius-round;
7
+ display: flex;
8
+ flex: 0 0 wp-vars.$grid-unit-50;
9
+ height: wp-vars.$grid-unit-50;
10
+ justify-content: center;
11
+ width: wp-vars.$grid-unit-50;
12
+
13
+ svg {
14
+ display: block;
15
+ fill: currentcolor;
16
+ height: wp-vars.$grid-unit-30;
17
+ width: wp-vars.$grid-unit-30;
18
+ }
19
+
20
+ &--active-campaign {
21
+ background: #004cff;
22
+ }
23
+
24
+ &--mailchimp {
25
+ background: #ffe01b;
26
+ }
27
+
28
+ &--constant-contact {
29
+ background: wp-colors.$gray-100;
30
+ }
31
+
32
+ &--manual {
33
+ background: wp-colors.$gray-800;
34
+ color: wp-colors.$white;
35
+
36
+ // Set the fill on the path directly so the card's active/hover state
37
+ // (which recolours header svgs) can't override the white manual mark.
38
+ svg path {
39
+ fill: currentcolor;
40
+ }
41
+ }
42
+ }
@@ -1,6 +1,29 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { atSymbol } from '@wordpress/icons';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import activeCampaign from './active-campaign';
10
+ import constantContact from './constant-contact';
11
+ import mailchimp from './mailchimp';
12
+
1
13
  export { default as activeCampaign } from './active-campaign';
2
14
  export { default as constantContact } from './constant-contact';
3
15
  export { default as fundraiseUp } from './fundraise-up';
4
16
  export { default as mailchimp } from './mailchimp';
5
17
  export { default as salesforce } from './salesforce';
6
18
  export { default as wisepops } from './wisepops';
19
+
20
+ // ESP provider brand marks keyed by the slug the backend reports
21
+ // (`Newspack_Newsletters::service_provider()`). Rendered via IntegrationIcon.
22
+ export const espProviderIcons = {
23
+ active_campaign: activeCampaign,
24
+ mailchimp,
25
+ constant_contact: constantContact,
26
+ manual: atSymbol,
27
+ };
28
+
29
+ export const espProviderOrder = [ 'active_campaign', 'mailchimp', 'constant_contact', 'manual' ];
@@ -99,3 +99,7 @@
99
99
  .modal-open .components-popover__content {
100
100
  opacity: 0;
101
101
  }
102
+
103
+ .newspack-modal__footer {
104
+ margin-top: 32px;
105
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import { NavigableRegion } from '@wordpress/admin-ui';
5
+ import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
6
+ import { cloneElement, isValidElement } from '@wordpress/element';
7
+ import { Icon } from '@wordpress/icons';
8
+ import { Stack, Text } from '@wordpress/ui';
9
+
10
+ /**
11
+ * External dependencies.
12
+ */
13
+ import classnames from 'classnames';
14
+ import { newspack } from 'newspack-icons';
15
+
16
+ /**
17
+ * Internal dependencies.
18
+ */
19
+ import Breadcrumbs from '../breadcrumbs';
20
+ import './style.scss';
21
+
22
+ /**
23
+ * Newspack page region: a sticky header block (breadcrumbs, actions, optional
24
+ * tabbed navigation) followed by the page content.
25
+ *
26
+ * This intentionally does not use `@wordpress/admin-ui`'s `Page`: that
27
+ * component has no slot for content rendered between the header and the body
28
+ * inside the sticky region, which is exactly where the tab bar must live. Until
29
+ * admin-ui grows such a slot, the header markup is assembled here from the same
30
+ * design-system primitives and tokens, so it can be swapped back later.
31
+ *
32
+ * @param {Object} props
33
+ * @param {Array} props.breadcrumbItems Trail items: `{ label, url? }`.
34
+ * @param {*} [props.badges]
35
+ * @param {*} [props.subTitle]
36
+ * @param {*} [props.actions]
37
+ * @param {*} [props.tabbedNavigation] A `TabbedNavigation` element; its bar renders inside
38
+ * the sticky header block and the page children render
39
+ * inside its active tab panel — or, when no visible tab
40
+ * owns the route, as a sibling of the panels.
41
+ * @param {string} [props.className]
42
+ * @param {*} props.children
43
+ * @return {JSX.Element} Page component.
44
+ */
45
+ const Page = ( { breadcrumbItems = [], badges, subTitle, actions, tabbedNavigation, className, children } ) => {
46
+ const currentLabel = breadcrumbItems[ breadcrumbItems.length - 1 ]?.label;
47
+
48
+ const renderShell = ( tabBar, content ) => (
49
+ <>
50
+ <Stack direction="column" className="newspack-page__header-region">
51
+ <Stack direction="column" className="newspack-page__header">
52
+ <Stack direction="row" gap="sm" justify="space-between">
53
+ <Stack direction="row" gap="sm" align="center" justify="start">
54
+ <div className="newspack-page__header-visual" aria-hidden="true">
55
+ <Icon icon={ newspack } />
56
+ </div>
57
+ <HStack className="newspack-page__breadcrumbs" justify="flex-start">
58
+ <Breadcrumbs items={ breadcrumbItems } />
59
+ </HStack>
60
+ { badges }
61
+ </Stack>
62
+ { actions && (
63
+ <Stack direction="row" gap="sm" align="center" className="newspack-page__header-actions">
64
+ { actions }
65
+ </Stack>
66
+ ) }
67
+ </Stack>
68
+ { subTitle && (
69
+ <Text render={ <p /> } variant="body-md" className="newspack-page__header-subtitle">
70
+ { subTitle }
71
+ </Text>
72
+ ) }
73
+ </Stack>
74
+ { tabBar }
75
+ </Stack>
76
+ { content }
77
+ </>
78
+ );
79
+
80
+ return (
81
+ <NavigableRegion className={ classnames( 'newspack-page', className ) } ariaLabel={ currentLabel }>
82
+ { isValidElement( tabbedNavigation )
83
+ ? cloneElement( tabbedNavigation, { renderShell, content: children } )
84
+ : renderShell( null, children ) }
85
+ </NavigableRegion>
86
+ );
87
+ };
88
+
89
+ export default Page;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * External dependencies.
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+ import { HashRouter } from 'react-router-dom';
6
+
7
+ /**
8
+ * Internal dependencies.
9
+ */
10
+ import Page from './';
11
+
12
+ const renderInRouter = ui => render( <HashRouter>{ ui }</HashRouter> );
13
+
14
+ describe( 'Page', () => {
15
+ it( 'renders the breadcrumb current item as h1 and the content', () => {
16
+ renderInRouter(
17
+ <Page breadcrumbItems={ [ { label: 'Audience', url: '/' }, { label: 'Access control' } ] }>
18
+ <div>Body content</div>
19
+ </Page>
20
+ );
21
+ expect( screen.getByRole( 'heading', { level: 1 } ) ).toHaveTextContent( 'Access control' );
22
+ expect( screen.getByText( 'Body content' ) ).toBeInTheDocument();
23
+ } );
24
+
25
+ it( 'renders actions and subtitle', () => {
26
+ renderInRouter(
27
+ <Page breadcrumbItems={ [ { label: 'Audience' } ] } subTitle="Manage readers" actions={ <button>Add new</button> }>
28
+ <div />
29
+ </Page>
30
+ );
31
+ expect( screen.getByRole( 'button', { name: 'Add new' } ) ).toBeInTheDocument();
32
+ expect( screen.getByText( 'Manage readers' ) ).toBeInTheDocument();
33
+ } );
34
+
35
+ it( 'names the page region after the current breadcrumb item', () => {
36
+ renderInRouter(
37
+ <Page breadcrumbItems={ [ { label: 'Audience', url: '/' }, { label: 'Access control' } ] }>
38
+ <div />
39
+ </Page>
40
+ );
41
+ expect( screen.getByRole( 'region', { name: 'Access control' } ) ).toBeInTheDocument();
42
+ } );
43
+ } );
@@ -0,0 +1,86 @@
1
+ @use "../../../colors/colors.module" as colors;
2
+ @use "~@wordpress/base-styles/colors" as wp-colors;
3
+ @use "~@wordpress/base-styles/mixins" as wp-mixins;
4
+ @use "~@wordpress/base-styles/variables" as wp-vars;
5
+
6
+ // Design-system tokens ride with the Page header into any consuming bundle.
7
+ // Known tech debt: this reaches past the package's public exports entry
8
+ // (`@wordpress/theme/design-tokens.css`) into its internal layout, because
9
+ // postcss-import cannot resolve exports maps, and resolving the public
10
+ // specifier through sass/webpack leaks a bogus `wp-theme/design-tokens.css`
11
+ // script dependency into consumers' asset manifests, which silently blocks
12
+ // their enqueues. If upstream moves `src/prebuilt/`, this fails loudly at
13
+ // build time.
14
+ /* stylelint-disable-next-line no-invalid-position-at-import-rule -- sass @use must precede the CSS @import; postcss-import inlines it at build time. */
15
+ @import "@wordpress/theme/src/prebuilt/css/design-tokens.css";
16
+
17
+ // Newspack brand mapping: the wpds brand color tokens ship in WordPress
18
+ // blue; remap them onto the Newspack primary scale so every design-system
19
+ // component (focus rings, brand buttons, toggles) renders in brand colors
20
+ // without per-component overrides.
21
+ /* stylelint-disable plugin-wpds/no-setting-wpds-custom-properties -- deliberate Newspack brand override of the wpds brand tokens. */
22
+ :root {
23
+ --wpds-color-background-interactive-brand-strong: #{colors.$primary-600};
24
+ --wpds-color-background-interactive-brand-strong-active: #{colors.$primary-800};
25
+ --wpds-color-background-interactive-brand-weak-active: #{colors.$primary-000};
26
+ --wpds-color-background-surface-brand: #{colors.$primary-000};
27
+ --wpds-color-background-thumb-brand: #{colors.$primary-600};
28
+ --wpds-color-background-thumb-brand-active: #{colors.$primary-600};
29
+ --wpds-color-foreground-interactive-brand: #{colors.$primary-600};
30
+ --wpds-color-foreground-interactive-brand-active: #{colors.$primary-800};
31
+ --wpds-color-foreground-interactive-brand-disabled: #{colors.$primary-400};
32
+ --wpds-color-stroke-focus: #{colors.$primary-600};
33
+ --wpds-color-stroke-interactive-brand: #{colors.$primary-600};
34
+ --wpds-color-stroke-interactive-brand-active: #{colors.$primary-800};
35
+ --wpds-color-stroke-interactive-brand-disabled: #{colors.$primary-100};
36
+ --wpds-color-stroke-surface-brand: #{colors.$primary-000};
37
+ --wpds-color-stroke-surface-brand-strong: #{colors.$primary-600};
38
+ }
39
+ /* stylelint-enable plugin-wpds/no-setting-wpds-custom-properties */
40
+
41
+ [role="region"].newspack-page {
42
+ background-color: wp-colors.$white;
43
+ }
44
+
45
+ .newspack-page__header-region {
46
+ @include wp-mixins.break-medium {
47
+ position: sticky;
48
+ top: var(--wp-admin--admin-bar--height);
49
+ z-index: 3;
50
+ }
51
+ }
52
+
53
+ .newspack-page__header {
54
+ /* stylelint-disable-next-line plugin-wpds/no-unknown-ds-tokens -- the plugin's token list lags the prebuilt design-tokens.css names. */
55
+ background: var(--wpds-color-background-surface-neutral-strong);
56
+ border-block-end: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral-weak);
57
+ padding: var(--wpds-dimension-padding-lg) var(--wpds-dimension-padding-2xl);
58
+ }
59
+
60
+ .newspack-page__header-actions {
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ .newspack-page__header-subtitle {
65
+ /* stylelint-disable-next-line plugin-wpds/no-unknown-ds-tokens -- the plugin's token list lags the prebuilt design-tokens.css names. */
66
+ color: var(--wpds-color-foreground-content-neutral-weak);
67
+ padding-block-end: var(--wpds-dimension-padding-xs);
68
+ }
69
+
70
+ .newspack-page__header-visual {
71
+ display: grid;
72
+ flex-shrink: 0;
73
+ height: 24px;
74
+ place-items: center;
75
+ width: 24px;
76
+
77
+ svg path {
78
+ transform: scale(1.1111);
79
+ transform-box: view-box;
80
+ transform-origin: center;
81
+ }
82
+ }
83
+
84
+ .newspack-page__breadcrumbs {
85
+ min-height: wp-vars.$button-size;
86
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Internal dependencies.
3
+ */
4
+ import Router from './proxied-imports/router';
5
+
6
+ const { matchPath } = Router;
7
+
8
+ /**
9
+ * Whether a wildcard pattern (`'/base/*'` or `'/base*'`) matches a pathname.
10
+ * The wildcard is anchored at a `/` boundary: `'/segments*'` matches
11
+ * `/segments` and `/segments/123`, never `/segments-old`.
12
+ *
13
+ * @param {string} pattern Pattern ending in `*`.
14
+ * @param {string} pathname Current router pathname.
15
+ * @return {boolean} Whether the pattern matches.
16
+ */
17
+ const matchesWildcard = ( pattern, pathname ) => {
18
+ const base = pattern.slice( 0, -1 ).replace( /\/$/, '' );
19
+ return pathname === base || pathname.startsWith( base + '/' );
20
+ };
21
+
22
+ /**
23
+ * Single route matcher shared by breadcrumb selection and tabbed navigation.
24
+ *
25
+ * An item matches when one of its `activeTabPaths` hits (exact string, or a
26
+ * `*` wildcard anchored at a `/` boundary), or — falling through on a miss —
27
+ * when its `path` matches the pathname. Path matching is prefix-based except
28
+ * for the root path and items opting in via `exact: true`, mirroring how the
29
+ * wizard registers its `<Route>`s (`exact={ section.exact ?? false }`).
30
+ *
31
+ * @param {Object} item Route item (`{ path?, exact?, activeTabPaths? }`).
32
+ * @param {string} pathname Current router pathname.
33
+ * @return {boolean} Whether the item matches the pathname.
34
+ */
35
+ export const matchesRoute = ( item, pathname ) => {
36
+ if ( Array.isArray( item.activeTabPaths ) ) {
37
+ const wildcardHit = item.activeTabPaths.some( path => ( path.endsWith( '*' ) ? matchesWildcard( path, pathname ) : path === pathname ) );
38
+ if ( wildcardHit ) {
39
+ return true;
40
+ }
41
+ }
42
+ if ( ! item.path ) {
43
+ return false;
44
+ }
45
+ const exact = '/' === item.path || item.exact === true;
46
+ return !! matchPath( pathname, { path: item.path, exact } );
47
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Internal dependencies.
3
+ */
4
+ import { matchesRoute } from './route-match';
5
+
6
+ describe( 'matchesRoute', () => {
7
+ it( 'matches the root path exactly, never as a prefix', () => {
8
+ expect( matchesRoute( { path: '/' }, '/' ) ).toBe( true );
9
+ expect( matchesRoute( { path: '/' }, '/segments' ) ).toBe( false );
10
+ } );
11
+
12
+ it( 'matches a path as a prefix by default, at segment boundaries', () => {
13
+ expect( matchesRoute( { path: '/segments' }, '/segments' ) ).toBe( true );
14
+ expect( matchesRoute( { path: '/segments' }, '/segments/123' ) ).toBe( true );
15
+ expect( matchesRoute( { path: '/segments' }, '/segments-old' ) ).toBe( false );
16
+ expect( matchesRoute( { path: '/segments' }, '/donations' ) ).toBe( false );
17
+ } );
18
+
19
+ it( 'matches exactly when the item opts in via exact', () => {
20
+ expect( matchesRoute( { path: '/segments', exact: true }, '/segments' ) ).toBe( true );
21
+ expect( matchesRoute( { path: '/segments', exact: true }, '/segments/123' ) ).toBe( false );
22
+ } );
23
+
24
+ it( 'matches an activeTabPaths entry exactly', () => {
25
+ const item = { path: '/other', activeTabPaths: [ '/segments/new' ] };
26
+ expect( matchesRoute( item, '/segments/new' ) ).toBe( true );
27
+ expect( matchesRoute( item, '/segments/123' ) ).toBe( false );
28
+ } );
29
+
30
+ it( 'anchors an activeTabPaths wildcard at a segment boundary', () => {
31
+ const item = { path: '/other', activeTabPaths: [ '/segments/*' ] };
32
+ expect( matchesRoute( item, '/segments' ) ).toBe( true );
33
+ expect( matchesRoute( item, '/segments/123' ) ).toBe( true );
34
+ expect( matchesRoute( item, '/segments-old' ) ).toBe( false );
35
+
36
+ const noSlash = { path: '/other', activeTabPaths: [ '/segments*' ] };
37
+ expect( matchesRoute( noSlash, '/segments' ) ).toBe( true );
38
+ expect( matchesRoute( noSlash, '/segments/123' ) ).toBe( true );
39
+ expect( matchesRoute( noSlash, '/segments-old' ) ).toBe( false );
40
+ } );
41
+
42
+ it( 'falls through to path matching when activeTabPaths misses', () => {
43
+ expect( matchesRoute( { path: '/segments', activeTabPaths: [ '/elsewhere' ] }, '/segments/123' ) ).toBe( true );
44
+ } );
45
+
46
+ it( 'never matches an item without a path when activeTabPaths misses', () => {
47
+ expect( matchesRoute( {}, '/segments' ) ).toBe( false );
48
+ expect( matchesRoute( { activeTabPaths: [ '/elsewhere' ] }, '/segments' ) ).toBe( false );
49
+ } );
50
+ } );
@@ -85,14 +85,18 @@ const SectionHeader = ( {
85
85
  pageHeader && 'newspack-section-header--page-header'
86
86
  );
87
87
 
88
- const HeadingTag = pageHeader ? 'h1' : `h${ heading }`;
88
+ // The breadcrumb `Page` owns the single page `<h1>`, so a `pageHeader` section
89
+ // is a secondary heading: its level follows `heading` (default 2). `pageHeader`
90
+ // controls only the enlarged, centered styling — not the tag. Pass `heading={ 1 }`
91
+ // on a headerless screen that needs the section header to be the page's h1.
92
+ const HeadingTag = `h${ heading }`;
89
93
 
90
94
  let titleContent = null;
91
95
 
92
96
  if ( typeof title === 'string' ) {
93
97
  titleContent = (
94
98
  <div className="newspack-section-header__title-container">
95
- <HeadingTag>
99
+ <HeadingTag className="newspack-section-header__title">
96
100
  { title }
97
101
  { badges?.length
98
102
  ? badges.map( ( badge, i ) => <Badge key={ i } text={ badge.label } level={ badge.level || 'default' } /> )
@@ -126,7 +130,7 @@ const SectionHeader = ( {
126
130
  </div>
127
131
  );
128
132
  } else if ( typeof title === 'function' ) {
129
- titleContent = <HeadingTag>{ title() }</HeadingTag>;
133
+ titleContent = <HeadingTag className="newspack-section-header__title">{ title() }</HeadingTag>;
130
134
  }
131
135
 
132
136
  return (
@@ -4,6 +4,7 @@
4
4
 
5
5
  @use "~@wordpress/base-styles/colors" as wp-colors;
6
6
  @use "~@wordpress/base-styles/variables" as wp-vars;
7
+ @use "~@wordpress/base-styles/mixins" as wp-mixins;
7
8
  @use "../../../colors/colors.module" as colors;
8
9
 
9
10
  .newspack-section-header {
@@ -12,6 +13,9 @@
12
13
 
13
14
  &__container {
14
15
  position: relative;
16
+ p {
17
+ text-wrap: balance;
18
+ }
15
19
  &:not(:first-child) {
16
20
  margin-top: 48px;
17
21
  }
@@ -84,17 +88,18 @@
84
88
  color: wp-colors.$gray-700;
85
89
  }
86
90
 
91
+ // Title sizing keyed to the class, not the tag, so demoting the heading level
92
+ // (h1 -> h2 when the breadcrumb Page owns the page h1) never changes its look,
93
+ // and genuine h2s elsewhere in the header are unaffected.
94
+ &__title {
95
+ @include wp-mixins.heading-x-large();
96
+ }
97
+
87
98
  &__title-container {
88
99
  align-items: center;
89
100
  display: flex;
90
101
  gap: 8px;
91
102
 
92
- h2 {
93
- font-size: wp-vars.$font-size-x-large;
94
- font-weight: wp-vars.$font-weight-medium;
95
- line-height: wp-vars.$font-line-height-x-large;
96
- }
97
-
98
103
  .newspack-section-header__menu,
99
104
  .newspack-section-header__secondary-action,
100
105
  .newspack-badge {
@@ -161,10 +166,5 @@
161
166
  fill: var(--wp-admin-theme-color);
162
167
  }
163
168
  }
164
- h1 {
165
- font-size: wp-vars.$font-size-x-large;
166
- font-weight: wp-vars.$font-weight-medium;
167
- line-height: wp-vars.$font-line-height-x-large;
168
- }
169
169
  }
170
170
  }
@@ -36,7 +36,7 @@ export default function GroupedSelectControl( { help, label, onChange, optgroups
36
36
  help={ help }
37
37
  className={ classnames( className, 'components-select-control' ) }
38
38
  >
39
- <div className="relative">
39
+ <div className="newspack-grouped-select-control__field">
40
40
  <select
41
41
  id={ id }
42
42
  className="components-select-control__input"
@@ -43,6 +43,10 @@
43
43
  .newspack-grouped-select-control {
44
44
  position: relative;
45
45
 
46
+ &__field {
47
+ position: relative;
48
+ }
49
+
46
50
  .components-select-control {
47
51
  &__input {
48
52
  appearance: none;
package/src/style.scss CHANGED
@@ -35,12 +35,9 @@
35
35
  // Checkboxes
36
36
  .newspack-checkbox-control {
37
37
  margin-top: 0 !important;
38
- .components-checkbox-control__label {
39
- font-weight: 600;
40
- }
41
38
 
42
39
  .components-base-control__help {
43
- margin-left: 32px;
40
+ margin-left: calc(var(--checkbox-input-size) + var(--checkbox-input-margin));
44
41
  margin-top: 0;
45
42
  }
46
43
  }