newspack-components 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/colors/colors.module.scss +1 -1
  2. package/dist/cjs/action-card/index.js +21 -12
  3. package/dist/cjs/action-card/style.scss +32 -14
  4. package/dist/cjs/autocomplete-tokenfield/index.js +2 -2
  5. package/dist/cjs/autocomplete-with-latest-posts/index.js +289 -0
  6. package/dist/cjs/autocomplete-with-latest-posts/style.scss +150 -0
  7. package/dist/cjs/badge/index.js +32 -0
  8. package/dist/cjs/badge/style.scss +36 -0
  9. package/dist/cjs/button/index.js +2 -0
  10. package/dist/cjs/card/core-card.js +141 -0
  11. package/dist/cjs/card/index.js +33 -0
  12. package/dist/cjs/card/style-core.scss +227 -0
  13. package/dist/cjs/card/style.scss +2 -2
  14. package/dist/cjs/card-settings-group/index.js +49 -0
  15. package/dist/cjs/card-settings-group/style.scss +33 -0
  16. package/dist/cjs/card-sortable-list/index.js +390 -0
  17. package/dist/cjs/card-sortable-list/style.scss +48 -0
  18. package/dist/cjs/confirm-dialog/index.js +59 -0
  19. package/dist/cjs/divider/index.js +42 -0
  20. package/dist/cjs/divider/style.scss +51 -0
  21. package/dist/cjs/form-token-field/style.scss +0 -5
  22. package/dist/cjs/grid/index.js +2 -1
  23. package/dist/cjs/grid/style.scss +63 -9
  24. package/dist/cjs/index.js +42 -0
  25. package/dist/cjs/modal/index.js +11 -4
  26. package/dist/cjs/modal/style.scss +43 -13
  27. package/dist/cjs/newspack-icon/style.scss +5 -3
  28. package/dist/cjs/popover/style.scss +1 -1
  29. package/dist/cjs/section-header/index.js +76 -17
  30. package/dist/cjs/section-header/style.scss +109 -4
  31. package/dist/cjs/style.scss +2 -0
  32. package/dist/cjs/waiting/index.js +3 -1
  33. package/dist/cjs/waiting/style.scss +4 -0
  34. package/dist/cjs/web-preview/index.js +98 -39
  35. package/dist/cjs/web-preview/style.scss +20 -53
  36. package/dist/cjs/with-wizard/index.js +1 -1
  37. package/dist/cjs/with-wizard/style.scss +24 -6
  38. package/dist/cjs/with-wizard-screen/style.scss +39 -10
  39. package/dist/cjs/wizard/components/WizardSnackbar.js +70 -0
  40. package/dist/cjs/wizard/components/style.scss +48 -0
  41. package/dist/cjs/wizard/index.js +97 -16
  42. package/dist/cjs/wizard/store/index.js +46 -0
  43. package/dist/esm/action-card/index.js +21 -12
  44. package/dist/esm/action-card/style.scss +32 -14
  45. package/dist/esm/autocomplete-tokenfield/index.js +2 -2
  46. package/dist/esm/autocomplete-with-latest-posts/index.js +281 -0
  47. package/dist/esm/autocomplete-with-latest-posts/style.scss +150 -0
  48. package/dist/esm/badge/index.js +24 -0
  49. package/dist/esm/badge/style.scss +36 -0
  50. package/dist/esm/button/index.js +2 -0
  51. package/dist/esm/card/core-card.js +133 -0
  52. package/dist/esm/card/index.js +33 -0
  53. package/dist/esm/card/style-core.scss +227 -0
  54. package/dist/esm/card/style.scss +2 -2
  55. package/dist/esm/card-settings-group/index.js +42 -0
  56. package/dist/esm/card-settings-group/style.scss +33 -0
  57. package/dist/esm/card-sortable-list/index.js +381 -0
  58. package/dist/esm/card-sortable-list/style.scss +48 -0
  59. package/dist/esm/confirm-dialog/index.js +49 -0
  60. package/dist/esm/divider/index.js +34 -0
  61. package/dist/esm/divider/style.scss +51 -0
  62. package/dist/esm/form-token-field/style.scss +0 -5
  63. package/dist/esm/grid/index.js +2 -1
  64. package/dist/esm/grid/style.scss +63 -9
  65. package/dist/esm/index.js +6 -0
  66. package/dist/esm/modal/index.js +11 -4
  67. package/dist/esm/modal/style.scss +43 -13
  68. package/dist/esm/newspack-icon/style.scss +5 -3
  69. package/dist/esm/popover/style.scss +1 -1
  70. package/dist/esm/section-header/index.js +75 -18
  71. package/dist/esm/section-header/style.scss +109 -4
  72. package/dist/esm/style.scss +2 -0
  73. package/dist/esm/waiting/index.js +3 -1
  74. package/dist/esm/waiting/style.scss +4 -0
  75. package/dist/esm/web-preview/index.js +101 -42
  76. package/dist/esm/web-preview/style.scss +20 -53
  77. package/dist/esm/with-wizard/index.js +1 -1
  78. package/dist/esm/with-wizard/style.scss +24 -6
  79. package/dist/esm/with-wizard-screen/style.scss +39 -10
  80. package/dist/esm/wizard/components/WizardSnackbar.js +64 -0
  81. package/dist/esm/wizard/components/style.scss +48 -0
  82. package/dist/esm/wizard/index.js +101 -20
  83. package/dist/esm/wizard/store/index.js +46 -0
  84. package/package.json +5 -6
  85. package/shared/author-context.js +26 -0
  86. package/shared/hooks/use-coauthors.js +220 -0
  87. package/shared/hooks/use-coauthors.test.js +624 -0
  88. package/shared/hooks/use-custom-byline.js +47 -0
  89. package/shared/hooks/use-custom-byline.test.js +133 -0
  90. package/shared/scss/_mixins.scss +46 -0
  91. package/src/action-card/action-card.d.ts +7 -3
  92. package/src/action-card/index.js +24 -11
  93. package/src/action-card/style.scss +32 -14
  94. package/src/autocomplete-tokenfield/index.js +2 -2
  95. package/src/autocomplete-with-latest-posts/index.js +290 -0
  96. package/src/autocomplete-with-latest-posts/style.scss +150 -0
  97. package/src/badge/index.tsx +24 -0
  98. package/src/badge/style.scss +36 -0
  99. package/src/button/index.tsx +3 -2
  100. package/src/card/core-card.js +147 -0
  101. package/src/card/index.js +39 -1
  102. package/src/card/style-core.scss +227 -0
  103. package/src/card/style.scss +2 -2
  104. package/src/card-settings-group/README.md +43 -0
  105. package/src/card-settings-group/index.tsx +53 -0
  106. package/src/card-settings-group/style.scss +33 -0
  107. package/src/card-sortable-list/README.md +51 -0
  108. package/src/card-sortable-list/index.tsx +412 -0
  109. package/src/card-sortable-list/style.scss +48 -0
  110. package/src/confirm-dialog/index.tsx +64 -0
  111. package/src/divider/README.md +25 -0
  112. package/src/divider/index.js +31 -0
  113. package/src/divider/style.scss +51 -0
  114. package/src/form-token-field/style.scss +0 -5
  115. package/src/grid/index.tsx +2 -1
  116. package/src/grid/style.scss +63 -9
  117. package/src/index.js +6 -0
  118. package/src/modal/index.js +12 -3
  119. package/src/modal/style.scss +43 -13
  120. package/src/newspack-icon/style.scss +5 -3
  121. package/src/popover/style.scss +1 -1
  122. package/src/section-header/index.js +72 -13
  123. package/src/section-header/style.scss +109 -4
  124. package/src/style.scss +2 -0
  125. package/src/waiting/index.js +2 -1
  126. package/src/waiting/style.scss +4 -0
  127. package/src/web-preview/index.js +90 -35
  128. package/src/web-preview/style.scss +20 -53
  129. package/src/with-wizard/index.js +1 -1
  130. package/src/with-wizard/style.scss +24 -6
  131. package/src/with-wizard-screen/style.scss +39 -10
  132. package/src/wizard/components/WizardSnackbar.js +58 -0
  133. package/src/wizard/components/style.scss +48 -0
  134. package/src/wizard/index.js +108 -14
  135. package/src/wizard/store/index.js +26 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import debounce from 'lodash/debounce';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import apiFetch from '@wordpress/api-fetch';
