@siratik/wp-component-library 0.1.0 → 0.1.2

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 (96) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +51 -5
  3. package/components/attachment-image/README.md +118 -0
  4. package/components/attachment-image/index.js +105 -0
  5. package/components/attributes-panel/README.md +92 -0
  6. package/components/attributes-panel/index.js +324 -0
  7. package/components/badge/README.md +61 -0
  8. package/components/badge/index.js +105 -0
  9. package/components/block-appender/README.md +158 -0
  10. package/components/block-appender/index.js +135 -0
  11. package/components/block-extension/index.js +214 -0
  12. package/components/business-hours/SpecialHoursControl.js +211 -0
  13. package/components/business-hours/WeeklyHoursControl.js +419 -0
  14. package/components/business-hours/index.js +8 -0
  15. package/components/button/README.md +172 -0
  16. package/components/button/index.js +84 -0
  17. package/components/captcha-placeholder/CloudflareTurnstile.js +111 -0
  18. package/components/captcha-placeholder/GoogleRecaptcha.js +132 -0
  19. package/components/captcha-placeholder/HCaptcha.js +99 -0
  20. package/components/captcha-placeholder/README.md +228 -0
  21. package/components/captcha-placeholder/index.js +39 -0
  22. package/components/captcha-placeholder/styles.js +33 -0
  23. package/components/geocoder/AddressEntryControl.js +390 -0
  24. package/components/geocoder/GeocoderSearchControl.js +353 -0
  25. package/components/geocoder/countryOptions.js +66 -0
  26. package/components/geocoder/geocoder.js +400 -0
  27. package/components/geocoder/index.js +16 -0
  28. package/components/icon-picker/Icon.js +53 -0
  29. package/components/icon-picker/IconPicker.js +70 -0
  30. package/components/icon-picker/IconPickerModal.js +474 -0
  31. package/components/icon-picker/IconPickerToolbarButton.js +60 -0
  32. package/components/icon-picker/InlineIconPicker.js +92 -0
  33. package/components/icon-picker/api.js +181 -0
  34. package/components/icon-picker/config-bootstrap.js +108 -0
  35. package/components/icon-picker/hooks.js +157 -0
  36. package/components/icon-picker/index.js +39 -0
  37. package/components/icon-picker/load-script.js +33 -0
  38. package/components/icon-picker/loaders.js +79 -0
  39. package/components/icon-picker/store.js +223 -0
  40. package/components/icon-picker/styles.js +232 -0
  41. package/components/icon-picker/utils.js +138 -0
  42. package/components/layout/README.md +268 -0
  43. package/components/link-tools-panel/README.md +49 -0
  44. package/components/link-tools-panel/index.js +198 -0
  45. package/components/link-tools-panel/styles.js +90 -0
  46. package/components/media/README.md +288 -0
  47. package/components/media/SettingsMediaUpload.js +187 -0
  48. package/components/media/editor-media-upload.js +65 -0
  49. package/components/media/image-controls.js +196 -0
  50. package/components/media/index.js +13 -0
  51. package/components/media/inspector-media-upload.js +290 -0
  52. package/components/media/toolbar-media-upload.js +53 -0
  53. package/components/media/utils/aspect-ratios.js +118 -0
  54. package/components/meta/MetaPanel.js +75 -0
  55. package/components/meta/README.md +46 -0
  56. package/components/meta/index.js +6 -0
  57. package/components/meta/meta-gated-editor.js +282 -0
  58. package/components/query/README.md +340 -0
  59. package/components/query/cardTemplatePartPanel.js +69 -0
  60. package/components/query/cardTemplatePartSelect.js +165 -0
  61. package/components/query/cardTemplatePartUtils.js +119 -0
  62. package/components/query/columnCountControl.js +63 -0
  63. package/components/query/displayTypeSelect.js +52 -0
  64. package/components/query/examples.js +166 -0
  65. package/components/query/hideCurrentPostControl.js +47 -0
  66. package/components/query/index.js +20 -0
  67. package/components/query/manualPostSelector.js +125 -0
  68. package/components/query/manualTermSelector.js +141 -0
  69. package/components/query/orderBySelect.js +95 -0
  70. package/components/query/postTypeSelect.js +67 -0
  71. package/components/query/postsPerPageControl.js +51 -0
  72. package/components/query/selectionModeControl.js +61 -0
  73. package/components/query/taxonomySelect.js +139 -0
  74. package/components/query/utils.js +75 -0
  75. package/components/repeater/README.md +159 -0
  76. package/components/repeater/index.js +829 -0
  77. package/components/segmented-bar/README.md +30 -0
  78. package/components/segmented-bar/index.js +68 -0
  79. package/components/sortable-select/README.md +153 -0
  80. package/components/sortable-select/custom-sortable-token.js +127 -0
  81. package/components/sortable-select/index.js +693 -0
  82. package/components/style-picker/README.md +36 -0
  83. package/components/style-picker/index.js +92 -0
  84. package/components/variable-field/README.md +299 -0
  85. package/components/variable-field/index.js +592 -0
  86. package/components/variable-field/value-to-chip-html.js +38 -0
  87. package/components/variable-inserter/README.md +68 -0
  88. package/components/variable-inserter/index.js +168 -0
  89. package/constants/statusColors.js +16 -6
  90. package/editor.js +16 -0
  91. package/index.js +127 -0
  92. package/package.json +27 -16
  93. package/utils/kitaris-localize.js +128 -0
  94. package/utils/slug-aware-color-setter.js +27 -0
  95. package/build/index.asset.php +0 -20
  96. package/build/index.js +0 -1233
