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,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
+ }
@@ -0,0 +1,43 @@
1
+ # CardSettingsGroup
2
+
3
+ A collapsible settings card that renders a title, optional description, and optional icon in the header. Children are rendered only when the card is active.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `actionType` | `'chevron'` \| `'toggle'` \| `'button'` \| `'link'` \| `'none'` | `'chevron'` | Controls the interactive element rendered in the card header. |
10
+ | `children` | `React.ReactNode` | — | Content displayed inside the card body. Only rendered when `isActive` is `true`. |
11
+ | `description` | `string` | `''` | Optional description rendered below the title in the card header. |
12
+ | `icon` | `React.ReactNode` | `null` | Optional icon rendered in the card header with a colored background. |
13
+ | `isActive` | `boolean` | `false` | When `true`, the card is expanded and children are visible. |
14
+ | `onEnable` | `() => void` | `() => {}` | Callback fired when the card header is clicked. Typically used to toggle `isActive`. |
15
+ | `title` | `string` | — | (**Required**) Title rendered in the card header. |
16
+
17
+ ## Usage
18
+
19
+ ```jsx
20
+ import { CardSettingsGroup } from 'newspack-components';
21
+ import { megaphone } from '@wordpress/icons';
22
+
23
+ // Basic collapsible settings group
24
+ <CardSettingsGroup
25
+ title="Email Notifications"
26
+ description="Configure how readers are notified."
27
+ isActive={ isEnabled }
28
+ onEnable={ () => setIsEnabled( ! isEnabled ) }
29
+ >
30
+ <p>Settings content shown when active.</p>
31
+ </CardSettingsGroup>
32
+
33
+ // With an icon and toggle action type
34
+ <CardSettingsGroup
35
+ actionType="toggle"
36
+ icon={ megaphone }
37
+ title="Push Notifications"
38
+ isActive={ isEnabled }
39
+ onEnable={ () => setIsEnabled( ! isEnabled ) }
40
+ >
41
+ <p>Settings content shown when active.</p>
42
+ </CardSettingsGroup>
43
+ ```
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Card - Settings group component.
3
+ */
4
+
5
+ /**
6
+ * External dependencies
7
+ */
8
+ import classNames from 'classnames';
9
+
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import { Card } from '../';
14
+ import './style.scss';
15
+
16
+ const CardSettingsGroup = ( {
17
+ actionType = 'none',
18
+ children,
19
+ className,
20
+ disabled = false,
21
+ icon = null,
22
+ headerAction,
23
+ title = '',
24
+ description = '',
25
+ isActive = false,
26
+ onEnable = () => {},
27
+ onHeaderClick = () => {},
28
+ }: {
29
+ actionType?: 'chevron' | 'toggle' | 'button' | 'link' | 'none';
30
+ children?: React.ReactNode;
31
+ className?: string;
32
+ disabled?: boolean;
33
+ icon?: React.ReactNode;
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
+ };
45
+ description?: string;
46
+ isActive?: boolean;
47
+ onEnable?: () => void;
48
+ onHeaderClick?: () => void;
49
+ } ) => {
50
+ return (
51
+ <Card
52
+ className={ classNames( 'newspack-card--core--settings-group', className ) }
53
+ actionType={ actionType }
54
+ isSmall
55
+ __experimentalCoreCard
56
+ __experimentalCoreProps={ {
57
+ header: (
58
+ <>
59
+ <h3>{ title }</h3>
60
+ { description && <p>{ description }</p> }
61
+ </>
62
+ ),
63
+ headerAction,
64
+ onHeaderClick,
65
+ onToggle: onEnable,
66
+ disabled,
67
+ icon,
68
+ iconBackgroundColor: true,
69
+ isActive,
70
+ title,
71
+ } }
72
+ >
73
+ { isActive && children }
74
+ </Card>
75
+ );
76
+ };
77
+
78
+ export default CardSettingsGroup;
@@ -0,0 +1,33 @@
1
+ .newspack-card--core--settings-group {
2
+ .components-card__body {
3
+ > .newspack-notice {
4
+ margin: 16px 0;
5
+ }
6
+ > .components-base-control {
7
+ &:first-child {
8
+ margin-top: 0;
9
+ }
10
+ + .components-base-control,
11
+ + .newspack-form-token-field {
12
+ margin-top: 16px;
13
+ }
14
+ }
15
+
16
+ /** ToggleControl switch should appear after the label. */
17
+ > .components-toggle-control {
18
+ .components-h-stack {
19
+ .components-form-toggle {
20
+ order: 2;
21
+ }
22
+ }
23
+ .components-toggle-control__help {
24
+ margin-inline-start: auto;
25
+ }
26
+ }
27
+ .newspack-grid:last-child,
28
+ .components-base-control:last-child,
29
+ p:last-child {
30
+ margin-bottom: 0;
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,51 @@
1
+ # CardSortableList
2
+
3
+ A drag-and-drop sortable list of cards. Each item displays a title and a status badge. Items can be reordered by dragging with a pointer or by using the up/down chevron buttons (keyboard accessible). The list locks its height during a drag to prevent layout shifts.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `disabled` | `boolean` | `false` | When `true`, all drag and button controls are disabled via `Disabled`. |
10
+ | `items` | `DraggableItem[]` | `[]` | Array of items to render. See the `DraggableItem` shape below. |
11
+ | `onDragCallback` | `(index: number, targetIndex: number) => void` | `() => {}` | Callback fired after a reorder completes (drag-and-drop or button move). Receives the original index and the new index. |
12
+
13
+ ### `DraggableItem`
14
+
15
+ | Field | Type | Description |
16
+ |-------|------|-------------|
17
+ | `id` | `string` \| `number` | Unique identifier for the item. Used as the React `key`. |
18
+ | `title` | `string` | Label rendered in the card header. |
19
+ | `badgeLevel` | `'default'` \| `'success'` \| `'info'` \| `'warning'` \| `'error'` | Visual style of the status badge. |
20
+ | `badgeText` | `string` | Text displayed inside the badge. |
21
+
22
+ ## Usage
23
+
24
+ ```jsx
25
+ import { CardSortableList } from 'newspack-components';
26
+
27
+ const [ items, setItems ] = useState( [
28
+ { id: 1, title: 'Homepage', badgeLevel: 'success', badgeText: 'Active' },
29
+ { id: 2, title: 'About', badgeLevel: 'default', badgeText: 'Draft' },
30
+ { id: 3, title: 'Contact', badgeLevel: 'warning', badgeText: 'Review' },
31
+ ] );
32
+
33
+ const handleReorder = ( fromIndex, toIndex ) => {
34
+ const reordered = [ ...items ];
35
+ const [ moved ] = reordered.splice( fromIndex, 1 );
36
+ reordered.splice( toIndex, 0, moved );
37
+ setItems( reordered );
38
+ };
39
+
40
+ <CardSortableList
41
+ items={ items }
42
+ onDragCallback={ handleReorder }
43
+ />
44
+
45
+ // Disabled state (controls are rendered but non-interactive)
46
+ <CardSortableList
47
+ items={ items }
48
+ disabled={ true }
49
+ onDragCallback={ handleReorder }
50
+ />
51
+ ```
@@ -0,0 +1,435 @@
1
+ /**
2
+ * Card - Sortable list component.
3
+ */
4
+
5
+ /**
6
+ * WordPress dependencies.
7
+ */
8
+ import { Disabled, Draggable, __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
9
+ import { useEffect, useLayoutEffect, useRef, useState } from '@wordpress/element';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import { Badge, Card } from '../';
15
+ import './style.scss';
16
+
17
+ /**
18
+ * External dependencies
19
+ */
20
+ import classNames from 'classnames';
21
+
22
+ const DROP_ANIMATION_DURATION = 400; // ms — must match $drop-duration in style.scss
23
+ const BUTTON_MOVE_DURATION = 200; // ms — must match $shift-duration in style.scss
24
+
25
+ type DraggableItemAction = {
26
+ label: string;
27
+ icon?: unknown;
28
+ action: () => void;
29
+ disabled?: boolean;
30
+ destructive?: boolean;
31
+ };
32
+
33
+ type DraggableItem = {
34
+ id: string | number;
35
+ title: string;
36
+ description?: string;
37
+ badgeLevel: 'default' | 'success' | 'info' | 'warning' | 'error';
38
+ badgeText: string;
39
+ toggleChecked?: boolean;
40
+ onToggleChange?: () => void;
41
+ actions?: DraggableItemAction[];
42
+ };
43
+
44
+ type DragMeasurements = {
45
+ lockedHeight: number;
46
+ sourceHeight: number;
47
+ // Height of each item (including the VStack gap after it, except the last).
48
+ itemStrides: number[];
49
+ // Top edge of each item, used for drop position hit-testing in onDragEnd.
50
+ itemTops: number[];
51
+ };
52
+
53
+ const CardSortableList = ( {
54
+ disabled = false,
55
+ items = [],
56
+ onDragCallback = () => {},
57
+ }: {
58
+ disabled?: boolean;
59
+ items?: DraggableItem[];
60
+ onDragCallback?: ( index: number, targetIndex: number ) => void;
61
+ } ) => {
62
+ const [ sortedItems, setSortedItems ] = useState( items );
63
+ const [ draggingIndex, setDraggingIndex ] = useState< number | null >( null );
64
+ const [ hoverIndex, setHoverIndex ] = useState< number | null >( null );
65
+ const [ droppedIndex, setDroppedIndex ] = useState< number | null >( null );
66
+ const [ measurements, setMeasurements ] = useState< DragMeasurements | null >( null );
67
+ const listRef = useRef< HTMLDivElement | null >( null );
68
+ const itemRefs = useRef< ( HTMLDivElement | null )[] >( [] );
69
+ const dropAnimationTimer = useRef< ReturnType< typeof setTimeout > | null >( null );
70
+ const buttonMoveTimer = useRef< ReturnType< typeof setTimeout > | null >( null );
71
+ const buttonFlipRef = useRef< {
72
+ fromIndex: number;
73
+ toIndex: number;
74
+ stride: number;
75
+ direction: number;
76
+ } | null >( null );
77
+ const [ buttonMoveId, setButtonMoveId ] = useState( 0 );
78
+ const documentDragOverRef = useRef< ( ( e: Event ) => void ) | null >( null );
79
+
80
+ // Keep sortedItems in sync when the items prop changes externally (e.g. after a save).
81
+ useEffect( () => {
82
+ setSortedItems( items );
83
+ }, [ items ] );
84
+
85
+ // Clean up any pending animation timer on unmount.
86
+ useEffect( () => {
87
+ return () => {
88
+ if ( dropAnimationTimer.current ) {
89
+ clearTimeout( dropAnimationTimer.current );
90
+ }
91
+ if ( buttonMoveTimer.current ) {
92
+ clearTimeout( buttonMoveTimer.current );
93
+ }
94
+ if ( documentDragOverRef.current ) {
95
+ document.removeEventListener( 'dragover', documentDragOverRef.current );
96
+ }
97
+ };
98
+ }, [] );
99
+
100
+ /**
101
+ * Handle a chevron-button move. Records item positions before the reorder so
102
+ * the FLIP animation can play after React commits the new DOM order.
103
+ */
104
+ const handleButtonMove = ( fromIndex: number, toIndex: number ) => {
105
+ if ( toIndex < 0 || toIndex >= sortedItems.length ) {
106
+ return;
107
+ }
108
+
109
+ const fromEl = itemRefs.current[ fromIndex ];
110
+ const toEl = itemRefs.current[ toIndex ];
111
+ if ( ! fromEl || ! toEl ) {
112
+ return;
113
+ }
114
+
115
+ const stride = Math.abs( toEl.getBoundingClientRect().top - fromEl.getBoundingClientRect().top );
116
+ const direction = toIndex > fromIndex ? 1 : -1;
117
+ buttonFlipRef.current = { fromIndex, toIndex, stride, direction };
118
+
119
+ const reordered = [ ...sortedItems ];
120
+ const [ moved ] = reordered.splice( fromIndex, 1 );
121
+ reordered.splice( toIndex, 0, moved );
122
+ setSortedItems( reordered );
123
+ setButtonMoveId( id => id + 1 );
124
+ };
125
+
126
+ /**
127
+ * FLIP Invert+Play step: runs synchronously after React commits the reordered
128
+ * DOM. Animate each swapped item from its previous position to its new one
129
+ * using inline CSS transitions (no CSS classes, to avoid creating a new
130
+ * containing block that would break position:fixed on the Draggable clone).
131
+ */
132
+ useLayoutEffect( () => {
133
+ const flipData = buttonFlipRef.current;
134
+ if ( ! flipData ) {
135
+ return;
136
+ }
137
+ buttonFlipRef.current = null;
138
+
139
+ const { fromIndex, toIndex, stride, direction } = flipData;
140
+
141
+ const startAnimation = ( el: HTMLDivElement | null, startY: number ) => {
142
+ if ( ! el ) {
143
+ return;
144
+ }
145
+ // Snap to the starting position with no transition.
146
+ el.style.transition = 'none';
147
+ el.style.transform = `translateY(${ startY }px)`;
148
+ // Force a reflow so the browser registers the starting position
149
+ // before we switch to the animated transition.
150
+ el.getBoundingClientRect();
151
+ // Animate from startY back to 0 (its new natural position).
152
+ el.style.transition = `transform ${ BUTTON_MOVE_DURATION }ms ease-out`;
153
+ el.style.transform = '';
154
+ };
155
+
156
+ // Moved item is now at toIndex: animate it in from its old position.
157
+ startAnimation( itemRefs.current[ toIndex ], -direction * stride );
158
+ // Displaced item is now at fromIndex: animate it in from its old position.
159
+ startAnimation( itemRefs.current[ fromIndex ], direction * stride );
160
+
161
+ if ( buttonMoveTimer.current ) {
162
+ clearTimeout( buttonMoveTimer.current );
163
+ }
164
+ buttonMoveTimer.current = setTimeout( () => {
165
+ // Clear inline animation styles before calling the external callback so
166
+ // the subsequent React re-render doesn't fight a live animation.
167
+ [ fromIndex, toIndex ].forEach( i => {
168
+ const el = itemRefs.current[ i ];
169
+ if ( el ) {
170
+ el.style.transition = '';
171
+ el.style.transform = '';
172
+ }
173
+ } );
174
+
175
+ // Move focus to the matching chevron button on the card at toIndex.
176
+ // Focus before onDragCallback so a parent rerender can't steal focus.
177
+ const targetEl = itemRefs.current[ toIndex ];
178
+ if ( targetEl ) {
179
+ const moveButtons = targetEl.querySelectorAll< HTMLButtonElement >(
180
+ '.newspack-card--core__header__draggable-controls__move-buttons button'
181
+ );
182
+ // direction > 0 = moved down → prefer the down button (index 1).
183
+ // direction < 0 = moved up → prefer the up button (index 0).
184
+ const preferred = direction > 0 ? moveButtons[ 1 ] : moveButtons[ 0 ];
185
+ const fallback = direction > 0 ? moveButtons[ 0 ] : moveButtons[ 1 ];
186
+ if ( preferred && ! preferred.disabled ) {
187
+ preferred.focus();
188
+ } else if ( fallback && ! fallback.disabled ) {
189
+ fallback.focus();
190
+ }
191
+ }
192
+
193
+ onDragCallback( fromIndex, toIndex );
194
+ buttonMoveTimer.current = null;
195
+ }, BUTTON_MOVE_DURATION );
196
+ }, [ buttonMoveId ] ); // eslint-disable-line react-hooks/exhaustive-deps
197
+
198
+ const handleDragStart = ( index: number ) => {
199
+ // Cancel any in-progress button-move animation so no item wrapper keeps
200
+ // an inline transform. A transformed ancestor breaks position:fixed on
201
+ // the Draggable clone (CSS spec: fixed positioning is relative to the
202
+ // nearest ancestor with a transform/filter/perspective).
203
+ if ( buttonMoveTimer.current ) {
204
+ clearTimeout( buttonMoveTimer.current );
205
+ buttonMoveTimer.current = null;
206
+ itemRefs.current.forEach( el => {
207
+ if ( el ) {
208
+ el.style.transition = '';
209
+ el.style.transform = '';
210
+ }
211
+ } );
212
+ }
213
+
214
+ const listEl = listRef.current;
215
+ if ( listEl ) {
216
+ const itemEls = itemRefs.current;
217
+ const rects = itemEls.map( el => el?.getBoundingClientRect() );
218
+
219
+ // Stride = item height + gap to next item (difference between consecutive tops).
220
+ const itemStrides = rects.map( ( rect, i ) => {
221
+ if ( ! rect ) {
222
+ return 0;
223
+ }
224
+ const nextRect = rects[ i + 1 ];
225
+ return nextRect ? nextRect.top - rect.top : rect.height;
226
+ } );
227
+
228
+ setMeasurements( {
229
+ lockedHeight: listEl.getBoundingClientRect().height,
230
+ sourceHeight: rects[ index ]?.height ?? 0,
231
+ itemStrides,
232
+ itemTops: rects.map( rect => rect?.top ?? 0 ),
233
+ } );
234
+ }
235
+ // Make the entire document a valid drop target so the browser skips
236
+ // its snap-back animation when the cursor is released outside the list.
237
+ const preventSnapback = ( e: Event ) => e.preventDefault();
238
+ document.addEventListener( 'dragover', preventSnapback );
239
+ documentDragOverRef.current = preventSnapback;
240
+
241
+ setDraggingIndex( index );
242
+ setDroppedIndex( null );
243
+ };
244
+
245
+ const clearDragState = () => {
246
+ if ( documentDragOverRef.current ) {
247
+ document.removeEventListener( 'dragover', documentDragOverRef.current );
248
+ documentDragOverRef.current = null;
249
+ }
250
+ setDraggingIndex( null );
251
+ setHoverIndex( null );
252
+ setMeasurements( null );
253
+ };
254
+
255
+ /**
256
+ * Determine the drop target index from cursor coordinates and the item
257
+ * top positions snapshotted at drag start. Mirrors the midpoint logic
258
+ * previously used in getDropIndex but works without a live event target.
259
+ */
260
+ const getDropIndexFromCursor = ( clientY: number, m: DragMeasurements, sourceIndex: number ): number => {
261
+ const { itemTops, itemStrides, sourceHeight } = m;
262
+ for ( let i = 0; i < itemTops.length; i++ ) {
263
+ const movingDown = sourceIndex > i;
264
+ const midpoint = itemTops[ i ] + ( movingDown ? itemStrides[ i ] : itemStrides[ i ] - sourceHeight );
265
+ if ( clientY < midpoint ) {
266
+ return i;
267
+ }
268
+ }
269
+ return itemTops.length; // below all items
270
+ };
271
+
272
+ const handleDragEnd = ( event: DragEvent ) => {
273
+ // Take a local copy of measurements before clearDragState nulls it.
274
+ const m = measurements;
275
+ const sourceIndex = draggingIndex;
276
+
277
+ clearDragState();
278
+
279
+ if ( m === null || sourceIndex === null ) {
280
+ return;
281
+ }
282
+
283
+ const dropIndex = getDropIndexFromCursor( event.clientY, m, sourceIndex );
284
+
285
+ // Compute the destination index in the post-removal array.
286
+ // After splicing out the dragged item, indices above it shift down by one.
287
+ const insertIndex = dropIndex > sourceIndex ? dropIndex - 1 : dropIndex;
288
+ if ( insertIndex === sourceIndex ) {
289
+ return;
290
+ }
291
+
292
+ const reordered = [ ...sortedItems ];
293
+ const [ moved ] = reordered.splice( sourceIndex, 1 );
294
+ reordered.splice( insertIndex, 0, moved );
295
+
296
+ // First render: show items in their new order with no animation.
297
+ // The dragging CSS classes are already cleared by clearDragState() above,
298
+ // so no transitions will fire on this paint.
299
+ setSortedItems( reordered );
300
+
301
+ // Next frame: trigger the drop animation on the newly-positioned item,
302
+ // then call onDragCallback after the animation completes.
303
+ requestAnimationFrame( () => {
304
+ setDroppedIndex( insertIndex );
305
+ dropAnimationTimer.current = setTimeout( () => {
306
+ setDroppedIndex( null );
307
+ onDragCallback( sourceIndex, insertIndex );
308
+ }, DROP_ANIMATION_DURATION );
309
+ } );
310
+ };
311
+
312
+ const handleDragOver = ( event: React.DragEvent< HTMLDivElement >, index: number ) => {
313
+ event.preventDefault();
314
+ if ( ! measurements ) {
315
+ return;
316
+ }
317
+ const rect = event.currentTarget.getBoundingClientRect();
318
+ const midpoint = rect.top + rect.height / 2;
319
+ setHoverIndex( event.clientY < midpoint ? index : index + 1 );
320
+ };
321
+
322
+ /**
323
+ * Compute the translateY for a non-source item at `index` given the current
324
+ * draggingIndex and hoverIndex, so that items visually shift to show the gap
325
+ * while the container's layout height stays fixed.
326
+ *
327
+ * The source item has been visually hidden but still occupies its layout slot.
328
+ * Items between the source and the hover position need to slide by the source
329
+ * item's stride (height + gap) to either fill the vacated source slot or make
330
+ * room for the incoming gap at the hover position.
331
+ */
332
+ const getTranslateY = ( index: number ): number => {
333
+ if ( draggingIndex === null || hoverIndex === null || ! measurements ) {
334
+ return 0;
335
+ }
336
+ if ( index === draggingIndex ) {
337
+ return 0;
338
+ }
339
+
340
+ const { itemStrides, sourceHeight } = measurements;
341
+ const sourceStride = itemStrides[ draggingIndex ] ?? sourceHeight;
342
+
343
+ if ( hoverIndex > draggingIndex ) {
344
+ // Dragging downward: items strictly between source and hover position slide up.
345
+ if ( index > draggingIndex && index < hoverIndex ) {
346
+ return -sourceStride;
347
+ }
348
+ } else if ( index >= hoverIndex && index < draggingIndex ) {
349
+ // Dragging upward: items between hover position and source slide down.
350
+ return sourceStride;
351
+ }
352
+
353
+ return 0;
354
+ };
355
+
356
+ const isDragging = draggingIndex !== null;
357
+
358
+ return (
359
+ <VStack
360
+ ref={ listRef }
361
+ className={ classNames(
362
+ 'newspack-card--core--sortable-list',
363
+ disabled && 'newspack-card--core--sortable-list__is-disabled',
364
+ isDragging && 'newspack-card--core--sortable-list__is-dragging'
365
+ ) }
366
+ style={ measurements ? { height: measurements.lockedHeight } : undefined }
367
+ spacing={ 4 }
368
+ >
369
+ { sortedItems.map( ( item, index ) => {
370
+ const translateY = getTranslateY( index );
371
+ return (
372
+ <Disabled key={ item.id } isDisabled={ disabled }>
373
+ <div
374
+ ref={ el => {
375
+ itemRefs.current[ index ] = el;
376
+ } }
377
+ className={ classNames( 'newspack-card--core--sortable-list__item', {
378
+ 'is-source': draggingIndex === index,
379
+ 'is-dropped': droppedIndex === index,
380
+ } ) }
381
+ style={ translateY ? { transform: `translateY(${ translateY }px)` } : { transition: ! isDragging ? 'none' : undefined } }
382
+ id={ `draggable-card-${ index }` }
383
+ onDragOver={ e => handleDragOver( e, index ) }
384
+ >
385
+ <Draggable
386
+ transferData={ {} }
387
+ cloneClassname="newspack-card--core--sortable-list__item__clone"
388
+ elementId={ `draggable-card-${ index }` }
389
+ onDragStart={ () => handleDragStart( index ) }
390
+ onDragEnd={ handleDragEnd }
391
+ appendToOwnerDocument
392
+ >
393
+ { ( { onDraggableStart, onDraggableEnd } ) => (
394
+ <Card
395
+ isSmall
396
+ draggable
397
+ onDragStart={ onDraggableStart }
398
+ onDragEnd={ onDraggableEnd }
399
+ __experimentalCoreCard
400
+ __experimentalCoreProps={ {
401
+ header: (
402
+ <>
403
+ <h3>
404
+ { item.title }
405
+ <Badge level={ item.badgeLevel } text={ item.badgeText } />
406
+ </h3>
407
+ { item.description && <p>{ item.description }</p> }
408
+ </>
409
+ ),
410
+ isDraggable: true,
411
+ isFirstTarget: index === 0,
412
+ isLastTarget: index === sortedItems.length - 1,
413
+ dragIndex: index,
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
+ } ),
423
+ } }
424
+ />
425
+ ) }
426
+ </Draggable>
427
+ </div>
428
+ </Disabled>
429
+ );
430
+ } ) }
431
+ </VStack>
432
+ );
433
+ };
434
+
435
+ export default CardSortableList;