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.
- package/CHANGELOG.md +11 -0
- package/DEVELOPMENT.md +868 -0
- package/colors/colors.module.scss +52 -2
- package/copy-styles.js +3 -2
- package/dist/cjs/accordion/index.js +17 -12
- package/dist/cjs/action-card/index.js +176 -139
- package/dist/cjs/autocomplete-tokenfield/index.js +254 -234
- package/dist/cjs/autocomplete-with-latest-posts/index.js +172 -110
- package/dist/cjs/autocomplete-with-suggestions/index.js +229 -126
- package/dist/cjs/badge/index.js +7 -7
- package/dist/cjs/box-contrast/index.js +19 -15
- package/dist/cjs/button/index.js +41 -26
- package/dist/cjs/button-props/button-props.js +5 -5
- package/dist/cjs/button-props/index.js +2 -2
- package/dist/cjs/card/core-card.js +110 -52
- package/dist/cjs/card/index.js +74 -67
- package/dist/cjs/card/style-core.scss +93 -25
- package/dist/cjs/card/style.scss +2 -2
- package/dist/cjs/card-feature/index.js +141 -0
- package/dist/cjs/card-feature/style.scss +75 -0
- package/dist/cjs/card-form/index.js +144 -0
- package/dist/cjs/card-form/style.scss +20 -0
- package/dist/cjs/card-settings-group/index.js +36 -16
- package/dist/cjs/card-sortable-list/index.js +176 -140
- package/dist/cjs/category-autocomplete/index.js +175 -155
- package/dist/cjs/color-picker/index.js +41 -23
- package/dist/cjs/color-picker/index.test.js +82 -0
- package/dist/cjs/color-picker/style.scss +11 -13
- package/dist/cjs/confirm-dialog/index.js +86 -22
- package/dist/cjs/consts.js +1 -1
- package/dist/cjs/custom-select-control/index.js +39 -28
- package/dist/cjs/dataviews/index.js +38 -0
- package/dist/cjs/dataviews/style.scss +9 -0
- package/dist/cjs/divider/index.js +26 -21
- package/dist/cjs/footer/index.js +36 -30
- package/dist/cjs/form-token-field/index.js +51 -40
- package/dist/cjs/global-notices/index.js +5 -5
- package/dist/cjs/grid/index.js +25 -19
- package/dist/cjs/grid/style.scss +6 -1
- package/dist/cjs/handoff/index.js +204 -156
- package/dist/cjs/handoff-message/index.js +12 -7
- package/dist/cjs/hooks/index.js +10 -8
- package/dist/cjs/hooks/use-confirm-dialog.js +79 -0
- package/dist/cjs/hooks/use-unsaved-changes-dialog.js +144 -0
- package/dist/cjs/hooks/useObjectState.js +21 -12
- package/dist/cjs/hooks/useObjectState.test.js +55 -33
- package/dist/cjs/hooks/useOnClickOutside.js +6 -6
- package/dist/cjs/hooks/usePrompt.js +10 -13
- package/dist/cjs/image-upload/index.js +114 -123
- package/dist/cjs/image-upload/index.test.js +11 -13
- package/dist/cjs/image-upload/style.scss +12 -40
- package/dist/cjs/index.js +136 -104
- package/dist/cjs/info-button/index.js +43 -32
- package/dist/cjs/integration-icons/active-campaign.js +22 -0
- package/dist/cjs/integration-icons/constant-contact.js +28 -0
- package/dist/cjs/integration-icons/fundraise-up.js +25 -0
- package/dist/cjs/integration-icons/index.js +48 -0
- package/dist/cjs/integration-icons/mailchimp.js +19 -0
- package/dist/cjs/integration-icons/salesforce.js +17 -0
- package/dist/cjs/integration-icons/wisepops.js +20 -0
- package/dist/cjs/modal/index.js +22 -19
- package/dist/cjs/modal/style.scss +4 -0
- package/dist/cjs/newspack-icon/index.js +45 -38
- package/dist/cjs/notice/index.js +64 -55
- package/dist/cjs/plugin-installer/index.js +259 -253
- package/dist/cjs/plugin-settings/SettingsSection.js +66 -60
- package/dist/cjs/plugin-settings/index.js +217 -200
- package/dist/cjs/plugin-toggle/index.js +192 -199
- package/dist/cjs/popover/index.js +41 -28
- package/dist/cjs/position-control/index.js +24 -23
- package/dist/cjs/progress-bar/index.js +71 -59
- package/dist/cjs/progress-bar/index.test.js +62 -68
- package/dist/cjs/proxied-imports/router.js +3 -3
- package/dist/cjs/radio-control/index.js +40 -27
- package/dist/cjs/section-header/index.js +103 -58
- package/dist/cjs/section-header/style.scss +31 -11
- package/dist/cjs/select-control/ButtonGroupControl.js +26 -21
- package/dist/cjs/select-control/GroupedSelectControl.js +41 -35
- package/dist/cjs/select-control/index.js +52 -40
- package/dist/cjs/settings/MinMaxSetting.js +30 -20
- package/dist/cjs/settings/SettingsCard.js +20 -17
- package/dist/cjs/settings/SettingsSection.js +22 -21
- package/dist/cjs/settings/index.js +5 -5
- package/dist/cjs/sortable-newsletter-list-control/index.js +78 -57
- package/dist/cjs/steps-list/index.js +44 -34
- package/dist/cjs/steps-list-item/index.js +47 -39
- package/dist/cjs/style-card/index.js +70 -63
- package/dist/cjs/tabbed-navigation/index.js +38 -36
- package/dist/cjs/text-control/index.js +27 -25
- package/dist/cjs/utils/color.js +4 -4
- package/dist/cjs/utils/editor-toolbar-back-button.js +61 -0
- package/dist/cjs/utils/index.js +31 -19
- package/dist/cjs/utils/style.scss +8 -0
- package/dist/cjs/waiting/index.js +48 -37
- package/dist/cjs/web-preview/index.js +217 -211
- package/dist/cjs/with-wizard/index.js +362 -364
- package/dist/cjs/with-wizard/style.scss +4 -54
- package/dist/cjs/with-wizard-screen/index.js +54 -58
- package/dist/cjs/with-wizard-screen/style.scss +41 -3
- package/dist/cjs/wizard/components/WizardError.js +20 -18
- package/dist/cjs/wizard/components/WizardSnackbar.js +26 -23
- package/dist/cjs/wizard/index.js +176 -114
- package/dist/cjs/wizard/store/index.js +171 -123
- package/dist/cjs/wizard/store/utils.js +13 -8
- package/dist/esm/accordion/index.js +14 -9
- package/dist/esm/action-card/index.js +168 -123
- package/dist/esm/autocomplete-tokenfield/index.js +248 -222
- package/dist/esm/autocomplete-with-latest-posts/index.js +169 -103
- package/dist/esm/autocomplete-with-suggestions/index.js +226 -119
- package/dist/esm/badge/index.js +5 -5
- package/dist/esm/box-contrast/index.js +15 -11
- package/dist/esm/button/index.js +39 -21
- package/dist/esm/button-props/button-props.js +1 -1
- package/dist/esm/card/core-card.js +109 -47
- package/dist/esm/card/index.js +69 -54
- package/dist/esm/card/style-core.scss +93 -25
- package/dist/esm/card/style.scss +2 -2
- package/dist/esm/card-feature/index.js +131 -0
- package/dist/esm/card-feature/style.scss +75 -0
- package/dist/esm/card-form/index.js +134 -0
- package/dist/esm/card-form/style.scss +20 -0
- package/dist/esm/card-settings-group/index.js +33 -14
- package/dist/esm/card-sortable-list/index.js +170 -126
- package/dist/esm/category-autocomplete/index.js +168 -140
- package/dist/esm/color-picker/index.js +39 -21
- package/dist/esm/color-picker/index.test.js +78 -0
- package/dist/esm/color-picker/style.scss +11 -13
- package/dist/esm/confirm-dialog/index.js +85 -18
- package/dist/esm/consts.js +1 -1
- package/dist/esm/custom-select-control/index.js +34 -15
- package/dist/esm/dataviews/index.js +34 -0
- package/dist/esm/dataviews/style.scss +9 -0
- package/dist/esm/divider/index.js +24 -16
- package/dist/esm/footer/index.js +34 -28
- package/dist/esm/form-token-field/index.js +46 -27
- package/dist/esm/global-notices/index.js +3 -3
- package/dist/esm/grid/index.js +23 -14
- package/dist/esm/grid/style.scss +6 -1
- package/dist/esm/handoff/index.js +199 -143
- package/dist/esm/handoff-message/index.js +10 -6
- package/dist/esm/hooks/index.js +8 -6
- package/dist/esm/hooks/use-confirm-dialog.js +73 -0
- package/dist/esm/hooks/use-unsaved-changes-dialog.js +136 -0
- package/dist/esm/hooks/useObjectState.js +19 -9
- package/dist/esm/hooks/useObjectState.test.js +54 -32
- package/dist/esm/hooks/useOnClickOutside.js +4 -4
- package/dist/esm/hooks/usePrompt.js +9 -11
- package/dist/esm/image-upload/index.js +110 -111
- package/dist/esm/image-upload/index.test.js +11 -13
- package/dist/esm/image-upload/style.scss +12 -40
- package/dist/esm/index.js +7 -1
- package/dist/esm/info-button/index.js +38 -19
- package/dist/esm/integration-icons/active-campaign.js +16 -0
- package/dist/esm/integration-icons/constant-contact.js +22 -0
- package/dist/esm/integration-icons/fundraise-up.js +19 -0
- package/dist/esm/integration-icons/index.js +6 -0
- package/dist/esm/integration-icons/mailchimp.js +13 -0
- package/dist/esm/integration-icons/salesforce.js +11 -0
- package/dist/esm/integration-icons/wisepops.js +14 -0
- package/dist/esm/modal/index.js +20 -13
- package/dist/esm/modal/style.scss +4 -0
- package/dist/esm/newspack-icon/index.js +40 -25
- package/dist/esm/notice/index.js +59 -42
- package/dist/esm/plugin-installer/index.js +255 -243
- package/dist/esm/plugin-settings/SettingsSection.js +61 -50
- package/dist/esm/plugin-settings/index.js +212 -189
- package/dist/esm/plugin-toggle/index.js +187 -190
- package/dist/esm/popover/index.js +35 -16
- package/dist/esm/position-control/index.js +22 -18
- package/dist/esm/progress-bar/index.js +66 -49
- package/dist/esm/progress-bar/index.test.js +61 -67
- package/dist/esm/radio-control/index.js +34 -15
- package/dist/esm/section-header/index.js +102 -57
- package/dist/esm/section-header/style.scss +31 -11
- package/dist/esm/select-control/ButtonGroupControl.js +23 -18
- package/dist/esm/select-control/GroupedSelectControl.js +38 -30
- package/dist/esm/select-control/index.js +47 -27
- package/dist/esm/settings/MinMaxSetting.js +27 -16
- package/dist/esm/settings/SettingsCard.js +18 -13
- package/dist/esm/settings/SettingsSection.js +20 -19
- package/dist/esm/settings/index.js +3 -3
- package/dist/esm/sortable-newsletter-list-control/index.js +74 -53
- package/dist/esm/steps-list/index.js +39 -21
- package/dist/esm/steps-list-item/index.js +42 -26
- package/dist/esm/style-card/index.js +65 -50
- package/dist/esm/tabbed-navigation/index.js +35 -33
- package/dist/esm/text-control/index.js +25 -19
- package/dist/esm/utils/color.js +4 -4
- package/dist/esm/utils/editor-toolbar-back-button.js +53 -0
- package/dist/esm/utils/index.js +29 -16
- package/dist/esm/utils/style.scss +8 -0
- package/dist/esm/waiting/index.js +43 -24
- package/dist/esm/web-preview/index.js +213 -201
- package/dist/esm/with-wizard/index.js +360 -358
- package/dist/esm/with-wizard/style.scss +4 -54
- package/dist/esm/with-wizard-screen/index.js +48 -46
- package/dist/esm/with-wizard-screen/style.scss +41 -3
- package/dist/esm/wizard/components/WizardError.js +18 -16
- package/dist/esm/wizard/components/WizardSnackbar.js +23 -17
- package/dist/esm/wizard/index.js +171 -103
- package/dist/esm/wizard/store/index.js +169 -116
- package/dist/esm/wizard/store/utils.js +13 -6
- package/package.json +30 -16
- package/release.config.js +1 -0
- package/shared/hooks/use-coauthors.js +195 -27
- package/shared/hooks/use-coauthors.test.js +232 -6
- package/shared/scss/_mixins.scss +13 -5
- package/src/accordion/index.js +8 -7
- package/src/action-card/index.js +15 -1
- package/src/badge/index.tsx +3 -1
- package/src/card/core-card.js +95 -18
- package/src/card/index.js +1 -0
- package/src/card/style-core.scss +93 -25
- package/src/card/style.scss +2 -2
- package/src/card-feature/README.md +188 -0
- package/src/card-feature/index.tsx +186 -0
- package/src/card-feature/style.scss +75 -0
- package/src/card-form/README.md +113 -0
- package/src/card-form/index.tsx +128 -0
- package/src/card-form/style.scss +20 -0
- package/src/card-settings-group/index.tsx +28 -3
- package/src/card-sortable-list/index.tsx +27 -4
- package/src/color-picker/index.js +15 -8
- package/src/color-picker/index.test.js +52 -0
- package/src/color-picker/style.scss +11 -13
- package/src/confirm-dialog/README.md +128 -0
- package/src/confirm-dialog/index.tsx +74 -6
- package/src/dataviews/index.tsx +34 -0
- package/src/dataviews/style.scss +9 -0
- package/src/grid/style.scss +6 -1
- package/src/handoff/README.md +93 -0
- package/src/handoff/index.js +46 -8
- package/src/hooks/index.ts +2 -1
- package/src/hooks/use-confirm-dialog.tsx +80 -0
- package/src/hooks/use-unsaved-changes-dialog.tsx +140 -0
- package/src/image-upload/index.js +10 -15
- package/src/image-upload/style.scss +12 -40
- package/src/index.js +7 -1
- package/src/integration-icons/active-campaign.js +14 -0
- package/src/integration-icons/constant-contact.js +22 -0
- package/src/integration-icons/fundraise-up.js +18 -0
- package/src/integration-icons/index.js +6 -0
- package/src/integration-icons/mailchimp.js +10 -0
- package/src/integration-icons/salesforce.js +10 -0
- package/src/integration-icons/wisepops.js +14 -0
- package/src/modal/style.scss +4 -0
- package/src/plugin-installer/index.js +30 -19
- package/src/section-header/index.js +56 -24
- package/src/section-header/style.scss +31 -11
- package/src/select-control/index.js +4 -0
- package/src/utils/editor-toolbar-back-button.tsx +48 -0
- package/src/utils/index.js +6 -0
- package/src/utils/style.scss +8 -0
- package/src/with-wizard/style.scss +4 -54
- package/src/with-wizard-screen/style.scss +41 -3
- package/src/wizard/index.js +90 -53
- package/src/wizard/store/index.js +2 -2
- package/dist/cjs/button-card/index.js +0 -72
- package/dist/cjs/button-card/style.scss +0 -180
- package/dist/esm/button-card/index.js +0 -64
- package/dist/esm/button-card/style.scss +0 -180
- package/src/button-card/index.js +0 -52
- 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 = '
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
) }
|