@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,135 @@
1
+ // WordPress dependencies.
2
+ import { store as blockEditorStore, Inserter } from "@wordpress/block-editor";
3
+ import { Button } from "@wordpress/components";
4
+ import { useSelect } from "@wordpress/data";
5
+ import { Icon, plus } from "@wordpress/icons";
6
+
7
+ import { __ } from "@wordpress/i18n";
8
+
9
+ function CustomBlockAppender({
10
+ rootClientId,
11
+ appenderTitle,
12
+ appenderLabel,
13
+ appenderClassModifier,
14
+ }) {
15
+ return (
16
+ <Inserter
17
+ __experimentalIsQuick
18
+ rootClientId={rootClientId}
19
+ renderToggle={({ onToggle, disabled }) => (
20
+ <Button
21
+ showTooltip={true}
22
+ className={`block-list-appender__toggle built-block-appender--${appenderClassModifier} `}
23
+ onClick={onToggle}
24
+ disabled={disabled}
25
+ label={appenderLabel}
26
+ icon={plus}
27
+ >
28
+ {appenderTitle}
29
+ </Button>
30
+ )}
31
+ isAppender
32
+ />
33
+ );
34
+ }
35
+
36
+ function CustomInspectorAppender({
37
+ rootClientId,
38
+ appenderTitle,
39
+ appenderLabel,
40
+ appenderClassModifier,
41
+ }) {
42
+ return (
43
+ <Inserter
44
+ __experimentalIsQuick
45
+ rootClientId={rootClientId}
46
+ renderToggle={({ onToggle, disabled }) => (
47
+ <Button
48
+ className={`built-block-appender built-block-appender--${appenderClassModifier} `}
49
+ onClick={onToggle}
50
+ disabled={disabled}
51
+ label={appenderLabel}
52
+ variant="secondary"
53
+ >
54
+ <Icon icon={plus} />
55
+ {appenderTitle}
56
+ </Button>
57
+ )}
58
+ isAppender
59
+ />
60
+ );
61
+ }
62
+
63
+ function CustomColumnAppender({
64
+ rootClientId,
65
+ appenderTitle,
66
+ appenderLabel,
67
+ appenderClassModifier,
68
+ }) {
69
+ return (
70
+ <Inserter
71
+ __experimentalIsQuick
72
+ rootClientId={rootClientId}
73
+ renderToggle={({ onToggle, disabled }) => (
74
+ <div className=" built-block-appender-column__wrapper">
75
+ <Button
76
+ showTooltip={true}
77
+ className={`block-list-appender__toggle built-block-appender-column__appender built-block-appender--${appenderClassModifier} `}
78
+ onClick={onToggle}
79
+ disabled={disabled}
80
+ label={appenderLabel}
81
+ icon={plus}
82
+ >
83
+ {appenderTitle}
84
+ </Button>
85
+ </div>
86
+ )}
87
+ isAppender
88
+ />
89
+ );
90
+ }
91
+
92
+ const CustomInlineAppender = ({
93
+ clientId,
94
+ label = __("Add Block", "wp-component-library"),
95
+ }) => {
96
+ const isActive = useSelect(
97
+ (select) => {
98
+ const { isBlockSelected, hasSelectedInnerBlock } =
99
+ select(blockEditorStore);
100
+ return (
101
+ isBlockSelected(clientId) ||
102
+ hasSelectedInnerBlock(clientId, true)
103
+ );
104
+ },
105
+ [clientId],
106
+ );
107
+
108
+ if (!isActive) {
109
+ return null;
110
+ }
111
+
112
+ return (
113
+ <Inserter
114
+ __experimentalIsQuick
115
+ rootClientId={clientId}
116
+ renderToggle={({ onToggle, disabled }) => (
117
+ <Button
118
+ className="block-list-appender__toggle"
119
+ icon={plus}
120
+ label={label}
121
+ onClick={onToggle}
122
+ disabled={disabled}
123
+ />
124
+ )}
125
+ isAppender
126
+ />
127
+ );
128
+ };
129
+
130
+ export {
131
+ CustomBlockAppender,
132
+ CustomColumnAppender,
133
+ CustomInlineAppender,
134
+ CustomInspectorAppender,
135
+ };
@@ -0,0 +1,214 @@
1
+ /**
2
+ * registerBlockExtension / unregisterBlockExtension
3
+ *
4
+ * Drop-in replacement for @10up/block-components registerBlockExtension.
5
+ * Fully backwards-compatible API — same arguments, same behaviour.
6
+ *
7
+ * Registers four WordPress hooks per extension:
8
+ * 1. blocks.registerBlockType — merges extra `attributes`
9
+ * 2. editor.BlockEdit — injects `Edit` controls (before | after block)
10
+ * 3. editor.BlockListBlock — applies `classNameGenerator` / `inlineStyleGenerator` in editor
11
+ * 4. blocks.getSaveContent.extraProps — applies class/style to saved HTML
12
+ *
13
+ * @param {string|string[]} blockName
14
+ * Block name(s) to extend. Pass `"*"` or `"all"` to match every block.
15
+ * @param {Object} options
16
+ * @param {Object} [options.attributes={}] Extra attribute definitions.
17
+ * @param {Function} [options.classNameGenerator] (attributes) => string
18
+ * @param {Function} [options.inlineStyleGenerator] (attributes) => Object
19
+ * @param {Function} options.Edit React component for inspector controls.
20
+ * @param {string} options.extensionName Unique name used in filter namespaces.
21
+ * @param {'before'|'after'} [options.order='after'] Whether Edit renders before or after BlockEdit.
22
+ */
23
+
24
+ import { addFilter, removeFilter } from "@wordpress/hooks";
25
+ import { createHigherOrderComponent } from "@wordpress/compose";
26
+ import { createElement, Fragment } from "@wordpress/element";
27
+
28
+ /**
29
+ * Minimal className utility — avoids adding clsx as a dependency.
30
+ *
31
+ * @param {...(string|null|undefined|false)} classes
32
+ * @returns {string}
33
+ */
34
+ function cx(...classes) {
35
+ return classes.filter(Boolean).join(" ").trim();
36
+ }
37
+
38
+ export function registerBlockExtension(
39
+ blockName,
40
+ {
41
+ attributes = {},
42
+ classNameGenerator = () => "",
43
+ inlineStyleGenerator,
44
+ Edit,
45
+ extensionName,
46
+ order = "after",
47
+ },
48
+ ) {
49
+ const isArray = Array.isArray(blockName);
50
+
51
+ // Normalise wildcard
52
+ if (blockName === "*") { blockName = "all"; }
53
+
54
+ /**
55
+ * Returns true when `name` should be extended.
56
+ *
57
+ * @param {string} name Block name.
58
+ * @returns {boolean}
59
+ */
60
+ const matches = (name) => {
61
+ if (blockName === "all") { return true; }
62
+ if (isArray) { return blockName.includes(name); }
63
+ return name === blockName;
64
+ };
65
+
66
+ // Build a stable namespace segment for hook IDs.
67
+ const ns = isArray ? blockName.join("-") : blockName;
68
+ const prefix = `wpcl/${ns}/${extensionName}`;
69
+
70
+ // ── 1. Attributes ────────────────────────────────────────────────────────
71
+
72
+ addFilter(
73
+ "blocks.registerBlockType",
74
+ `${prefix}/addAttributesToBlock`,
75
+ (settings, name) => {
76
+ if (!matches(name)) { return settings; }
77
+ return {
78
+ ...settings,
79
+ attributes: { ...settings.attributes, ...attributes },
80
+ };
81
+ },
82
+ );
83
+
84
+ // ── 2. Editor controls (BlockEdit HOC) ───────────────────────────────────
85
+
86
+ const withEdit = createHigherOrderComponent(
87
+ (BlockEdit) => (props) => {
88
+ const { name, isSelected } = props;
89
+
90
+ if (!matches(name)) { return createElement(BlockEdit, props); }
91
+
92
+ const before = order === "before" && isSelected;
93
+ const after = order === "after" && isSelected;
94
+ // Fallback: any unrecognised order value renders after (same as 10up)
95
+ const fallback = !before && !after && isSelected;
96
+
97
+ return createElement(
98
+ Fragment,
99
+ null,
100
+ before && createElement(Edit, props),
101
+ createElement(BlockEdit, props),
102
+ after && createElement(Edit, props),
103
+ fallback && createElement(Edit, props),
104
+ );
105
+ },
106
+ "addSettingsToBlock",
107
+ );
108
+
109
+ addFilter("editor.BlockEdit", `${prefix}/addSettingsToBlock`, withEdit);
110
+
111
+ // ── 3. Class + style in editor (BlockListBlock HOC) ──────────────────────
112
+
113
+ const withBlockListBlock = createHigherOrderComponent(
114
+ (BlockListBlock) => (props) => {
115
+ const {
116
+ name,
117
+ attributes: blockAttrs,
118
+ className = "",
119
+ style = {},
120
+ wrapperProps,
121
+ } = props;
122
+
123
+ if (!matches(name)) { return createElement(BlockListBlock, props); }
124
+
125
+ const generatedClass = classNameGenerator(blockAttrs);
126
+
127
+ let mergedStyle = { ...style };
128
+ let generatedStyle = null;
129
+
130
+ if (typeof inlineStyleGenerator === "function") {
131
+ generatedStyle = inlineStyleGenerator(blockAttrs);
132
+ mergedStyle = {
133
+ ...style,
134
+ ...(wrapperProps?.style ?? {}),
135
+ ...generatedStyle,
136
+ };
137
+ }
138
+
139
+ if (!generatedClass && !generatedStyle) {
140
+ return createElement(BlockListBlock, props);
141
+ }
142
+
143
+ const mergedClass = cx(className, generatedClass);
144
+
145
+ return createElement(BlockListBlock, {
146
+ ...props,
147
+ className: mergedClass,
148
+ wrapperProps: { ...wrapperProps, style: mergedStyle },
149
+ });
150
+ },
151
+ "addAdditionalPropertiesInEditor",
152
+ );
153
+
154
+ addFilter(
155
+ "editor.BlockListBlock",
156
+ `${prefix}/addAdditionalPropertiesInEditor`,
157
+ withBlockListBlock,
158
+ );
159
+
160
+ // ── 4. Class + style in saved HTML ───────────────────────────────────────
161
+
162
+ addFilter(
163
+ "blocks.getSaveContent.extraProps",
164
+ `${prefix}/addAdditionalPropertiesToSavedMarkup`,
165
+ (extraProps, blockType, blockAttrs) => {
166
+ const { className = "", style = {} } = extraProps;
167
+
168
+ if (!matches(blockType.name)) { return extraProps; }
169
+
170
+ const generatedClass = classNameGenerator(blockAttrs);
171
+
172
+ let mergedStyle = { ...style };
173
+ let generatedStyle = null;
174
+
175
+ if (typeof inlineStyleGenerator === "function") {
176
+ generatedStyle = inlineStyleGenerator(blockAttrs);
177
+ mergedStyle = { ...style, ...generatedStyle };
178
+ }
179
+
180
+ if (!generatedClass && !generatedStyle) { return extraProps; }
181
+
182
+ const mergedClass = cx(className, generatedClass);
183
+
184
+ return { ...extraProps, className: mergedClass, style: mergedStyle };
185
+ },
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Remove a previously registered block extension.
191
+ *
192
+ * @param {string|string[]} blockName Same value passed to registerBlockExtension.
193
+ * @param {string} extensionName Same extensionName passed to registerBlockExtension.
194
+ */
195
+ export function unregisterBlockExtension(blockName, extensionName) {
196
+ if (!blockName || !extensionName) { return; }
197
+
198
+ if (blockName === "*") { blockName = "all"; }
199
+
200
+ const isArray = Array.isArray(blockName);
201
+ const ns = isArray ? blockName.join("-") : blockName;
202
+ const prefix = `wpcl/${ns}/${extensionName}`;
203
+
204
+ removeFilter("blocks.registerBlockType", `${prefix}/addAttributesToBlock`);
205
+ removeFilter("editor.BlockEdit", `${prefix}/addSettingsToBlock`);
206
+ removeFilter(
207
+ "editor.BlockListBlock",
208
+ `${prefix}/addAdditionalPropertiesInEditor`,
209
+ );
210
+ removeFilter(
211
+ "blocks.getSaveContent.extraProps",
212
+ `${prefix}/addAdditionalPropertiesToSavedMarkup`,
213
+ );
214
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Holiday / special hours list: date ranges (MM-DD) that are closed or have
3
+ * their own hours. Controlled: `entries` is the special_opening_hours array,
4
+ * `onChange` receives the whole next array.
5
+ *
6
+ * @package Siratik/WP-Component-Library
7
+ */
8
+
9
+ import {
10
+ Flex,
11
+ FlexBlock,
12
+ FlexItem,
13
+ TextControl,
14
+ TimePicker,
15
+ ToggleControl,
16
+ } from "@wordpress/components";
17
+ import { useState } from "@wordpress/element";
18
+ import { __ } from "@wordpress/i18n";
19
+ import { DragHandle, RemoveButton, Repeater } from "../repeater";
20
+
21
+ function generateId() {
22
+ return Math.random().toString(36).slice(2, 10);
23
+ }
24
+
25
+ function timeStringToObject(str) {
26
+ if (!str) {
27
+ return { hours: 9, minutes: 0 };
28
+ }
29
+ const [h, m] = str.split(":");
30
+ return { hours: parseInt(h, 10), minutes: parseInt(m, 10) };
31
+ }
32
+
33
+ function timeObjectToString(obj) {
34
+ if (!obj || typeof obj.hours !== "number") {
35
+ return "09:00";
36
+ }
37
+ return `${String(obj.hours).padStart(2, "0")}:${String(obj.minutes ?? 0).padStart(2, "0")}`;
38
+ }
39
+
40
+ /**
41
+ * A single holiday row rendered inside the Repeater.
42
+ */
43
+ function HolidayRow({ item, onUpdate, is24HourFormat = false }) {
44
+ const [openTime, setOpenTime] = useState(() =>
45
+ timeStringToObject(item.opens || "09:00"),
46
+ );
47
+ const [closeTime, setCloseTime] = useState(() =>
48
+ timeStringToObject(item.closes || "17:00"),
49
+ );
50
+
51
+ const update = (field, value) => onUpdate({ ...item, [field]: value });
52
+
53
+ const handleOpenChange = (obj) => {
54
+ setOpenTime(obj);
55
+ update("opens", timeObjectToString(obj));
56
+ };
57
+ const handleCloseChange = (obj) => {
58
+ setCloseTime(obj);
59
+ update("closes", timeObjectToString(obj));
60
+ };
61
+
62
+ return (
63
+ <Flex gap={3} align="start" wrap>
64
+ <DragHandle />
65
+
66
+ <FlexBlock style={{ minWidth: "130px" }}>
67
+ <TextControl
68
+ __next40pxDefaultSize
69
+ __nextHasNoMarginBottom
70
+ label={__("Label", "wp-component-library")}
71
+ help={__("e.g. Christmas Day", "wp-component-library")}
72
+ value={item.label ?? ""}
73
+ onChange={(v) => update("label", v)}
74
+ />
75
+ </FlexBlock>
76
+
77
+ <FlexBlock style={{ minWidth: "110px" }}>
78
+ <TextControl
79
+ __next40pxDefaultSize
80
+ __nextHasNoMarginBottom
81
+ label={__("From (MM-DD)", "wp-component-library")}
82
+ // eslint-disable-next-line @wordpress/i18n-hyphenated-range -- MM-DD date, not a range.
83
+ help={__("e.g. 12-25", "wp-component-library")}
84
+ value={item.date_from ?? ""}
85
+ onChange={(v) => update("date_from", v)}
86
+ placeholder="12-25"
87
+ maxLength={5}
88
+ />
89
+ </FlexBlock>
90
+
91
+ <FlexBlock style={{ minWidth: "110px" }}>
92
+ <TextControl
93
+ __next40pxDefaultSize
94
+ __nextHasNoMarginBottom
95
+ label={__("To (MM-DD)", "wp-component-library")}
96
+ // eslint-disable-next-line @wordpress/i18n-hyphenated-range -- MM-DD date, not a range.
97
+ help={__("e.g. 12-25", "wp-component-library")}
98
+ value={item.date_to ?? ""}
99
+ onChange={(v) => update("date_to", v)}
100
+ placeholder="12-25"
101
+ maxLength={5}
102
+ />
103
+ </FlexBlock>
104
+
105
+ <FlexItem style={{ minWidth: "90px", paddingTop: "28px" }}>
106
+ <ToggleControl
107
+ __nextHasNoMarginBottom
108
+ label={__("Closed", "wp-component-library")}
109
+ checked={!!item.closed}
110
+ onChange={(v) => update("closed", v)}
111
+ />
112
+ </FlexItem>
113
+
114
+ <FlexItem style={{ paddingTop: "26px" }}>
115
+ {item.closed ? (
116
+ <span style={{ color: "#757575", fontStyle: "italic" }}>
117
+ {__("Closed all day", "wp-component-library")}
118
+ </span>
119
+ ) : (
120
+ <Flex align="center" gap={1}>
121
+ <TimePicker.TimeInput
122
+ value={openTime}
123
+ onChange={handleOpenChange}
124
+ is12Hour={!is24HourFormat}
125
+ />
126
+ <span style={{ color: "#757575" }}>–</span>
127
+ <TimePicker.TimeInput
128
+ value={closeTime}
129
+ onChange={handleCloseChange}
130
+ is12Hour={!is24HourFormat}
131
+ />
132
+ </Flex>
133
+ )}
134
+ </FlexItem>
135
+
136
+ <RemoveButton />
137
+ </Flex>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * @param {Object} props
143
+ * @param {Array} props.entries Special hours entries.
144
+ * @param {Function} props.onChange Receives the next entries array.
145
+ * @param {boolean} [props.is24HourFormat]
146
+ */
147
+ export function SpecialHoursControl({
148
+ entries,
149
+ onChange,
150
+ is24HourFormat = false,
151
+ }) {
152
+ const safeEntries = Array.isArray(entries) ? entries : [];
153
+
154
+ const handleAdd = () => {
155
+ onChange([
156
+ ...safeEntries,
157
+ {
158
+ id: generateId(),
159
+ label: "",
160
+ date_from: "",
161
+ date_to: "",
162
+ closed: true,
163
+ opens: "09:00",
164
+ closes: "17:00",
165
+ },
166
+ ]);
167
+ };
168
+
169
+ const handleRemove = (id) => {
170
+ onChange(safeEntries.filter((e) => e.id !== id));
171
+ };
172
+
173
+ const handleReorder = (reordered) => {
174
+ onChange(reordered);
175
+ };
176
+
177
+ const handleUpdate = (id, updated) => {
178
+ onChange(safeEntries.map((e) => (e.id === id ? updated : e)));
179
+ };
180
+
181
+ return (
182
+ <div className="special-hours-section">
183
+ <h4 style={{ marginBottom: "0.75rem", marginTop: 0 }}>
184
+ {__("Holiday / Special Hours", "wp-component-library")}
185
+ </h4>
186
+ <Repeater
187
+ items={safeEntries}
188
+ renderItem={(item) => (
189
+ <HolidayRow
190
+ item={item}
191
+ is24HourFormat={is24HourFormat}
192
+ onUpdate={(updated) =>
193
+ handleUpdate(item.id, updated)
194
+ }
195
+ />
196
+ )}
197
+ onAdd={handleAdd}
198
+ onRemove={handleRemove}
199
+ onReorder={handleReorder}
200
+ renderMode="integrated"
201
+ addButtonText={__("Add exception", "wp-component-library")}
202
+ emptyStateText={__(
203
+ "No special hours set. Add exceptions for holidays, early closings, or special events.",
204
+ "wp-component-library",
205
+ )}
206
+ />
207
+ </div>
208
+ );
209
+ }
210
+
211
+ export default SpecialHoursControl;