newspack-components 4.3.0 → 4.4.0-alpha.1

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 (263) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/DEVELOPMENT.md +868 -0
  3. package/colors/colors.module.scss +52 -2
  4. package/copy-styles.js +3 -2
  5. package/dist/cjs/accordion/index.js +17 -12
  6. package/dist/cjs/action-card/index.js +176 -139
  7. package/dist/cjs/autocomplete-tokenfield/index.js +254 -234
  8. package/dist/cjs/autocomplete-with-latest-posts/index.js +172 -110
  9. package/dist/cjs/autocomplete-with-suggestions/index.js +229 -126
  10. package/dist/cjs/badge/index.js +7 -7
  11. package/dist/cjs/box-contrast/index.js +19 -15
  12. package/dist/cjs/button/index.js +41 -26
  13. package/dist/cjs/button-props/button-props.js +5 -5
  14. package/dist/cjs/button-props/index.js +2 -2
  15. package/dist/cjs/card/core-card.js +110 -52
  16. package/dist/cjs/card/index.js +74 -67
  17. package/dist/cjs/card/style-core.scss +93 -25
  18. package/dist/cjs/card/style.scss +2 -2
  19. package/dist/cjs/card-feature/index.js +141 -0
  20. package/dist/cjs/card-feature/style.scss +75 -0
  21. package/dist/cjs/card-form/index.js +144 -0
  22. package/dist/cjs/card-form/style.scss +20 -0
  23. package/dist/cjs/card-settings-group/index.js +36 -16
  24. package/dist/cjs/card-sortable-list/index.js +176 -140
  25. package/dist/cjs/category-autocomplete/index.js +175 -155
  26. package/dist/cjs/color-picker/index.js +41 -23
  27. package/dist/cjs/color-picker/index.test.js +82 -0
  28. package/dist/cjs/color-picker/style.scss +11 -13
  29. package/dist/cjs/confirm-dialog/index.js +86 -22
  30. package/dist/cjs/consts.js +1 -1
  31. package/dist/cjs/custom-select-control/index.js +39 -28
  32. package/dist/cjs/dataviews/index.js +38 -0
  33. package/dist/cjs/dataviews/style.scss +9 -0
  34. package/dist/cjs/divider/index.js +26 -21
  35. package/dist/cjs/footer/index.js +36 -30
  36. package/dist/cjs/form-token-field/index.js +51 -40
  37. package/dist/cjs/global-notices/index.js +5 -5
  38. package/dist/cjs/grid/index.js +25 -19
  39. package/dist/cjs/grid/style.scss +6 -1
  40. package/dist/cjs/handoff/index.js +204 -156
  41. package/dist/cjs/handoff-message/index.js +12 -7
  42. package/dist/cjs/hooks/index.js +10 -8
  43. package/dist/cjs/hooks/use-confirm-dialog.js +79 -0
  44. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +144 -0
  45. package/dist/cjs/hooks/useObjectState.js +21 -12
  46. package/dist/cjs/hooks/useObjectState.test.js +55 -33
  47. package/dist/cjs/hooks/useOnClickOutside.js +6 -6
  48. package/dist/cjs/hooks/usePrompt.js +10 -13
  49. package/dist/cjs/image-upload/index.js +114 -123
  50. package/dist/cjs/image-upload/index.test.js +11 -13
  51. package/dist/cjs/image-upload/style.scss +12 -40
  52. package/dist/cjs/index.js +136 -104
  53. package/dist/cjs/info-button/index.js +43 -32
  54. package/dist/cjs/integration-icons/active-campaign.js +22 -0
  55. package/dist/cjs/integration-icons/constant-contact.js +28 -0
  56. package/dist/cjs/integration-icons/fundraise-up.js +25 -0
  57. package/dist/cjs/integration-icons/index.js +48 -0
  58. package/dist/cjs/integration-icons/mailchimp.js +19 -0
  59. package/dist/cjs/integration-icons/salesforce.js +17 -0
  60. package/dist/cjs/integration-icons/wisepops.js +20 -0
  61. package/dist/cjs/modal/index.js +22 -19
  62. package/dist/cjs/modal/style.scss +4 -0
  63. package/dist/cjs/newspack-icon/index.js +45 -38
  64. package/dist/cjs/notice/index.js +64 -55
  65. package/dist/cjs/plugin-installer/index.js +259 -253
  66. package/dist/cjs/plugin-settings/SettingsSection.js +66 -60
  67. package/dist/cjs/plugin-settings/index.js +217 -200
  68. package/dist/cjs/plugin-toggle/index.js +192 -199
  69. package/dist/cjs/popover/index.js +41 -28
  70. package/dist/cjs/position-control/index.js +24 -23
  71. package/dist/cjs/progress-bar/index.js +71 -59
  72. package/dist/cjs/progress-bar/index.test.js +62 -68
  73. package/dist/cjs/proxied-imports/router.js +3 -3
  74. package/dist/cjs/radio-control/index.js +40 -27
  75. package/dist/cjs/section-header/index.js +103 -58
  76. package/dist/cjs/section-header/style.scss +31 -11
  77. package/dist/cjs/select-control/ButtonGroupControl.js +26 -21
  78. package/dist/cjs/select-control/GroupedSelectControl.js +41 -35
  79. package/dist/cjs/select-control/index.js +52 -40
  80. package/dist/cjs/settings/MinMaxSetting.js +30 -20
  81. package/dist/cjs/settings/SettingsCard.js +20 -17
  82. package/dist/cjs/settings/SettingsSection.js +22 -21
  83. package/dist/cjs/settings/index.js +5 -5
  84. package/dist/cjs/sortable-newsletter-list-control/index.js +78 -57
  85. package/dist/cjs/steps-list/index.js +44 -34
  86. package/dist/cjs/steps-list-item/index.js +47 -39
  87. package/dist/cjs/style-card/index.js +70 -63
  88. package/dist/cjs/tabbed-navigation/index.js +38 -36
  89. package/dist/cjs/text-control/index.js +27 -25
  90. package/dist/cjs/utils/color.js +4 -4
  91. package/dist/cjs/utils/editor-toolbar-back-button.js +61 -0
  92. package/dist/cjs/utils/index.js +31 -19
  93. package/dist/cjs/utils/style.scss +8 -0
  94. package/dist/cjs/waiting/index.js +48 -37
  95. package/dist/cjs/web-preview/index.js +217 -211
  96. package/dist/cjs/with-wizard/index.js +362 -364
  97. package/dist/cjs/with-wizard/style.scss +4 -54
  98. package/dist/cjs/with-wizard-screen/index.js +54 -58
  99. package/dist/cjs/with-wizard-screen/style.scss +41 -3
  100. package/dist/cjs/wizard/components/WizardError.js +20 -18
  101. package/dist/cjs/wizard/components/WizardSnackbar.js +26 -23
  102. package/dist/cjs/wizard/index.js +176 -114
  103. package/dist/cjs/wizard/store/index.js +171 -123
  104. package/dist/cjs/wizard/store/utils.js +13 -8
  105. package/dist/esm/accordion/index.js +14 -9
  106. package/dist/esm/action-card/index.js +168 -123
  107. package/dist/esm/autocomplete-tokenfield/index.js +248 -222
  108. package/dist/esm/autocomplete-with-latest-posts/index.js +169 -103
  109. package/dist/esm/autocomplete-with-suggestions/index.js +226 -119
  110. package/dist/esm/badge/index.js +5 -5
  111. package/dist/esm/box-contrast/index.js +15 -11
  112. package/dist/esm/button/index.js +39 -21
  113. package/dist/esm/button-props/button-props.js +1 -1
  114. package/dist/esm/card/core-card.js +109 -47
  115. package/dist/esm/card/index.js +69 -54
  116. package/dist/esm/card/style-core.scss +93 -25
  117. package/dist/esm/card/style.scss +2 -2
  118. package/dist/esm/card-feature/index.js +131 -0
  119. package/dist/esm/card-feature/style.scss +75 -0
  120. package/dist/esm/card-form/index.js +134 -0
  121. package/dist/esm/card-form/style.scss +20 -0
  122. package/dist/esm/card-settings-group/index.js +33 -14
  123. package/dist/esm/card-sortable-list/index.js +170 -126
  124. package/dist/esm/category-autocomplete/index.js +168 -140
  125. package/dist/esm/color-picker/index.js +39 -21
  126. package/dist/esm/color-picker/index.test.js +78 -0
  127. package/dist/esm/color-picker/style.scss +11 -13
  128. package/dist/esm/confirm-dialog/index.js +85 -18
  129. package/dist/esm/consts.js +1 -1
  130. package/dist/esm/custom-select-control/index.js +34 -15
  131. package/dist/esm/dataviews/index.js +34 -0
  132. package/dist/esm/dataviews/style.scss +9 -0
  133. package/dist/esm/divider/index.js +24 -16
  134. package/dist/esm/footer/index.js +34 -28
  135. package/dist/esm/form-token-field/index.js +46 -27
  136. package/dist/esm/global-notices/index.js +3 -3
  137. package/dist/esm/grid/index.js +23 -14
  138. package/dist/esm/grid/style.scss +6 -1
  139. package/dist/esm/handoff/index.js +199 -143
  140. package/dist/esm/handoff-message/index.js +10 -6
  141. package/dist/esm/hooks/index.js +8 -6
  142. package/dist/esm/hooks/use-confirm-dialog.js +73 -0
  143. package/dist/esm/hooks/use-unsaved-changes-dialog.js +136 -0
  144. package/dist/esm/hooks/useObjectState.js +19 -9
  145. package/dist/esm/hooks/useObjectState.test.js +54 -32
  146. package/dist/esm/hooks/useOnClickOutside.js +4 -4
  147. package/dist/esm/hooks/usePrompt.js +9 -11
  148. package/dist/esm/image-upload/index.js +110 -111
  149. package/dist/esm/image-upload/index.test.js +11 -13
  150. package/dist/esm/image-upload/style.scss +12 -40
  151. package/dist/esm/index.js +7 -1
  152. package/dist/esm/info-button/index.js +38 -19
  153. package/dist/esm/integration-icons/active-campaign.js +16 -0
  154. package/dist/esm/integration-icons/constant-contact.js +22 -0
  155. package/dist/esm/integration-icons/fundraise-up.js +19 -0
  156. package/dist/esm/integration-icons/index.js +6 -0
  157. package/dist/esm/integration-icons/mailchimp.js +13 -0
  158. package/dist/esm/integration-icons/salesforce.js +11 -0
  159. package/dist/esm/integration-icons/wisepops.js +14 -0
  160. package/dist/esm/modal/index.js +20 -13
  161. package/dist/esm/modal/style.scss +4 -0
  162. package/dist/esm/newspack-icon/index.js +40 -25
  163. package/dist/esm/notice/index.js +59 -42
  164. package/dist/esm/plugin-installer/index.js +255 -243
  165. package/dist/esm/plugin-settings/SettingsSection.js +61 -50
  166. package/dist/esm/plugin-settings/index.js +212 -189
  167. package/dist/esm/plugin-toggle/index.js +187 -190
  168. package/dist/esm/popover/index.js +35 -16
  169. package/dist/esm/position-control/index.js +22 -18
  170. package/dist/esm/progress-bar/index.js +66 -49
  171. package/dist/esm/progress-bar/index.test.js +61 -67
  172. package/dist/esm/radio-control/index.js +34 -15
  173. package/dist/esm/section-header/index.js +102 -57
  174. package/dist/esm/section-header/style.scss +31 -11
  175. package/dist/esm/select-control/ButtonGroupControl.js +23 -18
  176. package/dist/esm/select-control/GroupedSelectControl.js +38 -30
  177. package/dist/esm/select-control/index.js +47 -27
  178. package/dist/esm/settings/MinMaxSetting.js +27 -16
  179. package/dist/esm/settings/SettingsCard.js +18 -13
  180. package/dist/esm/settings/SettingsSection.js +20 -19
  181. package/dist/esm/settings/index.js +3 -3
  182. package/dist/esm/sortable-newsletter-list-control/index.js +74 -53
  183. package/dist/esm/steps-list/index.js +39 -21
  184. package/dist/esm/steps-list-item/index.js +42 -26
  185. package/dist/esm/style-card/index.js +65 -50
  186. package/dist/esm/tabbed-navigation/index.js +35 -33
  187. package/dist/esm/text-control/index.js +25 -19
  188. package/dist/esm/utils/color.js +4 -4
  189. package/dist/esm/utils/editor-toolbar-back-button.js +53 -0
  190. package/dist/esm/utils/index.js +29 -16
  191. package/dist/esm/utils/style.scss +8 -0
  192. package/dist/esm/waiting/index.js +43 -24
  193. package/dist/esm/web-preview/index.js +213 -201
  194. package/dist/esm/with-wizard/index.js +360 -358
  195. package/dist/esm/with-wizard/style.scss +4 -54
  196. package/dist/esm/with-wizard-screen/index.js +48 -46
  197. package/dist/esm/with-wizard-screen/style.scss +41 -3
  198. package/dist/esm/wizard/components/WizardError.js +18 -16
  199. package/dist/esm/wizard/components/WizardSnackbar.js +23 -17
  200. package/dist/esm/wizard/index.js +171 -103
  201. package/dist/esm/wizard/store/index.js +169 -116
  202. package/dist/esm/wizard/store/utils.js +13 -6
  203. package/package.json +30 -16
  204. package/release.config.js +1 -0
  205. package/shared/hooks/use-coauthors.js +195 -27
  206. package/shared/hooks/use-coauthors.test.js +232 -6
  207. package/shared/scss/_mixins.scss +13 -5
  208. package/src/accordion/index.js +8 -7
  209. package/src/action-card/index.js +15 -1
  210. package/src/badge/index.tsx +3 -1
  211. package/src/card/core-card.js +95 -18
  212. package/src/card/index.js +1 -0
  213. package/src/card/style-core.scss +93 -25
  214. package/src/card/style.scss +2 -2
  215. package/src/card-feature/README.md +188 -0
  216. package/src/card-feature/index.tsx +186 -0
  217. package/src/card-feature/style.scss +75 -0
  218. package/src/card-form/README.md +113 -0
  219. package/src/card-form/index.tsx +128 -0
  220. package/src/card-form/style.scss +20 -0
  221. package/src/card-settings-group/index.tsx +28 -3
  222. package/src/card-sortable-list/index.tsx +27 -4
  223. package/src/color-picker/index.js +15 -8
  224. package/src/color-picker/index.test.js +52 -0
  225. package/src/color-picker/style.scss +11 -13
  226. package/src/confirm-dialog/README.md +128 -0
  227. package/src/confirm-dialog/index.tsx +74 -6
  228. package/src/dataviews/index.tsx +34 -0
  229. package/src/dataviews/style.scss +9 -0
  230. package/src/grid/style.scss +6 -1
  231. package/src/handoff/README.md +93 -0
  232. package/src/handoff/index.js +46 -8
  233. package/src/hooks/index.ts +2 -1
  234. package/src/hooks/use-confirm-dialog.tsx +80 -0
  235. package/src/hooks/use-unsaved-changes-dialog.tsx +140 -0
  236. package/src/image-upload/index.js +10 -15
  237. package/src/image-upload/style.scss +12 -40
  238. package/src/index.js +7 -1
  239. package/src/integration-icons/active-campaign.js +14 -0
  240. package/src/integration-icons/constant-contact.js +22 -0
  241. package/src/integration-icons/fundraise-up.js +18 -0
  242. package/src/integration-icons/index.js +6 -0
  243. package/src/integration-icons/mailchimp.js +10 -0
  244. package/src/integration-icons/salesforce.js +10 -0
  245. package/src/integration-icons/wisepops.js +14 -0
  246. package/src/modal/style.scss +4 -0
  247. package/src/plugin-installer/index.js +30 -19
  248. package/src/section-header/index.js +56 -24
  249. package/src/section-header/style.scss +31 -11
  250. package/src/select-control/index.js +4 -0
  251. package/src/utils/editor-toolbar-back-button.tsx +48 -0
  252. package/src/utils/index.js +6 -0
  253. package/src/utils/style.scss +8 -0
  254. package/src/with-wizard/style.scss +4 -54
  255. package/src/with-wizard-screen/style.scss +41 -3
  256. package/src/wizard/index.js +90 -53
  257. package/src/wizard/store/index.js +2 -2
  258. package/dist/cjs/button-card/index.js +0 -72
  259. package/dist/cjs/button-card/style.scss +0 -180
  260. package/dist/esm/button-card/index.js +0 -64
  261. package/dist/esm/button-card/style.scss +0 -180
  262. package/src/button-card/index.js +0 -52
  263. package/src/button-card/style.scss +0 -180
