newspack-components 4.7.0 → 4.8.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 (251) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/DEVELOPMENT.md +42 -21
  3. package/dist/cjs/_mixins.scss +13 -0
  4. package/dist/cjs/_variables.scss +12 -0
  5. package/dist/cjs/action-card/index.js +18 -10
  6. package/dist/cjs/action-card/index.test.js +91 -0
  7. package/dist/cjs/action-card/style.scss +2 -40
  8. package/dist/cjs/button/index.js +8 -3
  9. package/dist/cjs/button/index.test.js +61 -5
  10. package/dist/cjs/card/core-card.js +0 -2
  11. package/dist/cjs/card/style-core.scss +7 -5
  12. package/dist/cjs/card-feature/index.js +75 -61
  13. package/dist/cjs/card-feature/index.test.js +396 -54
  14. package/dist/cjs/card-feature/style.scss +20 -31
  15. package/dist/cjs/card-form/index.js +5 -5
  16. package/dist/cjs/card-sortable-list/index.js +5 -3
  17. package/dist/cjs/card-sortable-list/index.test.js +50 -0
  18. package/dist/cjs/collapsible-group/context.js +19 -0
  19. package/dist/cjs/collapsible-group/index.js +24 -0
  20. package/dist/cjs/collapsible-group/index.test.js +215 -0
  21. package/dist/cjs/collapsible-group/item.js +61 -0
  22. package/dist/cjs/collapsible-group/root.js +62 -0
  23. package/dist/cjs/collapsible-group/style.scss +47 -0
  24. package/dist/cjs/collapsible-group/types.js +5 -0
  25. package/dist/cjs/empty-state/actions.js +45 -0
  26. package/dist/cjs/empty-state/context.js +41 -0
  27. package/dist/cjs/empty-state/header.js +66 -0
  28. package/dist/cjs/empty-state/index.js +31 -0
  29. package/dist/cjs/empty-state/index.test.js +303 -0
  30. package/dist/cjs/empty-state/root.js +56 -0
  31. package/dist/cjs/empty-state/style.scss +51 -0
  32. package/dist/cjs/empty-state/types.js +5 -0
  33. package/dist/cjs/index.js +34 -20
  34. package/dist/cjs/info-button/index.js +59 -43
  35. package/dist/cjs/info-button/index.test.js +199 -0
  36. package/dist/cjs/info-button/style.scss +41 -9
  37. package/dist/cjs/modal/style.scss +12 -12
  38. package/dist/cjs/newspack-icon/style.scss +4 -4
  39. package/dist/cjs/notice/style.scss +3 -3
  40. package/dist/cjs/page/style.scss +16 -16
  41. package/dist/cjs/plugin-installer/index.js +5 -9
  42. package/dist/cjs/plugin-installer/style.scss +3 -14
  43. package/dist/cjs/section-header/index.js +31 -18
  44. package/dist/cjs/section-header/index.test.js +40 -0
  45. package/dist/cjs/section-header/style.scss +22 -6
  46. package/dist/cjs/settings/DateRangeSetting.js +309 -0
  47. package/dist/cjs/settings/SettingsSection.js +19 -5
  48. package/dist/cjs/settings/SettingsSection.test.js +49 -0
  49. package/dist/cjs/settings/index.js +3 -1
  50. package/dist/cjs/settings/style.scss +40 -7
  51. package/dist/cjs/stat-card/body.js +41 -0
  52. package/dist/cjs/stat-card/constants.js +11 -0
  53. package/dist/cjs/stat-card/context.js +54 -0
  54. package/dist/cjs/stat-card/delta.js +91 -0
  55. package/dist/cjs/stat-card/footer.js +72 -0
  56. package/dist/cjs/stat-card/index.js +47 -0
  57. package/dist/cjs/stat-card/index.test.js +938 -0
  58. package/dist/cjs/stat-card/label.js +66 -0
  59. package/dist/cjs/stat-card/root.js +69 -0
  60. package/dist/cjs/stat-card/secondary.js +38 -0
  61. package/dist/cjs/stat-card/style.scss +89 -0
  62. package/dist/cjs/stat-card/types.js +5 -0
  63. package/dist/cjs/stat-card/value.js +78 -0
  64. package/dist/cjs/status-indicator/index.js +66 -0
  65. package/dist/cjs/status-indicator/index.test.js +101 -0
  66. package/dist/cjs/status-indicator/statuses.js +39 -0
  67. package/dist/cjs/status-indicator/style.scss +9 -0
  68. package/dist/cjs/status-indicator/types.js +5 -0
  69. package/dist/cjs/style.scss +9 -9
  70. package/dist/cjs/tabbed-navigation/style.scss +6 -4
  71. package/dist/cjs/types.js +5 -0
  72. package/dist/cjs/web-preview/style.scss +11 -11
  73. package/dist/cjs/with-wizard/style.scss +10 -10
  74. package/dist/cjs/with-wizard-screen/style.scss +9 -9
  75. package/dist/cjs/wizard/index.js +33 -4
  76. package/dist/cjs/wizard/store/index.js +23 -1
  77. package/dist/esm/_mixins.scss +13 -0
  78. package/dist/esm/_variables.scss +12 -0
  79. package/dist/esm/action-card/index.js +18 -10
  80. package/dist/esm/action-card/index.test.js +89 -0
  81. package/dist/esm/action-card/style.scss +2 -40
  82. package/dist/esm/button/index.js +9 -4
  83. package/dist/esm/button/index.test.js +63 -5
  84. package/dist/esm/card/core-card.js +0 -2
  85. package/dist/esm/card/style-core.scss +7 -5
  86. package/dist/esm/card-feature/index.js +78 -62
  87. package/dist/esm/card-feature/index.test.js +397 -55
  88. package/dist/esm/card-feature/style.scss +20 -31
  89. package/dist/esm/card-form/index.js +5 -5
  90. package/dist/esm/card-sortable-list/index.js +6 -4
  91. package/dist/esm/card-sortable-list/index.test.js +46 -0
  92. package/dist/esm/collapsible-group/context.js +13 -0
  93. package/dist/esm/collapsible-group/index.js +17 -0
  94. package/dist/esm/collapsible-group/index.test.js +215 -0
  95. package/dist/esm/collapsible-group/item.js +53 -0
  96. package/dist/esm/collapsible-group/root.js +54 -0
  97. package/dist/esm/collapsible-group/style.scss +47 -0
  98. package/dist/esm/collapsible-group/types.js +1 -0
  99. package/dist/esm/empty-state/actions.js +37 -0
  100. package/dist/esm/empty-state/context.js +35 -0
  101. package/dist/esm/empty-state/header.js +58 -0
  102. package/dist/esm/empty-state/index.js +23 -0
  103. package/dist/esm/empty-state/index.test.js +299 -0
  104. package/dist/esm/empty-state/root.js +56 -0
  105. package/dist/esm/empty-state/style.scss +51 -0
  106. package/dist/esm/empty-state/types.js +1 -0
  107. package/dist/esm/index.js +4 -2
  108. package/dist/esm/info-button/index.js +54 -43
  109. package/dist/esm/info-button/index.test.js +195 -0
  110. package/dist/esm/info-button/style.scss +41 -9
  111. package/dist/esm/modal/style.scss +12 -12
  112. package/dist/esm/newspack-icon/style.scss +4 -4
  113. package/dist/esm/notice/style.scss +3 -3
  114. package/dist/esm/page/style.scss +16 -16
  115. package/dist/esm/plugin-installer/index.js +5 -9
  116. package/dist/esm/plugin-installer/style.scss +3 -14
  117. package/dist/esm/section-header/index.js +27 -14
  118. package/dist/esm/section-header/index.test.js +36 -0
  119. package/dist/esm/section-header/style.scss +22 -6
  120. package/dist/esm/settings/DateRangeSetting.js +304 -0
  121. package/dist/esm/settings/SettingsSection.js +16 -3
  122. package/dist/esm/settings/SettingsSection.test.js +45 -0
  123. package/dist/esm/settings/index.js +3 -1
  124. package/dist/esm/settings/style.scss +40 -7
  125. package/dist/esm/stat-card/body.js +36 -0
  126. package/dist/esm/stat-card/constants.js +5 -0
  127. package/dist/esm/stat-card/context.js +48 -0
  128. package/dist/esm/stat-card/delta.js +86 -0
  129. package/dist/esm/stat-card/footer.js +67 -0
  130. package/dist/esm/stat-card/index.js +33 -0
  131. package/dist/esm/stat-card/index.test.js +939 -0
  132. package/dist/esm/stat-card/label.js +61 -0
  133. package/dist/esm/stat-card/root.js +64 -0
  134. package/dist/esm/stat-card/secondary.js +33 -0
  135. package/dist/esm/stat-card/style.scss +89 -0
  136. package/dist/esm/stat-card/types.js +1 -0
  137. package/dist/esm/stat-card/value.js +73 -0
  138. package/dist/esm/status-indicator/index.js +52 -0
  139. package/dist/esm/status-indicator/index.test.js +96 -0
  140. package/dist/esm/status-indicator/statuses.js +33 -0
  141. package/dist/esm/status-indicator/style.scss +9 -0
  142. package/dist/esm/status-indicator/types.js +1 -0
  143. package/dist/esm/style.scss +9 -9
  144. package/dist/esm/tabbed-navigation/style.scss +6 -4
  145. package/dist/esm/types.js +1 -0
  146. package/dist/esm/web-preview/style.scss +11 -11
  147. package/dist/esm/with-wizard/style.scss +10 -10
  148. package/dist/esm/with-wizard-screen/style.scss +9 -9
  149. package/dist/esm/wizard/index.js +38 -7
  150. package/dist/esm/wizard/store/index.js +23 -1
  151. package/package.json +1 -1
  152. package/src/_mixins.scss +13 -0
  153. package/src/_variables.scss +12 -0
  154. package/src/action-card/action-card.d.ts +3 -2
  155. package/src/action-card/index.js +12 -12
  156. package/src/action-card/index.test.js +56 -0
  157. package/src/action-card/style.scss +2 -40
  158. package/src/button/index.test.js +52 -5
  159. package/src/button/index.tsx +9 -4
  160. package/src/card/core-card.js +0 -2
  161. package/src/card/style-core.scss +7 -5
  162. package/src/card-feature/README.md +68 -27
  163. package/src/card-feature/index.test.js +242 -27
  164. package/src/card-feature/index.tsx +77 -67
  165. package/src/card-feature/style.scss +20 -31
  166. package/src/card-form/README.md +10 -10
  167. package/src/card-form/index.tsx +4 -8
  168. package/src/card-sortable-list/README.md +4 -5
  169. package/src/card-sortable-list/index.test.js +29 -0
  170. package/src/card-sortable-list/index.tsx +7 -4
  171. package/src/collapsible-group/README.md +50 -0
  172. package/src/collapsible-group/context.ts +13 -0
  173. package/src/collapsible-group/index.test.js +168 -0
  174. package/src/collapsible-group/index.tsx +17 -0
  175. package/src/collapsible-group/item.tsx +42 -0
  176. package/src/collapsible-group/root.tsx +42 -0
  177. package/src/collapsible-group/style.scss +47 -0
  178. package/src/collapsible-group/types.ts +22 -0
  179. package/src/drawer/README.md +2 -2
  180. package/src/empty-state/README.md +165 -0
  181. package/src/empty-state/actions.tsx +37 -0
  182. package/src/empty-state/context.ts +40 -0
  183. package/src/empty-state/header.tsx +48 -0
  184. package/src/empty-state/index.test.js +247 -0
  185. package/src/empty-state/index.tsx +22 -0
  186. package/src/empty-state/root.tsx +39 -0
  187. package/src/empty-state/style.scss +51 -0
  188. package/src/empty-state/types.ts +33 -0
  189. package/src/index.js +4 -2
  190. package/src/info-button/README.md +115 -0
  191. package/src/info-button/index.test.js +163 -0
  192. package/src/info-button/index.tsx +64 -0
  193. package/src/info-button/style.scss +41 -9
  194. package/src/modal/style.scss +12 -12
  195. package/src/newspack-icon/style.scss +4 -4
  196. package/src/notice/style.scss +3 -3
  197. package/src/page/style.scss +16 -16
  198. package/src/plugin-installer/index.js +3 -9
  199. package/src/plugin-installer/style.scss +3 -14
  200. package/src/section-header/index.js +27 -14
  201. package/src/section-header/index.test.js +28 -0
  202. package/src/section-header/style.scss +22 -6
  203. package/src/settings/DateRangeSetting.js +266 -0
  204. package/src/settings/SettingsSection.js +24 -4
  205. package/src/settings/SettingsSection.test.js +45 -0
  206. package/src/settings/index.js +2 -0
  207. package/src/settings/style.scss +40 -7
  208. package/src/stat-card/README.md +434 -0
  209. package/src/stat-card/body.tsx +28 -0
  210. package/src/stat-card/constants.ts +5 -0
  211. package/src/stat-card/context.ts +53 -0
  212. package/src/stat-card/delta.tsx +88 -0
  213. package/src/stat-card/footer.tsx +68 -0
  214. package/src/stat-card/index.test.js +818 -0
  215. package/src/stat-card/index.tsx +48 -0
  216. package/src/stat-card/label.tsx +61 -0
  217. package/src/stat-card/root.tsx +40 -0
  218. package/src/stat-card/secondary.tsx +27 -0
  219. package/src/stat-card/style.scss +89 -0
  220. package/src/stat-card/types.ts +84 -0
  221. package/src/stat-card/value.tsx +72 -0
  222. package/src/status-indicator/README.md +123 -0
  223. package/src/status-indicator/index.test.js +70 -0
  224. package/src/status-indicator/index.tsx +39 -0
  225. package/src/status-indicator/statuses.ts +35 -0
  226. package/src/status-indicator/style.scss +9 -0
  227. package/src/status-indicator/types.ts +28 -0
  228. package/src/style.scss +9 -9
  229. package/src/tabbed-navigation/style.scss +6 -4
  230. package/src/types.ts +25 -0
  231. package/src/web-preview/style.scss +11 -11
  232. package/src/with-wizard/style.scss +10 -10
  233. package/src/with-wizard-screen/style.scss +9 -9
  234. package/src/wizard/index.js +49 -7
  235. package/src/wizard/store/index.js +26 -1
  236. package/dist/cjs/accordion/index.js +0 -72
  237. package/dist/cjs/accordion/index.test.js +0 -80
  238. package/dist/cjs/accordion/style.scss +0 -35
  239. package/dist/cjs/badge/index.js +0 -32
  240. package/dist/cjs/badge/style.scss +0 -36
  241. package/dist/esm/accordion/index.js +0 -63
  242. package/dist/esm/accordion/index.test.js +0 -79
  243. package/dist/esm/accordion/style.scss +0 -35
  244. package/dist/esm/badge/index.js +0 -24
  245. package/dist/esm/badge/style.scss +0 -36
  246. package/src/accordion/index.js +0 -46
  247. package/src/accordion/index.test.js +0 -62
  248. package/src/accordion/style.scss +0 -35
  249. package/src/badge/index.tsx +0 -26
  250. package/src/badge/style.scss +0 -36
  251. package/src/info-button/index.js +0 -38
