@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,95 @@
1
+ import {
2
+ SelectControl,
3
+ __experimentalToolsPanelItem as ToolsPanelItem,
4
+ } from "@wordpress/components";
5
+ import { __ } from "@wordpress/i18n";
6
+
7
+ /**
8
+ * Default order options - can be imported and used as a base
9
+ */
10
+ export const defaultOrderOptions = [
11
+ { label: "Custom Order", value: "menu_order_asc" },
12
+ { label: "Date (Newest → Oldest)", value: "date_desc" },
13
+ { label: "Date (Oldest → Newest)", value: "date_asc" },
14
+ { label: "Title (A → Z)", value: "title_asc" },
15
+ { label: "Title (Z → A)", value: "title_desc" },
16
+ { label: "Random", value: "rand_asc" },
17
+ ];
18
+
19
+ /**
20
+ * Standalone order by selection control
21
+ *
22
+ * @param {Object} props Component props
23
+ * @param {Object} props.attributes Block attributes
24
+ * @param {Function} props.setAttributes Function to set attributes
25
+ * @param {Array} props.orderOptions Custom order options (replaces defaults if provided)
26
+ * @param {Array} props.additionalOrderOptions Additional options to add to defaults (deprecated, use orderOptions instead)
27
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
28
+ * @returns {WPElement} Element to render
29
+ */
30
+ /**
31
+ * Split an order option value ("menu_order_asc") into orderby and order at
32
+ * its last underscore: orderby values can hold underscores themselves.
33
+ *
34
+ * @param {string} value Option value, `${orderby}_${order}`.
35
+ * @return {{ orderPostsBy: string, orderPostsDirection: string }} Attributes.
36
+ */
37
+ export function parseOrderValue(value) {
38
+ const at = value.lastIndexOf("_");
39
+ return {
40
+ orderPostsBy: value.slice(0, at),
41
+ orderPostsDirection: value.slice(at + 1),
42
+ };
43
+ }
44
+
45
+ function OrderBySelect({
46
+ attributes: { orderPostsBy, orderPostsDirection },
47
+ setAttributes,
48
+ orderOptions,
49
+ additionalOrderOptions = [], // Keep for backward compatibility
50
+ isShownByDefault = true,
51
+ }) {
52
+ const defaultOrderPostsBy = "date";
53
+ const defaultOrderPostsDirection = "desc";
54
+
55
+ // Determine which options to use
56
+ let finalOptions;
57
+ if (orderOptions) {
58
+ // Use custom options if provided
59
+ finalOptions = orderOptions;
60
+ } else if (additionalOrderOptions.length > 0) {
61
+ // Use defaults + additional for backward compatibility
62
+ finalOptions = [...defaultOrderOptions, ...additionalOrderOptions];
63
+ } else {
64
+ // Use defaults
65
+ finalOptions = defaultOrderOptions;
66
+ }
67
+
68
+ return (
69
+ <ToolsPanelItem
70
+ hasValue={() =>
71
+ orderPostsBy !== defaultOrderPostsBy ||
72
+ orderPostsDirection !== defaultOrderPostsDirection
73
+ }
74
+ label={__("Order By", "built_starter")}
75
+ onDeselect={() =>
76
+ setAttributes({
77
+ orderPostsBy: defaultOrderPostsBy,
78
+ orderPostsDirection: defaultOrderPostsDirection,
79
+ })
80
+ }
81
+ isShownByDefault={isShownByDefault}
82
+ >
83
+ <SelectControl
84
+ __nextHasNoMarginBottom={true}
85
+ __next40pxDefaultSize
86
+ label="Order By"
87
+ value={`${orderPostsBy}_${orderPostsDirection}`}
88
+ options={finalOptions}
89
+ onChange={(value) => setAttributes(parseOrderValue(value))}
90
+ />
91
+ </ToolsPanelItem>
92
+ );
93
+ }
94
+
95
+ export { OrderBySelect };
@@ -0,0 +1,67 @@
1
+ import {
2
+ SelectControl,
3
+ __experimentalToolsPanelItem as ToolsPanelItem,
4
+ } from "@wordpress/components";
5
+ import { useSelect } from "@wordpress/data";
6
+ import { __ } from "@wordpress/i18n";
7
+
8
+ /**
9
+ * Standalone post type selection control
10
+ *
11
+ * @param {Object} props Component props
12
+ * @param {Object} props.attributes Block attributes
13
+ * @param {Function} props.setAttributes Function to set attributes
14
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
15
+ * @returns {WPElement} Element to render
16
+ */
17
+ function PostTypeSelect({
18
+ attributes: { postType },
19
+ setAttributes,
20
+ isShownByDefault = true,
21
+ }) {
22
+ const defaultPostType = "post";
23
+
24
+ const getPostTypeList = useSelect((select) =>
25
+ select("core").getPostTypes({ per_page: 20 }),
26
+ );
27
+
28
+ const getPostTypesOptions = () => {
29
+ let filteredPostTypes = [];
30
+ let postTypesOptions = [];
31
+
32
+ if (getPostTypeList) {
33
+ filteredPostTypes = getPostTypeList.filter(
34
+ (type) =>
35
+ type.viewable === true &&
36
+ type.rest_base !== "media" &&
37
+ type.rest_base !== "pages",
38
+ );
39
+ postTypesOptions = filteredPostTypes.map((type) => ({
40
+ label: type.name,
41
+ value: type.slug,
42
+ }));
43
+ }
44
+
45
+ return postTypesOptions;
46
+ };
47
+
48
+ return (
49
+ <ToolsPanelItem
50
+ hasValue={() => postType !== defaultPostType}
51
+ label={__("Select Post Type", "built_starter")}
52
+ onDeselect={() => setAttributes({ postType: defaultPostType })}
53
+ isShownByDefault={isShownByDefault}
54
+ >
55
+ <SelectControl
56
+ __nextHasNoMarginBottom={true}
57
+ __next40pxDefaultSize
58
+ label={__("Select Post Type", "built_starter")}
59
+ onChange={(value) => setAttributes({ postType: value })}
60
+ options={getPostTypesOptions()}
61
+ value={postType}
62
+ />
63
+ </ToolsPanelItem>
64
+ );
65
+ }
66
+
67
+ export { PostTypeSelect };
@@ -0,0 +1,51 @@
1
+ import {
2
+ RangeControl,
3
+ __experimentalToolsPanelItem as ToolsPanelItem,
4
+ } from "@wordpress/components";
5
+ import { __ } from "@wordpress/i18n";
6
+
7
+ /**
8
+ * Standalone posts per page control
9
+ *
10
+ * @param {Object} props Component props
11
+ * @param {Object} props.attributes Block attributes
12
+ * @param {Function} props.setAttributes Function to set attributes
13
+ * @param {string} props.label Label for the control
14
+ * @param {number} props.min Minimum value
15
+ * @param {number} props.max Maximum value
16
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: false)
17
+ * @param {number} props.defaultValue The block's default count (default: 6)
18
+ * @returns {WPElement} Element to render
19
+ */
20
+ function PostsPerPageControl({
21
+ attributes: { postsPerPage },
22
+ setAttributes,
23
+ label = __("Amount to display", "wp-component-library"),
24
+ min = 1,
25
+ max = 50,
26
+ isShownByDefault = false,
27
+ defaultValue = 6,
28
+ }) {
29
+ return (
30
+ <ToolsPanelItem
31
+ hasValue={() => postsPerPage !== defaultValue}
32
+ label={label}
33
+ onDeselect={() => setAttributes({ postsPerPage: defaultValue })}
34
+ isShownByDefault={isShownByDefault}
35
+ >
36
+ <RangeControl
37
+ __nextHasNoMarginBottom={true}
38
+ __next40pxDefaultSize
39
+ label={label}
40
+ value={postsPerPage}
41
+ onChange={(postsPerPageNew) =>
42
+ setAttributes({ postsPerPage: postsPerPageNew })
43
+ }
44
+ min={min}
45
+ max={max}
46
+ />
47
+ </ToolsPanelItem>
48
+ );
49
+ }
50
+
51
+ export { PostsPerPageControl };
@@ -0,0 +1,61 @@
1
+ import {
2
+ __experimentalToggleGroupControl as ToggleGroupControl,
3
+ __experimentalToggleGroupControlOption as ToggleGroupControlOption,
4
+ __experimentalToolsPanelItem as ToolsPanelItem,
5
+ } from "@wordpress/components";
6
+ import { __ } from "@wordpress/i18n";
7
+
8
+ /**
9
+ * Standalone selection mode control (auto vs manual)
10
+ *
11
+ * @param {Object} props Component props
12
+ * @param {Object} props.attributes Block attributes
13
+ * @param {Function} props.setAttributes Function to set attributes
14
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
15
+ * @returns {WPElement} Element to render
16
+ */
17
+ function SelectionModeControl({
18
+ attributes: { selectionMode = "auto" },
19
+ setAttributes,
20
+ isShownByDefault = true,
21
+ }) {
22
+ const defaultSelectionMode = "auto";
23
+
24
+ const handleSelectionModeChange = (value) => {
25
+ setAttributes({
26
+ selectionMode: value,
27
+ // Preserve existing selections when switching modes
28
+ });
29
+ };
30
+
31
+ return (
32
+ <ToolsPanelItem
33
+ hasValue={() => selectionMode !== defaultSelectionMode}
34
+ label={__("Selection Mode", "wp-component-library")}
35
+ onDeselect={() =>
36
+ setAttributes({ selectionMode: defaultSelectionMode })
37
+ }
38
+ isShownByDefault={isShownByDefault}
39
+ >
40
+ <ToggleGroupControl
41
+ __next40pxDefaultSize={true}
42
+ __nextHasNoMarginBottom
43
+ label={__("Selection Mode", "wp-component-library")}
44
+ value={selectionMode}
45
+ onChange={handleSelectionModeChange}
46
+ isBlock
47
+ >
48
+ <ToggleGroupControlOption
49
+ label={__("Auto", "wp-component-library")}
50
+ value="auto"
51
+ />
52
+ <ToggleGroupControlOption
53
+ label={__("Manual", "wp-component-library")}
54
+ value="manual"
55
+ />
56
+ </ToggleGroupControl>
57
+ </ToolsPanelItem>
58
+ );
59
+ }
60
+
61
+ export { SelectionModeControl };
@@ -0,0 +1,139 @@
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
+
9
+ /**
10
+ * Standalone taxonomy selection control
11
+ *
12
+ * @param {Object} props Component props
13
+ * @param {Object} props.attributes Block attributes
14
+ * @param {Function} props.setAttributes Function to set attributes
15
+ * @param {string} props.postType The post type to get taxonomies for
16
+ * @param {boolean} props.showNoneOption Whether to show a "none" option (default: false)
17
+ * @param {string} props.noneOptionLabel Label for the "none" option (default: "No Taxonomy Filter")
18
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: false)
19
+ * @returns {WPElement} Element to render
20
+ */
21
+ function TaxonomySelect({
22
+ attributes: { selectedTaxonomy },
23
+ setAttributes,
24
+ postType,
25
+ showNoneOption = false,
26
+ noneOptionLabel = "No Taxonomy Filter",
27
+ isShownByDefault = false,
28
+ }) {
29
+ const defaultSelectedTaxonomy = "";
30
+
31
+ // Get all taxonomies (fetch once, cache the result)
32
+ const allTaxonomies = useSelect((select) => {
33
+ return select("core").getTaxonomies({
34
+ per_page: -1,
35
+ });
36
+ }, []);
37
+
38
+ // Memoize the filtered taxonomies to prevent unnecessary re-renders
39
+ const filteredTaxonomies = useMemo(() => {
40
+ if (!postType || !allTaxonomies) {
41
+ return [];
42
+ }
43
+ return allTaxonomies.filter((tax) => tax.types.includes(postType));
44
+ }, [allTaxonomies, postType]);
45
+
46
+ // Auto-select first taxonomy when post type changes (only if showNoneOption is false)
47
+ useEffect(() => {
48
+ if (filteredTaxonomies.length > 0) {
49
+ // Only set if current selection is empty or not in the filtered list
50
+ const isCurrentSelectionValid =
51
+ selectedTaxonomy === "" || // Empty is always valid when showNoneOption is true
52
+ filteredTaxonomies.some((tax) => tax.slug === selectedTaxonomy);
53
+
54
+ if (!isCurrentSelectionValid) {
55
+ // If showNoneOption is true, default to empty, otherwise select first taxonomy
56
+ const defaultValue = showNoneOption
57
+ ? ""
58
+ : filteredTaxonomies[0].slug;
59
+ setAttributes({
60
+ selectedTaxonomy: defaultValue,
61
+ selectedTerms: [], // Clear terms when taxonomy changes
62
+ });
63
+ } else if (
64
+ !showNoneOption &&
65
+ !selectedTaxonomy &&
66
+ filteredTaxonomies.length > 0
67
+ ) {
68
+ // If showNoneOption is false and no selection, select first taxonomy
69
+ setAttributes({
70
+ selectedTaxonomy: filteredTaxonomies[0].slug,
71
+ selectedTerms: [],
72
+ });
73
+ }
74
+ } else if (filteredTaxonomies.length === 0 && selectedTaxonomy) {
75
+ // Clear selection if no taxonomies available
76
+ setAttributes({
77
+ selectedTaxonomy: defaultSelectedTaxonomy,
78
+ selectedTerms: [],
79
+ });
80
+ }
81
+ }, [filteredTaxonomies, selectedTaxonomy, setAttributes, showNoneOption]);
82
+
83
+ // Memoize taxonomy options to prevent recreation
84
+ const taxonomyOptions = useMemo(() => {
85
+ return filteredTaxonomies.map((tax) => ({
86
+ label: tax.labels.singular_name || tax.name,
87
+ value: tax.slug,
88
+ }));
89
+ }, [filteredTaxonomies]);
90
+
91
+ // Memoize all options to prevent recreation
92
+ const allOptions = useMemo(() => {
93
+ return showNoneOption
94
+ ? [{ label: noneOptionLabel, value: "" }, ...taxonomyOptions]
95
+ : taxonomyOptions;
96
+ }, [showNoneOption, noneOptionLabel, taxonomyOptions]);
97
+
98
+ return (
99
+ <ToolsPanelItem
100
+ hasValue={() => selectedTaxonomy !== defaultSelectedTaxonomy}
101
+ label={__("Select Taxonomy", "built")}
102
+ onDeselect={() =>
103
+ setAttributes({
104
+ selectedTaxonomy: defaultSelectedTaxonomy,
105
+ selectedTerms: [],
106
+ })
107
+ }
108
+ isShownByDefault={isShownByDefault}
109
+ >
110
+ <SelectControl
111
+ __nextHasNoMarginBottom={true}
112
+ __next40pxDefaultSize
113
+ label={__("Select Taxonomy", "built")}
114
+ value={selectedTaxonomy}
115
+ options={allOptions}
116
+ onChange={(value) =>
117
+ setAttributes({
118
+ selectedTaxonomy: value,
119
+ selectedTerms: [], // Clear terms when taxonomy changes
120
+ })
121
+ }
122
+ disabled={!postType || filteredTaxonomies.length === 0}
123
+ help={
124
+ // eslint-disable-next-line no-nested-ternary -- render branches.
125
+ !postType
126
+ ? __("Please select a post type first", "built")
127
+ : filteredTaxonomies.length === 0
128
+ ? __(
129
+ "No taxonomies available for this post type",
130
+ "built",
131
+ )
132
+ : undefined
133
+ }
134
+ />
135
+ </ToolsPanelItem>
136
+ );
137
+ }
138
+
139
+ export { TaxonomySelect };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useSelect } from '@wordpress/data';
5
+ import { useMemo } from '@wordpress/element';
6
+
7
+ /**
8
+ * Utility functions for query components
9
+ */
10
+
11
+ /**
12
+ * Reorder an array of items based on a given order of IDs
13
+ *
14
+ * @param {Array} items - Array of objects to reorder
15
+ * @param {Array} orderedIds - Array of IDs in the desired order
16
+ * @param {Function} getItemId - Function to extract ID from an item (default: item => item.id)
17
+ * @returns {Array} Reordered array
18
+ */
19
+ export function reorderByIds(items, orderedIds, getItemId = (item) => item.id) {
20
+ if (!items || !orderedIds || orderedIds.length === 0) {
21
+ return items || [];
22
+ }
23
+
24
+ // Create a map for quick lookup
25
+ const itemMap = new Map();
26
+ items.forEach((item) => {
27
+ itemMap.set(getItemId(item), item);
28
+ });
29
+
30
+ // Build the ordered array
31
+ const ordered = [];
32
+ orderedIds.forEach((id) => {
33
+ const item = itemMap.get(id);
34
+ if (item) {
35
+ ordered.push(item);
36
+ }
37
+ });
38
+
39
+ return ordered;
40
+ }
41
+
42
+ // Shared empty result: useSelect compares results, so a new [] per call would
43
+ // re-render and log "useSelect returns different values".
44
+ const NO_TERMS = [];
45
+
46
+ /**
47
+ * Hook to fetch and reorder terms based on selectedTerms order
48
+ *
49
+ * Example usage:
50
+ * ```js
51
+ * const orderedTerms = useOrderedTerms(selectedTerms, taxonomy);
52
+ * ```
53
+ */
54
+ export function useOrderedTerms(selectedTerms, taxonomy) {
55
+ const { terms } = useSelect(
56
+ (select) => {
57
+ if (!taxonomy || !selectedTerms || selectedTerms.length === 0) {
58
+ return { terms: NO_TERMS };
59
+ }
60
+
61
+ const fetchedTerms = select('core').getEntityRecords('taxonomy', taxonomy, {
62
+ include: selectedTerms,
63
+ per_page: selectedTerms.length,
64
+ });
65
+
66
+ return { terms: fetchedTerms || NO_TERMS };
67
+ },
68
+ [selectedTerms, taxonomy]
69
+ );
70
+
71
+ // Reorder terms to match selectedTerms order
72
+ return useMemo(() => {
73
+ return reorderByIds(terms, selectedTerms);
74
+ }, [terms, selectedTerms]);
75
+ }
@@ -0,0 +1,159 @@
1
+ # Repeater Component
2
+
3
+ A flexible repeater component with drag-and-drop functionality powered by @dnd-kit. This component allows users to add, remove, and reorder items in a list with a smooth dragging experience.
4
+
5
+ ## Features
6
+
7
+ - **Drag and Drop**: Smooth drag-and-drop reordering using @dnd-kit
8
+ - **Keyboard Navigation**: Full keyboard support for accessibility
9
+ - **Flexible Content**: Render any content inside repeater items via renderItem prop
10
+ - **Collapsible Rows**: Optional header summary with expand/collapse (opt-in)
11
+ - **Min/Max Limits**: Configure minimum and maximum number of items
12
+ - **Customizable**: Custom labels, empty states, and styling
13
+ - **Accessible**: ARIA labels and keyboard navigation support
14
+
15
+ ## Usage
16
+
17
+ ```jsx
18
+ import { Repeater } from '@siratik/wp-component-library/components/repeater';
19
+ import { useState } from '@wordpress/element';
20
+
21
+ function MyComponent() {
22
+ const [items, setItems] = useState([
23
+ { id: '1', title: 'First Item', content: 'Some content' },
24
+ { id: '2', title: 'Second Item', content: 'More content' }
25
+ ]);
26
+
27
+ const handleAdd = () => {
28
+ const newItem = {
29
+ id: Date.now().toString(),
30
+ title: '',
31
+ content: ''
32
+ };
33
+ setItems([...items, newItem]);
34
+ };
35
+
36
+ const handleRemove = (id) => {
37
+ setItems(items.filter(item => item.id !== id));
38
+ };
39
+
40
+ const handleReorder = (newItems) => {
41
+ setItems(newItems);
42
+ };
43
+
44
+ const renderItem = (item) => (
45
+ <div>
46
+ <TextControl
47
+ label="Title"
48
+ value={item.title}
49
+ onChange={(title) => updateItem(item.id, { title })}
50
+ />
51
+ <TextareaControl
52
+ label="Content"
53
+ value={item.content}
54
+ onChange={(content) => updateItem(item.id, { content })}
55
+ />
56
+ </div>
57
+ );
58
+
59
+ return (
60
+ <Repeater
61
+ items={items}
62
+ renderItem={renderItem}
63
+ onAdd={handleAdd}
64
+ onRemove={handleRemove}
65
+ onReorder={handleReorder}
66
+ addButtonText="Add New Item"
67
+ emptyStateText="No items yet. Click below to add one."
68
+ maxItems={10}
69
+ minItems={1}
70
+ />
71
+ );
72
+ }
73
+ ```
74
+
75
+ ## Collapsible items
76
+
77
+ Pass `collapsible` to show a header row with an expand/collapse control. Use `renderSummary` for whatever should stay visible when the item is closed (name, enable toggle, badge, etc.). `renderItem` is only mounted while the item is expanded.
78
+
79
+ Existing items start collapsed (`defaultExpanded={false}`). A single newly added item starts open (`expandNewItems`). Drag overlay shows the collapsed header only.
80
+
81
+ When `collapsible` is on, Repeater owns the header chrome (drag handle, chevron, remove). Put interactive header content in `renderSummary` — do not place `DragHandle` or `RemoveButton` there.
82
+
83
+ ```jsx
84
+ <Repeater
85
+ items={items}
86
+ collapsible
87
+ enableReorder={false}
88
+ renderSummary={(item) => (
89
+ <>
90
+ <ToggleControl
91
+ __nextHasNoMarginBottom
92
+ label=""
93
+ checked={item.enabled}
94
+ onChange={(enabled) => updateItem(item.id, { enabled })}
95
+ />
96
+ <span className="built-repeater__summary-name">
97
+ {item.name || "Untitled"}
98
+ </span>
99
+ </>
100
+ )}
101
+ renderItem={(item) => (
102
+ <TextControl
103
+ label="Name"
104
+ value={item.name}
105
+ onChange={(name) => updateItem(item.id, { name })}
106
+ />
107
+ )}
108
+ onAdd={handleAdd}
109
+ onRemove={handleRemove}
110
+ />
111
+ ```
112
+
113
+ ## Props
114
+
115
+ | Prop | Type | Default | Description |
116
+ |------|------|---------|-------------|
117
+ | `items` | `Array` | `[]` | Array of items with unique `id` property |
118
+ | `renderItem` | `Function` | Required | Function to render each item's content. Receives `(item, index)`. When `collapsible` is true, this is the expanded body only. |
119
+ | `onAdd` | `Function` | Required | Callback when add button is clicked |
120
+ | `onRemove` | `Function` | Required | Callback when remove button is clicked |
121
+ | `onReorder` | `Function` | Required | Callback when items are reordered |
122
+ | `className` | `String` | `""` | Additional CSS classes |
123
+ | `addButtonText` | `String` | `"Add Item"` | Text for the add button |
124
+ | `emptyStateText` | `String` | `"No items added yet..."` | Text shown when list is empty |
125
+ | `maxItems` | `Number` | `null` | Maximum number of items allowed |
126
+ | `minItems` | `Number` | `0` | Minimum number of items required |
127
+ | `renderMode` | `String` | `"default"` | `"default"` uses built-in drag/remove chrome. `"integrated"` lets `renderItem` place `DragHandle` and `RemoveButton`. Ignored when `collapsible` is true. |
128
+ | `enableReorder` | `Boolean` | `true` | Whether items can be dragged to reorder. Set to `false` for lists where order doesn't matter (e.g. a set of find/replace pairs) — no drag handle renders, and `onReorder` is not required. |
129
+ | `collapsible` | `Boolean` | `false` | Collapse each item behind a header. Existing callers omit this and keep always-open rows. |
130
+ | `renderSummary` | `Function` | `null` | Header content shown while collapsed (and still shown while expanded). Receives `(item, index)`. |
131
+ | `defaultExpanded` | `Boolean` | `false` | When `collapsible`, existing items start expanded. |
132
+ | `expandNewItems` | `Boolean` | `true` | When `collapsible`, a single newly added item starts expanded. |
133
+
134
+ ## Keyboard Shortcuts
135
+
136
+ - **Tab**: Navigate between items and controls
137
+ - **Space/Enter**: Activate drag handle
138
+ - **Arrow Keys**: Move item up/down when dragging
139
+ - **Escape**: Cancel drag operation
140
+
141
+ ## Styling
142
+
143
+ The component provides these CSS classes for customization:
144
+
145
+ - `.wpcl-repeater--collapsible` - Root class when `collapsible` is true
146
+ - `.wpcl-repeater__item--dragging` - Applied to the placeholder while an item is being dragged
147
+ - `.wpcl-repeater__item--integrated` - Applied in integrated render mode
148
+ - `.wpcl-repeater__item--collapsible` - Collapsible item (header + optional body)
149
+ - `.wpcl-repeater__item--expanded` - Collapsible item that is currently open
150
+ - `.built-repeater__items` - Items container
151
+ - `.built-repeater__item-handle` - Drag handle
152
+ - `.built-repeater__item-header` - Collapsible header row
153
+ - `.built-repeater__item-toggle` - Expand/collapse button
154
+ - `.built-repeater__summary` - Always-visible header content
155
+ - `.built-repeater__summary-name` - Optional truncated label in the header
156
+ - `.built-repeater__item-content` - Item content area
157
+ - `.built-repeater__item-actions` - Action buttons area
158
+ - `.built-repeater__empty-state` - Empty state message
159
+ - `.built-repeater__add-button` - Add button container