@valu/block-components 0.1.0

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.
@@ -0,0 +1,2026 @@
1
+ import { forwardRef, useId, useState, useCallback, useRef, useMemo, useEffect } from 'react';
2
+ import { QueryClientProvider, QueryClient, useInfiniteQuery, keepPreviousData } from '@tanstack/react-query';
3
+ import { VisuallyHidden, Icon, Popover, Spinner, Button } from '@wordpress/components';
4
+ import { __, sprintf, _n } from '@wordpress/i18n';
5
+ import { search, closeSmall, dragHandle, caution, chevronUp, chevronDown, people } from '@wordpress/icons';
6
+ import styled from '@emotion/styled';
7
+ import { css, CacheProvider } from '@emotion/react';
8
+ import { v4 } from 'uuid';
9
+ import { decodeEntities } from '@wordpress/html-entities';
10
+ import { filterURLForDisplay, safeDecodeURI, addQueryArgs } from '@wordpress/url';
11
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
12
+ import { useSelect } from '@wordpress/data';
13
+ import apiFetch from '@wordpress/api-fetch';
14
+ import { speak } from '@wordpress/a11y';
15
+ import createCache from '@emotion/cache';
16
+ import { useSensors, useSensor, MouseSensor, TouchSensor, KeyboardSensor, DndContext, closestCenter, DragOverlay, defaultDropAnimation } from '@dnd-kit/core';
17
+ import { sortableKeyboardCoordinates, arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
18
+ import { CSS } from '@dnd-kit/utilities';
19
+ import { store } from '@wordpress/core-data';
20
+
21
+ // components/content-picker/ContentPicker.tsx
22
+
23
+ // components/content-picker/styles/tokens.ts
24
+ var tokens = {
25
+ accent: "var(--wp-admin-theme-color, #3858e9)",
26
+ accentHover: "color-mix(in srgb, var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)) 4%, transparent)",
27
+ accentActive: "color-mix(in srgb, var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)) 8%, transparent)",
28
+ bg: "var(--wp-components-color-background, #ffffff)",
29
+ fg: "var(--wp-components-color-foreground, #1e1e1e)",
30
+ gray100: "var(--wp-components-color-gray-100, #f0f0f0)",
31
+ gray200: "var(--wp-components-color-gray-200, #ebebeb)",
32
+ gray300: "var(--wp-components-color-gray-300, #dcdcde)",
33
+ gray600: "var(--wp-components-color-gray-600, #757575)",
34
+ gray900: "var(--wp-components-color-gray-900, #1e1e1e)",
35
+ danger: "#cc1818",
36
+ dangerSoft: "#fef7f7",
37
+ dangerLine: "#f0b7b7",
38
+ success: "#007017",
39
+ warning: "#bd8600",
40
+ radiusSm: "2px",
41
+ radiusMd: "4px",
42
+ rowMinHeight: "40px",
43
+ suggestionMinHeight: "44px",
44
+ thumbSm: "24px",
45
+ thumbMd: "32px",
46
+ thumbLg: "40px",
47
+ shadowPopover: "0 6px 24px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.06)",
48
+ spaceXs: "4px",
49
+ spaceSm: "8px",
50
+ spaceMd: "12px",
51
+ spaceLg: "16px",
52
+ transitionFast: "120ms ease-out",
53
+ fontSizeBody: "13px",
54
+ fontSizeSmall: "11px",
55
+ fontWeightMedium: 500,
56
+ fontWeightBold: 600
57
+ };
58
+
59
+ // components/content-picker/styles/index.ts
60
+ var focusRing = css`
61
+ outline: 2px solid ${tokens.accent};
62
+ outline-offset: 1px;
63
+ `;
64
+ css`
65
+ position: absolute !important;
66
+ clip: rect(1px, 1px, 1px, 1px);
67
+ width: 1px;
68
+ height: 1px;
69
+ overflow: hidden;
70
+ white-space: nowrap;
71
+ `;
72
+ var Root = styled.div`
73
+ color: ${tokens.fg};
74
+ font-size: ${tokens.fontSizeBody};
75
+
76
+ * {
77
+ box-sizing: border-box;
78
+ }
79
+ `;
80
+ styled.label`
81
+ display: block;
82
+ margin-bottom: ${tokens.spaceXs};
83
+ font-weight: ${tokens.fontWeightMedium};
84
+ font-size: ${tokens.fontSizeSmall};
85
+ text-transform: uppercase;
86
+ letter-spacing: 0.02em;
87
+ color: ${tokens.gray900};
88
+ `;
89
+ styled.p`
90
+ margin: ${tokens.spaceXs} 0 0;
91
+ color: ${tokens.gray600};
92
+ font-size: ${tokens.fontSizeSmall};
93
+ `;
94
+ var InputShell = styled.div`
95
+ position: relative;
96
+ display: flex;
97
+ align-items: center;
98
+ gap: ${tokens.spaceSm};
99
+ background: ${tokens.bg};
100
+ border: 1px solid ${tokens.gray300};
101
+ border-radius: ${tokens.radiusMd};
102
+ padding: 6px ${tokens.spaceMd};
103
+ min-height: 40px;
104
+ transition: border-color ${tokens.transitionFast}, box-shadow ${tokens.transitionFast};
105
+
106
+ &:focus-within {
107
+ border-color: ${tokens.accent};
108
+ box-shadow: 0 0 0 1px ${tokens.accent};
109
+ }
110
+ `;
111
+ var Input = styled.input`
112
+ flex: 1;
113
+ background: transparent;
114
+ font: inherit;
115
+ color: ${tokens.fg};
116
+ min-width: 0;
117
+
118
+ /*
119
+ * WP admin's global input styles (\`input[type="text"]\` in various
120
+ * editor/sidebar stylesheets) set their own border + box-shadow at a
121
+ * higher specificity than our Emotion class. \`!important\` is the
122
+ * pragmatic reset here — we provide the focus chrome on the enclosing
123
+ * \`InputShell\`, so every input-level border/outline/box-shadow
124
+ * contribution is noise.
125
+ */
126
+ &,
127
+ &:focus,
128
+ &:focus-visible,
129
+ &:hover {
130
+ border: 0 !important;
131
+ outline: 0 !important;
132
+ box-shadow: none !important;
133
+ }
134
+
135
+ &::placeholder {
136
+ color: ${tokens.gray600};
137
+ }
138
+ `;
139
+ var SuggestionsShell = styled.div`
140
+ background: ${tokens.bg};
141
+ border: 1px solid ${tokens.gray300};
142
+ border-radius: ${tokens.radiusMd};
143
+ overflow: hidden;
144
+ max-height: 380px;
145
+ display: flex;
146
+ flex-direction: column;
147
+
148
+ ${({ inline }) => inline ? css`
149
+ margin-top: ${tokens.spaceSm};
150
+ ` : css`
151
+ box-shadow: ${tokens.shadowPopover};
152
+ `}
153
+ `;
154
+ var ListboxScroll = styled.div`
155
+ overflow-y: auto;
156
+ flex: 1;
157
+ min-height: 0;
158
+ `;
159
+ var StickyHeader = styled.div`
160
+ position: sticky;
161
+ top: 0;
162
+ z-index: 2;
163
+ background: ${tokens.bg};
164
+ `;
165
+ var Listbox = styled.ul`
166
+ list-style: none;
167
+ margin: 0;
168
+ padding: ${tokens.spaceXs} 0;
169
+ `;
170
+ var GroupHeading = styled.div`
171
+ padding: 4px ${tokens.spaceMd};
172
+ font-size: 10px !important;
173
+ font-weight: ${tokens.fontWeightBold};
174
+ letter-spacing: 0.08em;
175
+ text-transform: uppercase !important;
176
+ color: ${tokens.gray600};
177
+ background: ${tokens.gray100};
178
+ line-height: 1.8;
179
+
180
+ [role='group']:not(:first-child) > & {
181
+ border-top: 1px solid ${tokens.gray200};
182
+ }
183
+ `;
184
+ var PrimaryText = styled.span`
185
+ font-size: ${tokens.fontSizeBody};
186
+ font-weight: ${tokens.fontWeightMedium};
187
+ color: ${({ muted }) => muted ? tokens.gray600 : tokens.gray900};
188
+ white-space: nowrap;
189
+ overflow: hidden;
190
+ text-overflow: ellipsis;
191
+ display: block;
192
+ line-height: 1.3;
193
+ transition: color ${tokens.transitionFast};
194
+ `;
195
+ var Option = styled.li`
196
+ display: flex;
197
+ align-items: center;
198
+ gap: ${tokens.spaceSm};
199
+ padding: 4px ${tokens.spaceMd};
200
+ cursor: ${({ disabled }) => disabled ? "not-allowed" : "pointer"};
201
+ opacity: ${({ disabled }) => disabled ? 0.5 : 1};
202
+ min-height: ${tokens.suggestionMinHeight};
203
+ background: ${({ highlighted }) => highlighted ? tokens.accentActive : "transparent"};
204
+ position: relative;
205
+ transition: background ${tokens.transitionFast};
206
+
207
+ &:hover {
208
+ background: ${({ highlighted, disabled }) => disabled ? "transparent" : highlighted ? tokens.accentActive : tokens.accentHover};
209
+ }
210
+
211
+ /*
212
+ * Target the title via a stable data attribute rather than Emotion's
213
+ * generated class — the popover body has its own Emotion cache, so
214
+ * class-name interpolation isn't guaranteed to match across caches.
215
+ */
216
+ &:hover [data-valu-title] {
217
+ color: ${tokens.accent};
218
+ }
219
+
220
+ ${({ highlighted }) => highlighted && css`
221
+ [data-valu-title] {
222
+ color: ${tokens.accent};
223
+ }
224
+ &::before {
225
+ content: '';
226
+ position: absolute;
227
+ left: 0;
228
+ top: 4px;
229
+ bottom: 4px;
230
+ width: 2px;
231
+ border-radius: 0 ${tokens.radiusSm} ${tokens.radiusSm} 0;
232
+ background: ${tokens.accent};
233
+ }
234
+ `}
235
+ `;
236
+ var Thumb = styled.div`
237
+ flex: 0 0 auto;
238
+ width: ${({ size, hasImage, avatar }) => avatar ? "22px" : hasImage ? tokens.thumbMd : size === "md" ? tokens.thumbMd : size === "lg" ? tokens.thumbLg : size === "sm" ? tokens.thumbSm : "22px"};
239
+ height: ${({ size, hasImage, avatar }) => avatar ? "22px" : hasImage ? tokens.thumbMd : size === "md" ? tokens.thumbMd : size === "lg" ? tokens.thumbLg : size === "sm" ? tokens.thumbSm : "22px"};
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ color: ${({ missing }) => missing ? tokens.danger : tokens.gray600};
244
+
245
+ ${({ hasImage, avatar }) => avatar ? css`
246
+ border-radius: 999px;
247
+ overflow: hidden;
248
+ ` : hasImage ? css`
249
+ border: 1px solid ${tokens.gray200};
250
+ border-radius: ${tokens.radiusSm};
251
+ overflow: hidden;
252
+ background: ${tokens.gray100};
253
+ ` : void 0}
254
+
255
+ img {
256
+ width: 100%;
257
+ height: 100%;
258
+ object-fit: cover;
259
+ display: block;
260
+ }
261
+
262
+ svg {
263
+ width: 22px;
264
+ height: 22px;
265
+ }
266
+ `;
267
+ var ItemBody = styled.div`
268
+ flex: 1;
269
+ min-width: 0;
270
+ display: flex;
271
+ flex-direction: column;
272
+ gap: 0;
273
+ `;
274
+ var SecondaryText = styled.span`
275
+ font-size: ${tokens.fontSizeSmall};
276
+ color: ${tokens.gray600};
277
+ white-space: nowrap;
278
+ overflow: hidden;
279
+ text-overflow: ellipsis;
280
+ display: block;
281
+ line-height: 1.3;
282
+ `;
283
+ styled.div`
284
+ display: flex;
285
+ align-items: center;
286
+ gap: ${tokens.spaceXs};
287
+ flex-wrap: wrap;
288
+ margin-top: 0;
289
+ `;
290
+ var TypeHint = styled.span`
291
+ flex: 0 0 auto;
292
+ font-size: ${tokens.fontSizeSmall};
293
+ color: ${tokens.gray600};
294
+ margin-left: ${tokens.spaceSm};
295
+ white-space: nowrap;
296
+ `;
297
+ var ActionColumn = styled.div`
298
+ flex: 0 0 auto;
299
+ display: flex;
300
+ align-items: center;
301
+ gap: ${tokens.spaceXs};
302
+ margin-left: auto;
303
+ `;
304
+ var SelectedListEl = styled.ul`
305
+ list-style: none;
306
+ margin: ${tokens.spaceSm} 0 0;
307
+ padding: 0;
308
+ display: flex;
309
+ flex-direction: column;
310
+ gap: 2px;
311
+ `;
312
+ var SelectedRow = styled.li`
313
+ display: flex;
314
+ align-items: center;
315
+ gap: ${tokens.spaceSm};
316
+ padding: 2px ${tokens.spaceSm};
317
+ background: ${({ missing }) => missing ? tokens.dangerSoft : tokens.bg};
318
+ border: 1px solid ${({ missing }) => missing ? tokens.dangerLine : tokens.gray300};
319
+ border-radius: ${tokens.radiusMd};
320
+ min-height: ${tokens.rowMinHeight};
321
+ box-shadow: ${({ dragging }) => dragging ? tokens.shadowPopover : "none"};
322
+ transition: box-shadow ${tokens.transitionFast}, border-color ${tokens.transitionFast};
323
+
324
+ &:hover {
325
+ border-color: ${({ missing }) => missing ? tokens.dangerLine : tokens.gray600};
326
+ }
327
+ `;
328
+ var DragHandleButton = styled.button`
329
+ flex: 0 0 auto;
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: center;
333
+ width: 20px;
334
+ height: 20px;
335
+ border: 0;
336
+ background: transparent;
337
+ color: ${tokens.gray600};
338
+ cursor: grab;
339
+ border-radius: ${tokens.radiusSm};
340
+ padding: 0;
341
+
342
+ &:hover {
343
+ background: ${tokens.gray100};
344
+ color: ${tokens.gray900};
345
+ }
346
+
347
+ &:focus-visible {
348
+ ${focusRing}
349
+ }
350
+
351
+ &:active {
352
+ cursor: grabbing;
353
+ }
354
+
355
+ &[disabled] {
356
+ cursor: not-allowed;
357
+ opacity: 0.4;
358
+ }
359
+
360
+ svg {
361
+ width: 14px;
362
+ height: 14px;
363
+ }
364
+ `;
365
+ var EmptyState = styled.div`
366
+ padding: ${tokens.spaceLg} ${tokens.spaceMd};
367
+ text-align: center;
368
+ color: ${tokens.gray600};
369
+ font-size: ${tokens.fontSizeBody};
370
+ `;
371
+ var LoadingState = styled.div`
372
+ padding: ${tokens.spaceLg} ${tokens.spaceMd};
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ gap: ${tokens.spaceSm};
377
+ color: ${tokens.gray600};
378
+ font-size: ${tokens.fontSizeBody};
379
+ `;
380
+ var ErrorState = styled.div`
381
+ padding: ${tokens.spaceMd};
382
+ background: ${tokens.dangerSoft};
383
+ color: ${tokens.danger};
384
+ border: 1px solid ${tokens.dangerLine};
385
+ border-radius: ${tokens.radiusMd};
386
+ font-size: ${tokens.fontSizeBody};
387
+ `;
388
+ var MaxedOutBanner = styled.div`
389
+ display: flex;
390
+ align-items: center;
391
+ gap: ${tokens.spaceSm};
392
+ padding: ${tokens.spaceSm} ${tokens.spaceMd};
393
+ background: ${tokens.gray100};
394
+ color: ${tokens.gray900};
395
+ border: 1px dashed ${tokens.gray300};
396
+ border-radius: ${tokens.radiusMd};
397
+ font-size: ${tokens.fontSizeSmall};
398
+ `;
399
+ var CountPill = styled.span`
400
+ display: inline-flex;
401
+ align-items: center;
402
+ padding: 2px 8px;
403
+ background: ${tokens.gray100};
404
+ color: ${tokens.gray900};
405
+ border-radius: 999px;
406
+ font-size: 10px;
407
+ font-weight: ${tokens.fontWeightBold};
408
+ letter-spacing: 0.04em;
409
+ `;
410
+ var ChipWrap = styled.div`
411
+ display: inline-flex;
412
+ align-items: center;
413
+ gap: ${tokens.spaceSm};
414
+ background: ${tokens.gray900};
415
+ color: #fff;
416
+ border-radius: ${tokens.radiusMd};
417
+ padding: 6px ${tokens.spaceMd};
418
+ max-width: 280px;
419
+ box-shadow: ${tokens.shadowPopover};
420
+ `;
421
+ var ChipTitle = styled.span`
422
+ font-size: ${tokens.fontSizeSmall};
423
+ font-weight: ${tokens.fontWeightMedium};
424
+ white-space: nowrap;
425
+ overflow: hidden;
426
+ text-overflow: ellipsis;
427
+ `;
428
+ var LoadMoreRow = styled.li`
429
+ list-style: none;
430
+ display: flex;
431
+ justify-content: center;
432
+ padding: ${tokens.spaceSm};
433
+ `;
434
+ var SearchIconWrap = styled.span`
435
+ flex: 0 0 auto;
436
+ display: flex;
437
+ align-items: center;
438
+ color: ${tokens.gray600};
439
+ `;
440
+ var HeaderRow = styled.div`
441
+ display: flex;
442
+ align-items: center;
443
+ justify-content: space-between;
444
+ gap: ${tokens.spaceSm};
445
+ margin-top: ${tokens.spaceSm};
446
+ margin-bottom: ${tokens.spaceXs};
447
+ `;
448
+ var HeaderTitle = styled.h3`
449
+ margin: 0;
450
+ font-size: 10px !important;
451
+ text-transform: uppercase !important;
452
+ letter-spacing: 0.04em;
453
+ font-weight: ${tokens.fontWeightBold};
454
+ color: ${tokens.gray600};
455
+ line-height: 1.8;
456
+ `;
457
+ var generateKey = () => v4();
458
+ var decodeTitle = (raw) => {
459
+ if (typeof raw === "string") return decodeEntities(raw);
460
+ if (raw && typeof raw === "object" && "rendered" in raw) {
461
+ const rendered = raw.rendered;
462
+ if (typeof rendered === "string") return decodeEntities(rendered);
463
+ }
464
+ return "";
465
+ };
466
+ var toDisplayUrl = (url) => {
467
+ if (!url) return "";
468
+ try {
469
+ return filterURLForDisplay(safeDecodeURI(url)) ?? "";
470
+ } catch {
471
+ return url;
472
+ }
473
+ };
474
+ var groupByType = (items) => {
475
+ const map = /* @__PURE__ */ new Map();
476
+ for (const item of items) {
477
+ const bucket = map.get(item.type);
478
+ if (bucket) bucket.push(item);
479
+ else map.set(item.type, [item]);
480
+ }
481
+ return Array.from(map.entries());
482
+ };
483
+ var postTypeLabels = {
484
+ post: "Post",
485
+ page: "Page",
486
+ attachment: "Media"
487
+ };
488
+ var humanizeType = (type, kind) => {
489
+ if (kind === "user") return __("User", "valu-block-components");
490
+ if (postTypeLabels[type]) return postTypeLabels[type];
491
+ return type.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
492
+ };
493
+ var statusLabels = {
494
+ draft: "Draft",
495
+ pending: "Pending",
496
+ private: "Private",
497
+ future: "Scheduled",
498
+ trash: "Trash"
499
+ };
500
+ var humanizeStatus = (status) => {
501
+ if (!status || status === "publish") return null;
502
+ return statusLabels[status] ?? status;
503
+ };
504
+ var aria = (rootId, suffix) => `${rootId}-${suffix}`;
505
+ var isMaxedOut = (value, maxItems) => Number.isFinite(maxItems) && value.length >= maxItems;
506
+ var dedupeAgainst = (items, value, enabled) => {
507
+ if (!enabled || value.length === 0) return items;
508
+ const ids = new Set(value.map((v) => v.id));
509
+ return items.filter((i) => !ids.has(i.id));
510
+ };
511
+ var DEFAULT_ITEM_ICONS = {
512
+ user: people
513
+ };
514
+ var resolveItemIcon = (item, userIcons) => {
515
+ if (userIcons && item.type in userIcons) return userIcons[item.type];
516
+ if (item.type in DEFAULT_ITEM_ICONS) return DEFAULT_ITEM_ICONS[item.type];
517
+ return void 0;
518
+ };
519
+ var SearchCombobox = forwardRef(
520
+ ({
521
+ rootId,
522
+ value,
523
+ onChange,
524
+ onFocus,
525
+ onBlur,
526
+ onKeyDown,
527
+ placeholder,
528
+ label,
529
+ hideLabelFromVision,
530
+ help,
531
+ disabled,
532
+ open,
533
+ listboxId,
534
+ activeOptionId,
535
+ labels
536
+ }, ref) => {
537
+ const inputId = aria(rootId, "input");
538
+ const helpId = help ? aria(rootId, "help") : void 0;
539
+ return /* @__PURE__ */ jsxs("div", { children: [
540
+ hideLabelFromVision ? /* @__PURE__ */ jsx(VisuallyHidden, { as: "label", htmlFor: inputId, children: label }) : /* @__PURE__ */ jsx("label", { htmlFor: inputId, style: { display: "block", marginBottom: 4 }, children: label }),
541
+ /* @__PURE__ */ jsxs(InputShell, { "aria-disabled": disabled || void 0, children: [
542
+ /* @__PURE__ */ jsx(SearchIconWrap, { "aria-hidden": "true", children: /* @__PURE__ */ jsx(Icon, { icon: search, size: 20 }) }),
543
+ /* @__PURE__ */ jsx(
544
+ Input,
545
+ {
546
+ ref,
547
+ id: inputId,
548
+ type: "text",
549
+ role: "combobox",
550
+ "aria-expanded": open,
551
+ "aria-controls": listboxId,
552
+ "aria-autocomplete": "list",
553
+ "aria-activedescendant": activeOptionId,
554
+ "aria-describedby": helpId,
555
+ "aria-label": hideLabelFromVision ? label : void 0,
556
+ value,
557
+ placeholder: placeholder ?? labels.input ?? __("Search", "valu-block-components"),
558
+ disabled,
559
+ autoComplete: "off",
560
+ spellCheck: false,
561
+ onChange: (event) => onChange(event.target.value),
562
+ onFocus,
563
+ onBlur,
564
+ onKeyDown
565
+ }
566
+ ),
567
+ value.length > 0 && !disabled ? /* @__PURE__ */ jsx(
568
+ "button",
569
+ {
570
+ type: "button",
571
+ onClick: () => onChange(""),
572
+ "aria-label": __("Clear search", "valu-block-components"),
573
+ style: {
574
+ display: "flex",
575
+ alignItems: "center",
576
+ justifyContent: "center",
577
+ background: "transparent",
578
+ border: 0,
579
+ padding: 2,
580
+ cursor: "pointer",
581
+ borderRadius: 2,
582
+ color: "currentColor",
583
+ opacity: 0.6
584
+ },
585
+ children: /* @__PURE__ */ jsx(Icon, { icon: closeSmall, size: 18 })
586
+ }
587
+ ) : null
588
+ ] }),
589
+ help ? /* @__PURE__ */ jsx("p", { id: helpId, style: { margin: "4px 0 0", fontSize: 11, opacity: 0.7 }, children: help }) : null
590
+ ] });
591
+ }
592
+ );
593
+ SearchCombobox.displayName = "SearchCombobox";
594
+ var tones = {
595
+ neutral: { bg: tokens.gray100, fg: tokens.gray900, border: tokens.gray300 },
596
+ warn: { bg: "#fef3d0", fg: tokens.warning, border: "#f3d782" },
597
+ danger: { bg: tokens.dangerSoft, fg: tokens.danger, border: tokens.dangerLine },
598
+ success: { bg: "#e6f4ea", fg: tokens.success, border: "#a7d5b3" }
599
+ };
600
+ var toneFor = (status) => {
601
+ if (status === "trash") return "danger";
602
+ if (status === "draft" || status === "pending" || status === "future") return "warn";
603
+ if (status === "private") return "neutral";
604
+ return "neutral";
605
+ };
606
+ var Pill = styled.span`
607
+ display: inline-flex;
608
+ align-items: center;
609
+ padding: 1px 6px;
610
+ background: ${({ tone }) => tones[tone].bg};
611
+ color: ${({ tone }) => tones[tone].fg};
612
+ border: 1px solid ${({ tone }) => tones[tone].border};
613
+ border-radius: 999px;
614
+ font-size: 10px;
615
+ font-weight: ${tokens.fontWeightBold};
616
+ letter-spacing: 0.04em;
617
+ text-transform: uppercase;
618
+ white-space: nowrap;
619
+ line-height: 1.6;
620
+ `;
621
+ var StatusBadge = ({ status }) => {
622
+ const label = humanizeStatus(status);
623
+ if (!label) return null;
624
+ const translated = status === "draft" ? __("Draft", "valu-block-components") : status === "pending" ? __("Pending", "valu-block-components") : status === "private" ? __("Private", "valu-block-components") : status === "future" ? __("Scheduled", "valu-block-components") : status === "trash" ? __("Trash", "valu-block-components") : label;
625
+ return /* @__PURE__ */ jsx(Pill, { tone: toneFor(status), children: translated });
626
+ };
627
+ var SuggestionItem = ({
628
+ item,
629
+ optionId,
630
+ highlighted,
631
+ selected,
632
+ showTypeHint,
633
+ showUrl,
634
+ icons,
635
+ onHover,
636
+ onSelect,
637
+ render
638
+ }) => {
639
+ const ref = useRef(null);
640
+ useEffect(() => {
641
+ if (!highlighted || !ref.current) return;
642
+ ref.current.scrollIntoView({ block: "nearest" });
643
+ }, [highlighted]);
644
+ const handleClick = useCallback(
645
+ (event) => {
646
+ event.preventDefault();
647
+ onSelect();
648
+ },
649
+ [onSelect]
650
+ );
651
+ const handleMouseDown = useCallback((event) => {
652
+ event.preventDefault();
653
+ }, []);
654
+ if (render) {
655
+ return /* @__PURE__ */ jsx(
656
+ Option,
657
+ {
658
+ ref,
659
+ role: "option",
660
+ id: optionId,
661
+ "aria-selected": selected,
662
+ highlighted,
663
+ onMouseEnter: onHover,
664
+ onMouseDown: handleMouseDown,
665
+ onClick: handleClick,
666
+ children: render(item, { highlighted, selected })
667
+ }
668
+ );
669
+ }
670
+ const displayUrl = toDisplayUrl(item.url);
671
+ const hasAbnormalStatus = item.status && item.status !== "publish";
672
+ const icon = resolveItemIcon(item, icons);
673
+ const isAvatar = item.kind === "user" && Boolean(item.thumbnailUrl);
674
+ const hasVisual = Boolean(item.thumbnailUrl) || Boolean(icon);
675
+ return /* @__PURE__ */ jsxs(
676
+ Option,
677
+ {
678
+ ref,
679
+ role: "option",
680
+ id: optionId,
681
+ "aria-selected": selected,
682
+ highlighted,
683
+ onMouseEnter: onHover,
684
+ onMouseDown: handleMouseDown,
685
+ onClick: handleClick,
686
+ children: [
687
+ hasVisual ? /* @__PURE__ */ jsx(
688
+ Thumb,
689
+ {
690
+ hasImage: Boolean(item.thumbnailUrl) && !isAvatar,
691
+ avatar: isAvatar,
692
+ children: item.thumbnailUrl ? /* @__PURE__ */ jsx("img", { src: item.thumbnailUrl, alt: "", loading: "lazy" }) : icon ? /* @__PURE__ */ jsx(Icon, { icon, size: 22 }) : null
693
+ }
694
+ ) : null,
695
+ /* @__PURE__ */ jsxs(ItemBody, { children: [
696
+ /* @__PURE__ */ jsx(PrimaryText, { "data-valu-title": true, children: item.title }),
697
+ showUrl && displayUrl ? /* @__PURE__ */ jsx(SecondaryText, { children: displayUrl }) : null
698
+ ] }),
699
+ hasAbnormalStatus ? /* @__PURE__ */ jsx(StatusBadge, { status: item.status }) : null,
700
+ showTypeHint ? /* @__PURE__ */ jsx(TypeHint, { children: humanizeType(item.type, item.kind) }) : null
701
+ ]
702
+ }
703
+ );
704
+ };
705
+ var SuggestionList = ({
706
+ listboxId,
707
+ rootId,
708
+ groups,
709
+ suggestions,
710
+ kind,
711
+ loading,
712
+ fetchingMore,
713
+ hasMore,
714
+ error,
715
+ query,
716
+ highlightedIndex,
717
+ selectedKeys,
718
+ icons,
719
+ labels,
720
+ showUrl,
721
+ headerSlot,
722
+ renderSuggestion,
723
+ renderGroupHeading,
724
+ onHover,
725
+ onSelect,
726
+ onLoadMore
727
+ }) => {
728
+ const scrollRef = useRef(null);
729
+ const sentinelRef = useRef(null);
730
+ useEffect(() => {
731
+ if (!hasMore || !sentinelRef.current || !scrollRef.current) return;
732
+ const io = new IntersectionObserver(
733
+ (entries) => {
734
+ for (const e of entries) {
735
+ if (e.isIntersecting) onLoadMore();
736
+ }
737
+ },
738
+ { root: scrollRef.current, rootMargin: "120px" }
739
+ );
740
+ io.observe(sentinelRef.current);
741
+ return () => io.disconnect();
742
+ }, [hasMore, onLoadMore]);
743
+ const header = headerSlot ? /* @__PURE__ */ jsx(StickyHeader, { children: headerSlot }) : null;
744
+ if (error) {
745
+ return /* @__PURE__ */ jsxs(ListboxScroll, { ref: scrollRef, children: [
746
+ header,
747
+ /* @__PURE__ */ jsx(ErrorState, { role: "alert", children: labels.empty ?? __("Search failed. Please try again.", "valu-block-components") })
748
+ ] });
749
+ }
750
+ if (loading && suggestions.length === 0) {
751
+ return /* @__PURE__ */ jsxs(ListboxScroll, { ref: scrollRef, children: [
752
+ header,
753
+ /* @__PURE__ */ jsxs(LoadingState, { children: [
754
+ /* @__PURE__ */ jsx(Spinner, {}),
755
+ labels.loading ?? __("Searching\u2026", "valu-block-components")
756
+ ] })
757
+ ] });
758
+ }
759
+ if (suggestions.length === 0) {
760
+ const emptyMessage = query.trim().length > 0 ? labels.empty ?? __("No matches. Try a different search.", "valu-block-components") : __("Type to search.", "valu-block-components");
761
+ return /* @__PURE__ */ jsxs(ListboxScroll, { ref: scrollRef, children: [
762
+ header,
763
+ /* @__PURE__ */ jsx(EmptyState, { children: emptyMessage })
764
+ ] });
765
+ }
766
+ let runningIndex = 0;
767
+ const showGroupHeadings = groups.length > 1 || groups[0]?.[0] !== "__all__";
768
+ const showTypeHint = !showGroupHeadings;
769
+ return /* @__PURE__ */ jsxs(ListboxScroll, { ref: scrollRef, children: [
770
+ header,
771
+ /* @__PURE__ */ jsxs(
772
+ Listbox,
773
+ {
774
+ role: "listbox",
775
+ id: listboxId,
776
+ "aria-label": labels.input ?? __("Suggestions", "valu-block-components"),
777
+ children: [
778
+ groups.map(([type, items]) => {
779
+ const headingId = aria(rootId, `group-${type}`);
780
+ const optionNodes = items.map((item) => {
781
+ const index = runningIndex++;
782
+ const optionId = aria(rootId, `option-${index}`);
783
+ return /* @__PURE__ */ jsx(
784
+ SuggestionItem,
785
+ {
786
+ item,
787
+ optionId,
788
+ highlighted: index === highlightedIndex,
789
+ selected: selectedKeys.has(item.key),
790
+ showTypeHint,
791
+ showUrl,
792
+ icons,
793
+ onHover: () => onHover(index),
794
+ onSelect: () => onSelect(item),
795
+ render: renderSuggestion
796
+ },
797
+ item.key
798
+ );
799
+ });
800
+ if (!showGroupHeadings) return optionNodes;
801
+ return /* @__PURE__ */ jsxs("div", { role: "group", "aria-labelledby": headingId, children: [
802
+ /* @__PURE__ */ jsx(GroupHeading, { id: headingId, children: renderGroupHeading ? renderGroupHeading(type, items) : humanizeType(type, kind) }),
803
+ optionNodes
804
+ ] }, type);
805
+ }),
806
+ hasMore ? /* @__PURE__ */ jsx(LoadMoreRow, { children: /* @__PURE__ */ jsx(
807
+ Button,
808
+ {
809
+ variant: "tertiary",
810
+ onClick: onLoadMore,
811
+ disabled: fetchingMore,
812
+ isBusy: fetchingMore,
813
+ children: fetchingMore ? __("Loading\u2026", "valu-block-components") : __("Load more", "valu-block-components")
814
+ }
815
+ ) }) : null,
816
+ /* @__PURE__ */ jsx("div", { ref: sentinelRef, "aria-hidden": "true" })
817
+ ]
818
+ }
819
+ )
820
+ ] });
821
+ };
822
+ var Row = styled.div`
823
+ display: flex;
824
+ align-items: center;
825
+ gap: ${tokens.spaceXs};
826
+ padding: 6px ${tokens.spaceMd};
827
+ overflow-x: auto;
828
+ border-bottom: 1px solid ${tokens.gray200};
829
+ background: ${tokens.bg};
830
+
831
+ &::-webkit-scrollbar {
832
+ height: 4px;
833
+ }
834
+ &::-webkit-scrollbar-thumb {
835
+ background: ${tokens.gray300};
836
+ border-radius: 2px;
837
+ }
838
+ `;
839
+ var Chip = styled.button`
840
+ flex: 0 0 auto;
841
+ padding: 2px 10px;
842
+ font-size: ${tokens.fontSizeSmall};
843
+ font-weight: ${tokens.fontWeightMedium};
844
+ line-height: 1.6;
845
+ border-radius: 999px;
846
+ border: 1px solid ${({ active }) => active ? tokens.accent : tokens.gray300};
847
+ background: ${({ active }) => active ? tokens.accent : "transparent"};
848
+ color: ${({ active }) => active ? "#fff" : tokens.gray900};
849
+ cursor: pointer;
850
+ white-space: nowrap;
851
+ transition: background ${tokens.transitionFast}, border-color ${tokens.transitionFast};
852
+
853
+ &:hover {
854
+ border-color: ${tokens.accent};
855
+ }
856
+
857
+ &:focus-visible {
858
+ outline: 2px solid ${tokens.accent};
859
+ outline-offset: 1px;
860
+ }
861
+ `;
862
+ var TypeFilter = ({ types, kind, active, onChange, allLabel }) => {
863
+ if (types.length < 2) return null;
864
+ return /* @__PURE__ */ jsxs(Row, { role: "toolbar", "aria-label": __("Filter by type", "valu-block-components"), children: [
865
+ /* @__PURE__ */ jsx(
866
+ Chip,
867
+ {
868
+ type: "button",
869
+ active: active === null,
870
+ "aria-pressed": active === null,
871
+ onClick: () => onChange(null),
872
+ onMouseDown: (e) => e.preventDefault(),
873
+ children: allLabel ?? __("All", "valu-block-components")
874
+ }
875
+ ),
876
+ types.map((type) => /* @__PURE__ */ jsx(
877
+ Chip,
878
+ {
879
+ type: "button",
880
+ active: active === type,
881
+ "aria-pressed": active === type,
882
+ onClick: () => onChange(active === type ? null : type),
883
+ onMouseDown: (e) => e.preventDefault(),
884
+ children: humanizeType(type, kind)
885
+ },
886
+ type
887
+ ))
888
+ ] });
889
+ };
890
+ var pickThumbnail = (result) => {
891
+ const media = result._embedded?.["wp:featuredmedia"]?.[0];
892
+ if (!media) return void 0;
893
+ const sizes = media.media_details?.sizes;
894
+ return sizes?.thumbnail?.source_url ?? sizes?.medium?.source_url ?? media.source_url ?? void 0;
895
+ };
896
+ var pickStatus = (result) => result._embedded?.self?.[0]?.status;
897
+ var buildPath = (args) => {
898
+ const { kind, types, query, page, perPage } = args;
899
+ if (kind === "user") {
900
+ return addQueryArgs("/wp/v2/users", {
901
+ search: query,
902
+ per_page: perPage,
903
+ page,
904
+ context: "view"
905
+ });
906
+ }
907
+ const params = {
908
+ search: query,
909
+ type: kind,
910
+ per_page: perPage,
911
+ page,
912
+ _embed: "wp:featuredmedia,self"
913
+ };
914
+ if (types && types.length > 0) {
915
+ params.subtype = types.join(",");
916
+ }
917
+ return addQueryArgs("/wp/v2/search", params);
918
+ };
919
+ var parseTotalPages = (response) => {
920
+ const raw = response.headers.get("x-wp-totalpages");
921
+ if (!raw) return 1;
922
+ const parsed = parseInt(raw, 10);
923
+ return Number.isNaN(parsed) ? 1 : parsed;
924
+ };
925
+ var parseTotal = (response) => {
926
+ const raw = response.headers.get("x-wp-total");
927
+ if (!raw) return 0;
928
+ const parsed = parseInt(raw, 10);
929
+ return Number.isNaN(parsed) ? 0 : parsed;
930
+ };
931
+ var fetchSearch = async (args) => {
932
+ const rawPath = buildPath(args);
933
+ const path = args.request ? args.request(rawPath, { query: args.query, page: args.page, kind: args.kind }) : rawPath;
934
+ const response = await apiFetch({
935
+ path,
936
+ parse: false,
937
+ signal: args.signal
938
+ });
939
+ const data = await response.json();
940
+ const totalPages = parseTotalPages(response);
941
+ const total = parseTotal(response);
942
+ let items;
943
+ if (args.kind === "user") {
944
+ items = data.map((u) => ({
945
+ key: generateKey(),
946
+ id: u.id,
947
+ type: "user",
948
+ kind: "user",
949
+ title: decodeTitle(u.name),
950
+ url: u.link,
951
+ thumbnailUrl: u.avatar_urls?.["48"] ?? u.avatar_urls?.["96"],
952
+ raw: u
953
+ }));
954
+ } else {
955
+ items = data.map((r) => ({
956
+ key: generateKey(),
957
+ id: r.id,
958
+ type: r.subtype || r.type,
959
+ kind: args.kind,
960
+ title: decodeTitle(r.title),
961
+ url: r.url,
962
+ status: pickStatus(r),
963
+ thumbnailUrl: pickThumbnail(r),
964
+ raw: r
965
+ }));
966
+ }
967
+ return { items, page: args.page, hasMore: args.page < totalPages, total };
968
+ };
969
+
970
+ // components/content-picker/hooks/useContentSearch.ts
971
+ var contentSearchKey = (args) => [
972
+ "valu/content-search",
973
+ args.kind,
974
+ [...args.types ?? []].sort().join(","),
975
+ args.query,
976
+ args.perPage
977
+ ];
978
+ var useContentSearch = (args) => useInfiniteQuery({
979
+ queryKey: contentSearchKey(args),
980
+ queryFn: ({ pageParam, signal }) => fetchSearch({
981
+ query: args.query,
982
+ kind: args.kind,
983
+ types: args.types,
984
+ page: pageParam,
985
+ perPage: args.perPage,
986
+ request: args.request,
987
+ signal
988
+ }),
989
+ initialPageParam: 1,
990
+ getNextPageParam: (last) => last.hasMore ? last.page + 1 : void 0,
991
+ enabled: args.enabled ?? true,
992
+ staleTime: 3e4,
993
+ placeholderData: keepPreviousData
994
+ });
995
+ var useDebouncedValue = (value, delay) => {
996
+ const [debounced, setDebounced] = useState(value);
997
+ useEffect(() => {
998
+ const id = setTimeout(() => setDebounced(value), delay);
999
+ return () => clearTimeout(id);
1000
+ }, [value, delay]);
1001
+ return debounced;
1002
+ };
1003
+ var useLiveRegion = (debounceMs = 500) => {
1004
+ const timer = useRef(null);
1005
+ useEffect(
1006
+ () => () => {
1007
+ if (timer.current) clearTimeout(timer.current);
1008
+ },
1009
+ []
1010
+ );
1011
+ const announce = useCallback(
1012
+ (message, assertiveness = "polite") => {
1013
+ if (!message) return;
1014
+ if (timer.current) clearTimeout(timer.current);
1015
+ timer.current = setTimeout(() => speak(message, assertiveness), debounceMs);
1016
+ },
1017
+ [debounceMs]
1018
+ );
1019
+ const announceImmediate = useCallback(
1020
+ (message, assertiveness = "polite") => {
1021
+ if (!message) return;
1022
+ if (timer.current) clearTimeout(timer.current);
1023
+ speak(message, assertiveness);
1024
+ },
1025
+ []
1026
+ );
1027
+ return { announce, announceImmediate };
1028
+ };
1029
+ var useKeyboardNav = ({
1030
+ itemCount,
1031
+ onSelect,
1032
+ onEscape,
1033
+ wrap = true
1034
+ }) => {
1035
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
1036
+ const reset = useCallback(() => setHighlightedIndex(-1), []);
1037
+ const handleKeyDown = useCallback(
1038
+ (event) => {
1039
+ if (itemCount === 0) {
1040
+ if (event.key === "Escape") {
1041
+ event.preventDefault();
1042
+ onEscape?.();
1043
+ }
1044
+ return;
1045
+ }
1046
+ switch (event.key) {
1047
+ case "ArrowDown": {
1048
+ event.preventDefault();
1049
+ setHighlightedIndex((i) => {
1050
+ const next = i + 1;
1051
+ if (next >= itemCount) return wrap ? 0 : itemCount - 1;
1052
+ return next;
1053
+ });
1054
+ break;
1055
+ }
1056
+ case "ArrowUp": {
1057
+ event.preventDefault();
1058
+ setHighlightedIndex((i) => {
1059
+ if (i <= 0) return wrap ? itemCount - 1 : 0;
1060
+ return i - 1;
1061
+ });
1062
+ break;
1063
+ }
1064
+ case "Home": {
1065
+ event.preventDefault();
1066
+ setHighlightedIndex(0);
1067
+ break;
1068
+ }
1069
+ case "End": {
1070
+ event.preventDefault();
1071
+ setHighlightedIndex(itemCount - 1);
1072
+ break;
1073
+ }
1074
+ case "Enter": {
1075
+ if (highlightedIndex >= 0 && highlightedIndex < itemCount) {
1076
+ event.preventDefault();
1077
+ onSelect?.(highlightedIndex);
1078
+ }
1079
+ break;
1080
+ }
1081
+ case "Escape": {
1082
+ event.preventDefault();
1083
+ onEscape?.();
1084
+ break;
1085
+ }
1086
+ }
1087
+ },
1088
+ [itemCount, highlightedIndex, onSelect, onEscape, wrap]
1089
+ );
1090
+ return { highlightedIndex, setHighlightedIndex, reset, handleKeyDown };
1091
+ };
1092
+
1093
+ // components/content-picker/hooks/useContentPickerController.ts
1094
+ var useContentPickerController = (args) => {
1095
+ const {
1096
+ kind,
1097
+ types,
1098
+ perPage,
1099
+ inputDelayMs,
1100
+ excludeCurrentPost,
1101
+ uniqueItems,
1102
+ maxItems,
1103
+ groupByType: groupByTypeFlag,
1104
+ filter,
1105
+ request,
1106
+ value,
1107
+ onChange,
1108
+ active,
1109
+ fetchEmpty,
1110
+ onSelectSideEffect,
1111
+ onEscape
1112
+ } = args;
1113
+ const [query, setQuery] = useState("");
1114
+ const [activeTypeFilter, setActiveTypeFilter] = useState(null);
1115
+ const debouncedQuery = useDebouncedValue(query, inputDelayMs);
1116
+ const effectiveSearchTypes = useMemo(() => {
1117
+ if (activeTypeFilter && types && types.includes(activeTypeFilter)) {
1118
+ return [activeTypeFilter];
1119
+ }
1120
+ return types;
1121
+ }, [activeTypeFilter, types]);
1122
+ const currentPostId = useSelect((select2) => {
1123
+ if (!excludeCurrentPost || kind !== "post") return 0;
1124
+ const editor = select2("core/editor");
1125
+ return editor?.getCurrentPostId?.() ?? 0;
1126
+ }, [excludeCurrentPost, kind]);
1127
+ const enabled = active && (fetchEmpty || debouncedQuery.trim().length > 0);
1128
+ const query_ = useContentSearch({
1129
+ query: debouncedQuery,
1130
+ kind,
1131
+ types: effectiveSearchTypes,
1132
+ perPage,
1133
+ enabled,
1134
+ request
1135
+ });
1136
+ const suggestions = useMemo(() => {
1137
+ if (!query_.data) return [];
1138
+ const flat = query_.data.pages.flatMap((p) => p.items);
1139
+ let filtered = dedupeAgainst(flat, value, uniqueItems);
1140
+ if (currentPostId) {
1141
+ filtered = filtered.filter((item) => item.id !== currentPostId);
1142
+ }
1143
+ if (filter) {
1144
+ filtered = filter(filtered, { query: debouncedQuery, kind });
1145
+ }
1146
+ return filtered;
1147
+ }, [query_.data, value, uniqueItems, currentPostId, filter, debouncedQuery, kind]);
1148
+ const shouldGroup = groupByTypeFlag && (effectiveSearchTypes?.length ?? 0) > 1;
1149
+ const groups = useMemo(
1150
+ () => shouldGroup ? groupByType(suggestions) : [["__all__", suggestions]],
1151
+ [shouldGroup, suggestions]
1152
+ );
1153
+ const { announce, announceImmediate } = useLiveRegion();
1154
+ useEffect(() => {
1155
+ if (!active) return;
1156
+ if (debouncedQuery.trim().length === 0 && !fetchEmpty) return;
1157
+ if (query_.isFetching) return;
1158
+ if (query_.isError) {
1159
+ announce(__("Search failed. Try again.", "valu-block-components"));
1160
+ return;
1161
+ }
1162
+ const count = suggestions.length;
1163
+ announce(
1164
+ sprintf(
1165
+ /* translators: %d: number of search results */
1166
+ _n("%d result", "%d results", count, "valu-block-components"),
1167
+ count
1168
+ )
1169
+ );
1170
+ }, [suggestions.length, query_.isFetching, query_.isError, active, debouncedQuery, fetchEmpty, announce]);
1171
+ const maxedOut = isMaxedOut(value, maxItems);
1172
+ const replace = useCallback((items) => onChange(items), [onChange]);
1173
+ const select = useCallback(
1174
+ (item) => {
1175
+ if (maxedOut) return;
1176
+ const limited = maxItems === 1;
1177
+ const normalized = {
1178
+ ...item,
1179
+ key: item.key && item.key.length > 0 ? item.key : generateKey()
1180
+ };
1181
+ const next = limited ? [normalized] : [...value, normalized];
1182
+ onChange(next);
1183
+ announceImmediate(
1184
+ sprintf(
1185
+ /* translators: 1: title, 2: selected count */
1186
+ __("Added %1$s. %2$d selected.", "valu-block-components"),
1187
+ normalized.title,
1188
+ next.length
1189
+ )
1190
+ );
1191
+ onSelectSideEffect?.();
1192
+ },
1193
+ [maxedOut, maxItems, value, onChange, announceImmediate, onSelectSideEffect]
1194
+ );
1195
+ const remove = useCallback(
1196
+ (key) => {
1197
+ const target = value.find((i) => i.key === key);
1198
+ const next = value.filter((i) => i.key !== key);
1199
+ onChange(next);
1200
+ if (target) {
1201
+ announceImmediate(
1202
+ sprintf(
1203
+ /* translators: %s: title of the removed item */
1204
+ __("Removed %s.", "valu-block-components"),
1205
+ target.title
1206
+ )
1207
+ );
1208
+ }
1209
+ },
1210
+ [value, onChange, announceImmediate]
1211
+ );
1212
+ const reorder = useCallback(
1213
+ (from, to) => {
1214
+ if (from === to || from < 0 || to < 0 || from >= value.length || to >= value.length) {
1215
+ return;
1216
+ }
1217
+ const next = value.slice();
1218
+ const [moved] = next.splice(from, 1);
1219
+ next.splice(to, 0, moved);
1220
+ onChange(next);
1221
+ announceImmediate(
1222
+ sprintf(
1223
+ /* translators: 1: title, 2: position, 3: total */
1224
+ __("Moved %1$s to position %2$d of %3$d.", "valu-block-components"),
1225
+ moved.title,
1226
+ to + 1,
1227
+ next.length
1228
+ )
1229
+ );
1230
+ },
1231
+ [value, onChange, announceImmediate]
1232
+ );
1233
+ const { highlightedIndex, setHighlightedIndex, reset: resetHighlight, handleKeyDown } = useKeyboardNav({
1234
+ itemCount: suggestions.length,
1235
+ onSelect: (i) => {
1236
+ const item = suggestions[i];
1237
+ if (item) select(item);
1238
+ },
1239
+ onEscape
1240
+ });
1241
+ useEffect(() => {
1242
+ resetHighlight();
1243
+ }, [debouncedQuery, activeTypeFilter, resetHighlight]);
1244
+ return {
1245
+ query,
1246
+ setQuery,
1247
+ debouncedQuery,
1248
+ suggestions,
1249
+ groups,
1250
+ highlightedIndex,
1251
+ setHighlightedIndex,
1252
+ handleKeyDown,
1253
+ loading: query_.isFetching && !query_.data,
1254
+ error: query_.error,
1255
+ hasMore: Boolean(query_.hasNextPage),
1256
+ loadMore: () => {
1257
+ if (query_.hasNextPage && !query_.isFetchingNextPage) void query_.fetchNextPage();
1258
+ },
1259
+ fetchingMore: query_.isFetchingNextPage,
1260
+ select,
1261
+ remove,
1262
+ reorder,
1263
+ replace,
1264
+ maxedOut,
1265
+ total: query_.data?.pages[0]?.total ?? 0,
1266
+ activeTypeFilter,
1267
+ setActiveTypeFilter,
1268
+ effectiveSearchTypes
1269
+ };
1270
+ };
1271
+ var EmotionCacheProvider = ({ children }) => {
1272
+ const [head, setHead] = useState(null);
1273
+ const captureDocument = useCallback((node) => {
1274
+ if (!node) return;
1275
+ const owner = node.ownerDocument ?? document;
1276
+ setHead((prev) => prev === owner.head ? prev : owner.head);
1277
+ }, []);
1278
+ const cache = useMemo(() => {
1279
+ if (!head) return null;
1280
+ return createCache({ key: "valu-cp", container: head, prepend: true });
1281
+ }, [head]);
1282
+ if (!cache) {
1283
+ return /* @__PURE__ */ jsx("div", { ref: captureDocument, style: { display: "contents" } });
1284
+ }
1285
+ return /* @__PURE__ */ jsx(CacheProvider, { value: cache, children: /* @__PURE__ */ jsx("div", { ref: captureDocument, style: { display: "contents" }, children }) });
1286
+ };
1287
+ var ContentPickerPopover = (props) => {
1288
+ const {
1289
+ rootId,
1290
+ label,
1291
+ hideLabelFromVision,
1292
+ help,
1293
+ placeholder,
1294
+ kind,
1295
+ types,
1296
+ perPage,
1297
+ inputDelayMs,
1298
+ excludeCurrentPost,
1299
+ uniqueItems,
1300
+ maxItems,
1301
+ groupByType: groupByType2,
1302
+ fetchOnOpen,
1303
+ filter,
1304
+ request,
1305
+ renderSuggestion,
1306
+ renderGroupHeading,
1307
+ icons,
1308
+ disableTypeFilter,
1309
+ showUrl,
1310
+ value,
1311
+ onChange,
1312
+ labels
1313
+ } = props;
1314
+ const [open, setOpen] = useState(false);
1315
+ const inputRef = useRef(null);
1316
+ const popoverBodyRef = useRef(null);
1317
+ const clearQueryRef = useRef(null);
1318
+ const controller = useContentPickerController({
1319
+ kind,
1320
+ types,
1321
+ perPage,
1322
+ inputDelayMs,
1323
+ excludeCurrentPost,
1324
+ uniqueItems,
1325
+ maxItems,
1326
+ groupByType: groupByType2,
1327
+ filter,
1328
+ request,
1329
+ value,
1330
+ onChange,
1331
+ active: open,
1332
+ fetchEmpty: fetchOnOpen,
1333
+ onSelectSideEffect: () => {
1334
+ if (maxItems === 1) {
1335
+ setOpen(false);
1336
+ return;
1337
+ }
1338
+ clearQueryRef.current?.();
1339
+ requestAnimationFrame(() => inputRef.current?.focus());
1340
+ },
1341
+ onEscape: () => {
1342
+ setOpen(false);
1343
+ requestAnimationFrame(() => inputRef.current?.focus());
1344
+ }
1345
+ });
1346
+ clearQueryRef.current = () => controller.setQuery("");
1347
+ const listboxId = aria(rootId, "listbox");
1348
+ const activeOptionId = controller.highlightedIndex >= 0 ? aria(rootId, `option-${controller.highlightedIndex}`) : void 0;
1349
+ const selectedKeys = useMemo(() => new Set(value.map((v) => v.key)), [value]);
1350
+ const handleFocus = useCallback(() => {
1351
+ if (!controller.maxedOut) setOpen(true);
1352
+ }, [controller.maxedOut]);
1353
+ useEffect(() => {
1354
+ if (controller.maxedOut) setOpen(false);
1355
+ }, [controller.maxedOut]);
1356
+ useEffect(() => {
1357
+ if (!open) return;
1358
+ const handlePointerDown = (event) => {
1359
+ const target = event.target;
1360
+ if (!target) return;
1361
+ const input = inputRef.current;
1362
+ const body = popoverBodyRef.current;
1363
+ const anchorContainer = input?.closest("div") ?? input;
1364
+ if (anchorContainer && anchorContainer.contains(target)) return;
1365
+ if (body && body.contains(target)) return;
1366
+ setOpen(false);
1367
+ };
1368
+ const docs = /* @__PURE__ */ new Set();
1369
+ if (typeof document !== "undefined") docs.add(document);
1370
+ const inputDoc = inputRef.current?.ownerDocument;
1371
+ if (inputDoc) docs.add(inputDoc);
1372
+ docs.forEach((d) => d.addEventListener("mousedown", handlePointerDown, true));
1373
+ return () => {
1374
+ docs.forEach((d) => d.removeEventListener("mousedown", handlePointerDown, true));
1375
+ };
1376
+ }, [open]);
1377
+ const disabled = controller.maxedOut;
1378
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1379
+ /* @__PURE__ */ jsx(
1380
+ SearchCombobox,
1381
+ {
1382
+ ref: inputRef,
1383
+ rootId,
1384
+ value: controller.query,
1385
+ onChange: (next) => {
1386
+ controller.setQuery(next);
1387
+ if (!open && !disabled) setOpen(true);
1388
+ },
1389
+ onFocus: handleFocus,
1390
+ onKeyDown: (event) => {
1391
+ if (event.key === "ArrowDown" && !open && !disabled) {
1392
+ event.preventDefault();
1393
+ setOpen(true);
1394
+ return;
1395
+ }
1396
+ controller.handleKeyDown(event);
1397
+ },
1398
+ placeholder,
1399
+ label,
1400
+ hideLabelFromVision,
1401
+ help,
1402
+ disabled,
1403
+ open: open && !disabled,
1404
+ listboxId,
1405
+ activeOptionId,
1406
+ labels
1407
+ }
1408
+ ),
1409
+ disabled ? /* @__PURE__ */ jsx(MaxedOutBanner, { role: "status", style: { marginTop: 8 }, children: labels.limitReached ?? __("Selection limit reached.", "valu-block-components") }) : null,
1410
+ open && !disabled && inputRef.current ? /* @__PURE__ */ jsx(
1411
+ Popover,
1412
+ {
1413
+ anchor: inputRef.current.closest("div") ?? inputRef.current,
1414
+ placement: "bottom-start",
1415
+ focusOnMount: false,
1416
+ onClose: () => setOpen(false),
1417
+ onFocusOutside: () => setOpen(false),
1418
+ shift: true,
1419
+ flip: true,
1420
+ resize: true,
1421
+ offset: 4,
1422
+ className: "valu-content-picker__popover",
1423
+ children: /* @__PURE__ */ jsx(EmotionCacheProvider, { children: /* @__PURE__ */ jsx(
1424
+ "div",
1425
+ {
1426
+ ref: popoverBodyRef,
1427
+ style: {
1428
+ minWidth: inputRef.current.offsetWidth,
1429
+ width: "100%"
1430
+ },
1431
+ children: /* @__PURE__ */ jsx(
1432
+ SuggestionList,
1433
+ {
1434
+ listboxId,
1435
+ rootId,
1436
+ groups: controller.groups,
1437
+ suggestions: controller.suggestions,
1438
+ kind,
1439
+ loading: controller.loading,
1440
+ fetchingMore: controller.fetchingMore,
1441
+ hasMore: controller.hasMore,
1442
+ error: controller.error,
1443
+ query: controller.debouncedQuery,
1444
+ highlightedIndex: controller.highlightedIndex,
1445
+ selectedKeys,
1446
+ icons,
1447
+ labels,
1448
+ showUrl: showUrl ?? true,
1449
+ headerSlot: !disableTypeFilter && types && types.length > 1 ? /* @__PURE__ */ jsx(
1450
+ TypeFilter,
1451
+ {
1452
+ types,
1453
+ kind,
1454
+ active: controller.activeTypeFilter,
1455
+ onChange: controller.setActiveTypeFilter
1456
+ }
1457
+ ) : null,
1458
+ renderSuggestion,
1459
+ renderGroupHeading,
1460
+ onHover: (i) => controller.setHighlightedIndex(i),
1461
+ onSelect: (item) => controller.select(item),
1462
+ onLoadMore: () => controller.loadMore()
1463
+ }
1464
+ )
1465
+ }
1466
+ ) })
1467
+ }
1468
+ ) : null
1469
+ ] });
1470
+ };
1471
+ var ContentPickerInline = (props) => {
1472
+ const {
1473
+ rootId,
1474
+ label,
1475
+ hideLabelFromVision,
1476
+ help,
1477
+ placeholder,
1478
+ kind,
1479
+ types,
1480
+ perPage,
1481
+ inputDelayMs,
1482
+ excludeCurrentPost,
1483
+ uniqueItems,
1484
+ maxItems,
1485
+ groupByType: groupByType2,
1486
+ fetchOnOpen,
1487
+ filter,
1488
+ request,
1489
+ renderSuggestion,
1490
+ renderGroupHeading,
1491
+ icons,
1492
+ disableTypeFilter,
1493
+ showUrl,
1494
+ value,
1495
+ onChange,
1496
+ labels
1497
+ } = props;
1498
+ const inputRef = useRef(null);
1499
+ const clearQueryRef = useRef(null);
1500
+ const controller = useContentPickerController({
1501
+ kind,
1502
+ types,
1503
+ perPage,
1504
+ inputDelayMs,
1505
+ excludeCurrentPost,
1506
+ uniqueItems,
1507
+ maxItems,
1508
+ groupByType: groupByType2,
1509
+ filter,
1510
+ request,
1511
+ value,
1512
+ onChange,
1513
+ active: true,
1514
+ fetchEmpty: fetchOnOpen || value.length === 0,
1515
+ onSelectSideEffect: () => {
1516
+ clearQueryRef.current?.();
1517
+ requestAnimationFrame(() => inputRef.current?.focus());
1518
+ }
1519
+ });
1520
+ clearQueryRef.current = () => controller.setQuery("");
1521
+ const listboxId = aria(rootId, "listbox");
1522
+ const activeOptionId = controller.highlightedIndex >= 0 ? aria(rootId, `option-${controller.highlightedIndex}`) : void 0;
1523
+ const selectedKeys = useMemo(() => new Set(value.map((v) => v.key)), [value]);
1524
+ const disabled = controller.maxedOut;
1525
+ const shouldShowList = !disabled && (controller.debouncedQuery.trim().length > 0 || fetchOnOpen || value.length === 0);
1526
+ return /* @__PURE__ */ jsxs("div", { children: [
1527
+ /* @__PURE__ */ jsx(
1528
+ SearchCombobox,
1529
+ {
1530
+ ref: inputRef,
1531
+ rootId,
1532
+ value: controller.query,
1533
+ onChange: (next) => controller.setQuery(next),
1534
+ onKeyDown: controller.handleKeyDown,
1535
+ placeholder,
1536
+ label,
1537
+ hideLabelFromVision,
1538
+ help,
1539
+ disabled,
1540
+ open: shouldShowList,
1541
+ listboxId,
1542
+ activeOptionId,
1543
+ labels
1544
+ }
1545
+ ),
1546
+ disabled ? /* @__PURE__ */ jsx(MaxedOutBanner, { role: "status", style: { marginTop: 8 }, children: labels.limitReached ?? __("Selection limit reached.", "valu-block-components") }) : null,
1547
+ shouldShowList ? /* @__PURE__ */ jsx(SuggestionsShell, { inline: true, style: { marginTop: 8 }, children: /* @__PURE__ */ jsx(
1548
+ SuggestionList,
1549
+ {
1550
+ listboxId,
1551
+ rootId,
1552
+ groups: controller.groups,
1553
+ suggestions: controller.suggestions,
1554
+ kind,
1555
+ loading: controller.loading,
1556
+ fetchingMore: controller.fetchingMore,
1557
+ hasMore: controller.hasMore,
1558
+ error: controller.error,
1559
+ query: controller.debouncedQuery,
1560
+ highlightedIndex: controller.highlightedIndex,
1561
+ selectedKeys,
1562
+ icons,
1563
+ labels,
1564
+ showUrl: showUrl ?? true,
1565
+ headerSlot: !disableTypeFilter && types && types.length > 1 ? /* @__PURE__ */ jsx(
1566
+ TypeFilter,
1567
+ {
1568
+ types,
1569
+ kind,
1570
+ active: controller.activeTypeFilter,
1571
+ onChange: controller.setActiveTypeFilter
1572
+ }
1573
+ ) : null,
1574
+ renderSuggestion,
1575
+ renderGroupHeading,
1576
+ onHover: (i) => controller.setHighlightedIndex(i),
1577
+ onSelect: (item) => controller.select(item),
1578
+ onLoadMore: () => controller.loadMore()
1579
+ }
1580
+ ) }) : null
1581
+ ] });
1582
+ };
1583
+ var Pill2 = styled.span`
1584
+ display: inline-flex;
1585
+ align-items: center;
1586
+ padding: 1px 6px;
1587
+ background: transparent;
1588
+ color: ${tokens.gray600};
1589
+ border: 1px solid ${tokens.gray300};
1590
+ border-radius: 999px;
1591
+ font-size: 10px;
1592
+ font-weight: ${tokens.fontWeightBold};
1593
+ letter-spacing: 0.04em;
1594
+ text-transform: uppercase;
1595
+ white-space: nowrap;
1596
+ line-height: 1.6;
1597
+ `;
1598
+ var TypeBadge = ({ type, kind, label }) => /* @__PURE__ */ jsx(Pill2, { children: label ?? humanizeType(type, kind) });
1599
+ var SelectedItem = ({
1600
+ item,
1601
+ index,
1602
+ total,
1603
+ orderable,
1604
+ showTypeBadge,
1605
+ icons,
1606
+ labels,
1607
+ onRemove,
1608
+ onMoveUp,
1609
+ onMoveDown,
1610
+ render
1611
+ }) => {
1612
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
1613
+ id: item.key,
1614
+ disabled: !orderable
1615
+ });
1616
+ const style = {
1617
+ transform: CSS.Translate.toString(transform),
1618
+ transition,
1619
+ opacity: isDragging ? 0 : 1
1620
+ };
1621
+ const isMissing = item.missing === "deleted";
1622
+ const isTrash = item.missing === "trash";
1623
+ const missingLabel = labels.missing ?? __("This item is no longer available.", "valu-block-components");
1624
+ const trashLabel = labels.statusTrash ?? __("This item is in the trash.", "valu-block-components");
1625
+ const removeLabel = labels.remove ? labels.remove : sprintf(
1626
+ /* translators: %s: item title */
1627
+ __("Remove %s", "valu-block-components"),
1628
+ item.title
1629
+ );
1630
+ const moveUpLabel = labels.moveUp ? labels.moveUp : sprintf(
1631
+ /* translators: %s: item title */
1632
+ __("Move %s up", "valu-block-components"),
1633
+ item.title
1634
+ );
1635
+ const moveDownLabel = labels.moveDown ? labels.moveDown : sprintf(
1636
+ /* translators: %s: item title */
1637
+ __("Move %s down", "valu-block-components"),
1638
+ item.title
1639
+ );
1640
+ const hasAbnormalStatus = item.status && item.status !== "publish";
1641
+ const resolvedIcon = resolveItemIcon(item, icons);
1642
+ const hasThumbImage = Boolean(item.thumbnailUrl) && !isMissing && !isTrash;
1643
+ const isAvatar = item.kind === "user" && hasThumbImage;
1644
+ const hasVisual = isMissing || isTrash || hasThumbImage || Boolean(resolvedIcon);
1645
+ return /* @__PURE__ */ jsxs(
1646
+ SelectedRow,
1647
+ {
1648
+ ref: setNodeRef,
1649
+ style,
1650
+ dragging: isDragging,
1651
+ missing: isMissing || isTrash,
1652
+ "aria-setsize": total,
1653
+ "aria-posinset": index + 1,
1654
+ children: [
1655
+ orderable ? /* @__PURE__ */ jsx(
1656
+ DragHandleButton,
1657
+ {
1658
+ type: "button",
1659
+ "aria-label": labels.dragHandle ?? __("Reorder item", "valu-block-components"),
1660
+ ...attributes,
1661
+ ...listeners,
1662
+ children: /* @__PURE__ */ jsx(Icon, { icon: dragHandle, size: 14 })
1663
+ }
1664
+ ) : null,
1665
+ hasVisual ? /* @__PURE__ */ jsx(
1666
+ Thumb,
1667
+ {
1668
+ hasImage: hasThumbImage && !isAvatar,
1669
+ avatar: isAvatar,
1670
+ missing: isMissing || isTrash,
1671
+ children: isMissing || isTrash ? /* @__PURE__ */ jsx(Icon, { icon: caution, size: 22 }) : hasThumbImage ? /* @__PURE__ */ jsx("img", { src: item.thumbnailUrl, alt: "", loading: "lazy" }) : resolvedIcon ? /* @__PURE__ */ jsx(Icon, { icon: resolvedIcon, size: 22 }) : null
1672
+ }
1673
+ ) : null,
1674
+ render ? render(item) : /* @__PURE__ */ jsx(ItemBody, { children: /* @__PURE__ */ jsx(PrimaryText, { muted: isMissing || isTrash, title: item.title, children: isMissing ? missingLabel : isTrash ? trashLabel : item.title }) }),
1675
+ !isMissing && showTypeBadge ? /* @__PURE__ */ jsx(TypeBadge, { type: item.type, kind: item.kind }) : null,
1676
+ !isMissing && hasAbnormalStatus ? /* @__PURE__ */ jsx(StatusBadge, { status: item.status }) : null,
1677
+ /* @__PURE__ */ jsxs(ActionColumn, { children: [
1678
+ orderable ? /* @__PURE__ */ jsxs(Fragment, { children: [
1679
+ /* @__PURE__ */ jsx(
1680
+ Button,
1681
+ {
1682
+ size: "small",
1683
+ variant: "tertiary",
1684
+ icon: chevronUp,
1685
+ label: moveUpLabel,
1686
+ onClick: onMoveUp,
1687
+ disabled: index === 0 || !onMoveUp
1688
+ }
1689
+ ),
1690
+ /* @__PURE__ */ jsx(
1691
+ Button,
1692
+ {
1693
+ size: "small",
1694
+ variant: "tertiary",
1695
+ icon: chevronDown,
1696
+ label: moveDownLabel,
1697
+ onClick: onMoveDown,
1698
+ disabled: index === total - 1 || !onMoveDown
1699
+ }
1700
+ )
1701
+ ] }) : null,
1702
+ /* @__PURE__ */ jsx(
1703
+ Button,
1704
+ {
1705
+ size: "small",
1706
+ variant: "tertiary",
1707
+ icon: closeSmall,
1708
+ label: removeLabel,
1709
+ onClick: onRemove,
1710
+ isDestructive: true
1711
+ }
1712
+ )
1713
+ ] })
1714
+ ]
1715
+ }
1716
+ );
1717
+ };
1718
+ var DragChip = ({ title }) => /* @__PURE__ */ jsxs(ChipWrap, { children: [
1719
+ /* @__PURE__ */ jsx(Icon, { icon: dragHandle, size: 16 }),
1720
+ /* @__PURE__ */ jsx(ChipTitle, { children: title })
1721
+ ] });
1722
+ var entityKindFor = (kind) => {
1723
+ if (kind === "post") return "postType";
1724
+ if (kind === "term") return "taxonomy";
1725
+ return "root";
1726
+ };
1727
+ var entityNameFor = (kind, type) => {
1728
+ if (kind === "user") return "user";
1729
+ return type;
1730
+ };
1731
+ var useResolvedItems = (value) => {
1732
+ const distinctTypes = useMemo(() => {
1733
+ const map = /* @__PURE__ */ new Map();
1734
+ for (const item of value) {
1735
+ const key = `${item.kind}:${item.type}`;
1736
+ const bucket = map.get(key);
1737
+ if (bucket) bucket.ids.push(item.id);
1738
+ else map.set(key, { kind: item.kind, ids: [item.id] });
1739
+ }
1740
+ return Array.from(map.entries()).map(([key, v]) => ({ key, ...v }));
1741
+ }, [value]);
1742
+ const records = useSelect(
1743
+ (select) => {
1744
+ const core = select(store);
1745
+ const out = {};
1746
+ for (const item of value) {
1747
+ const kindKey = entityKindFor(item.kind);
1748
+ const nameKey = entityNameFor(item.kind, item.type);
1749
+ const record = core.getEntityRecord(kindKey, nameKey, item.id);
1750
+ const resolved = core.hasFinishedResolution("getEntityRecord", [
1751
+ kindKey,
1752
+ nameKey,
1753
+ item.id
1754
+ ]);
1755
+ out[`${item.kind}:${item.type}:${item.id}`] = {
1756
+ record: record ?? null,
1757
+ resolved
1758
+ };
1759
+ }
1760
+ return out;
1761
+ },
1762
+ [value, distinctTypes]
1763
+ );
1764
+ return useMemo(
1765
+ () => value.map((item) => {
1766
+ const resolvedEntry = records[`${item.kind}:${item.type}:${item.id}`];
1767
+ if (!resolvedEntry) return item;
1768
+ const { record, resolved } = resolvedEntry;
1769
+ if (!resolved) {
1770
+ return item;
1771
+ }
1772
+ if (!record) {
1773
+ return { ...item, missing: "deleted" };
1774
+ }
1775
+ if (record.status === "trash") {
1776
+ return { ...item, missing: "trash", status: "trash" };
1777
+ }
1778
+ const freshTitle = decodeTitle(record.title) || (record.name ? decodeTitle(record.name) : "");
1779
+ return {
1780
+ ...item,
1781
+ title: freshTitle || item.title,
1782
+ status: record.status ?? item.status,
1783
+ url: record.link ?? item.url
1784
+ };
1785
+ }),
1786
+ [value, records]
1787
+ );
1788
+ };
1789
+ var SelectedList = ({
1790
+ value,
1791
+ orderable,
1792
+ maxItems,
1793
+ labels,
1794
+ onChange,
1795
+ renderSelected,
1796
+ showHeader = true,
1797
+ showTypeBadge,
1798
+ icons
1799
+ }) => {
1800
+ const resolved = useResolvedItems(value);
1801
+ const [activeKey, setActiveKey] = useState(null);
1802
+ const sensors = useSensors(
1803
+ useSensor(MouseSensor, { activationConstraint: { distance: 5 } }),
1804
+ useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 6 } }),
1805
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
1806
+ );
1807
+ const ids = useMemo(() => resolved.map((i) => i.key), [resolved]);
1808
+ const activeItem = activeKey ? resolved.find((i) => i.key === activeKey) : null;
1809
+ const remove = useCallback(
1810
+ (key) => onChange(value.filter((i) => i.key !== key)),
1811
+ [value, onChange]
1812
+ );
1813
+ const reorder = useCallback(
1814
+ (from, to) => {
1815
+ if (from === to) return;
1816
+ onChange(arrayMove(value, from, to));
1817
+ },
1818
+ [value, onChange]
1819
+ );
1820
+ const handleDragStart = useCallback(
1821
+ (event) => setActiveKey(String(event.active.id)),
1822
+ []
1823
+ );
1824
+ const handleDragEnd = useCallback(
1825
+ (event) => {
1826
+ setActiveKey(null);
1827
+ const { active, over } = event;
1828
+ if (!over || active.id === over.id) return;
1829
+ const from = ids.indexOf(String(active.id));
1830
+ const to = ids.indexOf(String(over.id));
1831
+ if (from >= 0 && to >= 0) reorder(from, to);
1832
+ },
1833
+ [ids, reorder]
1834
+ );
1835
+ if (resolved.length === 0) return null;
1836
+ const isSingleSelect = maxItems === 1;
1837
+ const shouldShowHeader = showHeader && !isSingleSelect;
1838
+ const header = shouldShowHeader ? /* @__PURE__ */ jsxs(HeaderRow, { children: [
1839
+ /* @__PURE__ */ jsx(HeaderTitle, { children: resolved.length === 1 ? labels.selectedSingular ?? __("Selected", "valu-block-components") : labels.selectedPlural ?? __("Selected", "valu-block-components") }),
1840
+ /* @__PURE__ */ jsx(CountPill, { "aria-hidden": "true", children: Number.isFinite(maxItems) ? sprintf(
1841
+ /* translators: 1: current count, 2: max count */
1842
+ "%1$d / %2$d",
1843
+ resolved.length,
1844
+ maxItems
1845
+ ) : String(resolved.length) })
1846
+ ] }) : null;
1847
+ const rows = resolved.map((item, index) => /* @__PURE__ */ jsx(
1848
+ SelectedItem,
1849
+ {
1850
+ item,
1851
+ index,
1852
+ total: resolved.length,
1853
+ orderable,
1854
+ showTypeBadge,
1855
+ icons,
1856
+ labels,
1857
+ onRemove: () => remove(item.key),
1858
+ onMoveUp: index > 0 ? () => reorder(index, index - 1) : void 0,
1859
+ onMoveDown: index < resolved.length - 1 ? () => reorder(index, index + 1) : void 0,
1860
+ render: renderSelected
1861
+ },
1862
+ item.key
1863
+ ));
1864
+ if (!orderable) {
1865
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1866
+ header,
1867
+ /* @__PURE__ */ jsx(SelectedListEl, { role: "list", children: rows })
1868
+ ] });
1869
+ }
1870
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1871
+ header,
1872
+ /* @__PURE__ */ jsxs(
1873
+ DndContext,
1874
+ {
1875
+ sensors,
1876
+ collisionDetection: closestCenter,
1877
+ onDragStart: handleDragStart,
1878
+ onDragEnd: handleDragEnd,
1879
+ onDragCancel: () => setActiveKey(null),
1880
+ children: [
1881
+ /* @__PURE__ */ jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsx(SelectedListEl, { role: "list", children: rows }) }),
1882
+ /* @__PURE__ */ jsx(DragOverlay, { dropAnimation: defaultDropAnimation, children: activeItem ? /* @__PURE__ */ jsx(DragChip, { title: activeItem.title }) : null })
1883
+ ]
1884
+ }
1885
+ )
1886
+ ] });
1887
+ };
1888
+ var DEFAULT_LABELS = {};
1889
+ var defaultTypesFor = (kind) => {
1890
+ if (kind === "post") return ["post", "page"];
1891
+ if (kind === "term") return ["category", "post_tag"];
1892
+ return [];
1893
+ };
1894
+ var makeQueryClient = () => new QueryClient({
1895
+ defaultOptions: {
1896
+ queries: { retry: 1, refetchOnWindowFocus: false }
1897
+ }
1898
+ });
1899
+ var ContentPicker = ({
1900
+ label,
1901
+ hideLabelFromVision = false,
1902
+ help,
1903
+ kind = "post",
1904
+ types,
1905
+ variant = "popover",
1906
+ value: controlledValue,
1907
+ defaultValue,
1908
+ onChange,
1909
+ maxItems = Infinity,
1910
+ orderable,
1911
+ placeholder,
1912
+ perPage,
1913
+ inputDelayMs = 250,
1914
+ fetchOnOpen = false,
1915
+ excludeCurrentPost = true,
1916
+ uniqueItems = true,
1917
+ groupByType: groupByType2,
1918
+ showTypeBadge,
1919
+ icons,
1920
+ disableTypeFilter,
1921
+ showUrl,
1922
+ filter,
1923
+ request,
1924
+ renderSuggestion,
1925
+ renderSelected,
1926
+ renderGroupHeading,
1927
+ labels = DEFAULT_LABELS,
1928
+ className,
1929
+ id
1930
+ }) => {
1931
+ const reactId = useId();
1932
+ const rootId = id ?? `valu-cp-${reactId.replace(/:/g, "")}`;
1933
+ const [uncontrolled, setUncontrolled] = useState(defaultValue ?? []);
1934
+ const isControlled = controlledValue !== void 0;
1935
+ const value = isControlled ? controlledValue : uncontrolled;
1936
+ const handleChange = useCallback(
1937
+ (next) => {
1938
+ if (!isControlled) setUncontrolled(next);
1939
+ onChange?.(next);
1940
+ },
1941
+ [isControlled, onChange]
1942
+ );
1943
+ const effectiveTypes = types ?? defaultTypesFor(kind);
1944
+ const effectivePerPage = perPage ?? (variant === "popover" ? 10 : 20);
1945
+ const effectiveOrderable = orderable ?? (Number.isFinite(maxItems) ? maxItems > 1 : true);
1946
+ const effectiveGroupByType = groupByType2 ?? effectiveTypes.length > 1;
1947
+ const effectiveShowTypeBadge = showTypeBadge ?? effectiveTypes.length > 1;
1948
+ const queryClientRef = useRef(null);
1949
+ if (!queryClientRef.current) queryClientRef.current = makeQueryClient();
1950
+ const showInput = !(Number.isFinite(maxItems) && value.length >= maxItems);
1951
+ const variantProps = useMemo(
1952
+ () => ({
1953
+ rootId,
1954
+ label,
1955
+ hideLabelFromVision,
1956
+ help,
1957
+ placeholder,
1958
+ kind,
1959
+ types: effectiveTypes,
1960
+ perPage: effectivePerPage,
1961
+ inputDelayMs,
1962
+ excludeCurrentPost,
1963
+ uniqueItems,
1964
+ maxItems,
1965
+ groupByType: effectiveGroupByType,
1966
+ fetchOnOpen,
1967
+ filter,
1968
+ request,
1969
+ renderSuggestion,
1970
+ renderGroupHeading,
1971
+ icons,
1972
+ disableTypeFilter,
1973
+ showUrl,
1974
+ value,
1975
+ onChange: handleChange,
1976
+ labels
1977
+ }),
1978
+ [
1979
+ rootId,
1980
+ label,
1981
+ hideLabelFromVision,
1982
+ help,
1983
+ placeholder,
1984
+ kind,
1985
+ effectiveTypes,
1986
+ effectivePerPage,
1987
+ inputDelayMs,
1988
+ excludeCurrentPost,
1989
+ uniqueItems,
1990
+ maxItems,
1991
+ effectiveGroupByType,
1992
+ fetchOnOpen,
1993
+ filter,
1994
+ request,
1995
+ renderSuggestion,
1996
+ renderGroupHeading,
1997
+ icons,
1998
+ disableTypeFilter,
1999
+ showUrl,
2000
+ value,
2001
+ handleChange,
2002
+ labels
2003
+ ]
2004
+ );
2005
+ return /* @__PURE__ */ jsx(EmotionCacheProvider, { children: /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClientRef.current, children: /* @__PURE__ */ jsxs(Root, { id: rootId, className, children: [
2006
+ showInput ? variant === "popover" ? /* @__PURE__ */ jsx(ContentPickerPopover, { ...variantProps }) : /* @__PURE__ */ jsx(ContentPickerInline, { ...variantProps }) : null,
2007
+ /* @__PURE__ */ jsx(
2008
+ SelectedList,
2009
+ {
2010
+ value,
2011
+ orderable: effectiveOrderable && value.length > 1,
2012
+ maxItems,
2013
+ labels,
2014
+ onChange: handleChange,
2015
+ renderSelected,
2016
+ showHeader: value.length > 0,
2017
+ showTypeBadge: effectiveShowTypeBadge,
2018
+ icons
2019
+ }
2020
+ )
2021
+ ] }) }) });
2022
+ };
2023
+
2024
+ export { ContentPicker };
2025
+ //# sourceMappingURL=index.js.map
2026
+ //# sourceMappingURL=index.js.map