@@ -7,20 +7,19 @@ import classnames from 'classnames';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { __, _x, sprintf } from '@wordpress/i18n';
10
- import { isValidElement } from '@wordpress/element';
11
- import { DropdownMenu, __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
10
+ import { createElement, isValidElement } from '@wordpress/element';
11
+ import { useInstanceId } from '@wordpress/compose';
12
+ import { DropdownMenu } from '@wordpress/components';
12
13
  import { moreVertical } from '@wordpress/icons';
14
+ import { Badge, Card, Stack } from '@wordpress/ui';
13
15
 
14
16
  /**
15
17
  * Internal dependencies
16
18
  */
17
- import Badge from '../badge';
18
19
  import Button from '../button';
19
- import Card from '../card';
20
+ import type { BadgeIntent, CardBadge, HeadingLevel } from '../types';
20
21
  import './style.scss';
21
22
 
22
- type BadgeLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
23
-
24
23
  type CardFeatureIcon = {
25
24
  /** The icon node to render (e.g. a WordPress <Icon> component). */
26
25
  node: React.ReactNode;
@@ -31,7 +30,7 @@ type CardFeatureIcon = {
31
30
  /**
32
31
  * Border-radius of the icon container.
33
32
  * 'small' uses $radius-small (2px), 'full' uses $radius-round (50%).
34
- * Only relevant when backgroundColor is set.
33
+ * Only relevant when backgroundColor is set, where it defaults to 'small'.
35
34
  */
36
35
  radius?: 'small' | 'full';
37
36
  };
@@ -44,16 +43,19 @@ type MoreControl = {
44
43
 
45
44
  type CardFeatureProps = {
46
45
  title: string;
46
+ /** Heading level for the title. Defaults to 3, which sits under a `SectionHeader` or a `WizardsTab` heading. */
47
+ headingLevel?: HeadingLevel;
47
48
  description?: string;
48
49
  /** Icon shown beside the title: a descriptor (coloured badge) or a ready element rendered as-is. */
49
50
  icon?: CardFeatureIcon | React.ReactElement;
50
51
  /** Whether the feature is currently enabled. */
51
52
  enabled?: boolean;
52
53
  /**
53
- * When set, the card enters the "unmet requirements" state: an error
54
- * badge displays this string and the title/description are muted. By
55
- * default the primary button is disabled — set `requirementsActionable`
54
+ * When set, the card enters the "unmet requirements" state: an error badge
55
+ * displays this string and the title drops to the muted text colour. By
56
+ * default the primary button is blocked — set `requirementsActionable`
56
57
  * if the primary button is the remediation for the unmet requirement.
58
+ * Also the button's accessible description, so it must read after the label.
57
59
  */
58
60
  requirements?: string;
59
61
  /**
@@ -63,22 +65,24 @@ type CardFeatureProps = {
63
65
  * (e.g. can be disabled), unlike a hard-locked requirement.
64
66
  */
65
67
  requirementsActionable?: boolean;
66
- /** Primary button label when not enabled. Default: "Enable". */
68
+ /** Label for the primary button in its "Enable" states: not enabled, or enabled with an unmet requirement. Default: "Enable". */
67
69
  enableLabel?: string;
68
70
  /** Show the primary button as busy (spinner) and disabled while an action is in flight. */
69
71
  busy?: boolean;
70
- /** Primary button label when enabled. Default: "Configure". */
72
+ /** Label for the primary button in its "Configure" state: enabled, with no unmet requirement. Default: "Configure". */
71
73
  configureLabel?: string;
72
- /** Called when the primary button is clicked and the feature is not enabled. */
74
+ /**
75
+ * Called when the primary button is clicked while it reads "Enable". That is
76
+ * the not-enabled state, and also the enabled state with unmet requirements,
77
+ * where the requirement rather than the feature is what the button acts on.
78
+ */
73
79
  onEnable?: () => void;
74
- /** Called when the primary button is clicked and the feature is enabled. */
80
+ /** Called when the primary button is clicked while it reads "Configure": enabled, with no unmet requirements. */
75
81
  onConfigure?: () => void;
76
82
  /** Controls rendered inside the "More" dropdown, shown when enabled — including the unmet-requirements state when `requirementsActionable`. */
77
83
  moreControls?: MoreControl[];
78
- /** Badge text shown when enabled. Default: "Enabled". */
79
- badgeText?: string;
80
- /** Badge level shown when enabled. Default: "success". */
81
- badgeLevel?: BadgeLevel;
84
+ /** Badge shown when enabled. Ignored while `requirements` is set, which takes the badge. Defaults to "Enabled" at the "stable" intent. */
85
+ badge?: CardBadge;
82
86
  className?: string;
83
87
  };
84
88
 
@@ -91,6 +95,7 @@ type CardFeatureProps = {
91
95
  */
92
96
  const CardFeature = ( {
93
97
  title,
98
+ headingLevel = 3,
94
99
  description,
95
100
  icon,
96
101
  enabled = false,
@@ -102,20 +107,22 @@ const CardFeature = ( {
102
107
  onEnable,
103
108
  onConfigure,
104
109
  moreControls,
105
- badgeText,
106
- badgeLevel = 'success',
110
+ badge: badgeProp,
107
111
  className,
108
112
  }: CardFeatureProps ) => {
113
+ const instanceId = useInstanceId( CardFeature, 'newspack-card-feature' );
114
+ const badgeId = `${ instanceId }__badge`;
115
+ const describedById = requirements ? badgeId : undefined;
109
116
  const isMuted = !! requirements;
110
117
  const classes = classnames( 'newspack-card-feature', className, {
111
118
  'newspack-card-feature--muted': isMuted,
112
119
  } );
113
120
 
114
- let badge: { text: string; level: BadgeLevel } | undefined;
121
+ let badge: { label: string; intent: BadgeIntent } | undefined;
115
122
  if ( requirements ) {
116
- badge = { text: requirements, level: 'error' };
123
+ badge = { label: requirements, intent: 'high' };
117
124
  } else if ( enabled ) {
118
- badge = { text: badgeText ?? __( 'Enabled', 'newspack-plugin' ), level: badgeLevel };
125
+ badge = { label: badgeProp?.label ?? __( 'Enabled', 'newspack-plugin' ), intent: badgeProp?.intent ?? 'stable' };
119
126
  }
120
127
 
121
128
  const isConfigureState = enabled && ! requirements;
@@ -149,7 +156,9 @@ const CardFeature = ( {
149
156
  renderedIcon = icon;
150
157
  } else if ( iconDescriptor ) {
151
158
  renderedIcon = (
159
+ // Decorative: a vendor mark passed as `node` carries no aria-hidden of its own.
152
160
  <div
161
+ aria-hidden="true"
153
162
  className={ iconClasses }
154
163
  style={ {
155
164
  backgroundColor: iconDescriptor.backgroundColor,
@@ -162,51 +171,52 @@ const CardFeature = ( {
162
171
  }
163
172
 
164
173
  return (
165
- <Card
166
- className={ classes }
167
- __experimentalCoreCard
168
- __experimentalCoreProps={ {
169
- headerStyle: { padding: 32 },
170
- header: (
171
- <>
172
- <HStack alignment="top" spacing={ 4 }>
173
- <div className="newspack-card-feature__content">
174
- <h2 className="newspack-card-feature__title">{ title }</h2>
175
- { description && <p className="newspack-card-feature__description">{ description }</p> }
176
- </div>
177
- { renderedIcon }
178
- </HStack>
179
- <HStack alignment="edge">
180
- <HStack expanded={ false } spacing="8px">
181
- <Button
182
- variant={ isConfigureState ? 'tertiary' : 'secondary' }
183
- disabled={ ( isMuted && ! requirementsActionable ) || busy }
184
- isBusy={ busy }
185
- onClick={ handleButtonClick }
186
- size="compact"
187
- aria-label={ buttonAccessibleLabel }
188
- >
189
- { buttonLabel }
190
- </Button>
191
- { showMoreControls && (
192
- <DropdownMenu
193
- icon={ moreVertical }
194
- label={ sprintf(
195
- // translators: %s: the feature's name.
196
- __( 'More options for %s', 'newspack-plugin' ),
197
- title
198
- ) }
199
- controls={ moreControls }
200
- toggleProps={ { size: 'compact' } }
201
- />
174
+ <Card.Root className={ classes }>
175
+ <Card.Header>
176
+ <Stack direction="row" align="start" gap="lg">
177
+ <Stack className="newspack-card-feature__content" direction="column" gap="sm">
178
+ { createElement( `h${ headingLevel }`, { className: 'newspack-card-feature__title' }, title ) }
179
+ { description && <p className="newspack-card-feature__description">{ description }</p> }
180
+ </Stack>
181
+ { renderedIcon }
182
+ </Stack>
183
+ </Card.Header>
184
+ <Card.Content className="newspack-card-feature__actions">
185
+ <Stack direction="row" align="center" justify="space-between" gap="sm" wrap="wrap">
186
+ <Stack direction="row" align="center" gap="sm">
187
+ <Button
188
+ variant={ isConfigureState ? 'tertiary' : 'secondary' }
189
+ accessibleWhenDisabled
190
+ aria-describedby={ describedById }
191
+ aria-label={ buttonAccessibleLabel }
192
+ disabled={ ( isMuted && ! requirementsActionable ) || busy }
193
+ isBusy={ busy }
194
+ onClick={ handleButtonClick }
195
+ size="compact"
196
+ >
197
+ { buttonLabel }
198
+ </Button>
199
+ { showMoreControls && (
200
+ <DropdownMenu
201
+ icon={ moreVertical }
202
+ label={ sprintf(
203
+ // translators: %s: the feature's name.
204
+ __( 'More options for %s', 'newspack-plugin' ),
205
+ title
202
206
  ) }
203
- </HStack>
204
- { badge && <Badge text={ badge.text } level={ badge.level } /> }
205
- </HStack>
206
- </>
207
- ),
208
- } }
209
- />
207
+ controls={ moreControls }
208
+ toggleProps={ { size: 'compact' } }
209
+ />
210
+ ) }
211
+ </Stack>
212
+ { badge && (
213
+ <Badge id={ describedById } className="newspack-card-feature__badge" intent={ badge.intent }>
214
+ { badge.label }
215
+ </Badge>
216
+ ) }
217
+ </Stack>
218
+ </Card.Content>
219
+ </Card.Root>
210
220
  );
211
221
  };
212
222
 
@@ -2,55 +2,51 @@
2
2
  * CardFeature
3
3
  */
4
4
 
5
- @use "~@wordpress/base-styles/colors" as wp-colors;
5
+ @use "~@wordpress/base-styles/mixins" as wp-mixins;
6
6
  @use "~@wordpress/base-styles/variables" as wp-vars;
7
+ @use "../mixins" as np-mixins;
7
8
 
8
9
  .newspack-card-feature {
9
10
  height: 100%;
10
11
 
11
- .newspack-card--core__header {
12
- height: 100%;
13
- }
14
-
15
- .newspack-card--core__header-content {
16
- gap: 16px;
17
- display: flex;
18
- flex-direction: column;
19
- height: 100%;
12
+ &__actions {
13
+ margin-top: auto;
20
14
 
21
- // Push the action row (buttons + badge) to the bottom.
22
- > :last-child {
23
- margin-top: auto;
15
+ .newspack-card-feature__badge {
16
+ flex-shrink: 1;
17
+ min-width: 0;
24
18
  }
25
19
  }
26
20
 
27
21
  &__content {
28
22
  flex: 1;
29
- min-width: 0; // prevent flex blowout on long words
23
+ min-width: 0;
30
24
  }
31
25
 
32
26
  &__title {
33
- font-size: wp-vars.$font-size-x-large;
34
- line-height: wp-vars.$font-line-height-x-large;
35
- text-align: left;
27
+ @include wp-mixins.heading-x-large();
28
+
29
+ margin: 0;
36
30
  }
37
31
 
38
32
  &__description {
39
- text-align: left;
33
+ @include np-mixins.text-muted;
34
+
35
+ margin: 0;
40
36
  }
41
37
 
42
38
  &__icon {
43
39
  align-items: center;
44
40
  display: flex;
45
- flex: 0 0 40px;
46
- height: 40px;
41
+ flex: 0 0 wp-vars.$grid-unit-50;
42
+ height: wp-vars.$grid-unit-50;
47
43
  justify-content: center;
48
44
 
49
45
  svg {
50
46
  display: block;
51
47
  fill: currentcolor;
52
- height: 24px;
53
- width: 24px;
48
+ height: wp-vars.$grid-unit-30;
49
+ width: wp-vars.$grid-unit-30;
54
50
  }
55
51
 
56
52
  &--radius-small {
@@ -62,14 +58,7 @@
62
58
  }
63
59
  }
64
60
 
65
- // Unmet requirements state: lighter border, muted content.
66
- &.newspack-card-feature--muted {
67
- border-color: wp-colors.$gray-100;
68
- box-shadow: wp-colors.$gray-100 0 0 0 1px;
69
-
70
- .newspack-card-feature__title,
71
- .newspack-card-feature__description {
72
- color: wp-colors.$gray-700;
73
- }
61
+ &.newspack-card-feature--muted &__title {
62
+ @include np-mixins.text-muted;
74
63
  }
75
64
  }
@@ -33,7 +33,7 @@ const handleClose = () => setIsOpen( false );
33
33
  <CardForm
34
34
  title={ __( 'Above Header', 'newspack-plugin' ) }
35
35
  description={ __( 'Displays an ad above the site header.', 'newspack-plugin' ) }
36
- badge={ isEnabled ? { level: 'success', text: __( 'Enabled', 'newspack-plugin' ) } : undefined }
36
+ badge={ isEnabled ? { intent: 'stable', label: __( 'Enabled', 'newspack-plugin' ) } : undefined }
37
37
  actions={
38
38
  isEnabled ? (
39
39
  <Button variant="tertiary" size="compact" onClick={ () => isOpen ? handleClose() : setIsOpen( true ) }>
@@ -55,14 +55,16 @@ const handleClose = () => setIsOpen( false );
55
55
  </CardForm>
56
56
  ```
57
57
 
58
- ## With a custom badge level
58
+ ## With a custom badge intent
59
59
 
60
- The `badge` prop accepts any `BadgeLevel`. Use `warning` or `error` to communicate a degraded state.
60
+ The `badge` prop accepts any `BadgeIntent`. Use `medium` or `high` to communicate a degraded state.
61
+
62
+ Omitting `intent` defaults to `stable`, because a `CardForm` badge marks a feature that is switched on. That differs from `ActionCard` and `SectionHeader`, whose badges are free-form labels and so default to `none`.
61
63
 
62
64
  ```tsx
63
65
  <CardForm
64
66
  title={ __( 'Above Header', 'newspack-plugin' ) }
65
- badge={ { level: 'warning', text: __( 'Missing ad unit', 'newspack-plugin' ) } }
67
+ badge={ { intent: 'medium', label: __( 'Missing ad unit', 'newspack-plugin' ) } }
66
68
  actions={ <Button variant="tertiary" size="compact">{ __( 'Edit', 'newspack-plugin' ) }</Button> }
67
69
  isOpen={ false }
68
70
  />
@@ -91,11 +93,11 @@ Omit `badge` (or pass `undefined`) to show no badge at all.
91
93
  |---|---|---|---|
92
94
  | `title` | `string` | — | Card heading (**required**) |
93
95
  | `description` | `string` | — | Supporting text below the title |
94
- | `badge` | `{ text: string; level?: BadgeLevel }` | — | Badge shown next to the actions slot. Omit or pass `undefined` to hide. |
96
+ | `badge` | `{ label: string; intent?: BadgeIntent }` | — | Badge shown next to the actions slot. Omit or pass `undefined` to hide. |
95
97
  | `actions` | `React.ReactNode` | — | JSX rendered in the header action area (buttons, dropdowns, etc.) |
96
98
  | `isOpen` | `boolean` | `false` | When `true`, renders `children` in the card body and removes the header border |
97
99
  | `onRequestClose` | `() => void` | — | Called when the user presses Escape while focus is inside the open form |
98
- | `titleLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `3` | Heading level rendered for `title`. Pick the level that fits the surrounding document outline. |
100
+ | `titleLevel` | `2 \| 3 \| 4 \| 5 \| 6` | `3` | Heading level rendered for `title`. Pick the level that fits the surrounding document outline. |
99
101
  | `className` | `string` | — | Additional class name applied to the card element |
100
102
  | `children` | `React.ReactNode` | — | Form content rendered inside the card body when `isOpen` is `true` |
101
103
 
@@ -106,8 +108,6 @@ Omit `badge` (or pass `undefined`) to show no badge at all.
106
108
  - The Escape listener is scoped to the open form's body, so multiple open cards do not all close on a single keypress. If an inner control needs to consume Escape (for example, to close its own menu), call `event.preventDefault()` and CardForm will ignore it.
107
109
 
108
110
 
109
- ### `BadgeLevel`
111
+ ### `BadgeIntent`
110
112
 
111
- ```ts
112
- type BadgeLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
113
- ```
113
+ Defined in [`src/types.ts`](../types.ts), which derives the union from the `@wordpress/ui` `Badge` component so the values track the library instead of a copy kept here.
@@ -17,23 +17,19 @@ import classnames from 'classnames';
17
17
  import { useEffect, useRef, createElement } from '@wordpress/element';
18
18
  import { useInstanceId } from '@wordpress/compose';
19
19
  import { __experimentalHStack as HStack, __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
20
+ import { Badge } from '@wordpress/ui';
20
21
 
21
22
  /**
22
23
  * Internal dependencies
23
24
  */
24
- import Badge, { BadgeLevel } from '../badge';
25
25
  import Card from '../card';
26
+ import type { CardBadge, HeadingLevel } from '../types';
26
27
  import './style.scss';
27
28
 
28
- type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
29
-
30
29
  type CardFormProps = {
31
30
  title: string;
32
31
  description?: string;
33
- badge?: {
34
- text: string;
35
- level?: BadgeLevel;
36
- };
32
+ badge?: CardBadge;
37
33
  /** JSX rendered in the header action area (buttons, etc.). */
38
34
  actions?: React.ReactNode;
39
35
  /** When true, children are shown and the header border is removed. */
@@ -109,7 +105,7 @@ const CardForm = ( { title, description, badge, actions, isOpen = false, onReque
109
105
  { description && <p className="newspack-card-form__description">{ description }</p> }
110
106
  </VStack>
111
107
  <HStack spacing={ 2 } expanded={ false }>
112
- { badge && <Badge text={ badge.text } level={ badge.level ?? 'success' } /> }
108
+ { badge?.label && <Badge intent={ badge.intent ?? 'stable' }>{ badge.label }</Badge> }
113
109
  { actions }
114
110
  </HStack>
115
111
  </HStack>
@@ -16,8 +16,7 @@ A drag-and-drop sortable list of cards. Each item displays a title and a status
16
16
  |-------|------|-------------|
17
17
  | `id` | `string` \| `number` | Unique identifier for the item. Used as the React `key`. |
18
18
  | `title` | `string` | Label rendered in the card header. |
19
- | `badgeLevel` | `'default'` \| `'success'` \| `'info'` \| `'warning'` \| `'error'` | Visual style of the status badge. |
20
- | `badgeText` | `string` | Text displayed inside the badge. |
19
+ | `badge` | `{ label: string; intent?: BadgeIntent }` | Status badge for the row. Omit it, or leave `label` empty, to render no badge. `intent` defaults to `none`. See [`BadgeIntent`](../types.ts) for the available intents. |
21
20
 
22
21
  ## Usage
23
22
 
@@ -25,9 +24,9 @@ A drag-and-drop sortable list of cards. Each item displays a title and a status
25
24
  import { CardSortableList } from 'newspack-components';
26
25
 
27
26
  const [ items, setItems ] = useState( [
28
- { id: 1, title: 'Homepage', badgeLevel: 'success', badgeText: 'Active' },
29
- { id: 2, title: 'About', badgeLevel: 'default', badgeText: 'Draft' },
30
- { id: 3, title: 'Contact', badgeLevel: 'warning', badgeText: 'Review' },
27
+ { id: 1, title: 'Homepage', badge: { label: 'Active', intent: 'stable' } },
28
+ { id: 2, title: 'About', badge: { label: 'Draft', intent: 'draft' } },
29
+ { id: 3, title: 'Contact', badge: { label: 'Review', intent: 'medium' } },
31
30
  ] );
32
31
 
33
32
  const handleReorder = ( fromIndex, toIndex ) => {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * External dependencies.
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies.
8
+ */
9
+ import CardSortableList from './';
10
+
11
+ const items = [
12
+ { id: 1, title: 'Homepage', badge: { label: 'Active', intent: 'stable' } },
13
+ { id: 2, title: 'About', badge: { label: 'Draft', intent: 'draft' } },
14
+ ];
15
+
16
+ describe( 'CardSortableList', () => {
17
+ it( 'renders a badge for each item that has badge text', () => {
18
+ render( <CardSortableList items={ items } /> );
19
+ expect( screen.getByText( 'Active' ) ).toBeInTheDocument();
20
+ expect( screen.getByText( 'Draft' ) ).toBeInTheDocument();
21
+ } );
22
+
23
+ it( 'renders no badge for an item without badge text', () => {
24
+ const { container } = render( <CardSortableList items={ [ { id: 1, title: 'Homepage' } ] } /> );
25
+ expect( screen.getByText( 'Homepage' ) ).toBeInTheDocument();
26
+ // An unguarded Badge would still emit an empty span, so assert on element children.
27
+ expect( container.querySelector( 'h3' ).children ).toHaveLength( 0 );
28
+ } );
29
+ } );
@@ -7,11 +7,13 @@
7
7
  */
8
8
  import { Disabled, Draggable, __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
9
9
  import { useEffect, useLayoutEffect, useRef, useState } from '@wordpress/element';
10
+ import { Badge } from '@wordpress/ui';
10
11
 
11
12
  /**
12
13
  * Internal dependencies
13
14
  */
14
- import { Badge, Card } from '../';
15
+ import { Card } from '../';
16
+ import type { CardBadge } from '../types';
15
17
  import './style.scss';
16
18
 
17
19
  /**
@@ -34,8 +36,7 @@ type DraggableItem = {
34
36
  id: string | number;
35
37
  title: string;
36
38
  description?: React.ReactNode;
37
- badgeLevel: 'default' | 'success' | 'info' | 'warning' | 'error';
38
- badgeText: string;
39
+ badge?: CardBadge;
39
40
  toggleChecked?: boolean;
40
41
  onToggleChange?: () => void;
41
42
  actions?: DraggableItemAction[];
@@ -402,7 +403,9 @@ const CardSortableList = ( {
402
403
  <>
403
404
  <h3>
404
405
  { item.title }
405
- <Badge level={ item.badgeLevel } text={ item.badgeText } />
406
+ { item.badge?.label && (
407
+ <Badge intent={ item.badge.intent ?? 'none' }>{ item.badge.label }</Badge>
408
+ ) }
406
409
  </h3>
407
410
  { item.description && <p>{ item.description }</p> }
408
411
  </>
@@ -0,0 +1,50 @@
1
+ # CollapsibleGroup
2
+
3
+ A stack of independently collapsible items, separated by dividers and sitting flush with the surrounding column. Built on `Collapsible` from `@wordpress/ui`.
4
+
5
+ Each item is its own disclosure: opening one does not close the others. This is deliberately not a W3C accordion, which coordinates its panels and moves focus between headers with the arrow keys, so the component is not named for one. It is also not `CollapsibleCard` from the same library, which renders its root as a `Card` and so brings a border, a background and card padding; this component sits flush with the column around it.
6
+
7
+ A collapsed item is hidden with `hidden="until-found"`, so the browser's find-in-page can match text inside it and expand the item to reveal the result. Find-in-page can only match text that exists, so a collapsed item keeps its children mounted, out of the focus order and the accessibility tree but present in the DOM. Every item therefore renders on first paint, and a child's state and effects persist while it is closed. Keep expensive children, or anything that fetches on mount, behind the item's own open state.
8
+
9
+ ## Props
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | `className` | `string` | — | Additional class on the group wrapper. |
14
+ | `hideSingleTitle` | `boolean` | `false` | When the group holds exactly one item, render it open and drop its title. Use it where a group can collapse to a single section and the title would repeat the heading above it. |
15
+ | `gap` | `GapSize` | `'xl'` | `Stack` gap, on the design system's token scale (`'xl'` is 24px). The divider is a sibling of the items rather than part of one, so the gap applies on both sides of it and items sit twice this far apart: 49px at the default. |
16
+ | `titleLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | inherited, or `2` | Heading level for every item title. Set it once on the group so the items share one place in the document outline: under a section header rendered as `h2`, pass `3`. It changes the tag only, never the size, so the same group looks the same wherever it sits. A group nested inside another matches the level it inherits rather than descending a step, so give a nested group its own `titleLevel`. A value outside the range is clamped to the nearest valid level. |
17
+
18
+ Children must be `CollapsibleGroup.Item`. Bare text and numbers are dropped; any other element counts as one item and takes a divider, so a wrapper component, or a `Fragment` holding two items, puts the dividers in the wrong place.
19
+
20
+ ### `CollapsibleGroup.Item`
21
+
22
+ | Prop | Type | Default | Description |
23
+ |------|------|---------|-------------|
24
+ | `className` | `string` | — | Additional class on the item. |
25
+ | `defaultOpen` | `boolean` | `false` | Whether the item starts expanded. |
26
+ | `title` | `string` | — | Trigger label, rendered as a button inside the heading. Without a title there is no trigger and the content renders permanently open. |
27
+
28
+ ## Usage
29
+
30
+ ```jsx
31
+ import { CollapsibleGroup } from 'newspack-components';
32
+
33
+ <CollapsibleGroup titleLevel={ 3 }>
34
+ <CollapsibleGroup.Item title="Contact fields" defaultOpen>
35
+ …
36
+ </CollapsibleGroup.Item>
37
+ <CollapsibleGroup.Item title="Tags and segments">
38
+ …
39
+ </CollapsibleGroup.Item>
40
+ </CollapsibleGroup>
41
+
42
+ // A group that may collapse to one section
43
+ <CollapsibleGroup hideSingleTitle>
44
+ { groups.map( group => (
45
+ <CollapsibleGroup.Item key={ group.id } title={ group.section }>
46
+ …
47
+ </CollapsibleGroup.Item>
48
+ ) ) }
49
+ </CollapsibleGroup>
50
+ ```
@@ -0,0 +1,13 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { createContext, useContext } from '@wordpress/element';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type { HeadingLevel } from './types';
10
+
11
+ export const TitleLevelContext = createContext< HeadingLevel >( 2 );
12
+
13
+ export const useTitleLevel = (): HeadingLevel => useContext( TitleLevelContext );