@@ -0,0 +1,340 @@
1
+ # Query Controls
2
+
3
+ This directory contains standalone, modular query controls that can be composed together to build custom query interfaces. Each control is focused on a single responsibility and is always rendered as a ToolsPanelItem.
4
+
5
+ ## Individual Controls
6
+
7
+ These controls are always ToolsPanelItems that can be composed together in ToolsPanel containers:
8
+
9
+ ### Core Controls
10
+
11
+ - **`PostTypeSelect`** - Select post type
12
+ - **`OrderBySelect`** - Order posts by date, title, etc.
13
+ - **`SelectionModeControl`** - Toggle between auto and manual selection
14
+ - **`HideCurrentPostControl`** - Exclude the post being viewed from feed results on singles/templates (default on)
15
+
16
+ ### Manual Selection Controls
17
+
18
+ - **`ManualPostSelector`** - Search and select specific posts
19
+ - **`TermSelector`** - Search and select taxonomy terms
20
+ - **`TaxonomySelect`** - Select taxonomy for term filtering
21
+
22
+ ### Display Controls
23
+
24
+ - **`DisplayTypeSelect`** - Choose grid, list, slider, or pills display
25
+ - **`PostsPerPageControl`** - Control number of items to show
26
+ - **`ColumnCountControl`** - Set columns for grid or slides for slider
27
+
28
+ ## Usage
29
+
30
+ ### Basic Usage
31
+
32
+ ```jsx
33
+ import { __experimentalToolsPanel as ToolsPanel } from "@wordpress/components";
34
+ import { PostTypeSelect, OrderBySelect } from "../query";
35
+
36
+ function MyQueryInterface({ attributes, setAttributes }) {
37
+ const resetAll = () => {
38
+ setAttributes({
39
+ postType: "post",
40
+ orderPostsBy: "date",
41
+ orderPostsDirection: "desc",
42
+ });
43
+ };
44
+
45
+ return (
46
+ <ToolsPanel label="Query Settings" resetAll={resetAll}>
47
+ <PostTypeSelect
48
+ attributes={attributes}
49
+ setAttributes={setAttributes}
50
+ />
51
+ <OrderBySelect
52
+ attributes={attributes}
53
+ setAttributes={setAttributes}
54
+ />
55
+ </ToolsPanel>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ### Custom Order Options
61
+
62
+ ```jsx
63
+ import { OrderBySelect, defaultOrderOptions } from "../query";
64
+
65
+ function CustomOrderExample({ attributes, setAttributes }) {
66
+ // Option 1: Completely replace with custom options
67
+ const customOptions = [
68
+ { label: "Popularity", value: "meta_value_num_desc" },
69
+ { label: "Price (Low to High)", value: "meta_value_num_asc" },
70
+ { label: "Rating", value: "rating_desc" },
71
+ ];
72
+
73
+ // Option 2: Use defaults as a base and modify
74
+ const extendedOptions = [
75
+ ...defaultOrderOptions,
76
+ { label: "Custom Field", value: "meta_value_desc" },
77
+ { label: "View Count", value: "views_desc" },
78
+ ];
79
+
80
+ // Option 3: Mix defaults with custom (reorder, filter, etc.)
81
+ const mixedOptions = [
82
+ defaultOrderOptions[1], // Date (Newest → Oldest)
83
+ defaultOrderOptions[2], // Date (Oldest → Newest)
84
+ { label: "Featured First", value: "featured_desc" },
85
+ defaultOrderOptions[5], // Random
86
+ ];
87
+
88
+ return (
89
+ <ToolsPanel label="Query Settings" resetAll={resetAll}>
90
+ {/* Use completely custom options */}
91
+ <OrderBySelect
92
+ attributes={attributes}
93
+ setAttributes={setAttributes}
94
+ orderOptions={customOptions}
95
+ />
96
+
97
+ {/* Or use extended options */}
98
+ <OrderBySelect
99
+ attributes={attributes}
100
+ setAttributes={setAttributes}
101
+ orderOptions={extendedOptions}
102
+ />
103
+ </ToolsPanel>
104
+ );
105
+ }
106
+ ```
107
+
108
+ ### Complete Query Interface
109
+
110
+ ```jsx
111
+ import { __experimentalToolsPanel as ToolsPanel } from "@wordpress/components";
112
+ import {
113
+ PostTypeSelect,
114
+ OrderBySelect,
115
+ SelectionModeControl,
116
+ ManualPostSelector,
117
+ DisplayTypeSelect,
118
+ } from "../query";
119
+
120
+ function CompleteQueryInterface({ attributes, setAttributes }) {
121
+ const resetAll = () => {
122
+ setAttributes({
123
+ postType: "post",
124
+ orderPostsBy: "date",
125
+ orderPostsDirection: "desc",
126
+ selectionMode: "auto",
127
+ selectedPosts: [],
128
+ displayAs: "grid",
129
+ });
130
+ };
131
+
132
+ return (
133
+ <ToolsPanel label="Query Settings" resetAll={resetAll}>
134
+ <PostTypeSelect
135
+ attributes={attributes}
136
+ setAttributes={setAttributes}
137
+ />
138
+ <OrderBySelect
139
+ attributes={attributes}
140
+ setAttributes={setAttributes}
141
+ />
142
+ <SelectionModeControl
143
+ attributes={attributes}
144
+ setAttributes={setAttributes}
145
+ />
146
+ {attributes.selectionMode === "manual" && (
147
+ <ManualPostSelector
148
+ attributes={attributes}
149
+ setAttributes={setAttributes}
150
+ postType={attributes.postType}
151
+ />
152
+ )}
153
+ <DisplayTypeSelect
154
+ attributes={attributes}
155
+ setAttributes={setAttributes}
156
+ />
157
+ </ToolsPanel>
158
+ );
159
+ }
160
+ ```
161
+
162
+ ### Multiple Panels for Organization
163
+
164
+ ```jsx
165
+ function OrganizedInterface({ attributes, setAttributes }) {
166
+ return (
167
+ <>
168
+ <ToolsPanel label="Content Selection" resetAll={resetContent}>
169
+ <PostTypeSelect
170
+ attributes={attributes}
171
+ setAttributes={setAttributes}
172
+ />
173
+ <OrderBySelect
174
+ attributes={attributes}
175
+ setAttributes={setAttributes}
176
+ />
177
+ </ToolsPanel>
178
+
179
+ <ToolsPanel label="Display Settings" resetAll={resetDisplay}>
180
+ <DisplayTypeSelect
181
+ attributes={attributes}
182
+ setAttributes={setAttributes}
183
+ />
184
+ <PostsPerPageControl
185
+ attributes={attributes}
186
+ setAttributes={setAttributes}
187
+ />
188
+ <ColumnCountControl
189
+ attributes={attributes}
190
+ setAttributes={setAttributes}
191
+ />
192
+ </ToolsPanel>
193
+ </>
194
+ );
195
+ }
196
+ ```
197
+
198
+ ## Required Attributes
199
+
200
+ Make sure your block attributes include these for the controls to work properly:
201
+
202
+ ```jsx
203
+ // Required attributes for query controls
204
+ const attributes = {
205
+ // Core query attributes
206
+ postType: {
207
+ type: "string",
208
+ default: "post",
209
+ },
210
+ orderPostsBy: {
211
+ type: "string",
212
+ default: "date",
213
+ },
214
+ orderPostsDirection: {
215
+ type: "string",
216
+ default: "desc",
217
+ },
218
+
219
+ // Selection mode attributes
220
+ selectionMode: {
221
+ type: "string",
222
+ default: "auto",
223
+ },
224
+ selectedPosts: {
225
+ type: "array",
226
+ default: [],
227
+ },
228
+
229
+ // Taxonomy attributes
230
+ selectedTaxonomy: {
231
+ type: "string",
232
+ default: "",
233
+ },
234
+ selectedTerms: {
235
+ type: "array",
236
+ default: [],
237
+ },
238
+
239
+ // Display attributes
240
+ displayAs: {
241
+ type: "string",
242
+ default: "grid",
243
+ },
244
+ postsPerPage: {
245
+ type: "number",
246
+ default: 6,
247
+ },
248
+ columnCount: {
249
+ type: "number",
250
+ default: 3,
251
+ },
252
+ };
253
+ ```
254
+
255
+ ## Exports
256
+
257
+ ### Components
258
+
259
+ All individual controls are exported as named exports.
260
+
261
+ ### Utilities
262
+
263
+ - **`defaultOrderOptions`** - Array of default order options for OrderBySelect
264
+
265
+ ```jsx
266
+ import { OrderBySelect, defaultOrderOptions } from "../query";
267
+
268
+ // defaultOrderOptions contains:
269
+ // [
270
+ // { label: "Custom Order", value: "menu_order_asc" },
271
+ // { label: "Date (Newest → Oldest)", value: "date_desc" },
272
+ // { label: "Date (Oldest → Newest)", value: "date_asc" },
273
+ // { label: "Title (A → Z)", value: "title_asc" },
274
+ // { label: "Title (Z → A)", value: "title_desc" },
275
+ // { label: "Random", value: "rand_asc" },
276
+ // ]
277
+ ```
278
+
279
+ ## Control Props
280
+
281
+ ### Common Props
282
+
283
+ All controls accept these common props:
284
+
285
+ - **`attributes`** (Object) - Block attributes object
286
+ - **`setAttributes`** (Function) - Function to update block attributes
287
+
288
+ ### Specific Props
289
+
290
+ #### PostTypeSelect
291
+
292
+ - No additional props
293
+
294
+ #### OrderBySelect
295
+
296
+ - **`orderOptions`** (Array) - Custom order options (replaces defaults)
297
+ - **`additionalOrderOptions`** (Array) - Additional options to add to defaults (deprecated)
298
+
299
+ #### TaxonomySelect
300
+
301
+ - **`postType`** (String) - Post type to get taxonomies for
302
+
303
+ #### TermSelector
304
+
305
+ - **`selectedTaxonomy`** (String) - Taxonomy slug to get terms from
306
+
307
+ #### ManualPostSelector
308
+
309
+ - **`postType`** (String) - Post type to search (default: 'post')
310
+
311
+ #### DisplayTypeSelect
312
+
313
+ - **`allowedDisplayOptions`** (Array) - Allowed display types (default: ['grid', 'slider', 'list', 'pills'])
314
+
315
+ #### PostsPerPageControl
316
+
317
+ - **`label`** (String) - Label for control (default: 'Amount to Display')
318
+ - **`min`** (Number) - Minimum value (default: 1)
319
+ - **`max`** (Number) - Maximum value (default: 50)
320
+
321
+ #### ColumnCountControl
322
+
323
+ - **`min`** (Number) - Minimum value (default: 1)
324
+ - **`max`** (Number) - Maximum value (default: 4)
325
+
326
+ ## Design Principles
327
+
328
+ 1. **Single Responsibility** - Each control does one thing well
329
+ 2. **Always ToolsPanelItems** - Controls are consistent ToolsPanelItem components
330
+ 3. **Composable** - Controls can be mixed and matched as needed
331
+ 4. **Consistent API** - All controls follow the same prop patterns
332
+ 5. **No Forced Panels** - Controls don't create their own ToolsPanel containers
333
+
334
+ ## Architecture Benefits
335
+
336
+ - **Modular**: Use only the controls you need
337
+ - **Flexible**: Arrange controls in any panel structure
338
+ - **Consistent**: All controls work the same way
339
+ - **Maintainable**: Each control is focused and isolated
340
+ - **Composable**: Build complex interfaces from simple parts
@@ -0,0 +1,69 @@
1
+ import { __experimentalToolsPanel as ToolsPanel } from "@wordpress/components";
2
+ import { __ } from "@wordpress/i18n";
3
+ import { CardTemplatePartSelect } from "./cardTemplatePartSelect";
4
+ import { getDefaultCardSlugForPostType } from "./cardTemplatePartUtils";
5
+
6
+ /**
7
+ * Card layout ToolsPanel — matches core/query block extension UI.
8
+ *
9
+ * @param {Object} props Component props.
10
+ * @param {Object} props.attributes Block attributes (postType, templatePartSlug).
11
+ * @param {Function} props.setAttributes Set block attributes.
12
+ * @param {string} [props.postType] Post type override.
13
+ * @param {string} [props.area] Template part area (default kitaris-cards).
14
+ * @param {string} [props.defaultSlug] Default slug for reset / deselect.
15
+ * @param {Function} [props.slugFilter] Optional (slug, postType) => boolean.
16
+ * @param {Function} [props.onSlugChange] Optional side effect when slug changes.
17
+ * @param {string} [props.textDomain] i18n text domain.
18
+ * @param {boolean} [props.disableAutoFill] Passed to CardTemplatePartSelect.
19
+ * @param {boolean} [props.usePersistedSlugOnly] Passed to CardTemplatePartSelect.
20
+ * @returns {import('react').WPElement|null} Element to render.
21
+ */
22
+ function CardTemplatePartPanel({
23
+ attributes,
24
+ setAttributes,
25
+ postType: postTypeProp,
26
+ area,
27
+ defaultSlug: defaultSlugProp,
28
+ slugFilter,
29
+ onSlugChange,
30
+ textDomain = "built_starter",
31
+ disableAutoFill = false,
32
+ usePersistedSlugOnly = false,
33
+ }) {
34
+ const postType = postTypeProp || attributes?.postType || "post";
35
+ const resolvedDefault =
36
+ defaultSlugProp || getDefaultCardSlugForPostType(postType);
37
+
38
+ const resetCardLayout = () => {
39
+ setAttributes({ templatePartSlug: resolvedDefault });
40
+
41
+ // onSlugChange is optional; query blocks handle inner sync via setAttributes.
42
+ if (onSlugChange) {
43
+ onSlugChange(resolvedDefault);
44
+ }
45
+ };
46
+
47
+ return (
48
+ <ToolsPanel
49
+ // eslint-disable-next-line @wordpress/i18n-text-domain -- text domain is a prop for the consumer.
50
+ label={__("Card layout", textDomain)}
51
+ resetAll={resetCardLayout}
52
+ >
53
+ <CardTemplatePartSelect
54
+ attributes={attributes}
55
+ setAttributes={setAttributes}
56
+ postType={postType}
57
+ area={area}
58
+ defaultSlug={resolvedDefault}
59
+ slugFilter={slugFilter}
60
+ onSlugChange={onSlugChange}
61
+ textDomain={textDomain}
62
+ disableAutoFill={disableAutoFill}
63
+ usePersistedSlugOnly={usePersistedSlugOnly}
64
+ />
65
+ </ToolsPanel>
66
+ );
67
+ }
68
+
69
+ export { CardTemplatePartPanel };
@@ -0,0 +1,165 @@
1
+ import {
2
+ SelectControl,
3
+ __experimentalToolsPanelItem as ToolsPanelItem,
4
+ } from "@wordpress/components";
5
+ import { useSelect } from "@wordpress/data";
6
+ import { useEffect, useMemo } from "@wordpress/element";
7
+ import { __ } from "@wordpress/i18n";
8
+ import {
9
+ buildCardTemplatePartOptions,
10
+ getDefaultCardSlugForPostType,
11
+ } from "./cardTemplatePartUtils";
12
+
13
+ /**
14
+ * Card template part selector for query/post-feed blocks (kitaris-cards area).
15
+ *
16
+ * @param {Object} props Component props.
17
+ * @param {Object} props.attributes Block attributes (postType, templatePartSlug).
18
+ * @param {Function} props.setAttributes Set block attributes.
19
+ * @param {string} [props.postType] Post type override.
20
+ * @param {string} [props.area] Template part area (default kitaris-cards).
21
+ * @param {string} [props.defaultSlug] Default slug for deselect / empty state.
22
+ * @param {Function} [props.slugFilter] Optional (slug, postType) => boolean.
23
+ * @param {Function} [props.onSlugChange] Optional side effect when slug changes (e.g. sync inner blocks).
24
+ * @param {boolean} [props.isShownByDefault] ToolsPanelItem default visibility.
25
+ * @param {string} [props.textDomain] i18n text domain.
26
+ * @param {boolean} [props.disableAutoFill] Skip auto-setting default slug on mount.
27
+ * @param {boolean} [props.usePersistedSlugOnly] Select value is the saved slug only (no default fallback writes).
28
+ * @returns {import('react').WPElement|null} Element to render.
29
+ */
30
+ function CardTemplatePartSelect({
31
+ attributes,
32
+ setAttributes,
33
+ postType: postTypeProp,
34
+ area = "kitaris-cards",
35
+ defaultSlug: defaultSlugProp,
36
+ slugFilter,
37
+ onSlugChange,
38
+ isShownByDefault = true,
39
+ textDomain = "built_starter",
40
+ disableAutoFill = false,
41
+ usePersistedSlugOnly = false,
42
+ }) {
43
+ const postType = postTypeProp || attributes?.postType || "post";
44
+ const templatePartSlug = attributes?.templatePartSlug || "";
45
+ const resolvedDefault =
46
+ defaultSlugProp || getDefaultCardSlugForPostType(postType);
47
+
48
+ const templateParts = useSelect(
49
+ (select) =>
50
+ select("core").getEntityRecords("postType", "wp_template_part", {
51
+ per_page: 200,
52
+ area,
53
+ }),
54
+ [area],
55
+ );
56
+
57
+ const options = useMemo(() => {
58
+ const filtered = buildCardTemplatePartOptions(
59
+ templateParts,
60
+ postType,
61
+ slugFilter,
62
+ );
63
+
64
+ if (
65
+ !templatePartSlug ||
66
+ filtered.some((option) => option.value === templatePartSlug)
67
+ ) {
68
+ return filtered;
69
+ }
70
+
71
+ const savedPart = templateParts?.find(
72
+ (part) => part.slug === templatePartSlug,
73
+ );
74
+
75
+ if (!savedPart) {
76
+ return filtered;
77
+ }
78
+
79
+ return [
80
+ {
81
+ label:
82
+ savedPart.title?.rendered ||
83
+ savedPart.title ||
84
+ savedPart.slug,
85
+ value: savedPart.slug,
86
+ },
87
+ ...filtered,
88
+ ];
89
+ }, [templateParts, postType, slugFilter, templatePartSlug]);
90
+
91
+ const selectValue = usePersistedSlugOnly
92
+ ? templatePartSlug
93
+ : templatePartSlug || resolvedDefault;
94
+
95
+ useEffect(() => {
96
+ if (disableAutoFill) {
97
+ return;
98
+ }
99
+
100
+ if (!templatePartSlug && resolvedDefault && options.length > 0) {
101
+ const hasDefault = options.some((o) => o.value === resolvedDefault);
102
+ if (hasDefault) {
103
+ setAttributes({ templatePartSlug: resolvedDefault });
104
+ }
105
+ }
106
+ }, [
107
+ templatePartSlug,
108
+ resolvedDefault,
109
+ options,
110
+ setAttributes,
111
+ disableAutoFill,
112
+ ]);
113
+
114
+ if (!options.length) {
115
+ return null;
116
+ }
117
+
118
+ const handleChange = (value) => {
119
+ if (!value || value === templatePartSlug) {
120
+ return;
121
+ }
122
+
123
+ setAttributes({ templatePartSlug: value });
124
+
125
+ // Caller may already sync in setAttributes (e.g. core/query); skip duplicate updates.
126
+ if (onSlugChange) {
127
+ onSlugChange(value);
128
+ }
129
+ };
130
+
131
+ return (
132
+ <ToolsPanelItem
133
+ hasValue={() =>
134
+ !!templatePartSlug && templatePartSlug !== resolvedDefault
135
+ }
136
+ // eslint-disable-next-line @wordpress/i18n-text-domain -- text domain is a prop for the consumer.
137
+ label={__("Card layout", textDomain)}
138
+ onDeselect={() => {
139
+ setAttributes({ templatePartSlug: resolvedDefault });
140
+
141
+ if (onSlugChange) {
142
+ onSlugChange(resolvedDefault);
143
+ }
144
+ }}
145
+ isShownByDefault={isShownByDefault}
146
+ >
147
+ <SelectControl
148
+ __nextHasNoMarginBottom={true}
149
+ __next40pxDefaultSize
150
+ // eslint-disable-next-line @wordpress/i18n-text-domain -- text domain is a prop for the consumer.
151
+ label={__("Card layout", textDomain)}
152
+ // eslint-disable-next-line @wordpress/i18n-text-domain -- text domain is a prop for the consumer.
153
+ help={__(
154
+ "Choose which card template part to use for each post in the loop.",
155
+ textDomain,
156
+ )}
157
+ value={selectValue}
158
+ options={options}
159
+ onChange={handleChange}
160
+ />
161
+ </ToolsPanelItem>
162
+ );
163
+ }
164
+
165
+ export { CardTemplatePartSelect };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Resolve card template part options for a post type.
3
+ *
4
+ * Override via editor_experience.card_templates[postType] in the host plugin's
5
+ * localize config (window.kitaris_localize.blocks.editor_experience).
6
+ *
7
+ * @param {string} postType Post type slug.
8
+ * @returns {string[]} Slug prefixes — a part matches if slug equals prefix or starts with prefix + '-'.
9
+ */
10
+ export function getCardSlugPrefixesForPostType(postType) {
11
+ if (!postType) {
12
+ return ["card-"];
13
+ }
14
+
15
+ const config =
16
+ typeof window !== "undefined" &&
17
+ window.kitaris_localize?.blocks?.editor_experience?.card_templates?.[
18
+ postType
19
+ ];
20
+
21
+ if (Array.isArray(config) && config.length > 0) {
22
+ return config.map((entry) =>
23
+ typeof entry === "string" ? entry : entry?.slug,
24
+ ).filter(Boolean);
25
+ }
26
+
27
+ const knownPrefixes = {
28
+ kitaris_service: ["card-service"],
29
+ kitaris_team: ["card-team"],
30
+ kitaris_promotion: ["card-promotion"],
31
+ kitaris_coupon: ["card-promotion"],
32
+ kitaris_listing: ["kitaris_listing-card", "card-listing"],
33
+ kitaris_review: ["kitaris_review-card", "card-review"],
34
+ post: ["post-card", "card-post"],
35
+ page: ["page-card", "card-page"],
36
+ search: ["search-result-card", "card-search"],
37
+ };
38
+
39
+ if (knownPrefixes[postType]) {
40
+ return knownPrefixes[postType];
41
+ }
42
+
43
+ const base = postType.replace(/^kitaris_/, "");
44
+
45
+ return [`card-${base}`, `${postType}-card`];
46
+ }
47
+
48
+ /**
49
+ * @param {string} slug Template part slug.
50
+ * @param {string} postType Post type slug.
51
+ * @param {Function} [customFilter] Optional (slug, postType) => boolean.
52
+ * @returns {boolean}
53
+ */
54
+ export function cardTemplatePartMatchesPostType(slug, postType, customFilter) {
55
+ if (typeof customFilter === "function") {
56
+ return customFilter(slug, postType);
57
+ }
58
+
59
+ if (!slug) {
60
+ return false;
61
+ }
62
+
63
+ const prefixes = getCardSlugPrefixesForPostType(postType);
64
+
65
+ return prefixes.some(
66
+ (prefix) => slug === prefix || slug.startsWith(`${prefix}-`),
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Default card slug for a post type (first configured prefix).
72
+ *
73
+ * @param {string} postType Post type slug.
74
+ * @returns {string}
75
+ */
76
+ export function getDefaultCardSlugForPostType(postType) {
77
+ const defaults = {
78
+ kitaris_service: "card-service-001",
79
+ kitaris_team: "card-team-001",
80
+ kitaris_promotion: "card-promotion-001",
81
+ kitaris_coupon: "card-promotion-001",
82
+ kitaris_location: "card-location-001",
83
+ };
84
+
85
+ if (defaults[postType]) {
86
+ return defaults[postType];
87
+ }
88
+
89
+ const prefixes = getCardSlugPrefixesForPostType(postType);
90
+
91
+ return prefixes[0] || `${postType}-card`;
92
+ }
93
+
94
+ /**
95
+ * Build select options from template part entities.
96
+ *
97
+ * @param {Array} templateParts wp_template_part records.
98
+ * @param {string} postType Post type slug.
99
+ * @param {Function} [slugFilter] Optional filter.
100
+ * @returns {Array<{ label: string, value: string }>}
101
+ */
102
+ export function buildCardTemplatePartOptions(
103
+ templateParts,
104
+ postType,
105
+ slugFilter,
106
+ ) {
107
+ if (!templateParts?.length) {
108
+ return [];
109
+ }
110
+
111
+ const filtered = templateParts.filter((part) =>
112
+ cardTemplatePartMatchesPostType(part.slug, postType, slugFilter),
113
+ );
114
+
115
+ return filtered.map((part) => ({
116
+ label: part.title?.rendered || part.title || part.slug,
117
+ value: part.slug,
118
+ }));
119
+ }