newspack-components 4.2.0 → 4.4.0-epic-editor-refactor.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 (315) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/DEVELOPMENT.md +868 -0
  3. package/copy-styles.js +3 -2
  4. package/dist/cjs/accordion/index.js +17 -12
  5. package/dist/cjs/action-card/index.js +186 -140
  6. package/dist/cjs/action-card/style.scss +25 -2
  7. package/dist/cjs/autocomplete-tokenfield/index.js +254 -234
  8. package/dist/cjs/autocomplete-with-latest-posts/index.js +351 -0
  9. package/dist/cjs/autocomplete-with-latest-posts/style.scss +150 -0
  10. package/dist/cjs/autocomplete-with-suggestions/index.js +229 -126
  11. package/dist/cjs/badge/index.js +32 -0
  12. package/dist/cjs/badge/style.scss +36 -0
  13. package/dist/cjs/box-contrast/index.js +19 -15
  14. package/dist/cjs/button/index.js +41 -24
  15. package/dist/cjs/button-props/button-props.js +5 -5
  16. package/dist/cjs/button-props/index.js +2 -2
  17. package/dist/cjs/card/core-card.js +199 -0
  18. package/dist/cjs/card/index.js +75 -35
  19. package/dist/cjs/card/style-core.scss +295 -0
  20. package/dist/cjs/card/style.scss +4 -4
  21. package/dist/cjs/card-feature/index.js +141 -0
  22. package/dist/cjs/card-feature/style.scss +75 -0
  23. package/dist/cjs/card-form/index.js +144 -0
  24. package/dist/cjs/card-form/style.scss +20 -0
  25. package/dist/cjs/card-settings-group/index.js +69 -0
  26. package/dist/cjs/card-settings-group/style.scss +33 -0
  27. package/dist/cjs/card-sortable-list/index.js +426 -0
  28. package/dist/cjs/card-sortable-list/style.scss +48 -0
  29. package/dist/cjs/category-autocomplete/index.js +175 -155
  30. package/dist/cjs/color-picker/index.js +41 -23
  31. package/dist/cjs/color-picker/index.test.js +82 -0
  32. package/dist/cjs/color-picker/style.scss +11 -13
  33. package/dist/cjs/confirm-dialog/index.js +123 -0
  34. package/dist/cjs/consts.js +1 -1
  35. package/dist/cjs/custom-select-control/index.js +39 -28
  36. package/dist/cjs/dataviews/index.js +38 -0
  37. package/dist/cjs/dataviews/style.scss +9 -0
  38. package/dist/cjs/divider/index.js +47 -0
  39. package/dist/cjs/divider/style.scss +51 -0
  40. package/dist/cjs/footer/index.js +36 -30
  41. package/dist/cjs/form-token-field/index.js +51 -40
  42. package/dist/cjs/form-token-field/style.scss +0 -5
  43. package/dist/cjs/global-notices/index.js +5 -5
  44. package/dist/cjs/grid/index.js +25 -18
  45. package/dist/cjs/grid/style.scss +67 -8
  46. package/dist/cjs/handoff/index.js +204 -156
  47. package/dist/cjs/handoff-message/index.js +12 -7
  48. package/dist/cjs/hooks/index.js +10 -8
  49. package/dist/cjs/hooks/use-confirm-dialog.js +79 -0
  50. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +144 -0
  51. package/dist/cjs/hooks/useObjectState.js +21 -12
  52. package/dist/cjs/hooks/useObjectState.test.js +55 -33
  53. package/dist/cjs/hooks/useOnClickOutside.js +6 -6
  54. package/dist/cjs/hooks/usePrompt.js +10 -13
  55. package/dist/cjs/image-upload/index.js +114 -123
  56. package/dist/cjs/image-upload/index.test.js +11 -13
  57. package/dist/cjs/image-upload/style.scss +12 -40
  58. package/dist/cjs/index.js +166 -92
  59. package/dist/cjs/info-button/index.js +43 -32
  60. package/dist/cjs/integration-icons/active-campaign.js +22 -0
  61. package/dist/cjs/integration-icons/constant-contact.js +28 -0
  62. package/dist/cjs/integration-icons/fundraise-up.js +25 -0
  63. package/dist/cjs/integration-icons/index.js +48 -0
  64. package/dist/cjs/integration-icons/mailchimp.js +19 -0
  65. package/dist/cjs/integration-icons/salesforce.js +17 -0
  66. package/dist/cjs/integration-icons/wisepops.js +20 -0
  67. package/dist/cjs/modal/index.js +29 -19
  68. package/dist/cjs/modal/style.scss +47 -13
  69. package/dist/cjs/newspack-icon/index.js +45 -38
  70. package/dist/cjs/newspack-icon/style.scss +5 -3
  71. package/dist/cjs/notice/index.js +64 -55
  72. package/dist/cjs/plugin-installer/index.js +259 -253
  73. package/dist/cjs/plugin-settings/SettingsSection.js +66 -60
  74. package/dist/cjs/plugin-settings/index.js +217 -200
  75. package/dist/cjs/plugin-toggle/index.js +192 -199
  76. package/dist/cjs/popover/index.js +41 -28
  77. package/dist/cjs/position-control/index.js +24 -23
  78. package/dist/cjs/progress-bar/index.js +71 -59
  79. package/dist/cjs/progress-bar/index.test.js +62 -68
  80. package/dist/cjs/proxied-imports/router.js +3 -3
  81. package/dist/cjs/radio-control/index.js +40 -27
  82. package/dist/cjs/section-header/index.js +143 -39
  83. package/dist/cjs/section-header/style.scss +129 -4
  84. package/dist/cjs/select-control/ButtonGroupControl.js +26 -21
  85. package/dist/cjs/select-control/GroupedSelectControl.js +41 -35
  86. package/dist/cjs/select-control/index.js +52 -40
  87. package/dist/cjs/settings/MinMaxSetting.js +30 -20
  88. package/dist/cjs/settings/SettingsCard.js +20 -17
  89. package/dist/cjs/settings/SettingsSection.js +22 -21
  90. package/dist/cjs/settings/index.js +5 -5
  91. package/dist/cjs/sortable-newsletter-list-control/index.js +78 -57
  92. package/dist/cjs/steps-list/index.js +44 -34
  93. package/dist/cjs/steps-list-item/index.js +47 -39
  94. package/dist/cjs/style-card/index.js +70 -63
  95. package/dist/cjs/style.scss +2 -0
  96. package/dist/cjs/tabbed-navigation/index.js +38 -36
  97. package/dist/cjs/text-control/index.js +27 -25
  98. package/dist/cjs/utils/color.js +4 -4
  99. package/dist/cjs/utils/editor-toolbar-back-button.js +61 -0
  100. package/dist/cjs/utils/index.js +31 -19
  101. package/dist/cjs/utils/style.scss +8 -0
  102. package/dist/cjs/waiting/index.js +48 -35
  103. package/dist/cjs/waiting/style.scss +4 -0
  104. package/dist/cjs/web-preview/index.js +219 -154
  105. package/dist/cjs/web-preview/style.scss +20 -53
  106. package/dist/cjs/with-wizard/index.js +362 -364
  107. package/dist/cjs/with-wizard/style.scss +28 -60
  108. package/dist/cjs/with-wizard-screen/index.js +54 -58
  109. package/dist/cjs/with-wizard-screen/style.scss +77 -10
  110. package/dist/cjs/wizard/components/WizardError.js +20 -18
  111. package/dist/cjs/wizard/components/WizardSnackbar.js +73 -0
  112. package/dist/cjs/wizard/components/style.scss +48 -0
  113. package/dist/cjs/wizard/index.js +207 -64
  114. package/dist/cjs/wizard/store/index.js +191 -97
  115. package/dist/cjs/wizard/store/utils.js +13 -8
  116. package/dist/esm/accordion/index.js +14 -9
  117. package/dist/esm/action-card/index.js +178 -124
  118. package/dist/esm/action-card/style.scss +25 -2
  119. package/dist/esm/autocomplete-tokenfield/index.js +248 -222
  120. package/dist/esm/autocomplete-with-latest-posts/index.js +347 -0
  121. package/dist/esm/autocomplete-with-latest-posts/style.scss +150 -0
  122. package/dist/esm/autocomplete-with-suggestions/index.js +226 -119
  123. package/dist/esm/badge/index.js +24 -0
  124. package/dist/esm/badge/style.scss +36 -0
  125. package/dist/esm/box-contrast/index.js +15 -11
  126. package/dist/esm/button/index.js +39 -19
  127. package/dist/esm/button-props/button-props.js +1 -1
  128. package/dist/esm/card/core-card.js +195 -0
  129. package/dist/esm/card/index.js +70 -22
  130. package/dist/esm/card/style-core.scss +295 -0
  131. package/dist/esm/card/style.scss +4 -4
  132. package/dist/esm/card-feature/index.js +131 -0
  133. package/dist/esm/card-feature/style.scss +75 -0
  134. package/dist/esm/card-form/index.js +134 -0
  135. package/dist/esm/card-form/style.scss +20 -0
  136. package/dist/esm/card-settings-group/index.js +61 -0
  137. package/dist/esm/card-settings-group/style.scss +33 -0
  138. package/dist/esm/card-sortable-list/index.js +425 -0
  139. package/dist/esm/card-sortable-list/style.scss +48 -0
  140. package/dist/esm/category-autocomplete/index.js +168 -140
  141. package/dist/esm/color-picker/index.js +39 -21
  142. package/dist/esm/color-picker/index.test.js +78 -0
  143. package/dist/esm/color-picker/style.scss +11 -13
  144. package/dist/esm/confirm-dialog/index.js +116 -0
  145. package/dist/esm/consts.js +1 -1
  146. package/dist/esm/custom-select-control/index.js +34 -15
  147. package/dist/esm/dataviews/index.js +34 -0
  148. package/dist/esm/dataviews/style.scss +9 -0
  149. package/dist/esm/divider/index.js +42 -0
  150. package/dist/esm/divider/style.scss +51 -0
  151. package/dist/esm/footer/index.js +34 -28
  152. package/dist/esm/form-token-field/index.js +46 -27
  153. package/dist/esm/form-token-field/style.scss +0 -5
  154. package/dist/esm/global-notices/index.js +3 -3
  155. package/dist/esm/grid/index.js +23 -13
  156. package/dist/esm/grid/style.scss +67 -8
  157. package/dist/esm/handoff/index.js +199 -143
  158. package/dist/esm/handoff-message/index.js +10 -6
  159. package/dist/esm/hooks/index.js +8 -6
  160. package/dist/esm/hooks/use-confirm-dialog.js +73 -0
  161. package/dist/esm/hooks/use-unsaved-changes-dialog.js +136 -0
  162. package/dist/esm/hooks/useObjectState.js +19 -9
  163. package/dist/esm/hooks/useObjectState.test.js +54 -32
  164. package/dist/esm/hooks/useOnClickOutside.js +4 -4
  165. package/dist/esm/hooks/usePrompt.js +9 -11
  166. package/dist/esm/image-upload/index.js +110 -111
  167. package/dist/esm/image-upload/index.test.js +11 -13
  168. package/dist/esm/image-upload/style.scss +12 -40
  169. package/dist/esm/index.js +13 -1
  170. package/dist/esm/info-button/index.js +38 -19
  171. package/dist/esm/integration-icons/active-campaign.js +16 -0
  172. package/dist/esm/integration-icons/constant-contact.js +22 -0
  173. package/dist/esm/integration-icons/fundraise-up.js +19 -0
  174. package/dist/esm/integration-icons/index.js +6 -0
  175. package/dist/esm/integration-icons/mailchimp.js +13 -0
  176. package/dist/esm/integration-icons/salesforce.js +11 -0
  177. package/dist/esm/integration-icons/wisepops.js +14 -0
  178. package/dist/esm/modal/index.js +27 -13
  179. package/dist/esm/modal/style.scss +47 -13
  180. package/dist/esm/newspack-icon/index.js +40 -25
  181. package/dist/esm/newspack-icon/style.scss +5 -3
  182. package/dist/esm/notice/index.js +59 -42
  183. package/dist/esm/plugin-installer/index.js +255 -243
  184. package/dist/esm/plugin-settings/SettingsSection.js +61 -50
  185. package/dist/esm/plugin-settings/index.js +212 -189
  186. package/dist/esm/plugin-toggle/index.js +187 -190
  187. package/dist/esm/popover/index.js +35 -16
  188. package/dist/esm/position-control/index.js +22 -18
  189. package/dist/esm/progress-bar/index.js +66 -49
  190. package/dist/esm/progress-bar/index.test.js +61 -67
  191. package/dist/esm/radio-control/index.js +34 -15
  192. package/dist/esm/section-header/index.js +140 -38
  193. package/dist/esm/section-header/style.scss +129 -4
  194. package/dist/esm/select-control/ButtonGroupControl.js +23 -18
  195. package/dist/esm/select-control/GroupedSelectControl.js +38 -30
  196. package/dist/esm/select-control/index.js +47 -27
  197. package/dist/esm/settings/MinMaxSetting.js +27 -16
  198. package/dist/esm/settings/SettingsCard.js +18 -13
  199. package/dist/esm/settings/SettingsSection.js +20 -19
  200. package/dist/esm/settings/index.js +3 -3
  201. package/dist/esm/sortable-newsletter-list-control/index.js +74 -53
  202. package/dist/esm/steps-list/index.js +39 -21
  203. package/dist/esm/steps-list-item/index.js +42 -26
  204. package/dist/esm/style-card/index.js +65 -50
  205. package/dist/esm/style.scss +2 -0
  206. package/dist/esm/tabbed-navigation/index.js +35 -33
  207. package/dist/esm/text-control/index.js +25 -19
  208. package/dist/esm/utils/color.js +4 -4
  209. package/dist/esm/utils/editor-toolbar-back-button.js +53 -0
  210. package/dist/esm/utils/index.js +29 -16
  211. package/dist/esm/utils/style.scss +8 -0
  212. package/dist/esm/waiting/index.js +43 -22
  213. package/dist/esm/waiting/style.scss +4 -0
  214. package/dist/esm/web-preview/index.js +218 -147
  215. package/dist/esm/web-preview/style.scss +20 -53
  216. package/dist/esm/with-wizard/index.js +360 -358
  217. package/dist/esm/with-wizard/style.scss +28 -60
  218. package/dist/esm/with-wizard-screen/index.js +48 -46
  219. package/dist/esm/with-wizard-screen/style.scss +77 -10
  220. package/dist/esm/wizard/components/WizardError.js +18 -16
  221. package/dist/esm/wizard/components/WizardSnackbar.js +70 -0
  222. package/dist/esm/wizard/components/style.scss +48 -0
  223. package/dist/esm/wizard/index.js +207 -58
  224. package/dist/esm/wizard/store/index.js +189 -90
  225. package/dist/esm/wizard/store/utils.js +13 -6
  226. package/package.json +30 -17
  227. package/release.config.js +1 -0
  228. package/shared/author-context.js +26 -0
  229. package/shared/hooks/use-coauthors.js +388 -0
  230. package/shared/hooks/use-coauthors.test.js +850 -0
  231. package/shared/hooks/use-custom-byline.js +47 -0
  232. package/shared/hooks/use-custom-byline.test.js +133 -0
  233. package/shared/scss/_mixins.scss +54 -0
  234. package/src/accordion/index.js +8 -7
  235. package/src/action-card/action-card.d.ts +7 -3
  236. package/src/action-card/index.js +39 -12
  237. package/src/action-card/style.scss +25 -2
  238. package/src/autocomplete-tokenfield/index.js +2 -2
  239. package/src/autocomplete-with-latest-posts/index.js +290 -0
  240. package/src/autocomplete-with-latest-posts/style.scss +150 -0
  241. package/src/badge/index.tsx +26 -0
  242. package/src/badge/style.scss +36 -0
  243. package/src/button/index.tsx +3 -2
  244. package/src/card/core-card.js +224 -0
  245. package/src/card/index.js +40 -1
  246. package/src/card/style-core.scss +295 -0
  247. package/src/card/style.scss +4 -4
  248. package/src/card-feature/README.md +188 -0
  249. package/src/card-feature/index.tsx +186 -0
  250. package/src/card-feature/style.scss +75 -0
  251. package/src/card-form/README.md +113 -0
  252. package/src/card-form/index.tsx +128 -0
  253. package/src/card-form/style.scss +20 -0
  254. package/src/card-settings-group/README.md +43 -0
  255. package/src/card-settings-group/index.tsx +78 -0
  256. package/src/card-settings-group/style.scss +33 -0
  257. package/src/card-sortable-list/README.md +51 -0
  258. package/src/card-sortable-list/index.tsx +435 -0
  259. package/src/card-sortable-list/style.scss +48 -0
  260. package/src/color-picker/index.js +15 -8
  261. package/src/color-picker/index.test.js +52 -0
  262. package/src/color-picker/style.scss +11 -13
  263. package/src/confirm-dialog/README.md +128 -0
  264. package/src/confirm-dialog/index.tsx +132 -0
  265. package/src/dataviews/index.tsx +34 -0
  266. package/src/dataviews/style.scss +9 -0
  267. package/src/divider/README.md +25 -0
  268. package/src/divider/index.js +31 -0
  269. package/src/divider/style.scss +51 -0
  270. package/src/form-token-field/style.scss +0 -5
  271. package/src/grid/index.tsx +2 -1
  272. package/src/grid/style.scss +67 -8
  273. package/src/handoff/README.md +93 -0
  274. package/src/handoff/index.js +46 -8
  275. package/src/hooks/index.ts +2 -1
  276. package/src/hooks/use-confirm-dialog.tsx +80 -0
  277. package/src/hooks/use-unsaved-changes-dialog.tsx +140 -0
  278. package/src/image-upload/index.js +10 -15
  279. package/src/image-upload/style.scss +12 -40
  280. package/src/index.js +13 -1
  281. package/src/integration-icons/active-campaign.js +14 -0
  282. package/src/integration-icons/constant-contact.js +22 -0
  283. package/src/integration-icons/fundraise-up.js +18 -0
  284. package/src/integration-icons/index.js +6 -0
  285. package/src/integration-icons/mailchimp.js +10 -0
  286. package/src/integration-icons/salesforce.js +10 -0
  287. package/src/integration-icons/wisepops.js +14 -0
  288. package/src/modal/index.js +12 -3
  289. package/src/modal/style.scss +47 -13
  290. package/src/newspack-icon/style.scss +5 -3
  291. package/src/plugin-installer/index.js +30 -19
  292. package/src/section-header/index.js +105 -14
  293. package/src/section-header/style.scss +129 -4
  294. package/src/select-control/index.js +4 -0
  295. package/src/style.scss +2 -0
  296. package/src/utils/editor-toolbar-back-button.tsx +48 -0
  297. package/src/utils/index.js +6 -0
  298. package/src/utils/style.scss +8 -0
  299. package/src/waiting/index.js +2 -1
  300. package/src/waiting/style.scss +4 -0
  301. package/src/web-preview/index.js +90 -35
  302. package/src/web-preview/style.scss +20 -53
  303. package/src/with-wizard/index.js +1 -1
  304. package/src/with-wizard/style.scss +28 -60
  305. package/src/with-wizard-screen/style.scss +77 -10
  306. package/src/wizard/components/WizardSnackbar.js +58 -0
  307. package/src/wizard/components/style.scss +48 -0
  308. package/src/wizard/index.js +152 -21
  309. package/src/wizard/store/index.js +28 -2
  310. package/dist/cjs/button-card/index.js +0 -72
  311. package/dist/cjs/button-card/style.scss +0 -180
  312. package/dist/esm/button-card/index.js +0 -64
  313. package/dist/esm/button-card/style.scss +0 -180
  314. package/src/button-card/index.js +0 -52
  315. package/src/button-card/style.scss +0 -180
