@siratik/wp-component-library 0.1.1 → 0.1.3

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 +43 -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 +186 -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 +1 -1
  90. package/editor.js +16 -0
  91. package/index.js +127 -0
  92. package/package.json +21 -13
  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 -1242
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Icon Registry Store
3
+ *
4
+ * WordPress data store for managing icon sets and icons.
5
+ * Replaces the 10up `tenup/icons` store with the `wpcl/icons` store.
6
+ *
7
+ * Filters available via @wordpress/hooks:
8
+ * - wpcl.icons.iconSet — modify an icon set on registration
9
+ * - wpcl.icons.iconSets — modify the full sorted list of icon sets
10
+ * - wpcl.icons.icon — filter individual icons (return false to exclude)
11
+ *
12
+ * Icon sets may register with a lazy `load` function via registerIconSet() in api.js.
13
+ * Until loaded, placeholder icons (e.g. essential defaults) remain available.
14
+ */
15
+
16
+ import { createReduxStore, register, select } from "@wordpress/data";
17
+ import { applyFilters } from "@wordpress/hooks";
18
+
19
+ export const STORE_NAME = "wpcl/icons";
20
+
21
+ /** @typedef {'idle'|'loading'|'loaded'|'error'} IconSetLoadStatus */
22
+
23
+ const DEFAULT_STATE = {
24
+ iconSets: {},
25
+ };
26
+
27
+ const storeSelectors = {
28
+ /**
29
+ * Get all icon sets, sorted by priority descending (highest first).
30
+ * Applies the wpcl.icons.iconSets filter.
31
+ */
32
+ getIconSets(state) {
33
+ const sets = Object.values(state.iconSets);
34
+
35
+ const sorted = [...sets].sort((a, b) => {
36
+ const pa = a.priority ?? 0;
37
+ const pb = b.priority ?? 0;
38
+ if (pb !== pa) { return pb - pa; }
39
+ return a.name.localeCompare(b.name);
40
+ });
41
+
42
+ return applyFilters("wpcl.icons.iconSets", sorted);
43
+ },
44
+
45
+ /**
46
+ * The registered sets keyed by name, unfiltered and unsorted. The same
47
+ * object until a set changes, so hooks can memoize on it.
48
+ */
49
+ getIconSetsMap(state) {
50
+ return state.iconSets;
51
+ },
52
+
53
+ /**
54
+ * Get a specific icon set by name.
55
+ */
56
+ getIconSet(state, name) {
57
+ return state.iconSets[name] ?? null;
58
+ },
59
+
60
+ /**
61
+ * True when any registered set is still loading.
62
+ */
63
+ isIconSetsLoading(state) {
64
+ return Object.values(state.iconSets).some(
65
+ (set) => set.loadStatus === "loading",
66
+ );
67
+ },
68
+
69
+ /**
70
+ * True when any set is registered but not yet loaded (idle or loading).
71
+ */
72
+ hasPendingIconSets(state) {
73
+ return Object.values(state.iconSets).some(
74
+ (set) => set.loadStatus === "idle" || set.loadStatus === "loading",
75
+ );
76
+ },
77
+
78
+ /**
79
+ * Get all icons for a given set, with per-icon wpcl.icons.icon filter applied.
80
+ */
81
+ getIcons(state, name) {
82
+ const set = state.iconSets[name];
83
+ if (!set) { return []; }
84
+
85
+ return set.icons.reduce((acc, icon) => {
86
+ const filtered = applyFilters("wpcl.icons.icon", icon, set);
87
+ if (filtered !== false) {
88
+ acc.push(filtered);
89
+ }
90
+ return acc;
91
+ }, []);
92
+ },
93
+
94
+ /**
95
+ * Get a single icon by set + name.
96
+ */
97
+ getIcon(state, iconSetName, iconName) {
98
+ const set = state.iconSets[iconSetName];
99
+ if (!set) { return undefined; }
100
+ return set.icons.find((icon) => icon.name === iconName);
101
+ },
102
+ };
103
+
104
+ const storeActions = {
105
+ registerIconSet(iconSet) {
106
+ return { type: "REGISTER_ICON_SET", iconSet };
107
+ },
108
+ setIconSetLoading(name) {
109
+ return { type: "SET_ICON_SET_LOADING", name };
110
+ },
111
+ setIconSetIcons(name, icons) {
112
+ return { type: "SET_ICON_SET_ICONS", name, icons };
113
+ },
114
+ setIconSetError(name, message) {
115
+ return { type: "SET_ICON_SET_ERROR", name, message };
116
+ },
117
+ removeIconSet(name) {
118
+ return { type: "REMOVE_ICON_SET", name };
119
+ },
120
+ };
121
+
122
+ function reducer(state = DEFAULT_STATE, action) {
123
+ switch (action.type) {
124
+ case "REGISTER_ICON_SET": {
125
+ const iconSet = applyFilters(
126
+ "wpcl.icons.iconSet",
127
+ action.iconSet,
128
+ );
129
+ const previous = state.iconSets[iconSet.name];
130
+ const icons =
131
+ iconSet.icons ??
132
+ (previous?.icons?.length ? previous.icons : []);
133
+
134
+ return {
135
+ ...state,
136
+ iconSets: {
137
+ ...state.iconSets,
138
+ [iconSet.name]: {
139
+ ...iconSet,
140
+ icons,
141
+ loadStatus: iconSet.loadStatus ?? "loaded",
142
+ loadError: null,
143
+ },
144
+ },
145
+ };
146
+ }
147
+
148
+ case "SET_ICON_SET_LOADING": {
149
+ const set = state.iconSets[action.name];
150
+ if (!set) { return state; }
151
+ return {
152
+ ...state,
153
+ iconSets: {
154
+ ...state.iconSets,
155
+ [action.name]: {
156
+ ...set,
157
+ loadStatus: "loading",
158
+ loadError: null,
159
+ },
160
+ },
161
+ };
162
+ }
163
+
164
+ case "SET_ICON_SET_ICONS": {
165
+ const set = state.iconSets[action.name];
166
+ if (!set) { return state; }
167
+ return {
168
+ ...state,
169
+ iconSets: {
170
+ ...state.iconSets,
171
+ [action.name]: {
172
+ ...set,
173
+ icons: action.icons,
174
+ loadStatus: "loaded",
175
+ loadError: null,
176
+ },
177
+ },
178
+ };
179
+ }
180
+
181
+ case "SET_ICON_SET_ERROR": {
182
+ const set = state.iconSets[action.name];
183
+ if (!set) { return state; }
184
+ return {
185
+ ...state,
186
+ iconSets: {
187
+ ...state.iconSets,
188
+ [action.name]: {
189
+ ...set,
190
+ loadStatus: "error",
191
+ loadError: action.message,
192
+ },
193
+ },
194
+ };
195
+ }
196
+
197
+ case "REMOVE_ICON_SET": {
198
+ if (!state.iconSets[action.name]) { return state; }
199
+ const next = {
200
+ ...state,
201
+ iconSets: { ...state.iconSets },
202
+ };
203
+ delete next.iconSets[action.name];
204
+ return next;
205
+ }
206
+
207
+ default:
208
+ return state;
209
+ }
210
+ }
211
+
212
+ const iconStore = createReduxStore(STORE_NAME, {
213
+ reducer,
214
+ selectors: storeSelectors,
215
+ actions: storeActions,
216
+ });
217
+
218
+ // Register only once (safe to call multiple times)
219
+ if (!select(STORE_NAME)) {
220
+ register(iconStore);
221
+ }
222
+
223
+ export { iconStore };
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Emotion styled components for the Icon Picker.
3
+ */
4
+ import { css } from "@emotion/react";
5
+ import styled from "@emotion/styled";
6
+ import { Button } from "@wordpress/components";
7
+
8
+ /** Supports Phosphor (fill) and Lucide (stroke) SVG sources. */
9
+ const iconSvgColorStyles = css`
10
+ svg[fill="none"] {
11
+ fill: none;
12
+ stroke: currentColor;
13
+ }
14
+
15
+ svg:not([fill="none"]) {
16
+ fill: currentColor;
17
+ }
18
+ `;
19
+
20
+ // ─── Modal (matches core's icon block picker, WP 7.1) ─────────────────────────
21
+ // Values copy core's `.wp-block-icon__inserter-*` rules (block-library
22
+ // editor.css) and the vertical tabs of @wordpress/ui, which core bundles
23
+ // privately.
24
+
25
+ const WIDE = "@media (min-width: 782px)";
26
+
27
+ /** Sidebar: search + collection tabs. Fixed beside the grid on wide screens. */
28
+ export const PickerSidebar = styled.div`
29
+ display: flex;
30
+ flex-direction: column;
31
+ gap: 16px;
32
+ padding-bottom: 24px;
33
+
34
+ ${WIDE} {
35
+ overflow-x: visible;
36
+ overflow-y: auto;
37
+ position: absolute;
38
+ top: 72px;
39
+ left: 0;
40
+ bottom: 0;
41
+ width: 280px;
42
+ box-sizing: border-box;
43
+ padding-top: 24px;
44
+ padding-inline: 24px;
45
+ }
46
+ `;
47
+
48
+ export const TabList = styled.div`
49
+ display: flex;
50
+ flex-direction: column;
51
+ align-items: stretch;
52
+ `;
53
+
54
+ export const Tab = styled.button`
55
+ display: flex;
56
+ align-items: center;
57
+ width: 100%;
58
+ min-height: 40px;
59
+ padding: 8px 12px;
60
+ margin: 0;
61
+ border: none;
62
+ border-radius: 2px;
63
+ background: transparent;
64
+ box-shadow: none;
65
+ color: #1e1e1e;
66
+ font-family: inherit;
67
+ font-size: 13px;
68
+ font-weight: 400;
69
+ line-height: 1.2;
70
+ text-align: start;
71
+ white-space: nowrap;
72
+ cursor: pointer;
73
+ outline: none;
74
+
75
+ &[aria-selected="true"] {
76
+ background-color: #ededed;
77
+ }
78
+
79
+ &:focus-visible {
80
+ box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus, 2px)
81
+ var(--wp-admin-theme-color, #3858e9);
82
+ }
83
+
84
+ .wpcl-icon-picker__tab-label {
85
+ flex-grow: 1;
86
+ overflow: hidden;
87
+ text-overflow: ellipsis;
88
+ }
89
+
90
+ svg {
91
+ flex-shrink: 0;
92
+ margin-inline-end: -4px;
93
+ opacity: 0;
94
+ fill: currentColor;
95
+ }
96
+
97
+ &[aria-selected="true"] svg,
98
+ &:hover svg,
99
+ &:focus-visible svg {
100
+ opacity: 1;
101
+ }
102
+
103
+ &:dir(rtl) svg {
104
+ rotate: 180deg;
105
+ }
106
+
107
+ @media (forced-colors: active) {
108
+ &[aria-selected="true"] {
109
+ outline: 1px solid CanvasText;
110
+ }
111
+ }
112
+ `;
113
+
114
+ /** The current tab's icons. */
115
+ export const PickerPanel = styled.div`
116
+ outline: none;
117
+
118
+ ${WIDE} {
119
+ margin-inline-start: 280px;
120
+ }
121
+ `;
122
+
123
+ export const PickerLoading = styled.div`
124
+ display: flex;
125
+ justify-content: center;
126
+ padding: 24px;
127
+ `;
128
+
129
+ export const PickerNoResults = styled.div`
130
+ display: flex;
131
+ justify-content: center;
132
+ `;
133
+
134
+ export const IconGridList = styled.div`
135
+ display: grid;
136
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
137
+ `;
138
+
139
+ /** A components Button, so it keeps Button's hover, focus and primary colors. */
140
+ export const IconGridItem = styled(Button)`
141
+ &.components-button {
142
+ display: flex;
143
+ flex-direction: column;
144
+ height: auto !important;
145
+ }
146
+ `;
147
+
148
+ export const IconGridItemIcon = styled.span`
149
+ padding: 12px;
150
+
151
+ svg {
152
+ display: block;
153
+ width: 24px;
154
+ height: 24px;
155
+ }
156
+
157
+ ${iconSvgColorStyles}
158
+ `;
159
+
160
+ export const IconGridItemTitle = styled.span`
161
+ font-size: 12px;
162
+ padding: 4px 4px 8px;
163
+ overflow-wrap: break-word;
164
+ `;
165
+
166
+ // ─── Inspector panel picker ───────────────────────────────────────────────────
167
+
168
+ export const PickerPreview = styled.span`
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ width: 20px;
173
+ height: 20px;
174
+ overflow: hidden;
175
+ flex-shrink: 0;
176
+ color: inherit;
177
+
178
+ svg {
179
+ width: 20px;
180
+ height: 20px;
181
+ display: block;
182
+ }
183
+
184
+ ${iconSvgColorStyles}
185
+ `;
186
+
187
+ export const PickerName = styled.span`
188
+ overflow: hidden;
189
+ text-overflow: ellipsis;
190
+ white-space: nowrap;
191
+ `;
192
+
193
+ // ─── Toolbar picker ───────────────────────────────────────────────────────────
194
+
195
+ export const ToolbarIconWrap = styled.span`
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ width: 24px;
200
+ height: 24px;
201
+ overflow: hidden;
202
+ flex-shrink: 0;
203
+
204
+ /* Match wp-blocks toolbar rules; overrides SVG width/height attributes. */
205
+ svg {
206
+ display: block;
207
+ width: 24px !important;
208
+ height: 24px !important;
209
+ min-width: 0;
210
+ min-height: 0;
211
+ max-width: 24px;
212
+ max-height: 24px;
213
+ }
214
+
215
+ ${iconSvgColorStyles}
216
+ `;
217
+
218
+ // ─── Inline (canvas) picker ───────────────────────────────────────────────────
219
+ // div[role="button"] avoids UA button color which would break currentColor.
220
+
221
+ export const InlinePickerWrap = styled.div`
222
+ display: inline-flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ cursor: pointer;
226
+
227
+ svg {
228
+ display: block;
229
+ }
230
+
231
+ ${iconSvgColorStyles}
232
+ `;
@@ -0,0 +1,138 @@
1
+ import DOMPurify from "dompurify";
2
+
3
+ /**
4
+ * Shown when no icon is set: the kitaris-essential "star" icon
5
+ * (copied from wp-blocks src/util/essential-icons.js).
6
+ */
7
+ export const PLACEHOLDER_SVG =
8
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z"/></svg>';
9
+
10
+ /**
11
+ * Normalize and sanitize an SVG source string for safe innerHTML rendering.
12
+ *
13
+ * Strips leading HTML comments (e.g. Lucide license headers), extracts
14
+ * the root <svg> element, then sanitizes with DOMPurify to remove event
15
+ * handlers (onload, onerror, etc.), <script> tags, and javascript: hrefs.
16
+ *
17
+ * Icons never need these, so they are removed too:
18
+ * - <style> elements (inline SVG styles apply to the whole page) and
19
+ * <image> elements (they load remote files).
20
+ * - style attributes that could load a resource (url(), image(), @, or CSS
21
+ * escapes that could hide either).
22
+ * - href / xlink:href values that are not same-document "#" references.
23
+ *
24
+ * @param {string} source
25
+ * @returns {string}
26
+ */
27
+ export function safeSvgSource(source) {
28
+ if (typeof source !== "string") {
29
+ return "";
30
+ }
31
+
32
+ let trimmed = source.trim().replace(/^(\s*<!--[\s\S]*?-->\s*)+/, "").trim();
33
+
34
+ if (!trimmed.startsWith("<svg")) {
35
+ const match = trimmed.match(/<svg[\s\S]*<\/svg>/i);
36
+ trimmed = match ? match[0] : "";
37
+ }
38
+
39
+ if (!trimmed) {
40
+ return "";
41
+ }
42
+
43
+ const purifier = getIconPurifier();
44
+ if (!purifier) {
45
+ // No DOM to sanitize with, so render nothing rather than raw markup.
46
+ return "";
47
+ }
48
+
49
+ return purifier.sanitize(trimmed, {
50
+ USE_PROFILES: { svg: true, svgFilters: true },
51
+ FORBID_TAGS: ["style", "image"],
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Style values that could fetch a resource, or hide one behind CSS escapes.
57
+ */
58
+ const UNSAFE_STYLE_PATTERN = /url\s*\(|image|@|\\|expression/i;
59
+
60
+ let iconPurifier = null;
61
+
62
+ /**
63
+ * A DOMPurify instance of our own, so the attribute hook below never
64
+ * affects other DOMPurify users on the page.
65
+ *
66
+ * @returns {Object|null} DOMPurify instance, or null without a DOM.
67
+ */
68
+ function getIconPurifier() {
69
+ if (iconPurifier) {
70
+ return iconPurifier;
71
+ }
72
+
73
+ if (typeof window === "undefined") {
74
+ return null;
75
+ }
76
+
77
+ iconPurifier = DOMPurify(window);
78
+
79
+ iconPurifier.addHook("afterSanitizeAttributes", (node) => {
80
+ if (!node.hasAttribute) {
81
+ return;
82
+ }
83
+
84
+ ["href", "xlink:href"].forEach((name) => {
85
+ const value = node.getAttribute(name);
86
+ if (value !== null && !value.trim().startsWith("#")) {
87
+ node.removeAttribute(name);
88
+ }
89
+ });
90
+
91
+ const style = node.getAttribute("style");
92
+ if (style !== null && UNSAFE_STYLE_PATTERN.test(style)) {
93
+ node.removeAttribute("style");
94
+ }
95
+ });
96
+
97
+ return iconPurifier;
98
+ }
99
+
100
+ /** The "All" tab's value. */
101
+ export const ALL_TAB = "";
102
+
103
+ /**
104
+ * Whether a set is the one a saved icon belongs to: by name, or by one of
105
+ * the set's `aliases` (older set names, e.g. a theme set a core collection
106
+ * replaced).
107
+ *
108
+ * @param {Object} group
109
+ * @param {string} iconSet
110
+ * @returns {boolean}
111
+ */
112
+ export function isSetOf(group, iconSet) {
113
+ if (!iconSet) {
114
+ return false;
115
+ }
116
+ return (
117
+ group.name === iconSet ||
118
+ (Array.isArray(group.aliases) && group.aliases.includes(iconSet))
119
+ );
120
+ }
121
+
122
+ /**
123
+ * The tab the picker opens on: the selected icon's set, like core's Icon
124
+ * block, so the icon shows highlighted in a short list; "All" when no icon is
125
+ * selected (or its set is gone), so every icon is offered first.
126
+ *
127
+ * @param {Array} groups Icon sets ({ name, aliases, icons }).
128
+ * @param {string} [valueSet] The selected icon's set.
129
+ * @param {Function} isSelectedIcon Whether a grid icon is the selected one
130
+ * (for values without a set).
131
+ * @returns {string} A set name, or ALL_TAB.
132
+ */
133
+ export function openingTab(groups, valueSet, isSelectedIcon) {
134
+ const group = groups.find((item) =>
135
+ valueSet ? isSetOf(item, valueSet) : item.icons.some(isSelectedIcon),
136
+ );
137
+ return group?.name ?? ALL_TAB;
138
+ }