@longd/layout-ui 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,1083 @@
1
+ // src/layout/select.tsx
2
+ import * as React from "react";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+ import { Popover } from "@base-ui/react/popover";
12
+ import { Tooltip } from "@base-ui/react/tooltip";
13
+ import { Command } from "cmdk";
14
+ import {
15
+ DndContext,
16
+ KeyboardSensor,
17
+ PointerSensor,
18
+ closestCenter,
19
+ useSensor,
20
+ useSensors
21
+ } from "@dnd-kit/core";
22
+ import {
23
+ SortableContext,
24
+ arrayMove,
25
+ useSortable,
26
+ verticalListSortingStrategy
27
+ } from "@dnd-kit/sortable";
28
+ import {
29
+ restrictToFirstScrollableAncestor,
30
+ restrictToVerticalAxis
31
+ } from "@dnd-kit/modifiers";
32
+ import { defaultRangeExtractor, useVirtualizer } from "@tanstack/react-virtual";
33
+ import { Check, ChevronDown, GripVertical, X } from "lucide-react";
34
+
35
+ // src/lib/utils.ts
36
+ import { clsx } from "clsx";
37
+ import { twMerge } from "tailwind-merge";
38
+ function cn(...inputs) {
39
+ return twMerge(clsx(inputs));
40
+ }
41
+
42
+ // src/layout/select.tsx
43
+ import { jsx, jsxs } from "react/jsx-runtime";
44
+ function resolveIcon(icon) {
45
+ if (icon === void 0 || icon === null) return null;
46
+ if (typeof icon === "function") return icon();
47
+ return icon;
48
+ }
49
+ function flattenOptions(options) {
50
+ const result = [];
51
+ for (const opt of options) {
52
+ if (opt.children && opt.children.length > 0) {
53
+ result.push(...flattenOptions(opt.children));
54
+ } else {
55
+ result.push(opt);
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+ function buildDisplayRows(options) {
61
+ const rows = [];
62
+ for (const opt of options) {
63
+ if (opt.children && opt.children.length > 0) {
64
+ rows.push({
65
+ kind: "group-header",
66
+ label: opt.label,
67
+ groupValue: opt.value
68
+ });
69
+ for (const child of opt.children) {
70
+ rows.push({ kind: "option", option: child, groupValue: opt.value });
71
+ }
72
+ } else {
73
+ rows.push({ kind: "option", option: opt });
74
+ }
75
+ }
76
+ return rows;
77
+ }
78
+ function hasGroups(options) {
79
+ return options.some((o) => o.children && o.children.length > 0);
80
+ }
81
+ function optionEq(a, b) {
82
+ return a.value === b.value;
83
+ }
84
+ function isSelected(option, value) {
85
+ if (!value) return false;
86
+ if (Array.isArray(value)) return value.some((v) => optionEq(v, option));
87
+ return optionEq(value, option);
88
+ }
89
+ function SortableItem({ id, children, disabled }) {
90
+ const {
91
+ setNodeRef,
92
+ attributes,
93
+ listeners,
94
+ transform,
95
+ transition,
96
+ isDragging
97
+ } = useSortable({ id, disabled });
98
+ const style = {
99
+ transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
100
+ transition,
101
+ opacity: isDragging ? 0.5 : 1,
102
+ position: "relative",
103
+ zIndex: isDragging ? 50 : void 0
104
+ };
105
+ return /* @__PURE__ */ jsxs("div", { ref: setNodeRef, style, className: "flex items-center", children: [
106
+ !disabled && /* @__PURE__ */ jsx(
107
+ "button",
108
+ {
109
+ type: "button",
110
+ className: "flex shrink-0 cursor-grab items-center px-1 text-muted-foreground hover:text-foreground active:cursor-grabbing",
111
+ ...attributes,
112
+ ...listeners,
113
+ children: /* @__PURE__ */ jsx(GripVertical, { className: "size-3.5" })
114
+ }
115
+ ),
116
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children })
117
+ ] });
118
+ }
119
+ function MaybeTooltip({
120
+ tooltip,
121
+ children
122
+ }) {
123
+ if (!tooltip) return children;
124
+ return /* @__PURE__ */ jsxs(Tooltip.Root, { children: [
125
+ /* @__PURE__ */ jsx(Tooltip.Trigger, { render: children }),
126
+ /* @__PURE__ */ jsx(Tooltip.Portal, { children: /* @__PURE__ */ jsx(Tooltip.Positioner, { sideOffset: 6, children: /* @__PURE__ */ jsx(Tooltip.Popup, { className: "rounded-md bg-foreground px-2.5 py-1 text-xs text-background shadow-md", children: tooltip }) }) })
127
+ ] });
128
+ }
129
+ function Chip({
130
+ option,
131
+ onRemove,
132
+ readOnly,
133
+ disabled,
134
+ className,
135
+ partial
136
+ }) {
137
+ return /* @__PURE__ */ jsxs(
138
+ "span",
139
+ {
140
+ "data-partial-chip": partial || void 0,
141
+ className: cn(
142
+ "inline-flex max-w-35 items-center gap-1 rounded-md border border-border bg-secondary px-2 py-0.5 text-xs leading-5 text-secondary-foreground",
143
+ disabled && "opacity-50",
144
+ className
145
+ ),
146
+ children: [
147
+ option.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&_svg]:size-3", children: resolveIcon(option.icon) }),
148
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: option.label }),
149
+ !readOnly && !disabled && onRemove && /* @__PURE__ */ jsx(
150
+ "button",
151
+ {
152
+ type: "button",
153
+ className: "ml-0.5 flex shrink-0 items-center rounded-sm text-muted-foreground hover:text-foreground",
154
+ onClick: (e) => {
155
+ e.stopPropagation();
156
+ onRemove();
157
+ },
158
+ tabIndex: -1,
159
+ "aria-label": `Remove ${option.label}`,
160
+ children: /* @__PURE__ */ jsx(X, { className: "size-3" })
161
+ }
162
+ )
163
+ ]
164
+ }
165
+ );
166
+ }
167
+ function OverflowBadge({
168
+ items,
169
+ onRemove
170
+ }) {
171
+ if (items.length === 0) return null;
172
+ return /* @__PURE__ */ jsxs(Tooltip.Root, { children: [
173
+ /* @__PURE__ */ jsx(
174
+ Tooltip.Trigger,
175
+ {
176
+ render: /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center rounded-md border border-border bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground", children: [
177
+ "+",
178
+ items.length
179
+ ] })
180
+ }
181
+ ),
182
+ /* @__PURE__ */ jsx(Tooltip.Portal, { children: /* @__PURE__ */ jsx(Tooltip.Positioner, { sideOffset: 6, children: /* @__PURE__ */ jsx(Tooltip.Popup, { className: "max-w-xs rounded-md bg-foreground px-3 py-2 text-xs text-background shadow-md", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: items.map((item) => /* @__PURE__ */ jsxs(
183
+ "span",
184
+ {
185
+ className: "inline-flex items-center gap-1 rounded-md border border-background/20 bg-background/10 px-1.5 py-0.5 text-xs leading-4 text-background",
186
+ children: [
187
+ item.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&_svg]:size-3", children: resolveIcon(item.icon) }),
188
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: item.label }),
189
+ onRemove && /* @__PURE__ */ jsx(
190
+ "button",
191
+ {
192
+ type: "button",
193
+ className: "ml-0.5 flex shrink-0 items-center rounded-sm text-background/60 hover:text-background",
194
+ onClick: (e) => {
195
+ e.stopPropagation();
196
+ onRemove(item);
197
+ },
198
+ tabIndex: -1,
199
+ "aria-label": `Remove ${item.label}`,
200
+ children: /* @__PURE__ */ jsx(X, { className: "size-3" })
201
+ }
202
+ )
203
+ ]
204
+ },
205
+ item.value
206
+ )) }) }) }) })
207
+ ] });
208
+ }
209
+ function SingleTriggerContent({
210
+ value,
211
+ placeholder
212
+ }) {
213
+ if (!value) {
214
+ return /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: placeholder });
215
+ }
216
+ return /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 truncate", children: [
217
+ value.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&_svg]:size-4", children: resolveIcon(value.icon) }),
218
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: value.label })
219
+ ] });
220
+ }
221
+ function MultipleTriggerContent({
222
+ value,
223
+ placeholder,
224
+ collapsed,
225
+ showItemsLength,
226
+ onRemove,
227
+ readOnly,
228
+ disabled
229
+ }) {
230
+ const chipRowClass = "flex items-center gap-1";
231
+ const wrapperRef = useRef(null);
232
+ const measureRef = useRef(null);
233
+ const [visibleCount, setVisibleCount] = useState(value.length);
234
+ const [lastIsPartial, setLastIsPartial] = useState(false);
235
+ const [measured, setMeasured] = useState(false);
236
+ const measureCount = Math.min(value.length, 20);
237
+ const calculate = React.useEffectEvent(() => {
238
+ const measureContainer = measureRef.current;
239
+ if (!measureContainer) return;
240
+ const children = Array.from(measureContainer.children);
241
+ const containerRight = measureContainer.getBoundingClientRect().right;
242
+ const gap = parseFloat(getComputedStyle(measureContainer).columnGap) || 0;
243
+ const estimateBadgeWidth = (overflowCount) => overflowCount > 0 ? 14 + 8 * String(overflowCount).length : 0;
244
+ let count = 0;
245
+ let partial = false;
246
+ for (const child of children) {
247
+ const childRight = child.getBoundingClientRect().right;
248
+ const overflow = value.length - (count + 1);
249
+ const reserve = overflow > 0 ? Math.max(40, estimateBadgeWidth(overflow)) : 0;
250
+ if (childRight + reserve <= containerRight) {
251
+ count++;
252
+ } else {
253
+ break;
254
+ }
255
+ }
256
+ count = Math.max(1, count);
257
+ if (count < value.length && children.length >= count) {
258
+ const lastRight = children[count - 1].getBoundingClientRect().right;
259
+ const needsBadge = count + 1 < value.length;
260
+ const badgeReserve = needsBadge ? Math.max(40, estimateBadgeWidth(value.length - count - 1)) : 0;
261
+ const spaceForPartial = containerRight - badgeReserve - lastRight - gap;
262
+ const nextChip = value[count];
263
+ let minPartialWidth = 50;
264
+ if (nextChip.icon && children.length > count) {
265
+ const iconWrapper = children[count].firstElementChild;
266
+ if (iconWrapper) {
267
+ minPartialWidth += iconWrapper.getBoundingClientRect().width + gap;
268
+ }
269
+ }
270
+ if (spaceForPartial >= minPartialWidth) {
271
+ count++;
272
+ partial = true;
273
+ }
274
+ }
275
+ setVisibleCount(count);
276
+ setLastIsPartial(partial);
277
+ setMeasured(true);
278
+ });
279
+ useLayoutEffect(() => {
280
+ if (collapsed || value.length === 0) {
281
+ setVisibleCount(value.length);
282
+ setLastIsPartial(false);
283
+ setMeasured(true);
284
+ return;
285
+ }
286
+ const wrapper = wrapperRef.current;
287
+ const container = measureRef.current;
288
+ if (!wrapper || !container) return;
289
+ calculate();
290
+ const observer = new ResizeObserver(calculate);
291
+ observer.observe(wrapper);
292
+ return () => observer.disconnect();
293
+ }, [collapsed, value]);
294
+ if (value.length === 0) {
295
+ return /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: placeholder });
296
+ }
297
+ const measureLayer = !collapsed && /* @__PURE__ */ jsx(
298
+ "div",
299
+ {
300
+ ref: measureRef,
301
+ className: cn(
302
+ "pointer-events-none absolute inset-0 overflow-hidden opacity-0",
303
+ chipRowClass
304
+ ),
305
+ "aria-hidden": true,
306
+ children: value.slice(0, measureCount).map((opt) => /* @__PURE__ */ jsx(
307
+ Chip,
308
+ {
309
+ option: opt,
310
+ onRemove: onRemove ? () => {
311
+ } : void 0,
312
+ readOnly,
313
+ disabled,
314
+ className: "shrink-0"
315
+ },
316
+ opt.value
317
+ ))
318
+ }
319
+ );
320
+ const showContent = collapsed || measured;
321
+ if (!showContent) {
322
+ return /* @__PURE__ */ jsxs(
323
+ "div",
324
+ {
325
+ ref: wrapperRef,
326
+ className: cn("relative min-w-0 flex-1 overflow-hidden", chipRowClass),
327
+ children: [
328
+ measureLayer,
329
+ /* @__PURE__ */ jsxs("span", { className: "truncate text-muted-foreground", children: [
330
+ value.length,
331
+ " selected"
332
+ ] })
333
+ ]
334
+ }
335
+ );
336
+ }
337
+ let maxVisible = collapsed ? value.length : visibleCount;
338
+ const hasExplicitLimit = !collapsed && showItemsLength !== void 0;
339
+ if (hasExplicitLimit) {
340
+ maxVisible = showItemsLength;
341
+ }
342
+ const hasOverflow = maxVisible < value.length;
343
+ const displayed = value.slice(0, maxVisible);
344
+ const overflowItems = value.slice(maxVisible);
345
+ return /* @__PURE__ */ jsxs(
346
+ "div",
347
+ {
348
+ ref: wrapperRef,
349
+ className: cn("relative min-w-0 flex-1", chipRowClass),
350
+ children: [
351
+ measureLayer,
352
+ /* @__PURE__ */ jsx(
353
+ "div",
354
+ {
355
+ className: cn(
356
+ "min-w-0 flex-1",
357
+ chipRowClass,
358
+ collapsed ? "flex-wrap" : "overflow-hidden"
359
+ ),
360
+ children: displayed.map((opt, i) => {
361
+ const isPartial = (hasOverflow || lastIsPartial) && !hasExplicitLimit && i === displayed.length - 1;
362
+ const shouldShrink = isPartial || hasExplicitLimit;
363
+ return /* @__PURE__ */ jsx(
364
+ Chip,
365
+ {
366
+ option: opt,
367
+ onRemove: onRemove ? () => onRemove(opt) : void 0,
368
+ readOnly,
369
+ disabled,
370
+ partial: isPartial,
371
+ className: shouldShrink ? "min-w-0 shrink" : "shrink-0"
372
+ },
373
+ opt.value
374
+ );
375
+ })
376
+ }
377
+ ),
378
+ overflowItems.length > 0 && /* @__PURE__ */ jsx("span", { className: "shrink-0", children: /* @__PURE__ */ jsx(OverflowBadge, { items: overflowItems, onRemove }) })
379
+ ]
380
+ }
381
+ );
382
+ }
383
+ function OptionRow({
384
+ option,
385
+ selected,
386
+ renderItem,
387
+ onSelect,
388
+ highlighted = false
389
+ }) {
390
+ const content = renderItem ? renderItem(option, {
391
+ selected,
392
+ highlighted,
393
+ disabled: !!option.disabled
394
+ }) : /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-2", children: [
395
+ option.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&_svg]:size-4", children: resolveIcon(option.icon) }),
396
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: option.label }),
397
+ selected && /* @__PURE__ */ jsx("span", { className: "ml-auto flex shrink-0 items-center text-primary", children: /* @__PURE__ */ jsx(Check, { className: "size-4" }) })
398
+ ] });
399
+ const row = /* @__PURE__ */ jsx(
400
+ Command.Item,
401
+ {
402
+ value: `${option.value}`,
403
+ disabled: option.disabled,
404
+ onSelect: () => {
405
+ if (!option.disabled) onSelect(option);
406
+ },
407
+ className: cn(
408
+ "relative flex w-full cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
409
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
410
+ option.disabled && "pointer-events-none opacity-50"
411
+ ),
412
+ "data-highlighted": highlighted || void 0,
413
+ children: content
414
+ }
415
+ );
416
+ if (option.disabled && option.disabledTooltip) {
417
+ return /* @__PURE__ */ jsx(MaybeTooltip, { tooltip: option.disabledTooltip, children: /* @__PURE__ */ jsx("div", { children: row }) });
418
+ }
419
+ return row;
420
+ }
421
+ function VirtualList({
422
+ options,
423
+ items,
424
+ selectedValue,
425
+ renderItem,
426
+ onSelect,
427
+ sortable,
428
+ sortableAcrossGroups,
429
+ onSortEnd,
430
+ onGroupSortEnd,
431
+ onTreeSort
432
+ }) {
433
+ const parentRef = useRef(null);
434
+ const [activeId, setActiveId] = useState(null);
435
+ const [dragTree, setDragTree] = useState(null);
436
+ const effectiveOptions = dragTree ?? options;
437
+ const grouped = useMemo(() => hasGroups(effectiveOptions), [effectiveOptions]);
438
+ const displayRows = useMemo(
439
+ () => grouped ? buildDisplayRows(effectiveOptions) : void 0,
440
+ [grouped, effectiveOptions]
441
+ );
442
+ const flatDisplayRows = useMemo(
443
+ () => items.map((o) => ({ kind: "option", option: o })),
444
+ [items]
445
+ );
446
+ const virtualItems = displayRows ?? flatDisplayRows;
447
+ const activeIndex = useMemo(() => {
448
+ if (!activeId) return null;
449
+ const idx = virtualItems.findIndex(
450
+ (r) => r.kind === "option" && `${r.option.value}` === activeId
451
+ );
452
+ return idx !== -1 ? idx : null;
453
+ }, [activeId, virtualItems]);
454
+ const rangeExtractor = useCallback(
455
+ (range) => {
456
+ const result = defaultRangeExtractor(range);
457
+ if (activeIndex !== null && !result.includes(activeIndex)) {
458
+ result.push(activeIndex);
459
+ result.sort((a, b) => a - b);
460
+ }
461
+ return result;
462
+ },
463
+ [activeIndex]
464
+ );
465
+ const virtualizer = useVirtualizer({
466
+ count: virtualItems.length,
467
+ getScrollElement: () => parentRef.current,
468
+ estimateSize: (index) => virtualItems[index].kind === "group-header" ? 28 : 36,
469
+ overscan: 8,
470
+ rangeExtractor
471
+ });
472
+ useLayoutEffect(() => {
473
+ virtualizer.measure();
474
+ }, [virtualizer, displayRows]);
475
+ const sensors = useSensors(
476
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
477
+ useSensor(KeyboardSensor)
478
+ );
479
+ const flatSortableIds = useMemo(
480
+ () => virtualItems.filter(
481
+ (r) => r.kind === "option"
482
+ ).map((r) => `${r.option.value}`),
483
+ [virtualItems]
484
+ );
485
+ const sameGroupCollision = useCallback(
486
+ (args) => {
487
+ if (!displayRows) return closestCenter(args);
488
+ const draggedId = args.active.id;
489
+ const activeRow = displayRows.find(
490
+ (r) => r.kind === "option" && `${r.option.value}` === `${draggedId}`
491
+ );
492
+ if (!activeRow || activeRow.kind !== "option") return closestCenter(args);
493
+ const activeGroup = activeRow.groupValue;
494
+ const filtered = args.droppableContainers.filter((container) => {
495
+ const row = displayRows.find(
496
+ (r) => r.kind === "option" && `${r.option.value}` === `${container.id}`
497
+ );
498
+ return row && row.kind === "option" && row.groupValue === activeGroup;
499
+ });
500
+ return closestCenter({ ...args, droppableContainers: filtered });
501
+ },
502
+ [displayRows]
503
+ );
504
+ const sortingStrategy = useMemo(() => {
505
+ if (!grouped || !displayRows) return verticalListSortingStrategy;
506
+ const idToGroup = /* @__PURE__ */ new Map();
507
+ for (const row of displayRows) {
508
+ if (row.kind === "option") {
509
+ idToGroup.set(`${row.option.value}`, row.groupValue);
510
+ }
511
+ }
512
+ const noMove = { x: 0, y: 0, scaleX: 1, scaleY: 1 };
513
+ return (args) => {
514
+ const draggedId = flatSortableIds[args.activeIndex];
515
+ const currentId = flatSortableIds[args.index];
516
+ if (draggedId && currentId && idToGroup.get(draggedId) !== idToGroup.get(currentId)) {
517
+ return noMove;
518
+ }
519
+ return verticalListSortingStrategy(args);
520
+ };
521
+ }, [grouped, displayRows, flatSortableIds]);
522
+ const handleDragOver = useCallback(
523
+ (event) => {
524
+ if (!sortableAcrossGroups || !grouped) return;
525
+ const { active, over } = event;
526
+ if (!over || active.id === over.id) return;
527
+ const currentTree = dragTree ?? options;
528
+ const currentRows = buildDisplayRows(currentTree);
529
+ const activeRow = currentRows.find(
530
+ (r) => r.kind === "option" && `${r.option.value}` === `${active.id}`
531
+ );
532
+ const overRow = currentRows.find(
533
+ (r) => r.kind === "option" && `${r.option.value}` === `${over.id}`
534
+ );
535
+ if (!activeRow || activeRow.kind !== "option" || !overRow || overRow.kind !== "option")
536
+ return;
537
+ if (activeRow.groupValue === overRow.groupValue) return;
538
+ const newTree = currentTree.map((opt) => {
539
+ if (!opt.children) return opt;
540
+ if (opt.value === activeRow.groupValue) {
541
+ return {
542
+ ...opt,
543
+ children: opt.children.filter(
544
+ (c) => `${c.value}` !== `${active.id}`
545
+ )
546
+ };
547
+ }
548
+ if (opt.value === overRow.groupValue) {
549
+ const destChildren = opt.children.filter(
550
+ (c) => `${c.value}` !== `${active.id}`
551
+ );
552
+ const overIdx = destChildren.findIndex(
553
+ (c) => `${c.value}` === `${over.id}`
554
+ );
555
+ destChildren.splice(
556
+ overIdx !== -1 ? overIdx : destChildren.length,
557
+ 0,
558
+ activeRow.option
559
+ );
560
+ return { ...opt, children: destChildren };
561
+ }
562
+ return opt;
563
+ });
564
+ setDragTree(newTree);
565
+ },
566
+ [sortableAcrossGroups, grouped, dragTree, options]
567
+ );
568
+ const handleDragEndFlat = useCallback(
569
+ (event) => {
570
+ setActiveId(null);
571
+ setDragTree(null);
572
+ const { active, over } = event;
573
+ if (!over || active.id === over.id || !onSortEnd) return;
574
+ const oldIndex = items.findIndex((i) => `${i.value}` === `${active.id}`);
575
+ const newIndex = items.findIndex((i) => `${i.value}` === `${over.id}`);
576
+ if (oldIndex !== -1 && newIndex !== -1) {
577
+ onSortEnd(arrayMove(items, oldIndex, newIndex));
578
+ }
579
+ },
580
+ [items, onSortEnd]
581
+ );
582
+ const handleDragEndGrouped = useCallback(
583
+ (event) => {
584
+ setActiveId(null);
585
+ const { active, over } = event;
586
+ if (!over || active.id === over.id) {
587
+ if (dragTree) onTreeSort?.(dragTree);
588
+ setDragTree(null);
589
+ return;
590
+ }
591
+ if (!displayRows) {
592
+ setDragTree(null);
593
+ return;
594
+ }
595
+ const activeRow = displayRows.find(
596
+ (r) => r.kind === "option" && `${r.option.value}` === `${active.id}`
597
+ );
598
+ const overRow = displayRows.find(
599
+ (r) => r.kind === "option" && `${r.option.value}` === `${over.id}`
600
+ );
601
+ if (!activeRow || activeRow.kind !== "option" || !overRow || overRow.kind !== "option") {
602
+ setDragTree(null);
603
+ return;
604
+ }
605
+ const activeGroup = activeRow.groupValue;
606
+ const overGroup = overRow.groupValue;
607
+ const baseTree = dragTree ?? options;
608
+ if (activeGroup === overGroup) {
609
+ const groupChildren = displayRows.filter(
610
+ (r) => r.kind === "option" && r.groupValue === activeGroup
611
+ ).map((r) => r.option);
612
+ const oldIdx = groupChildren.findIndex(
613
+ (i) => `${i.value}` === `${active.id}`
614
+ );
615
+ const newIdx = groupChildren.findIndex(
616
+ (i) => `${i.value}` === `${over.id}`
617
+ );
618
+ if (oldIdx !== -1 && newIdx !== -1 && oldIdx !== newIdx) {
619
+ const reordered = arrayMove(groupChildren, oldIdx, newIdx);
620
+ if (dragTree) {
621
+ const finalTree = baseTree.map((opt) => {
622
+ if (opt.value === activeGroup && opt.children) {
623
+ return { ...opt, children: reordered };
624
+ }
625
+ return opt;
626
+ });
627
+ onTreeSort?.(finalTree);
628
+ } else {
629
+ onGroupSortEnd?.(activeGroup, reordered);
630
+ }
631
+ } else if (dragTree) {
632
+ onTreeSort?.(baseTree);
633
+ }
634
+ } else {
635
+ const finalTree = baseTree.map((opt) => {
636
+ if (!opt.children) return opt;
637
+ if (opt.value === activeGroup) {
638
+ return {
639
+ ...opt,
640
+ children: opt.children.filter(
641
+ (c) => `${c.value}` !== `${active.id}`
642
+ )
643
+ };
644
+ }
645
+ if (opt.value === overGroup) {
646
+ const destChildren = [...opt.children];
647
+ const overIdx = destChildren.findIndex(
648
+ (c) => `${c.value}` === `${over.id}`
649
+ );
650
+ destChildren.splice(
651
+ overIdx !== -1 ? overIdx + 1 : destChildren.length,
652
+ 0,
653
+ activeRow.option
654
+ );
655
+ return { ...opt, children: destChildren };
656
+ }
657
+ return opt;
658
+ });
659
+ onTreeSort?.(finalTree);
660
+ }
661
+ setDragTree(null);
662
+ },
663
+ [dragTree, options, displayRows, onTreeSort, onGroupSortEnd]
664
+ );
665
+ const listContent = /* @__PURE__ */ jsx(
666
+ "div",
667
+ {
668
+ ref: parentRef,
669
+ className: cn(
670
+ "max-h-75 overflow-x-hidden",
671
+ activeIndex !== null ? "overflow-y-visible" : "overflow-y-auto"
672
+ ),
673
+ children: /* @__PURE__ */ jsx(
674
+ "div",
675
+ {
676
+ style: {
677
+ height: `${virtualizer.getTotalSize()}px`,
678
+ position: "relative",
679
+ width: "100%"
680
+ },
681
+ children: virtualizer.getVirtualItems().map((vItem) => {
682
+ const displayRow = virtualItems[vItem.index];
683
+ if (displayRow.kind === "group-header") {
684
+ return /* @__PURE__ */ jsx(
685
+ "div",
686
+ {
687
+ style: {
688
+ position: "absolute",
689
+ top: 0,
690
+ left: 0,
691
+ width: "100%",
692
+ transform: `translateY(${vItem.start}px)`
693
+ },
694
+ "data-index": vItem.index,
695
+ ref: virtualizer.measureElement,
696
+ children: /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs font-semibold text-muted-foreground", children: displayRow.label })
697
+ },
698
+ `gh-${displayRow.groupValue}`
699
+ );
700
+ }
701
+ const option = displayRow.option;
702
+ const row = /* @__PURE__ */ jsx(
703
+ OptionRow,
704
+ {
705
+ option,
706
+ selected: isSelected(option, selectedValue),
707
+ renderItem,
708
+ onSelect
709
+ },
710
+ option.value
711
+ );
712
+ const wrappedRow = sortable ? /* @__PURE__ */ jsx(
713
+ SortableItem,
714
+ {
715
+ id: `${option.value}`,
716
+ disabled: option.disabled,
717
+ children: row
718
+ },
719
+ option.value
720
+ ) : row;
721
+ return /* @__PURE__ */ jsx(
722
+ "div",
723
+ {
724
+ style: {
725
+ position: "absolute",
726
+ top: 0,
727
+ left: 0,
728
+ width: "100%",
729
+ transform: `translateY(${vItem.start}px)`
730
+ },
731
+ "data-index": vItem.index,
732
+ ref: virtualizer.measureElement,
733
+ children: wrappedRow
734
+ },
735
+ option.value
736
+ );
737
+ })
738
+ }
739
+ )
740
+ }
741
+ );
742
+ if (sortable) {
743
+ const handleDragStart = (event) => {
744
+ setActiveId(`${event.active.id}`);
745
+ };
746
+ const handleCancel = () => {
747
+ setActiveId(null);
748
+ setDragTree(null);
749
+ };
750
+ return /* @__PURE__ */ jsx(
751
+ DndContext,
752
+ {
753
+ modifiers: [restrictToVerticalAxis, restrictToFirstScrollableAncestor],
754
+ sensors,
755
+ collisionDetection: grouped && !sortableAcrossGroups ? sameGroupCollision : closestCenter,
756
+ onDragStart: handleDragStart,
757
+ onDragOver: handleDragOver,
758
+ onDragEnd: grouped ? handleDragEndGrouped : handleDragEndFlat,
759
+ onDragCancel: handleCancel,
760
+ children: /* @__PURE__ */ jsx(SortableContext, { items: flatSortableIds, strategy: sortingStrategy, children: listContent })
761
+ }
762
+ );
763
+ }
764
+ return listContent;
765
+ }
766
+ function LayoutSelect(props) {
767
+ const {
768
+ type,
769
+ options,
770
+ placeholder = "Select item",
771
+ disabled = false,
772
+ readOnly = false,
773
+ error = false,
774
+ clearable = false,
775
+ className,
776
+ triggerClassName,
777
+ popupClassName,
778
+ renderTrigger,
779
+ renderItem,
780
+ listPrefix,
781
+ listSuffix,
782
+ queryFn,
783
+ label
784
+ } = props;
785
+ const [open, setOpen] = useState(false);
786
+ const [search, setSearch] = useState("");
787
+ const [asyncOptions, setAsyncOptions] = useState(null);
788
+ const [loading, setLoading] = useState(false);
789
+ const [internalSortedOptions, setInternalSortedOptions] = useState(null);
790
+ const currentValue = useMemo(() => {
791
+ if (type === "single") {
792
+ return props.selectValue ?? null;
793
+ }
794
+ return props.selectValue ?? [];
795
+ }, [type, props]);
796
+ const resolvedOptions = useMemo(() => {
797
+ const base = asyncOptions ?? options;
798
+ return internalSortedOptions ?? base;
799
+ }, [asyncOptions, options, internalSortedOptions]);
800
+ const flatOptions = useMemo(
801
+ () => flattenOptions(resolvedOptions),
802
+ [resolvedOptions]
803
+ );
804
+ const filteredOptions = useMemo(() => {
805
+ if (!search) return flatOptions;
806
+ const q = search.toLowerCase();
807
+ return flatOptions.filter((o) => o.label.toLowerCase().includes(q));
808
+ }, [flatOptions, search]);
809
+ const filteredGroupedOptions = useMemo(() => {
810
+ if (!search) return resolvedOptions;
811
+ const q = search.toLowerCase();
812
+ return resolvedOptions.map((opt) => {
813
+ if (opt.children && opt.children.length > 0) {
814
+ const matched = opt.children.filter(
815
+ (c) => c.label.toLowerCase().includes(q)
816
+ );
817
+ if (matched.length === 0) return null;
818
+ return { ...opt, children: matched };
819
+ }
820
+ return opt.label.toLowerCase().includes(q) ? opt : null;
821
+ }).filter(Boolean);
822
+ }, [resolvedOptions, search]);
823
+ useEffect(() => {
824
+ if (open && queryFn) {
825
+ let cancelled = false;
826
+ setLoading(true);
827
+ queryFn().then((data) => {
828
+ if (!cancelled) {
829
+ setAsyncOptions(data);
830
+ }
831
+ }).finally(() => {
832
+ if (!cancelled) setLoading(false);
833
+ });
834
+ return () => {
835
+ cancelled = true;
836
+ };
837
+ }
838
+ }, [open, queryFn]);
839
+ useEffect(() => {
840
+ if (!open) {
841
+ setSearch("");
842
+ }
843
+ }, [open]);
844
+ const handleSelect = useCallback(
845
+ (option) => {
846
+ if (readOnly) return;
847
+ if (type === "single") {
848
+ const onChange = props.onChange;
849
+ const current = currentValue;
850
+ if (clearable && current && optionEq(current, option)) {
851
+ onChange?.(null, option);
852
+ } else {
853
+ onChange?.(option, option);
854
+ }
855
+ setOpen(false);
856
+ } else {
857
+ const onChange = props.onChange;
858
+ const current = currentValue;
859
+ const exists = current.some((v) => optionEq(v, option));
860
+ if (exists) {
861
+ onChange?.(
862
+ current.filter((v) => !optionEq(v, option)),
863
+ option
864
+ );
865
+ } else {
866
+ onChange?.([...current, option], option);
867
+ }
868
+ }
869
+ },
870
+ [type, currentValue, clearable, readOnly, props]
871
+ );
872
+ const handleRemoveChip = useCallback(
873
+ (option) => {
874
+ if (type !== "multiple" || readOnly || disabled) return;
875
+ const onChange = props.onChange;
876
+ const current = currentValue;
877
+ onChange?.(
878
+ current.filter((v) => !optionEq(v, option)),
879
+ option
880
+ );
881
+ },
882
+ [type, currentValue, readOnly, disabled, props]
883
+ );
884
+ const handleToggleAll = useCallback(() => {
885
+ if (type !== "multiple" || readOnly || disabled) return;
886
+ const onChange = props.onChange;
887
+ const current = currentValue;
888
+ const selectableOptions = flatOptions.filter((o) => !o.disabled);
889
+ const allSelected2 = selectableOptions.length > 0 && selectableOptions.every((o) => current.some((v) => optionEq(v, o)));
890
+ if (allSelected2) {
891
+ const kept = current.filter(
892
+ (v) => selectableOptions.every((o) => !optionEq(o, v))
893
+ );
894
+ onChange?.(kept, selectableOptions[0]);
895
+ } else {
896
+ const missing = selectableOptions.filter(
897
+ (o) => !current.some((v) => optionEq(v, o))
898
+ );
899
+ onChange?.([...current, ...missing], selectableOptions[0]);
900
+ }
901
+ }, [type, currentValue, flatOptions, readOnly, disabled, props]);
902
+ const allSelected = useMemo(() => {
903
+ if (type !== "multiple") return false;
904
+ const current = currentValue;
905
+ const selectableOptions = flatOptions.filter((o) => !o.disabled);
906
+ return selectableOptions.length > 0 && selectableOptions.every((o) => current.some((v) => optionEq(v, o)));
907
+ }, [type, currentValue, flatOptions]);
908
+ const sortable = props.sortable ?? false;
909
+ const sortableAcrossGroups = props.sortable ? props.sortableAcrossGroups ?? false : false;
910
+ const consumerOnSortEnd = props.sortable ? props.onSortEnd : void 0;
911
+ const handleSortEnd = useCallback(
912
+ (sorted) => {
913
+ setInternalSortedOptions(sorted);
914
+ consumerOnSortEnd?.(sorted);
915
+ },
916
+ [consumerOnSortEnd]
917
+ );
918
+ const handleGroupSortEnd = useCallback(
919
+ (groupValue, reorderedChildren) => {
920
+ const updated = resolvedOptions.map((opt) => {
921
+ if (opt.value === groupValue && opt.children) {
922
+ return { ...opt, children: reorderedChildren };
923
+ }
924
+ return opt;
925
+ });
926
+ setInternalSortedOptions(updated);
927
+ consumerOnSortEnd?.(flattenOptions(updated));
928
+ },
929
+ [resolvedOptions, consumerOnSortEnd]
930
+ );
931
+ const handleTreeSort = useCallback(
932
+ (newTree) => {
933
+ setInternalSortedOptions(newTree);
934
+ consumerOnSortEnd?.(flattenOptions(newTree));
935
+ },
936
+ [consumerOnSortEnd]
937
+ );
938
+ const handleOpenChange = useCallback(
939
+ (nextOpen) => {
940
+ if (disabled || readOnly) return;
941
+ setOpen(nextOpen);
942
+ },
943
+ [disabled, readOnly]
944
+ );
945
+ const triggerContent = useMemo(() => {
946
+ if (renderTrigger) {
947
+ return renderTrigger({
948
+ value: currentValue,
949
+ open,
950
+ disabled,
951
+ readOnly,
952
+ error,
953
+ placeholder
954
+ });
955
+ }
956
+ if (type === "single") {
957
+ return /* @__PURE__ */ jsx(
958
+ SingleTriggerContent,
959
+ {
960
+ value: currentValue,
961
+ placeholder
962
+ }
963
+ );
964
+ }
965
+ return /* @__PURE__ */ jsx(
966
+ MultipleTriggerContent,
967
+ {
968
+ value: currentValue,
969
+ placeholder,
970
+ collapsed: props.collapsed,
971
+ showItemsLength: props.showItemsLength,
972
+ onRemove: handleRemoveChip,
973
+ readOnly,
974
+ disabled
975
+ }
976
+ );
977
+ }, [
978
+ renderTrigger,
979
+ type,
980
+ currentValue,
981
+ open,
982
+ disabled,
983
+ readOnly,
984
+ error,
985
+ placeholder,
986
+ props,
987
+ handleRemoveChip
988
+ ]);
989
+ return /* @__PURE__ */ jsx(Tooltip.Provider, { children: /* @__PURE__ */ jsxs("div", { className: cn("relative inline-flex flex-col", className), children: [
990
+ label && /* @__PURE__ */ jsx("label", { className: "mb-1 text-sm font-medium text-foreground", children: label }),
991
+ /* @__PURE__ */ jsxs(Popover.Root, { open, onOpenChange: handleOpenChange, children: [
992
+ /* @__PURE__ */ jsxs(
993
+ Popover.Trigger,
994
+ {
995
+ disabled,
996
+ render: /* @__PURE__ */ jsx(
997
+ "button",
998
+ {
999
+ type: "button",
1000
+ "aria-expanded": open,
1001
+ "aria-haspopup": "listbox",
1002
+ "aria-invalid": error || void 0,
1003
+ "data-readonly": readOnly || void 0,
1004
+ className: cn(
1005
+ "border-input flex min-h-9 w-full min-w-45 items-center gap-2 rounded-md border bg-transparent px-3 py-1.5 text-sm shadow-xs transition-[color,box-shadow,border-color] outline-none",
1006
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
1007
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
1008
+ "data-readonly:pointer-events-none data-readonly:opacity-70",
1009
+ "disabled:cursor-not-allowed disabled:opacity-50",
1010
+ "[&_svg:not([class*='text-'])]:text-muted-foreground",
1011
+ triggerClassName
1012
+ )
1013
+ }
1014
+ ),
1015
+ children: [
1016
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 items-center", children: triggerContent }),
1017
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-4 shrink-0 opacity-50" })
1018
+ ]
1019
+ }
1020
+ ),
1021
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, { sideOffset: 4, children: /* @__PURE__ */ jsx(
1022
+ Popover.Popup,
1023
+ {
1024
+ className: cn(
1025
+ "z-50 min-w-(--anchor-width) rounded-md border bg-popover text-popover-foreground shadow-md outline-none",
1026
+ "data-starting-style:scale-95 data-starting-style:opacity-0",
1027
+ "data-ending-style:scale-95 data-ending-style:opacity-0",
1028
+ "origin-(--transform-origin) transition-[transform,scale,opacity] duration-150",
1029
+ popupClassName
1030
+ ),
1031
+ children: /* @__PURE__ */ jsxs(Command, { shouldFilter: false, loop: true, children: [
1032
+ /* @__PURE__ */ jsx("div", { className: "border-b p-1", children: /* @__PURE__ */ jsx(
1033
+ Command.Input,
1034
+ {
1035
+ value: search,
1036
+ onValueChange: setSearch,
1037
+ placeholder: "Search...",
1038
+ className: "h-8 w-full rounded-sm bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground"
1039
+ }
1040
+ ) }),
1041
+ listPrefix && /* @__PURE__ */ jsx("div", { className: "border-b px-2 py-1.5", children: listPrefix }),
1042
+ type === "multiple" && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b px-1 py-1", children: /* @__PURE__ */ jsx(
1043
+ "button",
1044
+ {
1045
+ type: "button",
1046
+ onClick: handleToggleAll,
1047
+ className: "w-full rounded-sm px-2 py-1 text-left text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground",
1048
+ children: allSelected ? "Unselect all" : "Select all"
1049
+ }
1050
+ ) }),
1051
+ /* @__PURE__ */ jsxs(Command.List, { className: "p-1", children: [
1052
+ loading && /* @__PURE__ */ jsx(Command.Loading, { children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-4 text-sm text-muted-foreground", children: "Loading\u2026" }) }),
1053
+ !loading && filteredOptions.length === 0 && /* @__PURE__ */ jsx(Command.Empty, { className: "py-4 text-center text-sm text-muted-foreground", children: "No results found." }),
1054
+ !loading && filteredOptions.length > 0 && /* @__PURE__ */ jsx(
1055
+ VirtualList,
1056
+ {
1057
+ options: filteredGroupedOptions,
1058
+ items: filteredOptions,
1059
+ selectedValue: currentValue,
1060
+ renderItem,
1061
+ onSelect: handleSelect,
1062
+ sortable,
1063
+ sortableAcrossGroups,
1064
+ onSortEnd: handleSortEnd,
1065
+ onGroupSortEnd: handleGroupSortEnd,
1066
+ onTreeSort: handleTreeSort
1067
+ }
1068
+ )
1069
+ ] }),
1070
+ listSuffix && /* @__PURE__ */ jsx("div", { className: "border-t px-2 py-1.5", children: listSuffix })
1071
+ ] })
1072
+ }
1073
+ ) }) })
1074
+ ] })
1075
+ ] }) });
1076
+ }
1077
+ var select_default = LayoutSelect;
1078
+
1079
+ export {
1080
+ LayoutSelect,
1081
+ select_default
1082
+ };
1083
+ //# sourceMappingURL=chunk-CZ3IMHZ6.js.map