@siratik/wp-component-library 0.1.1 → 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 +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 +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 +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,353 @@
1
+ /**
2
+ * GeocoderSearchControl
3
+ *
4
+ * Reusable address/place search input wrapping the shared `geocodeAddress`
5
+ * (Nominatim) helper.
6
+ *
7
+ * Explicit-submit pattern: search fires only when the user presses Enter or
8
+ * clicks the search button — never as-you-type. This is required by the
9
+ * Nominatim usage policy, which forbids autocomplete-style request streams
10
+ * regardless of debounce: see https://operations.osmfoundation.org/policies/nominatim/
11
+ *
12
+ * The search input + suggestions dropdown live in their own
13
+ * `position: relative` container so the dropdown butts directly
14
+ * against the bottom of the input. Attribution sits in a sibling
15
+ * below that container, in normal flow, and is overlaid by the
16
+ * dropdown when results are visible.
17
+ *
18
+ * @package Siratik/WP-Component-Library
19
+ */
20
+
21
+ import { Button, SearchControl } from "@wordpress/components";
22
+ import {
23
+ useCallback,
24
+ useEffect,
25
+ useRef,
26
+ useState,
27
+ } from "@wordpress/element";
28
+ import { __ } from "@wordpress/i18n";
29
+ import { search as searchIcon } from "@wordpress/icons";
30
+
31
+ import {
32
+ geocodeAddress,
33
+ GEOCODER_ATTRIBUTION_HTML,
34
+ } from "./geocoder";
35
+
36
+ /**
37
+ * @param {Object} props
38
+ * @param {(result:GeocodeResult)=>void} props.onSelect Called when the user picks a suggestion.
39
+ * @param {string} [props.id] Stable id for the underlying SearchControl.
40
+ * @param {string} [props.placeholder]
41
+ * @param {number} [props.limit=8] Max suggestions returned.
42
+ * @param {string[]} [props.countryCodes] ISO 3166-1 alpha-2 list to bias results to.
43
+ * @param {number} [props.minLength=3]
44
+ * @param {boolean} [props.clearOnSelect=true]
45
+ * @param {boolean} [props.showAttribution=true]
46
+ */
47
+ export function GeocoderSearchControl({
48
+ onSelect,
49
+ id = "geocoder-search",
50
+ placeholder = __("Search address, city, or ZIP — then press Enter", "wp-component-library"),
51
+ limit = 8,
52
+ countryCodes,
53
+ minLength = 3,
54
+ clearOnSelect = true,
55
+ showAttribution = true,
56
+ }) {
57
+ const [value, setValue] = useState("");
58
+ const [suggestions, setSuggestions] = useState([]);
59
+ const [showSuggestions, setShowSuggestions] = useState(false);
60
+ const [hasSearched, setHasSearched] = useState(false);
61
+ const [isSearching, setIsSearching] = useState(false);
62
+ const [selectedIndex, setSelectedIndex] = useState(-1);
63
+
64
+ const containerRef = useRef(null);
65
+ const abortRef = useRef(null);
66
+
67
+ const runSearch = useCallback(
68
+ async (query) => {
69
+ const trimmed = (query || "").trim();
70
+ if (trimmed.length < minLength) {
71
+ setSuggestions([]);
72
+ setShowSuggestions(false);
73
+ setHasSearched(false);
74
+ return;
75
+ }
76
+
77
+ // User mashed Enter twice — cancel the prior request so the
78
+ // later one always wins.
79
+ if (abortRef.current) {
80
+ abortRef.current.abort();
81
+ }
82
+ const controller = new AbortController();
83
+ abortRef.current = controller;
84
+
85
+ setIsSearching(true);
86
+ setHasSearched(true);
87
+ try {
88
+ const results = await geocodeAddress({
89
+ query: trimmed,
90
+ limit,
91
+ countryCodes,
92
+ signal: controller.signal,
93
+ });
94
+ if (controller.signal.aborted) {
95
+ return;
96
+ }
97
+ setSuggestions(results);
98
+ setShowSuggestions(true);
99
+ setSelectedIndex(-1);
100
+ } finally {
101
+ if (abortRef.current === controller) {
102
+ abortRef.current = null;
103
+ setIsSearching(false);
104
+ }
105
+ }
106
+ },
107
+ [limit, countryCodes, minLength],
108
+ );
109
+
110
+ // Typing only updates local state. Editing after a search clears the
111
+ // stale dropdown so it's obvious the user needs to re-submit.
112
+ const handleChange = useCallback((next) => {
113
+ setValue(next);
114
+ setShowSuggestions(false);
115
+ setSuggestions([]);
116
+ setSelectedIndex(-1);
117
+ setHasSearched(false);
118
+ }, []);
119
+
120
+ const handleSelect = useCallback(
121
+ (suggestion) => {
122
+ onSelect?.(suggestion);
123
+ if (clearOnSelect) {
124
+ setValue("");
125
+ }
126
+ setSuggestions([]);
127
+ setShowSuggestions(false);
128
+ setSelectedIndex(-1);
129
+ setHasSearched(false);
130
+ },
131
+ [onSelect, clearOnSelect],
132
+ );
133
+
134
+ const handleKeyDown = useCallback(
135
+ (e) => {
136
+ // Enter picks the highlighted suggestion when the dropdown is
137
+ // open; otherwise it submits the search.
138
+ if (e.key === "Enter") {
139
+ e.preventDefault();
140
+ if (
141
+ showSuggestions &&
142
+ selectedIndex >= 0 &&
143
+ suggestions[selectedIndex]
144
+ ) {
145
+ handleSelect(suggestions[selectedIndex]);
146
+ return;
147
+ }
148
+ runSearch(value);
149
+ return;
150
+ }
151
+
152
+ if (e.key === "Escape") {
153
+ setShowSuggestions(false);
154
+ setSelectedIndex(-1);
155
+ return;
156
+ }
157
+
158
+ if (!showSuggestions || suggestions.length === 0) {
159
+ return;
160
+ }
161
+
162
+ if (e.key === "ArrowDown") {
163
+ e.preventDefault();
164
+ setSelectedIndex((prev) =>
165
+ prev < suggestions.length - 1 ? prev + 1 : prev,
166
+ );
167
+ } else if (e.key === "ArrowUp") {
168
+ e.preventDefault();
169
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
170
+ }
171
+ },
172
+ [
173
+ showSuggestions,
174
+ suggestions,
175
+ selectedIndex,
176
+ handleSelect,
177
+ runSearch,
178
+ value,
179
+ ],
180
+ );
181
+
182
+ // Click-outside to dismiss.
183
+ useEffect(() => {
184
+ if (!showSuggestions) {
185
+ return undefined;
186
+ }
187
+ const handler = (event) => {
188
+ if (
189
+ containerRef.current &&
190
+ !containerRef.current.contains(event.target)
191
+ ) {
192
+ setShowSuggestions(false);
193
+ setSelectedIndex(-1);
194
+ }
195
+ };
196
+ document.addEventListener("mousedown", handler);
197
+ return () => document.removeEventListener("mousedown", handler);
198
+ }, [showSuggestions]);
199
+
200
+ // Cancel any in-flight request on unmount so React doesn't try to
201
+ // set state on a torn-down component.
202
+ useEffect(() => {
203
+ return () => {
204
+ if (abortRef.current) {
205
+ abortRef.current.abort();
206
+ }
207
+ };
208
+ }, []);
209
+
210
+ const canSearch = value.trim().length >= minLength && !isSearching;
211
+
212
+ const showNoResults =
213
+ showSuggestions &&
214
+ !isSearching &&
215
+ hasSearched &&
216
+ suggestions.length === 0 &&
217
+ value.trim().length >= minLength;
218
+
219
+ return (
220
+ <div ref={containerRef} className="geocoder-search">
221
+ {/* Positioning context for the dropdown only — keeps the
222
+ suggestions visually flush with the bottom of the input. */}
223
+ <div style={{ position: "relative" }}>
224
+ <div style={{ display: "flex", gap: "6px", alignItems: "stretch" }}>
225
+ <div style={{ flex: 1, minWidth: 0 }}>
226
+ <SearchControl
227
+ __nextHasNoMarginBottom
228
+ id={id}
229
+ value={value}
230
+ onChange={handleChange}
231
+ onKeyDown={handleKeyDown}
232
+ placeholder={placeholder}
233
+ autoComplete="off"
234
+ />
235
+ </div>
236
+ <Button
237
+ __next40pxDefaultSize
238
+ variant="secondary"
239
+ icon={searchIcon}
240
+ label={__("Search", "wp-component-library")}
241
+ showTooltip
242
+ disabled={!canSearch}
243
+ onClick={() => runSearch(value)}
244
+ />
245
+ </div>
246
+
247
+ {isSearching && (
248
+ <DropdownPanel>
249
+ <div
250
+ style={{
251
+ padding: "8px 12px",
252
+ fontSize: "13px",
253
+ color: "#646970",
254
+ }}
255
+ >
256
+ {__("Searching…", "wp-component-library")}
257
+ </div>
258
+ </DropdownPanel>
259
+ )}
260
+
261
+ {!isSearching && showSuggestions && suggestions.length > 0 && (
262
+ <DropdownPanel>
263
+ {suggestions.map((s, idx) => (
264
+ <button
265
+ type="button"
266
+ key={s.id}
267
+ onMouseEnter={() => setSelectedIndex(idx)}
268
+ onClick={() => handleSelect(s)}
269
+ style={{
270
+ display: "block",
271
+ width: "100%",
272
+ textAlign: "left",
273
+ padding: "8px 12px",
274
+ fontSize: "13px",
275
+ border: "none",
276
+ borderBottom:
277
+ idx < suggestions.length - 1
278
+ ? "1px solid #f0f0f1"
279
+ : "none",
280
+ background:
281
+ idx === selectedIndex
282
+ ? "#f0f0f1"
283
+ : "#fff",
284
+ cursor: "pointer",
285
+ }}
286
+ >
287
+ {s.displayName}
288
+ </button>
289
+ ))}
290
+ </DropdownPanel>
291
+ )}
292
+
293
+ {showNoResults && (
294
+ <DropdownPanel>
295
+ <div
296
+ style={{
297
+ padding: "8px 12px",
298
+ fontSize: "13px",
299
+ color: "#646970",
300
+ fontStyle: "italic",
301
+ }}
302
+ >
303
+ {__("No matches found.", "wp-component-library")}
304
+ </div>
305
+ </DropdownPanel>
306
+ )}
307
+ </div>
308
+
309
+ {showAttribution && (
310
+ <div
311
+ style={{
312
+ marginTop: "4px",
313
+ fontSize: "11px",
314
+ color: "#757575",
315
+ lineHeight: 1.4,
316
+ }}
317
+ dangerouslySetInnerHTML={{
318
+ __html: GEOCODER_ATTRIBUTION_HTML,
319
+ }}
320
+ />
321
+ )}
322
+ </div>
323
+ );
324
+ }
325
+
326
+ /**
327
+ * Absolute-positioned panel that hangs directly below the search input.
328
+ */
329
+ function DropdownPanel({ children }) {
330
+ return (
331
+ <div
332
+ style={{
333
+ position: "absolute",
334
+ top: "100%",
335
+ left: 0,
336
+ right: 0,
337
+ marginTop: "-1px",
338
+ background: "#fff",
339
+ border: "1px solid #ddd",
340
+ borderTop: "none",
341
+ borderRadius: "0 0 2px 2px",
342
+ maxHeight: "260px",
343
+ overflowY: "auto",
344
+ zIndex: 1000,
345
+ boxShadow: "0 2px 6px rgba(0,0,0,0.08)",
346
+ }}
347
+ >
348
+ {children}
349
+ </div>
350
+ );
351
+ }
352
+
353
+ export default GeocoderSearchControl;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Country options for the address fields. Codes match Siratik\Utility\Utilities\CountryList;
3
+ * names come from the browser (Intl.DisplayNames) so they follow the admin's language.
4
+ *
5
+ * @package Siratik/WP-Component-Library
6
+ */
7
+
8
+ const COUNTRY_CODES = [
9
+ "AF", "AL", "DZ", "AD", "AO", "AG", "AR", "AM", "AU", "AT", "AZ", "BS", "BH", "BD", "BB", "BY",
10
+ "BE", "BZ", "BJ", "BT", "BO", "BA", "BW", "BR", "BN", "BG", "BF", "BI", "CV", "KH", "CM", "CA",
11
+ "CF", "TD", "CL", "CN", "CO", "KM", "CD", "CG", "CR", "HR", "CU", "CY", "CZ", "DK", "DJ", "DM",
12
+ "DO", "EC", "EG", "SV", "GQ", "ER", "EE", "SZ", "ET", "FJ", "FI", "FR", "GA", "GM", "GE", "DE",
13
+ "GH", "GR", "GD", "GT", "GN", "GW", "GY", "HT", "HN", "HU", "IS", "IN", "ID", "IR", "IQ", "IE",
14
+ "IL", "IT", "JM", "JP", "JO", "KZ", "KE", "KI", "KP", "KR", "XK", "KW", "KG", "LA", "LV", "LB",
15
+ "LS", "LR", "LY", "LI", "LT", "LU", "MG", "MW", "MY", "MV", "ML", "MT", "MH", "MR", "MU", "MX",
16
+ "FM", "MD", "MC", "MN", "ME", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "NZ", "NI", "NE", "NG",
17
+ "MK", "NO", "OM", "PK", "PW", "PS", "PA", "PG", "PY", "PE", "PH", "PL", "PT", "QA", "RO", "RU",
18
+ "RW", "KN", "LC", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SK", "SI", "SB",
19
+ "SO", "ZA", "SS", "ES", "LK", "SD", "SR", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", "TG",
20
+ "TO", "TT", "TN", "TR", "TM", "TV", "UG", "UA", "AE", "GB", "US", "UY", "UZ", "VU", "VA", "VE",
21
+ "VN", "YE", "ZM", "ZW",
22
+ ];
23
+
24
+ /**
25
+ * Display name for a country code, falling back to the code itself.
26
+ *
27
+ * @param {Intl.DisplayNames|null} names
28
+ * @param {string} code
29
+ * @returns {string}
30
+ */
31
+ function countryName(names, code) {
32
+ try {
33
+ return names?.of(code) || code;
34
+ } catch {
35
+ return code;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * SelectControl options sorted by name. A saved code that isn't in the list
41
+ * (e.g. a territory a search returned) is kept as an option so it isn't lost.
42
+ *
43
+ * @param {string} [currentCode] The saved country code.
44
+ * @returns {{label:string,value:string}[]}
45
+ */
46
+ export function getCountryOptions(currentCode = "") {
47
+ let names = null;
48
+ try {
49
+ names = new Intl.DisplayNames(
50
+ [document.documentElement.lang || "en"],
51
+ { type: "region" },
52
+ );
53
+ } catch {
54
+ names = null;
55
+ }
56
+
57
+ const codes = [...COUNTRY_CODES];
58
+ const current = String(currentCode || "").toUpperCase();
59
+ if (/^[A-Z]{2}$/.test(current) && !codes.includes(current)) {
60
+ codes.push(current);
61
+ }
62
+
63
+ return codes
64
+ .map((code) => ({ value: code, label: countryName(names, code) }))
65
+ .sort((a, b) => a.label.localeCompare(b.label));
66
+ }