10
+ import { Button, CheckboxControl, FormTokenField, Spinner } from '@wordpress/components';
11
+ import { decodeEntities } from '@wordpress/html-entities';
12
+ import { useEffect, useState } from '@wordpress/element';
13
+ import { __ } from '@wordpress/i18n';
14
+ import { addQueryArgs } from '@wordpress/url';
15
+
16
+ import './style.scss';
17
+
18
+ const AutocompleteWithLatestPosts = ( {
19
+ fetchSuggestions = false, // If passed, will use this function to fetch suggestions data.
20
+ help = __( 'Begin typing search term, click autocomplete result to select.', 'newspack-plugin' ),
21
+ hideHelp = true, // If true, all help text will be hidden.
22
+ hideFormTokenHelp = true, // If true, hides FormTokenField's built-in help text.
23
+ label = __( 'Search', 'newspack-plugin' ),
24
+ maxItemsToSuggest = 0, // If passed, will be used to determine "load more" state. Necessary if you want "load more" functionality when using a custom `fetchSuggestions` function.
25
+ multiSelect = false, // If true, component can select multiple values at once.
26
+ onChange = false, // Function to call when selections change.
27
+ selectedItems = [], // Array of saved items.
28
+ suggestionsToFetch = 20, // Number of suggestions to fetch per query.
29
+ } ) => {
30
+ const [ isLoading, setIsLoading ] = useState( true );
31
+ const [ isLoadingMore, setIsLoadingMore ] = useState( false );
32
+ const [ suggestions, setSuggestions ] = useState( [] );
33
+ const [ maxSuggestions, setMaxSuggestions ] = useState( 0 );
34
+ const [ searchSuggestions, setSearchSuggestions ] = useState( [] );
35
+ const [ validValues, setValidValues ] = useState( {} );
36
+ const [ isSearching, setIsSearching ] = useState( false );
37
+ // Leaving this in here so we can expand to use different post types in the future; right now it defaults to 'post'.
38
+ const postTypeToSearch = 'post';
39
+ const classNames = [ 'newspack-autocomplete-with-latest-posts' ];
40
+
41
+ if ( hideFormTokenHelp ) {
42
+ classNames.push( 'hide-form-token-help' );
43
+ }
44
+
45
+ if ( hideHelp ) {
46
+ classNames.push( 'hide-help' );
47
+ }
48
+
49
+ /**
50
+ * Debounced function to update search suggestions.
51
+ */
52
+ const debouncedUpdateSuggestions = debounce( input => {
53
+ if ( ! input || input.length < 2 ) {
54
+ setSearchSuggestions( [] );
55
+ setIsSearching( false );
56
+ return;
57
+ }
58
+
59
+ setIsSearching( true );
60
+ handleFetchSuggestions( input, 0, postTypeToSearch )
61
+ .then( _suggestions => {
62
+ setSearchSuggestions( _suggestions );
63
+ // Update valid values for token conversion
64
+ const newValidValues = { ...validValues };
65
+ _suggestions.forEach( suggestion => {
66
+ newValidValues[ suggestion.value ] = suggestion.label;
67
+ } );
68
+ setValidValues( newValidValues );
69
+ } )
70
+ .finally( () => setIsSearching( false ) );
71
+ }, 500 );
72
+
73
+ /**
74
+ * Fetch recent posts to show as suggestions.
75
+ */
76
+ useEffect( () => {
77
+ setIsLoading( true );
78
+ handleFetchSuggestions( null, 0, postTypeToSearch )
79
+ .then( _suggestions => {
80
+ if ( 0 < _suggestions.length ) {
81
+ setSuggestions( _suggestions );
82
+ // Update valid values for token conversion
83
+ const newValidValues = { ...validValues };
84
+ _suggestions.forEach( suggestion => {
85
+ newValidValues[ suggestion.value ] = suggestion.label;
86
+ } );
87
+ setValidValues( newValidValues );
88
+ }
89
+ } )
90
+ .finally( () => setIsLoading( false ) );
91
+ }, [] );
92
+
93
+ /**
94
+ * Fetch more suggestions.
95
+ */
96
+ useEffect( () => {
97
+ if ( isLoadingMore ) {
98
+ handleFetchSuggestions( null, suggestions.length, postTypeToSearch )
99
+ .then( _suggestions => {
100
+ if ( 0 < _suggestions.length ) {
101
+ setSuggestions( suggestions.concat( _suggestions ) );
102
+ }
103
+ } )
104
+ .finally( () => setIsLoadingMore( false ) );
105
+ }
106
+ }, [ isLoadingMore ] );
107
+
108
+ /**
109
+ * If passed a `fetchSuggestions` prop, use that, otherwise, build it based on the selected post type.
110
+ */
111
+ const handleFetchSuggestions = fetchSuggestions
112
+ ? fetchSuggestions
113
+ : async ( search = null, offset = 0, searchSlug = null ) => {
114
+ const postTypeSlug = searchSlug || postTypeToSearch;
115
+ const endpoint =
116
+ 'post' === postTypeSlug || 'page' === postTypeSlug
117
+ ? postTypeSlug + 's' // Default post type endpoints are plural.
118
+ : postTypeSlug; // Custom post type endpoints are singular.
119
+ const response = await apiFetch( {
120
+ parse: false,
121
+ path: addQueryArgs( '/wp/v2/' + endpoint, {
122
+ search,
123
+ offset,
124
+ per_page: suggestionsToFetch,
125
+ _fields: 'id,title',
126
+ } ),
127
+ } );
128
+
129
+ const total = parseInt( response.headers.get( 'x-wp-total' ) || 0 );
130
+ const posts = await response.json();
131
+
132
+ setMaxSuggestions( total );
133
+
134
+ // Format suggestions for FormTokenField display.
135
+ return posts.reduce( ( acc, post ) => {
136
+ acc.push( {
137
+ value: parseInt( post.id ),
138
+ label: decodeEntities( post?.title.rendered ) || __( '(no title)', 'newspack-plugin' ),
139
+ } );
140
+
141
+ return acc;
142
+ }, [] );
143
+ };
144
+
145
+ /**
146
+ * Get labels for token values.
147
+ */
148
+ const getLabelsForValues = values => {
149
+ return values.reduce( ( accumulator, value ) => {
150
+ if ( ! value ) {
151
+ return accumulator;
152
+ }
153
+ if ( value.label ) {
154
+ return [ ...accumulator, value.label ];
155
+ }
156
+ return validValues[ value ] ? [ ...accumulator, validValues[ value ] ] : accumulator;
157
+ }, [] );
158
+ };
159
+
160
+ /**
161
+ * Get values for token labels.
162
+ */
163
+ const getValuesForLabels = labels => {
164
+ // eslint-disable-next-line @typescript-eslint/no-shadow
165
+ return labels.reduce( ( acc, label ) => {
166
+ Object.keys( validValues ).forEach( key => {
167
+ if ( validValues[ key ] === label ) {
168
+ const value = isNaN( parseInt( key ) ) ? key.toString() : parseInt( key );
169
+ acc.push( { value, label } );
170
+ }
171
+ } );
172
+ return acc;
173
+ }, [] );
174
+ };
175
+
176
+ /**
177
+ * Handle FormTokenField onChange.
178
+ */
179
+ const handleTokenChange = tokenStrings => {
180
+ const newSelections = getValuesForLabels( tokenStrings );
181
+
182
+ // If only allowing one selection, just return the one selected item.
183
+ if ( ! multiSelect ) {
184
+ const newSelection = newSelections[ newSelections.length - 1 ]; // Get the last selected item
185
+ if ( newSelection ) {
186
+ return onChange( [ { ...newSelection, postType: postTypeToSearch } ] );
187
+ }
188
+ return onChange( [] );
189
+ }
190
+
191
+ // For multi-select, include currently selected post type in selection results.
192
+ onChange( newSelections.map( selection => ( { ...selection, postType: postTypeToSearch } ) ) );
193
+ };
194
+
195
+ /**
196
+ * Get tokens for FormTokenField.
197
+ */
198
+ const getTokens = () => {
199
+ return getLabelsForValues( selectedItems );
200
+ };
201
+
202
+ /**
203
+ * Render a single suggestion object that can be clicked to select it immediately.
204
+ *
205
+ * @param {Object} suggestion Suggestion object with value and label keys.
206
+ */
207
+ const renderSuggestion = suggestion => {
208
+ if ( multiSelect ) {
209
+ const isSelected = !! selectedItems.find(
210
+ _selection => parseInt( _selection.value ) === parseInt( suggestion.value ) && _selection.label === suggestion.label
211
+ );
212
+ return (
213
+ <CheckboxControl
214
+ key={ suggestion.value }
215
+ checked={ isSelected }
216
+ onChange={ () => {
217
+ // For multi-select, we need to toggle the selection
218
+ const currentTokens = getTokens();
219
+ const suggestionLabel = suggestion.label;
220
+
221
+ if ( isSelected ) {
222
+ // Remove the suggestion
223
+ const newTokens = currentTokens.filter( token => token !== suggestionLabel );
224
+ handleTokenChange( newTokens );
225
+ } else {
226
+ // Add the suggestion
227
+ handleTokenChange( [ ...currentTokens, suggestionLabel ] );
228
+ }
229
+ } }
230
+ label={ suggestion.label }
231
+ />
232
+ );
233
+ }
234
+ return (
235
+ <Button isLink key={ suggestion.value } onClick={ () => handleTokenChange( [ ...getTokens(), suggestion.label ] ) }>
236
+ { suggestion.label }
237
+ </Button>
238
+ );
239
+ };
240
+
241
+ /**
242
+ * Render a list of suggestions that can be clicked to select instead of searching by title.
243
+ */
244
+ const renderSuggestions = () => {
245
+ if ( 0 === suggestions.length ) {
246
+ return null;
247
+ }
248
+
249
+ const className = multiSelect
250
+ ? 'newspack-autocomplete-with-latest-posts__search-suggestions-multiselect'
251
+ : 'newspack-autocomplete-with-latest-posts__search-suggestions';
252
+
253
+ return (
254
+ <>
255
+ <div className="newspack-autocomplete-with-latest-posts__search-suggestions-container">
256
+ <p className="newspack-autocomplete-with-suggestions__label">{ __( 'Latest Posts', 'newspack-plugin' ) }</p>
257
+ <div className={ className }>
258
+ { suggestions.map( renderSuggestion ) }
259
+ { suggestions.length < ( maxItemsToSuggest || maxSuggestions ) && (
260
+ <Button disabled={ isLoadingMore } isSecondary onClick={ () => setIsLoadingMore( true ) }>
261
+ { isLoadingMore ? __( 'Loading…', 'newspack-plugin' ) : __( 'Load more', 'newspack-plugin' ) }
262
+ </Button>
263
+ ) }
264
+ </div>
265
+ </div>
266
+ </>
267
+ );
268
+ };
269
+
270
+ return (
271
+ <div className={ classNames.join( ' ' ) }>
272
+ <div className="newspack-autocomplete-with-latest-posts__input-container">
273
+ <FormTokenField
274
+ value={ getTokens() }
275
+ suggestions={ searchSuggestions.map( suggestion => suggestion.label ) }
276
+ onChange={ handleTokenChange }
277
+ onInputChange={ debouncedUpdateSuggestions }
278
+ label={ label }
279
+ help={ ! hideHelp && help }
280
+ __next40pxDefaultSize={ true }
281
+ __nextHasNoMarginBottom={ true }
282
+ />
283
+ { ( isLoading || isSearching ) && <Spinner /> }
284
+ </div>
285
+ { renderSuggestions() }
286
+ </div>
287
+ );
288
+ };
289
+
290
+ export default AutocompleteWithLatestPosts;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Styles for AutocompleteWithLatestPosts component.
3
+ */
4
+
5
+ @use "~@wordpress/base-styles/colors" as wp-colors;
6
+
7
+ .newspack-autocomplete-with-latest-posts {
8
+ display: grid;
9
+ gap: 16px;
10
+ width: 100%;
11
+
12
+ .components-select-control__input {
13
+ max-width: none;
14
+ }
15
+
16
+ .components-input-control__backdrop {
17
+ border-color: wp-colors.$gray-700 !important;
18
+ }
19
+
20
+ .newspack-autocomplete-with-latest-posts__label,
21
+ .components-form-token-field__label,
22
+ .components-input-control__label,
23
+ .components-checkbox-control__label {
24
+ color: wp-colors.$gray-900 !important;
25
+ }
26
+
27
+ .newspack-autocomplete-tokenfield__help {
28
+ font-size: inherit;
29
+ margin-bottom: 8px !important;
30
+ }
31
+
32
+ .components-form-token-field {
33
+ &__input-container {
34
+ border-color: wp-colors.$gray-700;
35
+ }
36
+
37
+ &__suggestion {
38
+ padding: 8px;
39
+ }
40
+ }
41
+
42
+ // Hide the built-in "Separate with commas or the Enter key." help text when hideFormTokenHelp is true
43
+ &.hide-form-token-help .components-form-token-field__help {
44
+ display: none;
45
+ }
46
+
47
+ p.newspack-autocomplete-with-latest-posts__label {
48
+ font-size: inherit;
49
+ margin: 0 0 8px;
50
+ }
51
+
52
+ &__selected-items {
53
+ border-bottom: 1px solid wp-colors.$gray-700;
54
+ display: flex;
55
+ flex-wrap: wrap;
56
+ gap: 8px;
57
+ margin-bottom: 16px;
58
+ padding-bottom: 12px;
59
+
60
+ p.newspack-autocomplete-with-latest-posts__label {
61
+ flex: 0 0 100%;
62
+ margin: 0;
63
+ }
64
+ }
65
+
66
+ &__selected-item-button.components-button.is-tertiary {
67
+ background-color: wp-colors.$gray-300;
68
+ border-radius: 2px;
69
+ color: wp-colors.$gray-900;
70
+ font-size: 13px;
71
+ height: auto;
72
+ margin: 0;
73
+ padding-left: 8px;
74
+ padding-right: 8px;
75
+
76
+ &::after {
77
+ content: "✕";
78
+ margin-left: 8px;
79
+ }
80
+
81
+ &:hover {
82
+ box-shadow: none;
83
+ color: black;
84
+ }
85
+ }
86
+
87
+ &__search-suggestions,
88
+ &__search-suggestions-multiselect {
89
+ max-height: 220px;
90
+ overflow: visible auto;
91
+ margin: 0 -4px;
92
+ padding: 4px 4px 0;
93
+
94
+ .components-button {
95
+ display: block;
96
+ line-height: normal;
97
+ margin: auto;
98
+
99
+ &.is-link {
100
+ color: wp-colors.$gray-900;
101
+ padding: 8px;
102
+ text-decoration: none;
103
+ width: 100%;
104
+
105
+ &:active:not(:disabled),
106
+ &:hover:not(:disabled) {
107
+ background: wp-colors.$gray-900;
108
+ background: var(--wp-admin-theme-color);
109
+ color: white;
110
+ }
111
+ }
112
+ &.is-secondary {
113
+ margin-bottom: 8px;
114
+ margin-top: 8px;
115
+ width: 100%;
116
+ }
117
+ }
118
+ }
119
+
120
+ &__search-suggestions {
121
+ border-radius: 2px;
122
+ border: 1px solid wp-colors.$gray-700;
123
+ padding: 0;
124
+
125
+ .components-button {
126
+ line-height: normal;
127
+
128
+ &:focus:not(:disabled) {
129
+ border-radius: 0;
130
+ box-shadow: inset 0 0 0 1.5px wp-colors.$gray-900;
131
+ box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
132
+ }
133
+
134
+ &.is-secondary {
135
+ width: calc(100% - 16px);
136
+ }
137
+ }
138
+ }
139
+
140
+ // Temp: CSS work-around.
141
+ .newspack-autocomplete-with-suggestions__label {
142
+ font-size: 11px;
143
+ font-weight: 500;
144
+ line-height: 1.4;
145
+ text-transform: uppercase;
146
+ display: block;
147
+ margin: 0 0 8px;
148
+ padding: 0;
149
+ }
150
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import './style.scss';
5
+
6
+ /**
7
+ * External dependencies
8
+ */
9
+ import classnames from 'classnames';
10
+
11
+ type BadgeProps = {
12
+ text: string;
13
+ level?: 'default' | 'info' | 'success' | 'warning' | 'error';
14
+ };
15
+
16
+ /**
17
+ * Badge component
18
+ */
19
+ const Badge = ( { text, level = 'default' }: BadgeProps ) => {
20
+ const classes = classnames( 'newspack-badge', `is-${ level }` );
21
+ return <span className={ classes }>{ text }</span>;
22
+ };
23
+
24
+ export default Badge;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Badge
3
+ */
4
+
5
+ @use "~@wordpress/base-styles/colors" as wp-colors;
6
+ @use "../../../colors/colors.module" as colors;
7
+
8
+ $badge-colors: (
9
+ "info": colors.$primary-600,
10
+ "warning": wp-colors.$alert-yellow,
11
+ "error": wp-colors.$alert-red,
12
+ "success": wp-colors.$alert-green,
13
+ );
14
+
15
+ .newspack-badge {
16
+ background-color: color-mix(in srgb, wp-colors.$white 90%, var(--base-color));
17
+ border-radius: 2px;
18
+ color: color-mix(in srgb, wp-colors.$black 50%, var(--base-color));
19
+ display: block;
20
+ flex: 0 0 auto;
21
+ font-size: 12px;
22
+ line-height: 1.5;
23
+ padding: 1px 4px;
24
+
25
+ &:where(.is-default) {
26
+ background-color: wp-colors.$gray-100;
27
+ color: wp-colors.$gray-800;
28
+ }
29
+
30
+ // Generate color variants
31
+ @each $type, $color in $badge-colors {
32
+ &.is-#{$type} {
33
+ --base-color: #{$color};
34
+ }
35
+ }
36
+ }
@@ -19,10 +19,11 @@ const { useHistory } = Router;
19
19
  type OriginalButtonProps = typeof BaseComponent.defaultProps;