@@ -0,0 +1,186 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { __ } from '@wordpress/i18n';
10
+ import { DropdownMenu, __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
11
+ import { moreVertical } from '@wordpress/icons';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import Badge from '../badge';
17
+ import Button from '../button';
18
+ import Card from '../card';
19
+ import './style.scss';
20
+
21
+ type BadgeLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
22
+
23
+ type CardFeatureIcon = {
24
+ /** The icon node to render (e.g. a WordPress <Icon> component). */
25
+ node: React.ReactNode;
26
+ /** SVG fill colour, applied via currentColor. */
27
+ fill?: string;
28
+ /** Background colour for the icon container. */
29
+ backgroundColor?: string;
30
+ /**
31
+ * Border-radius of the icon container.
32
+ * 'small' uses $radius-small (2px), 'full' uses $radius-round (50%).
33
+ * Only relevant when backgroundColor is set.
34
+ */
35
+ radius?: 'small' | 'full';
36
+ };
37
+
38
+ type MoreControl = {
39
+ title: string;
40
+ onClick: () => void;
41
+ icon?: React.ReactNode;
42
+ };
43
+
44
+ type CardFeatureProps = {
45
+ title: string;
46
+ description?: string;
47
+ /** Icon displayed on the right-hand side of the title and description. */
48
+ icon?: CardFeatureIcon;
49
+ /** Whether the feature is currently enabled. */
50
+ enabled?: boolean;
51
+ /**
52
+ * When set, the card enters the "unmet requirements" state: an error
53
+ * badge displays this string and the title/description are muted. By
54
+ * default the primary button is disabled — set `requirementsActionable`
55
+ * if the primary button is the remediation for the unmet requirement.
56
+ */
57
+ requirements?: string;
58
+ /**
59
+ * When `requirements` is set, keep the primary button clickable so the
60
+ * user can remediate the unmet requirement from this card.
61
+ */
62
+ requirementsActionable?: boolean;
63
+ /** Primary button label when not enabled. Default: "Enable". */
64
+ enableLabel?: string;
65
+ /** Primary button label when enabled. Default: "Configure". */
66
+ configureLabel?: string;
67
+ /** Called when the primary button is clicked and the feature is not enabled. */
68
+ onEnable?: () => void;
69
+ /** Called when the primary button is clicked and the feature is enabled. */
70
+ onConfigure?: () => void;
71
+ /** Controls rendered inside the "More" dropdown, shown only when enabled. */
72
+ moreControls?: MoreControl[];
73
+ /** Badge text shown when enabled. Default: "Enabled". */
74
+ badgeText?: string;
75
+ /** Badge level shown when enabled. Default: "success". */
76
+ badgeLevel?: BadgeLevel;
77
+ className?: string;
78
+ };
79
+
80
+ /**
81
+ * CardFeature component.
82
+ *
83
+ * A card for presenting a named feature or setting with a predictable
84
+ * action model: a primary button, an optional "More" dropdown when enabled,
85
+ * and an automatic badge reflecting the current state.
86
+ */
87
+ const CardFeature = ( {
88
+ title,
89
+ description,
90
+ icon,
91
+ enabled = false,
92
+ requirements,
93
+ requirementsActionable = false,
94
+ enableLabel,
95
+ configureLabel,
96
+ onEnable,
97
+ onConfigure,
98
+ moreControls,
99
+ badgeText,
100
+ badgeLevel = 'success',
101
+ className,
102
+ }: CardFeatureProps ) => {
103
+ const isMuted = !! requirements;
104
+ const classes = classnames( 'newspack-card-feature', className, {
105
+ 'newspack-card-feature--muted': isMuted,
106
+ } );
107
+
108
+ let badge: { text: string; level: BadgeLevel } | undefined;
109
+ if ( requirements ) {
110
+ badge = { text: requirements, level: 'error' };
111
+ } else if ( enabled ) {
112
+ badge = { text: badgeText ?? __( 'Enabled', 'newspack-plugin' ), level: badgeLevel };
113
+ }
114
+
115
+ const isConfigureState = enabled && ! requirements;
116
+ const buttonLabel = isConfigureState ? configureLabel ?? __( 'Configure', 'newspack-plugin' ) : enableLabel ?? __( 'Enable', 'newspack-plugin' );
117
+
118
+ const handleButtonClick = () => {
119
+ if ( isConfigureState ) {
120
+ onConfigure?.();
121
+ } else {
122
+ onEnable?.();
123
+ }
124
+ };
125
+
126
+ const iconClasses = icon
127
+ ? classnames( 'newspack-card-feature__icon', {
128
+ 'newspack-card-feature__icon--radius-small': !! icon.backgroundColor && icon.radius !== 'full',
129
+ 'newspack-card-feature__icon--radius-full': icon.radius === 'full',
130
+ } )
131
+ : undefined;
132
+
133
+ return (
134
+ <Card
135
+ className={ classes }
136
+ __experimentalCoreCard
137
+ __experimentalCoreProps={ {
138
+ headerStyle: { padding: 32 },
139
+ header: (
140
+ <>
141
+ <HStack alignment="top" spacing={ 4 }>
142
+ <div className="newspack-card-feature__content">
143
+ <h2 className="newspack-card-feature__title">{ title }</h2>
144
+ { description && <p className="newspack-card-feature__description">{ description }</p> }
145
+ </div>
146
+ { icon && (
147
+ <div
148
+ className={ iconClasses }
149
+ style={ {
150
+ backgroundColor: icon.backgroundColor,
151
+ color: icon.fill,
152
+ } }
153
+ >
154
+ { icon.node }
155
+ </div>
156
+ ) }
157
+ </HStack>
158
+ <HStack alignment="edge">
159
+ <HStack expanded={ false } spacing="8px">
160
+ <Button
161
+ variant={ isConfigureState ? 'tertiary' : 'secondary' }
162
+ disabled={ isMuted && ! requirementsActionable }
163
+ onClick={ handleButtonClick }
164
+ size="compact"
165
+ >
166
+ { buttonLabel }
167
+ </Button>
168
+ { isConfigureState && !! moreControls?.length && (
169
+ <DropdownMenu
170
+ icon={ moreVertical }
171
+ label={ __( 'More', 'newspack-plugin' ) }
172
+ controls={ moreControls }
173
+ toggleProps={ { size: 'compact' } }
174
+ />
175
+ ) }
176
+ </HStack>
177
+ { badge && <Badge text={ badge.text } level={ badge.level } /> }
178
+ </HStack>
179
+ </>
180
+ ),
181
+ } }
182
+ />
183
+ );
184
+ };
185
+
186
+ export default CardFeature;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * CardFeature
3
+ */
4
+
5
+ @use "~@wordpress/base-styles/colors" as wp-colors;
6
+ @use "~@wordpress/base-styles/variables" as wp-vars;
7
+
8
+ .newspack-card-feature {
9
+ height: 100%;
10
+
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%;
20
+
21
+ // Push the action row (buttons + badge) to the bottom.
22
+ > :last-child {
23
+ margin-top: auto;
24
+ }
25
+ }
26
+
27
+ &__content {
28
+ flex: 1;
29
+ min-width: 0; // prevent flex blowout on long words
30
+ }
31
+
32
+ &__title {
33
+ font-size: wp-vars.$font-size-x-large;
34
+ line-height: wp-vars.$font-line-height-x-large;
35
+ text-align: left;
36
+ }
37
+
38
+ &__description {
39
+ text-align: left;
40
+ }
41
+
42
+ &__icon {
43
+ align-items: center;
44
+ display: flex;
45
+ flex: 0 0 40px;
46
+ height: 40px;
47
+ justify-content: center;
48
+
49
+ svg {
50
+ display: block;
51
+ fill: currentcolor;
52
+ height: 24px;
53
+ width: 24px;
54
+ }
55
+
56
+ &--radius-small {
57
+ border-radius: wp-vars.$radius-small;
58
+ }
59
+
60
+ &--radius-full {
61
+ border-radius: wp-vars.$radius-round;
62
+ }
63
+ }
64
+
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
+ }
74
+ }
75
+ }
@@ -0,0 +1,113 @@
1
+ # CardForm
2
+
3
+ A card component for presenting a named setting or feature with an expandable inline form. When open, the card body reveals children (controls, fields, action buttons) and the header border is removed for a seamless look. Intended for lists of items that can each be independently enabled, edited, or configured without leaving the page.
4
+
5
+ ## Layout rules
6
+
7
+ - Stack multiple `CardForm` cards inside a `<VStack>` — they are designed to appear as a list.
8
+ - The `actions` slot sits to the right of the badge. Keep it to one button; if you need multiple actions, use an `HStack` with `expanded={ false }`.
9
+ - The form body (`children`) is only mounted when `isOpen` is `true`.
10
+
11
+ ## States
12
+
13
+ | State | `isOpen` | `badge` | `actions` example |
14
+ |---|---|---|---|
15
+ | **Disabled** | `false` | None | "Enable" (secondary) |
16
+ | **Enabling** | `true` | None | "Cancel" (tertiary) |
17
+ | **Enabled** | `false` | Success badge | "Edit" (tertiary) |
18
+ | **Editing** | `true` | Success badge | "Cancel" (tertiary) |
19
+
20
+ ## Basic usage — enable/edit pattern
21
+
22
+ ```tsx
23
+ import { __ } from '@wordpress/i18n';
24
+ import { useState } from '@wordpress/element';
25
+ import { Button } from '@wordpress/components';
26
+ import { CardForm } from '../../../../../packages/components/src';
27
+
28
+ const [ isOpen, setIsOpen ] = useState( false );
29
+ const [ isEnabled, setIsEnabled ] = useState( false );
30
+
31
+ const handleClose = () => setIsOpen( false );
32
+
33
+ <CardForm
34
+ title={ __( 'Above Header', 'newspack-plugin' ) }
35
+ description={ __( 'Displays an ad above the site header.', 'newspack-plugin' ) }
36
+ badge={ isEnabled ? { level: 'success', text: __( 'Enabled', 'newspack-plugin' ) } : undefined }
37
+ actions={
38
+ isEnabled ? (
39
+ <Button variant="tertiary" size="compact" onClick={ () => isOpen ? handleClose() : setIsOpen( true ) }>
40
+ { isOpen ? __( 'Cancel', 'newspack-plugin' ) : __( 'Edit', 'newspack-plugin' ) }
41
+ </Button>
42
+ ) : (
43
+ <Button variant="secondary" size="compact" onClick={ () => setIsOpen( true ) }>
44
+ { __( 'Enable', 'newspack-plugin' ) }
45
+ </Button>
46
+ )
47
+ }
48
+ isOpen={ isOpen }
49
+ onRequestClose={ handleClose }
50
+ >
51
+ { /* form controls */ }
52
+ <Button variant="primary" size="compact" onClick={ handleSave }>
53
+ { __( 'Update', 'newspack-plugin' ) }
54
+ </Button>
55
+ </CardForm>
56
+ ```
57
+
58
+ ## With a custom badge level
59
+
60
+ The `badge` prop accepts any `BadgeLevel`. Use `warning` or `error` to communicate a degraded state.
61
+
62
+ ```tsx
63
+ <CardForm
64
+ title={ __( 'Above Header', 'newspack-plugin' ) }
65
+ badge={ { level: 'warning', text: __( 'Missing ad unit', 'newspack-plugin' ) } }
66
+ actions={ <Button variant="tertiary" size="compact">{ __( 'Edit', 'newspack-plugin' ) }</Button> }
67
+ isOpen={ false }
68
+ />
69
+ ```
70
+
71
+ ## Without a badge
72
+
73
+ Omit `badge` (or pass `undefined`) to show no badge at all.
74
+
75
+ ```tsx
76
+ <CardForm
77
+ title={ __( 'Sticky Footer', 'newspack-plugin' ) }
78
+ description={ __( 'Pins an ad to the bottom of the viewport.', 'newspack-plugin' ) }
79
+ actions={
80
+ <Button variant="secondary" size="compact" onClick={ handleEnable }>
81
+ { __( 'Enable', 'newspack-plugin' ) }
82
+ </Button>
83
+ }
84
+ isOpen={ false }
85
+ />
86
+ ```
87
+
88
+ ## Props
89
+
90
+ | Prop | Type | Default | Description |
91
+ |---|---|---|---|
92
+ | `title` | `string` | — | Card heading (**required**) |
93
+ | `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. |
95
+ | `actions` | `React.ReactNode` | — | JSX rendered in the header action area (buttons, dropdowns, etc.) |
96
+ | `isOpen` | `boolean` | `false` | When `true`, renders `children` in the card body and removes the header border |
97
+ | `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. |
99
+ | `className` | `string` | — | Additional class name applied to the card element |
100
+ | `children` | `React.ReactNode` | — | Form content rendered inside the card body when `isOpen` is `true` |
101
+
102
+ ## Accessibility
103
+
104
+ - The body is rendered as a `role="region"` labelled by the title, so assistive tech announces it as a named region when focus enters.
105
+ - On open, focus moves to the first focusable element in the body (or to the region itself if none exist). On close, focus is restored to whatever was focused before opening — typically the trigger button.
106
+ - 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
+
108
+
109
+ ### `BadgeLevel`
110
+
111
+ ```ts
112
+ type BadgeLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
113
+ ```
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Card Form component.
3
+ *
4
+ * A card with an expandable inline form — title, description, optional badge,
5
+ * and an actions slot in the header. When `isOpen` is true, children are
6
+ * rendered in the card body and the header border is removed for a seamless look.
7
+ */
8
+
9
+ /**
10
+ * External dependencies
11
+ */
12
+ import classnames from 'classnames';
13
+
14
+ /**
15
+ * WordPress dependencies
16
+ */
17
+ import { useEffect, useRef, createElement } from '@wordpress/element';
18
+ import { useInstanceId } from '@wordpress/compose';
19
+ import { __experimentalHStack as HStack, __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
20
+
21
+ /**
22
+ * Internal dependencies
23
+ */
24
+ import Badge, { BadgeLevel } from '../badge';
25
+ import Card from '../card';
26
+ import './style.scss';
27
+
28
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
29
+
30
+ type CardFormProps = {
31
+ title: string;
32
+ description?: string;
33
+ badge?: {
34
+ text: string;
35
+ level?: BadgeLevel;
36
+ };
37
+ /** JSX rendered in the header action area (buttons, etc.). */
38
+ actions?: React.ReactNode;
39
+ /** When true, children are shown and the header border is removed. */
40
+ isOpen?: boolean;
41
+ /** Called when the user presses Escape while the form is open. */
42
+ onRequestClose?: () => void;
43
+ /** Heading level for the title. Defaults to 3. */
44
+ titleLevel?: HeadingLevel;
45
+ className?: string;
46
+ children?: React.ReactNode;
47
+ };
48
+
49
+ const CardForm = ( { title, description, badge, actions, isOpen = false, onRequestClose, titleLevel = 3, className, children }: CardFormProps ) => {
50
+ const bodyRef = useRef< HTMLDivElement | null >( null );
51
+ const previousActiveRef = useRef< HTMLElement | null >( null );
52
+ const instanceId = useInstanceId( CardForm, 'newspack-card-form' );
53
+ const titleId = `${ instanceId }__title`;
54
+ const bodyId = `${ instanceId }__body`;
55
+
56
+ // Scope Escape handling to the open form's body, so multiple open CardForms
57
+ // don't all close on a single keypress, and callers can preventDefault from
58
+ // inner controls (e.g. select menus) without tripping the close.
59
+ useEffect( () => {
60
+ if ( ! isOpen || ! onRequestClose ) {
61
+ return;
62
+ }
63
+ const node = bodyRef.current;
64
+ if ( ! node ) {
65
+ return;
66
+ }
67
+ const handleKeyDown = ( event: KeyboardEvent ) => {
68
+ if ( event.key === 'Escape' && ! event.defaultPrevented ) {
69
+ onRequestClose();
70
+ }
71
+ };
72
+ node.addEventListener( 'keydown', handleKeyDown );
73
+ return () => node.removeEventListener( 'keydown', handleKeyDown );
74
+ }, [ isOpen, onRequestClose ] );
75
+
76
+ // Move focus into the body on open and restore it to the trigger on close.
77
+ useEffect( () => {
78
+ if ( ! isOpen ) {
79
+ return;
80
+ }
81
+ const node = bodyRef.current;
82
+ if ( node ) {
83
+ previousActiveRef.current = ( node.ownerDocument?.activeElement ?? null ) as HTMLElement | null;
84
+ const focusable = node.querySelector< HTMLElement >(
85
+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
86
+ );
87
+ ( focusable ?? node ).focus();
88
+ }
89
+ return () => {
90
+ previousActiveRef.current?.focus?.();
91
+ };
92
+ }, [ isOpen ] );
93
+
94
+ const titleTag = `h${ titleLevel }`;
95
+
96
+ return (
97
+ <Card
98
+ className={ classnames( 'newspack-card-form', className, {
99
+ 'newspack-card-form--open': isOpen,
100
+ } ) }
101
+ __experimentalCoreCard
102
+ isSmall
103
+ __experimentalCoreProps={ {
104
+ hasHeaderBorder: ! isOpen,
105
+ header: (
106
+ <HStack justify="space-between" style={ { width: '100%' } }>
107
+ <VStack spacing={ 0 } style={ { flex: 1, minWidth: 0 } }>
108
+ { createElement( titleTag, { id: titleId, className: 'newspack-card-form__title' }, title ) }
109
+ { description && <p className="newspack-card-form__description">{ description }</p> }
110
+ </VStack>
111
+ <HStack spacing={ 2 } expanded={ false }>
112
+ { badge && <Badge text={ badge.text } level={ badge.level ?? 'success' } /> }
113
+ { actions }
114
+ </HStack>
115
+ </HStack>
116
+ ),
117
+ } }
118
+ >
119
+ { isOpen && (
120
+ <div ref={ bodyRef } id={ bodyId } role="region" aria-labelledby={ titleId } tabIndex={ -1 } className="newspack-card-form__body">
121
+ { children }
122
+ </div>
123
+ ) }
124
+ </Card>
125
+ );
126
+ };
127
+
128
+ export default CardForm;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * CardForm
3
+ */
4
+
5
+ @use "~@wordpress/base-styles/colors" as wp-colors;
6
+ @use "~@wordpress/base-styles/variables" as wp;
7
+
8
+ .newspack-card-form {
9
+ &__title {
10
+ font-size: wp.$font-size-large;
11
+ font-weight: 600;
12
+ line-height: wp.$font-line-height-large;
13
+ }
14
+
15
+ &__description {
16
+ color: wp-colors.$gray-700;
17
+ font-size: wp.$font-size-medium;
18
+ line-height: wp.$font-line-height-medium;
19
+ }
20
+ }
@@ -2,6 +2,11 @@
2
2
  * Card - Settings group component.
3
3
  */
4
4
 
5
+ /**
6
+ * External dependencies
7
+ */
8
+ import classNames from 'classnames';
9
+
5
10
  /**
6
11
  * Internal dependencies
7
12
  */
@@ -9,25 +14,42 @@ import { Card } from '../';
9
14
  import './style.scss';
10
15
 
11
16
  const CardSettingsGroup = ( {
12
- actionType = 'chevron',
17
+ actionType = 'none',
13
18
  children,
19
+ className,
20
+ disabled = false,
14
21
  icon = null,
22
+ headerAction,
15
23
  title = '',
16
24
  description = '',
17
25
  isActive = false,
18
26
  onEnable = () => {},
27
+ onHeaderClick = () => {},
19
28
  }: {
20
29
  actionType?: 'chevron' | 'toggle' | 'button' | 'link' | 'none';
21
30
  children?: React.ReactNode;
31
+ className?: string;
32
+ disabled?: boolean;
22
33
  icon?: React.ReactNode;
23
34
  title: string;
35
+ headerAction?: {
36
+ label: string;
37
+ icon?: React.ReactNode;
38
+ href?: string;
39
+ onClick?: () => void;
40
+ disabled?: boolean;
41
+ destructive?: boolean;
42
+ tone?: 'primary' | 'secondary' | 'tertiary' | 'link';
43
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'link';
44
+ };
24
45
  description?: string;
25
46
  isActive?: boolean;
26
47
  onEnable?: () => void;
48
+ onHeaderClick?: () => void;
27
49
  } ) => {
28
50
  return (
29
51
  <Card
30
- className="newspack-card--core--settings-group"
52
+ className={ classNames( 'newspack-card--core--settings-group', className ) }
31
53
  actionType={ actionType }
32
54
  isSmall
33
55
  __experimentalCoreCard
@@ -38,7 +60,10 @@ const CardSettingsGroup = ( {
38
60
  { description && <p>{ description }</p> }
39
61
  </>
40
62
  ),
41
- onHeaderClick: onEnable,
63
+ headerAction,
64
+ onHeaderClick,
65
+ onToggle: onEnable,
66
+ disabled,
42
67
  icon,
43
68
  iconBackgroundColor: true,
44
69
  isActive,
@@ -22,11 +22,23 @@ import classNames from 'classnames';
22
22
  const DROP_ANIMATION_DURATION = 400; // ms — must match $drop-duration in style.scss
23
23
  const BUTTON_MOVE_DURATION = 200; // ms — must match $shift-duration in style.scss
24
24
 
25
+ type DraggableItemAction = {
26
+ label: string;
27
+ icon?: unknown;
28
+ action: () => void;
29
+ disabled?: boolean;
30
+ destructive?: boolean;
31
+ };
32
+
25
33
  type DraggableItem = {
26
34
  id: string | number;
27
35
  title: string;
36
+ description?: string;
28
37
  badgeLevel: 'default' | 'success' | 'info' | 'warning' | 'error';
29
38
  badgeText: string;
39
+ toggleChecked?: boolean;
40
+ onToggleChange?: () => void;
41
+ actions?: DraggableItemAction[];
30
42
  };
31
43
 
32
44
  type DragMeasurements = {
@@ -387,16 +399,27 @@ const CardSortableList = ( {
387
399
  __experimentalCoreCard
388
400
  __experimentalCoreProps={ {
389
401
  header: (
390
- <h3>
391
- { item.title }
392
- <Badge level={ item.badgeLevel } text={ item.badgeText } />
393
- </h3>
402
+ <>
403
+ <h3>
404
+ { item.title }
405
+ <Badge level={ item.badgeLevel } text={ item.badgeText } />
406
+ </h3>
407
+ { item.description && <p>{ item.description }</p> }
408
+ </>
394
409
  ),
395
410
  isDraggable: true,
396
411
  isFirstTarget: index === 0,
397
412
  isLastTarget: index === sortedItems.length - 1,
398
413
  dragIndex: index,
399
414
  onDragCallback: handleButtonMove,
415
+ ...( item.onToggleChange !== undefined && {
416
+ actionType: 'toggle',
417
+ isActive: item.toggleChecked,
418
+ onToggle: item.onToggleChange,
419
+ } ),
420
+ ...( item.actions !== undefined && {
421
+ actions: item.actions,
422
+ } ),
400
423
  } }
401
424
  />
402
425
  ) }