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
@@ -0,0 +1,48 @@
1
+ /**
2
+ * StatCard
3
+ */
4
+
5
+ /**
6
+ * Internal dependencies.
7
+ */
8
+ import Body from './body';
9
+ import Delta from './delta';
10
+ import Footer from './footer';
11
+ import Label from './label';
12
+ import Root from './root';
13
+ import Secondary from './secondary';
14
+ import Value from './value';
15
+
16
+ export { STAT_CARD_NULL_GLYPH } from './constants';
17
+ export type {
18
+ StatCardBodyProps,
19
+ StatCardDeltaDirection,
20
+ StatCardDeltaProps,
21
+ StatCardDeltaTone,
22
+ StatCardFooterProps,
23
+ StatCardHeadingLevel,
24
+ StatCardLabelProps,
25
+ StatCardLabels,
26
+ StatCardRootProps,
27
+ StatCardSecondaryProps,
28
+ StatCardValue,
29
+ StatCardValueProps,
30
+ StatCardValueVariant,
31
+ } from './types';
32
+
33
+ // Compound components here export one namespace object, as Drawer does.
34
+ export const StatCard = {
35
+ Root,
36
+ Label,
37
+ Body,
38
+ Value,
39
+ Delta,
40
+ Secondary,
41
+ Footer,
42
+ };
43
+
44
+ Object.entries( StatCard ).forEach( ( [ name, part ] ) => {
45
+ ( part as { displayName?: string } ).displayName = `StatCard.${ name }`;
46
+ } );
47
+
48
+ export default StatCard;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import { forwardRef, useEffect } from '@wordpress/element';
5
+ import { Stack } from '@wordpress/ui';
6
+
7
+ /**
8
+ * External dependencies.
9
+ */
10
+ import classnames from 'classnames';
11
+
12
+ /**
13
+ * Internal dependencies.
14
+ */
15
+ import { useStatCardContext } from './context';
16
+ import type { StatCardHeadingLevel, StatCardLabelProps } from './types';
17
+
18
+ const headings = {
19
+ 2: 'h2',
20
+ 3: 'h3',
21
+ 4: 'h4',
22
+ 5: 'h5',
23
+ 6: 'h6',
24
+ } as const;
25
+
26
+ const Label = forwardRef< HTMLDivElement, StatCardLabelProps >( function Label( { suffix, heading, className, children, ...props }, ref ) {
27
+ const context = useStatCardContext();
28
+ const level = ( heading ?? context.heading ) as StatCardHeadingLevel;
29
+ // Consumers are largely untyped JS, where an out-of-range level would
30
+ // otherwise render an <h7>, which carries no heading role at all.
31
+ const Heading = headings[ level ] || headings[ 3 ];
32
+
33
+ useEffect( () => {
34
+ if ( 'production' === process.env.NODE_ENV || headings[ level ] ) {
35
+ return;
36
+ }
37
+ // eslint-disable-next-line no-console
38
+ console.warn(
39
+ `StatCard: unknown heading level "${ level }", falling back to 3. Set \`heading\` on StatCard.Root or StatCard.Label to one of ${ Object.keys(
40
+ headings
41
+ ).join( ', ' ) }.`
42
+ );
43
+ }, [ level ] );
44
+
45
+ return (
46
+ <Stack
47
+ ref={ ref }
48
+ direction="row"
49
+ align="flex-start"
50
+ justify="space-between"
51
+ gap="sm"
52
+ className={ classnames( 'newspack-stat-card__label', className ) }
53
+ { ...props }
54
+ >
55
+ <Heading className="newspack-stat-card__label-text">{ children }</Heading>
56
+ { suffix }
57
+ </Stack>
58
+ );
59
+ } );
60
+
61
+ export default Label;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import { forwardRef, useMemo } from '@wordpress/element';
5
+ // Aliased: this package exports a different `Card` of its own.
6
+ import { Card as UICard, Stack } from '@wordpress/ui';
7
+
8
+ /**
9
+ * External dependencies.
10
+ */
11
+ import classnames from 'classnames';
12
+
13
+ /**
14
+ * Internal dependencies.
15
+ */
16
+ import { resolveStatCardLabels, StatCardContext } from './context';
17
+ import type { StatCardRootProps } from './types';
18
+ import './style.scss';
19
+
20
+ const Root = forwardRef< HTMLDivElement, StatCardRootProps >( function Root( { heading = 3, labels, className, children, ...props }, ref ) {
21
+ // Keyed on the strings rather than the object: a caller writing `labels` inline
22
+ // hands over a fresh object each render, which would defeat the memo entirely.
23
+ const { notApplicable, up, down } = labels ?? {};
24
+ const context = useMemo(
25
+ () => ( { heading, labels: resolveStatCardLabels( { notApplicable, up, down } ) } ),
26
+ [ heading, notApplicable, up, down ]
27
+ );
28
+
29
+ return (
30
+ <StatCardContext.Provider value={ context }>
31
+ <UICard.Root ref={ ref } className={ classnames( 'newspack-stat-card', className ) } { ...props }>
32
+ <UICard.Content render={ <Stack direction="column" gap="sm" /> } className="newspack-stat-card__content">
33
+ { children }
34
+ </UICard.Content>
35
+ </UICard.Root>
36
+ </StatCardContext.Provider>
37
+ );
38
+ } );
39
+
40
+ export default Root;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import { forwardRef } from '@wordpress/element';
5
+
6
+ /**
7
+ * External dependencies.
8
+ */
9
+ import classnames from 'classnames';
10
+
11
+ /**
12
+ * Internal dependencies.
13
+ */
14
+ import { useStatCardContext } from './context';
15
+ import type { StatCardSecondaryProps } from './types';
16
+
17
+ const Secondary = forwardRef< HTMLDivElement, StatCardSecondaryProps >( function Secondary( { className, children, ...props }, ref ) {
18
+ useStatCardContext();
19
+
20
+ return (
21
+ <div ref={ ref } className={ classnames( 'newspack-stat-card__secondary', className ) } { ...props }>
22
+ { children }
23
+ </div>
24
+ );
25
+ } );
26
+
27
+ export default Secondary;
@@ -0,0 +1,89 @@
1
+ @use "~@wordpress/base-styles/colors" as wp-colors;
2
+ @use "~@wordpress/base-styles/variables" as wp-vars;
3
+ @use "~@wordpress/base-styles/mixins" as wp-mixins;
4
+ @use "../../../colors/colors.module" as colors;
5
+ @use "../variables" as np-vars;
6
+
7
+ $stat-card-min-height: wp-vars.$grid-unit * 21;
8
+
9
+ .newspack-stat-card {
10
+ --wp-admin-theme-color: #{colors.$primary-600};
11
+ --wp-admin-theme-color--rgb: #{colors.$primary-600--rgb};
12
+ --wp-admin-theme-color-darker-10: #{colors.$primary-700};
13
+ --wp-admin-theme-color-darker-10--rgb: #{colors.$primary-700--rgb};
14
+ --wp-admin-theme-color-darker-20: #{colors.$primary-800};
15
+ --wp-admin-theme-color-darker-20--rgb: #{colors.$primary-800--rgb};
16
+ --wp-admin-theme-color-lighter-10: #{colors.$primary-000};
17
+ --wp-admin-theme-color-lighter-10--rgb: #{colors.$primary-000--rgb};
18
+
19
+ container-type: inline-size;
20
+ display: flex;
21
+ flex-direction: column;
22
+ min-height: $stat-card-min-height;
23
+
24
+ &__content {
25
+ flex: 1;
26
+ }
27
+
28
+ &__label .newspack-info-button {
29
+ margin-block: calc((#{wp-vars.$font-line-height-small} - #{wp-vars.$button-size-small}) / 2);
30
+ }
31
+
32
+ &__label-text {
33
+ @include wp-mixins.heading-large();
34
+ color: wp-colors.$gray-900;
35
+ margin: 0;
36
+ }
37
+
38
+ &__body {
39
+ flex: 1;
40
+ }
41
+
42
+ &__value {
43
+ color: var(--wp-admin-theme-color);
44
+ font-size: np-vars.$font-size-3x-large;
45
+ font-size: clamp(#{wp-vars.$font-size-x-large}, 14cqi, #{np-vars.$font-size-3x-large});
46
+ font-variant-numeric: tabular-nums;
47
+ font-weight: wp-vars.$font-weight-medium;
48
+ line-height: np-vars.$font-line-height-3x-large;
49
+
50
+ &--text {
51
+ font-size: wp-vars.$font-size-x-large;
52
+ line-height: wp-vars.$font-line-height-x-large;
53
+ }
54
+ }
55
+
56
+ &__delta {
57
+ @include wp-mixins.heading-medium();
58
+ color: wp-colors.$gray-700;
59
+
60
+ &--positive {
61
+ color: colors.$success-600;
62
+ }
63
+
64
+ &--negative {
65
+ color: colors.$error-600;
66
+ }
67
+ }
68
+
69
+ &__action,
70
+ &__action.components-button {
71
+ font-size: wp-vars.$helptext-font-size;
72
+ line-height: wp-vars.$font-line-height-x-small;
73
+ }
74
+
75
+ &__secondary {
76
+ @include wp-mixins.heading-medium();
77
+ color: var(--wp-admin-theme-color);
78
+ margin: 0;
79
+ text-wrap: balance;
80
+ }
81
+
82
+ &__description {
83
+ color: wp-colors.$gray-700;
84
+ font-size: wp-vars.$helptext-font-size;
85
+ line-height: wp-vars.$font-line-height-x-small;
86
+ margin: 0;
87
+ text-wrap: balance;
88
+ }
89
+ }
@@ -0,0 +1,84 @@
1
+ export type StatCardHeadingLevel = 2 | 3 | 4 | 5 | 6;
2
+
3
+ export type StatCardValueVariant = 'figure' | 'text';
4
+
5
+ /** Pre-formatted by the caller. Null, undefined and a blank string all render the null glyph. */
6
+ export type StatCardValue = string | number | null | undefined;
7
+
8
+ type DivProps = Omit< React.ComponentPropsWithoutRef< 'div' >, 'children' >;
9
+
10
+ type SpanProps = Omit< React.ComponentPropsWithoutRef< 'span' >, 'children' >;
11
+
12
+ /** The strings the card speaks for itself, when the caller has supplied none of its own. */
13
+ export type StatCardLabels = {
14
+ /** Names the null glyph. */
15
+ notApplicable: string;
16
+ /** Spoken for an up arrow. */
17
+ up: string;
18
+ /** Spoken for a down arrow. */
19
+ down: string;
20
+ };
21
+
22
+ export type StatCardRootProps = DivProps & {
23
+ /** Heading level for `StatCard.Label`, read through context. */
24
+ heading?: StatCardHeadingLevel;
25
+ /** Replaces the spoken defaults for every card underneath, e.g. from a consumer's own text domain. */
26
+ labels?: Partial< StatCardLabels >;
27
+ /** Merged onto the card, which is the element the hero scale queries. */
28
+ className?: string;
29
+ children?: React.ReactNode;
30
+ };
31
+
32
+ export type StatCardLabelProps = DivProps & {
33
+ /** Rendered beside the heading rather than inside it, so a control here stays out of the document outline. */
34
+ suffix?: React.ReactNode;
35
+ /** Overrides the level set on `StatCard.Root`. */
36
+ heading?: StatCardHeadingLevel;
37
+ /** Merged onto the label row, not the heading. */
38
+ className?: string;
39
+ children?: React.ReactNode;
40
+ };
41
+
42
+ export type StatCardBodyProps = DivProps & {
43
+ className?: string;
44
+ children?: React.ReactNode;
45
+ };
46
+
47
+ export type StatCardValueProps = SpanProps & {
48
+ value: StatCardValue;
49
+ /** Spoken instead of the visible value, whose meaning may rest on punctuation. */
50
+ valueLabel?: string;
51
+ /** `text` drops the hero scale, for a phrase standing in for a number. */
52
+ variant?: StatCardValueVariant;
53
+ /** Rendered in a row beside the figure, e.g. a `StatCard.Delta`. */
54
+ suffix?: React.ReactNode;
55
+ className?: string;
56
+ };
57
+
58
+ export type StatCardDeltaDirection = 'up' | 'down';
59
+
60
+ export type StatCardDeltaTone = 'positive' | 'negative' | 'neutral';
61
+
62
+ export type StatCardDeltaProps = SpanProps & {
63
+ /** Which arrow to show. Says nothing about whether the change is good. */
64
+ direction: StatCardDeltaDirection;
65
+ /** Which colour to use. The caller decides, because a rise is not always good news. */
66
+ tone?: StatCardDeltaTone;
67
+ /** Spoken in place of "Up" or "Down". */
68
+ directionLabel?: string;
69
+ /** Spoken in place of the whole delta, arrow and change together. Wins over `directionLabel`. */
70
+ label?: string;
71
+ className?: string;
72
+ /** The change, pre-formatted. Must be non-interactive: `label` hides it from assistive technology. */
73
+ children?: React.ReactNode;
74
+ };
75
+
76
+ export type StatCardSecondaryProps = DivProps & {
77
+ className?: string;
78
+ children?: React.ReactNode;
79
+ };
80
+
81
+ export type StatCardFooterProps = DivProps & {
82
+ className?: string;
83
+ children?: React.ReactNode;
84
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import { forwardRef, useEffect } from '@wordpress/element';
5
+ import { Stack, VisuallyHidden } from '@wordpress/ui';
6
+
7
+ /**
8
+ * External dependencies.
9
+ */
10
+ import classnames from 'classnames';
11
+
12
+ /**
13
+ * Internal dependencies.
14
+ */
15
+ import { STAT_CARD_NULL_GLYPH } from './constants';
16
+ import { useStatCardContext } from './context';
17
+ import type { StatCardValueProps, StatCardValueVariant } from './types';
18
+
19
+ const variants: StatCardValueVariant[] = [ 'figure', 'text' ];
20
+
21
+ const Value = forwardRef< HTMLSpanElement, StatCardValueProps >( function Value(
22
+ { value, valueLabel, variant = 'figure', suffix, className, ...props },
23
+ ref
24
+ ) {
25
+ const { labels } = useStatCardContext();
26
+
27
+ useEffect( () => {
28
+ if ( 'production' === process.env.NODE_ENV || variants.includes( variant ) ) {
29
+ return;
30
+ }
31
+ // eslint-disable-next-line no-console
32
+ console.warn( `StatCard.Value: unknown variant "${ variant }", falling back to figure. Use one of ${ variants.join( ', ' ) }.` );
33
+ }, [ variant ] );
34
+
35
+ // A blank string is a missing figure too, and an empty hero would read as one
36
+ // that never loaded. A zero is a figure, so it stays out of this.
37
+ const isNull = null === value || undefined === value || ( 'string' === typeof value && '' === value.trim() );
38
+ const shown = isNull ? STAT_CARD_NULL_GLYPH : value;
39
+ // Trimmed, and `||` not `??`: a blank label is a missing one, and the glyph must never be left unnamed.
40
+ const spoken = valueLabel?.trim() || ( isNull ? labels.notApplicable : undefined );
41
+
42
+ const figure = (
43
+ <span
44
+ ref={ ref }
45
+ className={ classnames( 'newspack-stat-card__value', 'text' === variant && 'newspack-stat-card__value--text', className ) }
46
+ { ...props }
47
+ >
48
+ { spoken ? (
49
+ <>
50
+ { /* Hidden, not labelled: ARIA forbids naming a generic element, and `role="img"` announces a graphic. */ }
51
+ <span aria-hidden="true">{ shown }</span>
52
+ <VisuallyHidden render={ <span /> }>{ spoken }</VisuallyHidden>
53
+ </>
54
+ ) : (
55
+ shown
56
+ ) }
57
+ </span>
58
+ );
59
+
60
+ if ( ! suffix ) {
61
+ return figure;
62
+ }
63
+
64
+ return (
65
+ <Stack direction="row" align="baseline" gap="sm" className="newspack-stat-card__figure">
66
+ { figure }
67
+ { suffix }
68
+ </Stack>
69
+ );
70
+ } );
71
+
72
+ export default Value;
@@ -0,0 +1,123 @@
1
+ # StatusIndicator
2
+
3
+ A status glyph followed by its label, for the Status column of a DataView.
4
+
5
+ A badge is an attention marker. In a column where every row carries one it marks
6
+ nothing and adds a block of colour to each row, so the quiet treatment is the
7
+ default there and a badge is kept for the rare row that genuinely stands out,
8
+ such as a group with a seat request waiting on payment.
9
+
10
+ ## Importing
11
+
12
+ ```jsx
13
+ // The barrel.
14
+ import { StatusIndicator } from 'newspack-components';
15
+
16
+ // The component on its own.
17
+ import StatusIndicator from '../../packages/components/src/status-indicator';
18
+
19
+ // The vocabulary, for a column's own test. Deliberately not on the barrel: that
20
+ // entry also pulls `Page`'s `:root` token block and the wizards store into
21
+ // whichever bundle asks for it.
22
+ import { statusGlyph, STATUS_NAMES } from '../../packages/components/src/status-indicator';
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Name the status and the component draws it:
28
+
29
+ ```jsx
30
+ <StatusIndicator status="active">{ __( 'Active', 'newspack-plugin' ) }</StatusIndicator>;
31
+ ```
32
+
33
+ In a field definition, where the screen maps its own status keys onto the
34
+ vocabulary:
35
+
36
+ ```jsx
37
+ {
38
+ id: 'status',
39
+ label: __( 'Status', 'newspack-plugin' ),
40
+ getValue: ( { item } ) => item.status,
41
+ render: ( { item } ) => <StatusIndicator status={ STATUS_INDICATORS[ item.status ] }>{ item.status_label }</StatusIndicator>,
42
+ elements: statusElements,
43
+ filterBy: { operators: [ 'is' ] },
44
+ }
45
+ ```
46
+
47
+ ## Props
48
+
49
+ | Prop | Type | Required | Description |
50
+ | --- | --- | --- | --- |
51
+ | `status` | `StatusName` | one of the two | The status to draw, from the vocabulary below. |
52
+ | `icon` | `Icon`'s `icon` prop | one of the two | A glyph, for a field the vocabulary does not cover. |
53
+ | `children` | `ReactNode` | yes | The status label. `@wordpress/primitives` forces `aria-hidden` on the glyph, so this is the whole accessible name. |
54
+
55
+ `status` and `icon` are mutually exclusive, and one of them is required.
56
+ Anything else is spread onto the wrapper, a `Stack` from `@wordpress/ui`, which
57
+ takes the props of a `div`.
58
+
59
+ ## The vocabulary
60
+
61
+ | Name | Glyph | What it means |
62
+ | --- | --- | --- |
63
+ | `active` | check circle | Live now: a published plan, a running subscription. |
64
+ | `done` | check circle | Finished successfully: a sent newsletter, a completed sync. |
65
+ | `scheduled` | clock | Waiting for a date to arrive. |
66
+ | `draft` | half circle | Not live yet, or switched off. |
67
+ | `pending` | part-filled circle | Waiting on something outside the publisher's hands. |
68
+ | `attention` | exclamation circle | Live but needing a look, usually a payment. |
69
+ | `error` | error | Failed, and the one state that asks the reader to act. |
70
+ | `progress` | update | Running right now. |
71
+ | `cancelled` | slash circle | Stopped on purpose. |
72
+ | `ended` | slash circle | Stopped because its window closed. |
73
+ | `private` | lock | Live, but not publicly reachable. |
74
+ | `trash` | trash | Binned. |
75
+
76
+ The component owns this so one meaning draws one mark everywhere. Before it
77
+ existed the vocabulary lived in ten screen-level maps, and had already drifted:
78
+ Pending was the half circle in Subscribers and the part-filled one in the
79
+ Audience lists.
80
+
81
+ **No two statuses in one column may draw the same mark.** A DataViews Status
82
+ column offers its statuses as separate filters, so two that look identical make
83
+ two different states indistinguishable in the one place the difference matters.
84
+
85
+ Two names may share a glyph where they read differently at the call site but
86
+ mean the same to a reader: a sent newsletter is finished rather than live, and
87
+ an ad whose window closed was not cancelled. Splitting them leaves room to draw
88
+ them apart later without touching a consumer. The pairs are `active`/`done` and
89
+ `cancelled`/`ended`, and `index.test.js` pins that list, so a column keeps the
90
+ rule by using distinct names and at most one half of a pair. `statusGlyph` is
91
+ exported for the columns that want to assert it directly.
92
+
93
+ The glyph does the separating on its own: the component inherits its colour from
94
+ the surrounding text and tints nothing, so no status leans on colour to carry
95
+ its meaning. That also means a state that needs to shout cannot, which is the
96
+ trade the quiet treatment makes.
97
+
98
+ ## When to pass a glyph instead
99
+
100
+ `icon` is for fields that classify rather than track a lifecycle, where a status
101
+ name would be the wrong shape. Two exist: Plans' Availability (a gift, a lock
102
+ and a globe for Free, Private and Public) and newsletters' Visibility (a globe
103
+ or an envelope). Both are still Status-column-shaped in every other way, and
104
+ both keep the same distinctness rule.
105
+
106
+ ## The icon's footprint
107
+
108
+ `@wordpress/icons` draws a 16px glyph inside a 24px viewBox, so a 24px icon
109
+ carries 4px of transparent padding on every side. The component trims that back
110
+ to the visible footprint with a negative margin, which is what makes the 8px gap
111
+ measure 8px between the glyph and the label rather than 12px. An icon that fills
112
+ its viewBox would be cropped by 4px a side; the statuses all come from
113
+ `@wordpress/icons`, which does not.
114
+
115
+ The margin is derived rather than written as `-4px`, so it follows the two
116
+ values it depends on: `calc((#{wp-vars.$grid-unit-20} - #{wp-vars.$icon-size}) / 2)`,
117
+ the glyph's footprint minus the box it sits in, halved for one side. Sass folds
118
+ it to a literal at build time, so nothing is paid for at runtime.
119
+
120
+ The box it sits in is the `size={ 24 }` the component passes to `Icon`, which is
121
+ `$icon-size`. The two are written in different files, so anything that changes
122
+ the rendered size has to change the token the margin reads, or the trim stops
123
+ matching the padding it is there to remove.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { drafts, gift, published } from '@wordpress/icons';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import StatusIndicator, { statusGlyph, STATUS_NAMES } from '.';
15
+
16
+ describe( 'StatusIndicator', () => {
17
+ it( 'renders the glyph alongside the label', () => {
18
+ const { container } = render( <StatusIndicator status="active">Active</StatusIndicator> );
19
+ expect( screen.getByText( 'Active' ) ).toBeInTheDocument();
20
+ expect( container.querySelector( 'svg.newspack-status-indicator__icon' ) ).toBeInTheDocument();
21
+ } );
22
+
23
+ // The trim is what makes the 8px gap measure 8px, so it is styled through a
24
+ // class rather than left to the consumer.
25
+ it( 'carries the class the icon trim is scoped to', () => {
26
+ const { container } = render( <StatusIndicator status="active">Active</StatusIndicator> );
27
+ expect( container.querySelector( '.newspack-status-indicator' ) ).toBeInTheDocument();
28
+ expect( container.querySelector( '.newspack-status-indicator__icon' ) ).toBeInTheDocument();
29
+ } );
30
+
31
+ it( 'draws a named status the way the vocabulary says', () => {
32
+ const { container: named } = render( <StatusIndicator status="draft">Draft</StatusIndicator> );
33
+ const { container: given } = render( <StatusIndicator icon={ drafts }>Draft</StatusIndicator> );
34
+ expect( named.querySelector( 'svg' ).innerHTML ).toBe( given.querySelector( 'svg' ).innerHTML );
35
+ } );
36
+
37
+ it( 'draws the glyph it is given when there is no name for it', () => {
38
+ const { container: free } = render( <StatusIndicator icon={ gift }>Free</StatusIndicator> );
39
+ const { container: active } = render( <StatusIndicator icon={ published }>Active</StatusIndicator> );
40
+ expect( free.querySelector( 'svg' ).innerHTML ).not.toBe( active.querySelector( 'svg' ).innerHTML );
41
+ } );
42
+
43
+ it( 'keeps the consumer class and passes the rest through', () => {
44
+ const { container } = render(
45
+ <StatusIndicator className="custom" data-testid="status" status="active">
46
+ Active
47
+ </StatusIndicator>
48
+ );
49
+ const root = container.querySelector( '.newspack-status-indicator' );
50
+ expect( root ).toHaveClass( 'custom' );
51
+ expect( root ).toHaveAttribute( 'data-testid', 'status' );
52
+ } );
53
+ } );
54
+
55
+ describe( 'statusGlyph', () => {
56
+ it( 'draws every name in the vocabulary', () => {
57
+ STATUS_NAMES.forEach( name => expect( statusGlyph( name ) ).toBeTruthy() );
58
+ } );
59
+
60
+ // Pinning the complete list is what lets a column assert its own distinctness:
61
+ // any pair not named here reads apart.
62
+ it( 'shares a mark only where two names mean the same to a reader', () => {
63
+ const byGlyph = new Map();
64
+ STATUS_NAMES.forEach( name => byGlyph.set( statusGlyph( name ), [ ...( byGlyph.get( statusGlyph( name ) ) || [] ), name ] ) );
65
+ expect( [ ...byGlyph.values() ].filter( names => names.length > 1 ) ).toEqual( [
66
+ [ 'active', 'done' ],
67
+ [ 'cancelled', 'ended' ],
68
+ ] );
69
+ } );
70
+ } );
@@ -0,0 +1,39 @@
1
+ /**
2
+ * StatusIndicator
3
+ */
4
+
5
+ /**
6
+ * WordPress dependencies.
7
+ */
8
+ import { Icon } from '@wordpress/components';
9
+ import { forwardRef } from '@wordpress/element';
10
+ import { Stack } from '@wordpress/ui';
11
+
12
+ /**
13
+ * External dependencies.
14
+ */
15
+ import classnames from 'classnames';
16
+
17
+ /**
18
+ * Internal dependencies.
19
+ */
20
+ import type { StatusIndicatorProps } from './types';
21
+ import { statusGlyph } from './statuses';
22
+ import './style.scss';
23
+
24
+ const StatusIndicator = forwardRef< HTMLDivElement, StatusIndicatorProps >( function StatusIndicator(
25
+ { status, icon, className, children, ...props },
26
+ ref
27
+ ) {
28
+ return (
29
+ <Stack ref={ ref } direction="row" align="center" gap="sm" className={ classnames( 'newspack-status-indicator', className ) } { ...props }>
30
+ <Icon className="newspack-status-indicator__icon" icon={ status ? statusGlyph( status ) : icon } size={ 24 } />
31
+ <span>{ children }</span>
32
+ </Stack>
33
+ );
34
+ } );
35
+
36
+ export { statusGlyph, STATUS_NAMES } from './statuses';
37
+ export type { StatusName } from './statuses';
38
+
39
+ export default StatusIndicator;