@@ -0,0 +1,188 @@
1
+ # CardFeature
2
+
3
+ A card component for presenting a named feature or setting with a predictable, state-driven interaction model. It handles the primary button, an optional "More" dropdown, and a badge automatically based on the `enabled` and `requirements` props.
4
+
5
+ ## Layout rules
6
+
7
+ - **Maximum 2 cards per row.** Use `<Grid columns={ 2 }>` — never 3 or more. Cards are designed to be read, not scanned, and 3+ columns makes them too narrow for the content.
8
+ - The icon is always displayed on the **right-hand side**, aligned to the top of the content.
9
+
10
+ ## States
11
+
12
+ | State | Condition | Button | Dropdown | Badge |
13
+ |---|---|---|---|---|
14
+ | **Unmet requirements** | `requirements` is set | "Enable" — disabled (clickable if `requirementsActionable`) | Hidden | Error badge with `requirements` text |
15
+ | **Disabled** | `!enabled`, no requirements | "Enable" | Hidden | None |
16
+ | **Enabled** | `enabled`, no requirements | "Configure" | Shown if `moreControls` provided | Success badge ("Enabled") |
17
+
18
+ The card content (title + description) is visually muted (gray text, lighter border) when `requirements` is set.
19
+
20
+ ## Basic usage
21
+
22
+ ```tsx
23
+ import { __ } from '@wordpress/i18n';
24
+
25
+ <CardFeature
26
+ title={ __( 'Metered countdown', 'newspack-plugin' ) }
27
+ description={ __( 'Show a countdown banner letting readers know how many free views they have left.', 'newspack-plugin' ) }
28
+ enabled={ isEnabled }
29
+ onEnable={ () => setEnabled( true ) }
30
+ onConfigure={ () => history.push( '/settings/countdown' ) }
31
+ moreControls={ [
32
+ { title: __( 'Disable', 'newspack-plugin' ), onClick: () => setEnabled( false ) },
33
+ ] }
34
+ />
35
+ ```
36
+
37
+ ## With unmet requirements
38
+
39
+ When `requirements` is set the button is disabled and an error badge displays the string. The title and description are visually muted.
40
+
41
+ ```tsx
42
+ import { __ } from '@wordpress/i18n';
43
+
44
+ <CardFeature
45
+ title={ __( 'Metered countdown', 'newspack-plugin' ) }
46
+ description={ __( 'Show a countdown banner letting readers know how many free views they have left.', 'newspack-plugin' ) }
47
+ enabled={ isEnabled }
48
+ requirements={ __( 'Requires metering', 'newspack-plugin' ) }
49
+ onEnable={ () => setEnabled( true ) }
50
+ onConfigure={ () => history.push( '/settings/countdown' ) }
51
+ moreControls={ [
52
+ { title: __( 'Disable', 'newspack-plugin' ), onClick: () => setEnabled( false ) },
53
+ ] }
54
+ />
55
+ ```
56
+
57
+ ## With a custom icon
58
+
59
+ The `icon` prop accepts an object, not a bare node. Pass `node` for the icon element, `fill` to control the SVG colour (applied via `currentColor`), `backgroundColor` for a container background, and `radius` for the corner treatment.
60
+
61
+ The icon container is always **40 × 40 px** with the SVG at **24 × 24 px**. `radius` only applies when `backgroundColor` is set.
62
+
63
+ ```tsx
64
+ import { __ } from '@wordpress/i18n';
65
+ import { Icon, starFilled } from '@wordpress/icons';
66
+
67
+ // Icon with fill only
68
+ <CardFeature
69
+ title={ __( 'Content gifting', 'newspack-plugin' ) }
70
+ description={ __( 'Let subscribers share gated articles with non-subscribers.', 'newspack-plugin' ) }
71
+ icon={ { node: <Icon icon={ starFilled } />, fill: '#003da5' } }
72
+ enabled={ isEnabled }
73
+ onEnable={ handleEnable }
74
+ onConfigure={ handleConfigure }
75
+ moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
76
+ />
77
+
78
+ // Icon with circular background
79
+ <CardFeature
80
+ title={ __( 'Content gifting', 'newspack-plugin' ) }
81
+ description={ __( 'Let subscribers share gated articles with non-subscribers.', 'newspack-plugin' ) }
82
+ icon={ { node: <Icon icon={ starFilled } />, fill: '#003da5', backgroundColor: '#dfe7f4', radius: 'full' } }
83
+ enabled={ isEnabled }
84
+ onEnable={ handleEnable }
85
+ onConfigure={ handleConfigure }
86
+ moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
87
+ />
88
+ ```
89
+
90
+ ## With custom button labels
91
+
92
+ Override `enableLabel` and `configureLabel` to match the context of the feature.
93
+
94
+ ```tsx
95
+ import { __ } from '@wordpress/i18n';
96
+
97
+ <CardFeature
98
+ title={ __( 'Apple News', 'newspack-plugin' ) }
99
+ description={ __( 'Automatically publish articles to Apple News.', 'newspack-plugin' ) }
100
+ enabled={ isEnabled }
101
+ enableLabel={ __( 'Connect', 'newspack-plugin' ) }
102
+ configureLabel={ __( 'Manage connection', 'newspack-plugin' ) }
103
+ onEnable={ handleConnect }
104
+ onConfigure={ () => history.push( '/settings/apple-news' ) }
105
+ moreControls={ [ { title: __( 'Disconnect', 'newspack-plugin' ), onClick: handleDisconnect } ] }
106
+ />
107
+ ```
108
+
109
+ ## With a custom badge
110
+
111
+ Override `badgeText` and `badgeLevel` to change the badge shown when the feature is enabled. Available levels: `default`, `info`, `success`, `warning`, `error`.
112
+
113
+ ```tsx
114
+ import { __ } from '@wordpress/i18n';
115
+
116
+ <CardFeature
117
+ title={ __( 'Stripe', 'newspack-plugin' ) }
118
+ description={ __( 'Accept payments via Stripe.', 'newspack-plugin' ) }
119
+ enabled={ isEnabled }
120
+ badgeText={ __( 'Live mode', 'newspack-plugin' ) }
121
+ badgeLevel="info"
122
+ onEnable={ handleEnable }
123
+ onConfigure={ () => history.push( '/settings/stripe' ) }
124
+ moreControls={ [ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable } ] }
125
+ />
126
+ ```
127
+
128
+ ## With multiple "More" controls
129
+
130
+ `moreControls` accepts any number of items, each with a `title`, `onClick`, and an optional `icon`.
131
+
132
+ ```tsx
133
+ import { __ } from '@wordpress/i18n';
134
+
135
+ <CardFeature
136
+ title={ __( 'Newsletters', 'newspack-plugin' ) }
137
+ description={ __( 'Send newsletters directly from the WordPress editor.', 'newspack-plugin' ) }
138
+ enabled={ isEnabled }
139
+ onEnable={ handleEnable }
140
+ onConfigure={ () => history.push( '/settings/newsletters' ) }
141
+ moreControls={ [
142
+ { title: __( 'Edit', 'newspack-plugin' ), onClick: handleEdit },
143
+ { title: __( 'Preview', 'newspack-plugin' ), onClick: handlePreview },
144
+ { title: __( 'Disable', 'newspack-plugin' ), onClick: handleDisable },
145
+ ] }
146
+ />
147
+ ```
148
+
149
+ ## Props
150
+
151
+ | Prop | Type | Default | Description |
152
+ |---|---|---|---|
153
+ | `title` | `string` | — | Card heading (**required**) |
154
+ | `description` | `string` | — | Supporting text below the title |
155
+ | `icon` | `CardFeatureIcon` | — | Icon displayed on the right. See `CardFeatureIcon` below. |
156
+ | `enabled` | `boolean` | `false` | Whether the feature is currently enabled |
157
+ | `requirements` | `string` | — | When set, enters the unmet-requirements state; value is used as the error badge text |
158
+ | `requirementsActionable` | `boolean` | `false` | When `requirements` is set, keep the primary button clickable so it can remediate the unmet requirement |
159
+ | `enableLabel` | `string` | `"Enable"` | Primary button label when not enabled |
160
+ | `configureLabel` | `string` | `"Configure"` | Primary button label when enabled |
161
+ | `onEnable` | `() => void` | — | Called when the primary button is clicked and the feature is not enabled |
162
+ | `onConfigure` | `() => void` | — | Called when the primary button is clicked and the feature is enabled |
163
+ | `moreControls` | `MoreControl[]` | — | Items for the "More" dropdown, shown only when enabled |
164
+ | `badgeText` | `string` | `"Enabled"` | Badge text shown when enabled |
165
+ | `badgeLevel` | `BadgeLevel` | `"success"` | Badge level shown when enabled |
166
+ | `className` | `string` | — | Additional class name applied to the card element |
167
+
168
+ ### `CardFeatureIcon`
169
+
170
+ ```ts
171
+ type CardFeatureIcon = {
172
+ node: React.ReactNode; // The icon element to render
173
+ fill?: string; // SVG fill colour (applied via currentColor)
174
+ backgroundColor?: string; // Background colour of the 40×40 container
175
+ radius?: 'small' | 'full'; // 'small' = 2px ($radius-small), 'full' = 50% ($radius-round)
176
+ // Only applied when backgroundColor is set.
177
+ };
178
+ ```
179
+
180
+ ### `MoreControl`
181
+
182
+ ```ts
183
+ type MoreControl = {
184
+ title: string;
185
+ onClick: () => void;
186
+ icon?: React.ReactNode;
187
+ };
188
+ ```
@@ -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;