20
20
  type Props = OriginalButtonProps & {
21
21
  href?: string;
22
+ loading?: boolean;
22
23
  onClick?: () => void;
23
24
  };
24
25
 
25
- const Button = ( { href, onClick, ...otherProps }: Props ) => {
26
+ const Button = ( { href, loading = undefined, onClick, ...otherProps }: Props ) => {
26
27
  const history = useHistory();
27
28
  const [ isAwaitingOnClick, setIsAwaitingOnClick ] = useState( false );
28
29
 
@@ -42,7 +43,7 @@ const Button = ( { href, onClick, ...otherProps }: Props ) => {
42
43
  otherProps.disabled = true;
43
44
  }
44
45
  // @ts-expect-error - @wordpress/components' Button can only have either href or onClick, not both.
45
- return <BaseComponent { ...otherProps } />;
46
+ return <BaseComponent loading={ loading ? true : undefined } { ...otherProps } />;
46
47
  };
47
48
 
48
49
  export default Button;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Card using WP Core's Card component.
3
+ * https://wordpress.github.io/gutenberg/?path=/docs/components-card--docs
4
+ */
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { __ } from '@wordpress/i18n';
10
+ import { Button, Card as CardWrapper, CardHeader, CardFooter, DropdownMenu, MenuItem, ToggleControl } from '@wordpress/components';
11
+ import { Icon, chevronDown, chevronRight, chevronUp, dragHandle, moreVertical } from '@wordpress/icons';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import './style-core.scss';
17
+
18
+ /**
19
+ * External dependencies
20
+ */
21
+ import classNames from 'classnames';
22
+
23
+ const CoreCard = ( {
24
+ actions,
25
+ actionType,
26
+ as,
27
+ buttonsCard,
28
+ className,
29
+ footer,
30
+ header,
31
+ icon,
32
+ iconBackgroundColor,
33
+ isActive,
34
+ isDraggable,
35
+ isFirstTarget,
36
+ isLastTarget,
37
+ isNarrow,
38
+ isSmall,
39
+ dragIndex,
40
+ onDragCallback = () => {},
41
+ onHeaderClick,
42
+ noBorder,
43
+ noMargin,
44
+ children = null,
45
+ ...otherProps
46
+ } ) => {
47
+ const classes = classNames(
48
+ 'newspack-card--core',
49
+ className,
50
+ ( buttonsCard || as === 'a' ) && 'newspack-card--core__buttons-card',
51
+ isDraggable && 'newspack-card--core__is-draggable',
52
+ isNarrow && 'newspack-card--core__is-narrow',
53
+ isSmall && 'newspack-card--core__is-small',
54
+ icon && 'newspack-card--core__has-icon',
55
+ iconBackgroundColor && 'newspack-card--core__has-icon-background-color',
56
+ isActive && 'newspack-card--core__is-active',
57
+ children && 'newspack-card--core__has-children',
58
+ noMargin && 'newspack-card--core__no-margin'
59
+ );
60
+ let sizeProps = isSmall ? 'small' : otherProps.size;
61
+ if ( buttonsCard || as === 'a' ) {
62
+ if ( ! isSmall ) {
63
+ sizeProps = 'large';
64
+ }
65
+ if ( as !== 'a' ) {
66
+ otherProps.as = 'a'; // Render as an anchor tag.
67
+ }
68
+ }
69
+ if ( noBorder ) {
70
+ otherProps.isBorderless = true;
71
+ }
72
+ return (
73
+ <CardWrapper as={ as } className={ classes } { ...otherProps }>
74
+ { ( header || icon ) && (
75
+ <CardHeader
76
+ as={ onHeaderClick ? 'button' : undefined }
77
+ className={ classNames( 'newspack-card--core__header', isDraggable && 'newspack-card--core__header--is-draggable' ) }
78
+ size={ sizeProps }
79
+ onClick={ onHeaderClick }
80
+ >
81
+ { isDraggable && (
82
+ <div className="newspack-card--core__header__draggable-controls">
83
+ <div className="newspack-card--core__header__draggable-controls__drag-handle">
84
+ <Icon icon={ dragHandle } />
85
+ </div>
86
+ <div className="newspack-card--core__header__draggable-controls__move-buttons">
87
+ <Button
88
+ icon={ chevronUp }
89
+ onClick={ () => onDragCallback( dragIndex, dragIndex - 1 ) }
90
+ disabled={ isFirstTarget }
91
+ label={ __( 'Move one position up', 'newspack-plugin' ) }
92
+ size="small"
93
+ />
94
+ <Button
95
+ icon={ chevronDown }
96
+ onClick={ () => onDragCallback( dragIndex, dragIndex + 1 ) }
97
+ disabled={ isLastTarget }
98
+ label={ __( 'Move one position down', 'newspack-plugin' ) }
99
+ size="small"
100
+ />
101
+ </div>
102
+ </div>
103
+ ) }
104
+ { icon && (
105
+ <div className="newspack-card--core__icon">
106
+ <Icon icon={ icon } height={ isSmall ? 24 : 48 } width={ isSmall ? 24 : 48 } />
107
+ </div>
108
+ ) }
109
+ { header && <div className="newspack-card--core__header-content">{ header }</div> }
110
+ { ! actions?.length > 0 && actionType === 'chevron' && (
111
+ <Icon className="newspack-card--core__action" icon={ chevronRight } height={ 24 } width={ 24 } />
112
+ ) }
113
+ { ! actions?.length > 0 && actionType === 'toggle' && (
114
+ <ToggleControl
115
+ className="newspack-card--core__action"
116
+ label={ otherProps.title }
117
+ hideLabelFromVision
118
+ checked={ isActive }
119
+ onChange={ () => {} }
120
+ />
121
+ ) }
122
+ { actions?.length > 0 && (
123
+ <DropdownMenu icon={ moreVertical } label={ __( 'More', 'newspack-plugin' ) }>
124
+ { () =>
125
+ actions.map( ( action, index ) => (
126
+ <MenuItem
127
+ key={ index }
128
+ icon={ action.icon }
129
+ onClick={ action.action }
130
+ disabled={ action.disabled || false }
131
+ isDestructive={ action.destructive || false }
132
+ >
133
+ { action.label }
134
+ </MenuItem>
135
+ ) )
136
+ }
137
+ </DropdownMenu>
138
+ ) }
139
+ </CardHeader>
140
+ ) }
141
+ { children && <div className="newspack-card--core__body">{ children }</div> }
142
+ { footer && <CardFooter size={ sizeProps }>{ footer }</CardFooter> }
143
+ </CardWrapper>
144
+ );
145
+ };
146
+
147
+ export default CoreCard;