gridsmith-ui 0.21.0 → 0.22.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.
Files changed (41) hide show
  1. package/dist/chat.js +3 -3
  2. package/dist/chunk-2RV23OBK.js +1349 -0
  3. package/dist/chunk-2RV23OBK.js.map +1 -0
  4. package/dist/chunk-7TOJ4G7S.js +197 -0
  5. package/dist/chunk-7TOJ4G7S.js.map +1 -0
  6. package/dist/{chunk-GGVASDXT.js → chunk-AAYCQXZQ.js} +8 -155
  7. package/dist/chunk-AAYCQXZQ.js.map +1 -0
  8. package/dist/{chunk-ILIODA7W.js → chunk-CXMV7SXJ.js} +4 -3
  9. package/dist/chunk-CXMV7SXJ.js.map +1 -0
  10. package/dist/{chunk-EQDR57AM.js → chunk-EYMGLK4M.js} +3 -3
  11. package/dist/{chunk-EQDR57AM.js.map → chunk-EYMGLK4M.js.map} +1 -1
  12. package/dist/chunk-IVCVNAXG.js +115 -0
  13. package/dist/chunk-IVCVNAXG.js.map +1 -0
  14. package/dist/chunk-RG4KUCRS.js +9 -0
  15. package/dist/chunk-RG4KUCRS.js.map +1 -0
  16. package/dist/{chunk-XYDIBW3J.js → chunk-SBTBMAIU.js} +4 -3
  17. package/dist/chunk-SBTBMAIU.js.map +1 -0
  18. package/dist/data-grid.d.ts +38 -4
  19. package/dist/data-grid.js +304 -264
  20. package/dist/data-grid.js.map +1 -1
  21. package/dist/editor.js +1 -1
  22. package/dist/index.d.ts +94 -139
  23. package/dist/index.js +794 -1616
  24. package/dist/index.js.map +1 -1
  25. package/dist/kanban.js +2 -2
  26. package/dist/resizable.js +5 -4
  27. package/dist/resizable.js.map +1 -1
  28. package/dist/roots.css +10 -0
  29. package/dist/{table-utils-GounRE6s.d.ts → table-utils-BkjNEWts.d.ts} +5 -1
  30. package/dist/use-pagination-DiJo0aW5.d.ts +153 -0
  31. package/dist/virtualized.d.ts +11 -1
  32. package/dist/virtualized.js +67 -18
  33. package/dist/virtualized.js.map +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-GGVASDXT.js.map +0 -1
  36. package/dist/chunk-ILIODA7W.js.map +0 -1
  37. package/dist/chunk-JN37OVTP.js +0 -67
  38. package/dist/chunk-JN37OVTP.js.map +0 -1
  39. package/dist/chunk-TKVTZWBN.js +0 -50
  40. package/dist/chunk-TKVTZWBN.js.map +0 -1
  41. package/dist/chunk-XYDIBW3J.js.map +0 -1
@@ -0,0 +1,1349 @@
1
+ import { ControlSizeContext, triggerType, Button, useShellPreferences, PREFERENCE_HOUSE_DEFAULTS } from './chunk-7TOJ4G7S.js';
2
+ import { useReportedChange, useOptionalFormContext, usePopupFocus, focusAfterRemoval, usePressMarks, focusInitial, FormChangeCtx } from './chunk-RT7RYGC3.js';
3
+ import { useTextOverflow, Tooltip, usePosition } from './chunk-CXMV7SXJ.js';
4
+ import { Slot } from './chunk-7Q46JXSK.js';
5
+ import { useLatest } from './chunk-WH2FUFAC.js';
6
+ import { Icon } from './chunk-4LDYR7QP.js';
7
+ import { cn, ERROR_HAS_FOCUS_RING, HAS_FOCUS_RING, ERROR_FOCUS_RING, FOCUS_RING, ERROR_ACTIVE_RING, ACTIVE_RING, ERROR_FOCUS_WITHIN_RING, FOCUS_WITHIN_RING, GLASS_DROPDOWN } from './chunk-M2QRNBDQ.js';
8
+ import { createContext, useContext, useRef, useCallback, useEffect, useId, useState, Children, isValidElement, useMemo, cloneElement, useLayoutEffect } from 'react';
9
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
+ import { createPortal } from 'react-dom';
11
+
12
+ var UNCONTROLLED = /* @__PURE__ */ Symbol("uncontrolled");
13
+ var valueOf = (e) => {
14
+ const t = e.target;
15
+ if (!t) return void 0;
16
+ return t instanceof HTMLInputElement && (t.type === "checkbox" || t.type === "radio") ? t.checked : t.value;
17
+ };
18
+ var INPUT_HEIGHT = {
19
+ sm: "h-[var(--ds-control-height-sm)]",
20
+ md: "h-[var(--ds-control-height-md)]",
21
+ lg: "h-[var(--ds-control-height-lg)]"
22
+ };
23
+ var INPUT_TEXT = {
24
+ sm: "px-ds-2 py-ds-1 text-xs",
25
+ md: "px-ds-3 py-ds-2 text-sm",
26
+ lg: "px-ds-4 py-ds-3 text-base"
27
+ };
28
+ function Input({ className, size: sizeProp, error = false, prefix, suffix, ...props }) {
29
+ const contextSize = useContext(ControlSizeContext);
30
+ const size = sizeProp ?? contextSize ?? "md";
31
+ const onChange = useReportedChange(props.onChange, props.value === void 0 ? UNCONTROLLED : props.value, valueOf);
32
+ const borderColor = error ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)]";
33
+ const hoverBorder = !error && !props.disabled && !props.readOnly ? "hover:border-[var(--ds-input-border-hover)]" : "";
34
+ if (prefix || suffix) {
35
+ return /* @__PURE__ */ jsxs(
36
+ "div",
37
+ {
38
+ className: cn(
39
+ "flex items-center w-full",
40
+ INPUT_HEIGHT[size],
41
+ "bg-[var(--ds-input-bg)]",
42
+ "border-[length:var(--ds-border-width)]",
43
+ borderColor,
44
+ "rounded-[var(--ds-radius-md)]",
45
+ "shadow-[var(--ds-input-shadow)]",
46
+ hoverBorder,
47
+ error ? ERROR_HAS_FOCUS_RING : HAS_FOCUS_RING,
48
+ "transition-colors duration-[var(--ds-transition-speed)]",
49
+ props.disabled && "cursor-not-allowed opacity-disabled",
50
+ props.readOnly && "bg-[var(--ds-bg-muted)] cursor-default",
51
+ className
52
+ ),
53
+ children: [
54
+ prefix && /* @__PURE__ */ jsx("span", { className: "shrink-0 pl-ds-3 text-[var(--ds-text-muted)] flex items-center", children: prefix }),
55
+ /* @__PURE__ */ jsx(
56
+ "input",
57
+ {
58
+ "aria-invalid": error || void 0,
59
+ className: cn(
60
+ "flex-1 min-w-0 bg-transparent outline-none",
61
+ INPUT_TEXT[size],
62
+ "text-[var(--ds-text-primary)]",
63
+ "placeholder:text-[var(--ds-text-muted)]",
64
+ "disabled:cursor-not-allowed",
65
+ "read-only:text-[var(--ds-text-secondary)] read-only:cursor-default"
66
+ ),
67
+ ...props,
68
+ onChange: props.onChange || props.value === void 0 ? onChange : void 0
69
+ }
70
+ ),
71
+ suffix && /* @__PURE__ */ jsx("span", { className: "shrink-0 pr-ds-3 text-[var(--ds-text-muted)] flex items-center", children: suffix })
72
+ ]
73
+ }
74
+ );
75
+ }
76
+ return /* @__PURE__ */ jsx(
77
+ "input",
78
+ {
79
+ "aria-invalid": error || void 0,
80
+ className: cn(
81
+ "flex w-full",
82
+ INPUT_TEXT[size],
83
+ INPUT_HEIGHT[size],
84
+ "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)]",
85
+ "border-[length:var(--ds-border-width)]",
86
+ borderColor,
87
+ "rounded-[var(--ds-radius-md)]",
88
+ "shadow-[var(--ds-input-shadow)]",
89
+ "placeholder:text-[var(--ds-text-muted)]",
90
+ hoverBorder,
91
+ error ? ERROR_FOCUS_RING : FOCUS_RING,
92
+ "disabled:cursor-not-allowed disabled:opacity-disabled",
93
+ "read-only:bg-[var(--ds-bg-muted)] read-only:text-[var(--ds-text-secondary)] read-only:cursor-default",
94
+ "transition-colors duration-[var(--ds-transition-speed)]",
95
+ className
96
+ ),
97
+ ...props,
98
+ onChange: props.onChange || props.value === void 0 ? onChange : void 0
99
+ }
100
+ );
101
+ }
102
+ function Textarea({ className, autoResize, maxRows, error = false, ref, ...props }) {
103
+ const onChange = useReportedChange(props.onChange, props.value === void 0 ? UNCONTROLLED : props.value, valueOf);
104
+ const internalRef = useRef(null);
105
+ const textareaRef = ref ?? internalRef;
106
+ const resize = useCallback(() => {
107
+ const el = textareaRef.current;
108
+ if (!el || !autoResize) return;
109
+ el.style.height = "auto";
110
+ let target = el.scrollHeight;
111
+ if (maxRows) {
112
+ const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || 20;
113
+ const maxHeight = lineHeight * maxRows + parseFloat(getComputedStyle(el).paddingTop) + parseFloat(getComputedStyle(el).paddingBottom);
114
+ target = Math.min(target, maxHeight);
115
+ }
116
+ el.style.height = `${target}px`;
117
+ }, [autoResize, maxRows, textareaRef]);
118
+ useEffect(() => {
119
+ resize();
120
+ }, [resize, props.value, props.defaultValue]);
121
+ return /* @__PURE__ */ jsx(
122
+ "textarea",
123
+ {
124
+ ref: textareaRef,
125
+ "aria-invalid": error || void 0,
126
+ className: cn(
127
+ "flex min-h-[var(--ds-control-height-md)] w-full px-ds-3 py-ds-2 text-sm",
128
+ "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)]",
129
+ "border-[length:var(--ds-border-width)]",
130
+ error ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)] hover:border-[var(--ds-input-border-hover)]",
131
+ "rounded-[var(--ds-radius-md)]",
132
+ "shadow-[var(--ds-input-shadow)]",
133
+ "placeholder:text-[var(--ds-text-muted)]",
134
+ error ? ERROR_FOCUS_RING : FOCUS_RING,
135
+ "disabled:cursor-not-allowed disabled:opacity-disabled disabled:hover:border-[var(--ds-input-border)]",
136
+ "read-only:bg-[var(--ds-bg-muted)] read-only:text-[var(--ds-text-secondary)] read-only:cursor-default read-only:hover:border-[var(--ds-input-border)]",
137
+ "transition-colors duration-[var(--ds-transition-speed)]",
138
+ autoResize && "resize-none overflow-hidden",
139
+ className
140
+ ),
141
+ onInput: autoResize ? resize : void 0,
142
+ ...props,
143
+ onChange: props.onChange || props.value === void 0 ? onChange : void 0
144
+ }
145
+ );
146
+ }
147
+ function Label({ className, children, ...props }) {
148
+ return /* @__PURE__ */ jsx(
149
+ "label",
150
+ {
151
+ className: cn(
152
+ "text-sm font-medium leading-none text-[var(--ds-text-secondary)]",
153
+ "peer-disabled:cursor-not-allowed peer-disabled:opacity-disabled",
154
+ className
155
+ ),
156
+ ...props,
157
+ children
158
+ }
159
+ );
160
+ }
161
+ var TOGGLE_SIZES = {
162
+ sm: { w: 32, h: 18, thumb: 12 },
163
+ md: { w: 44, h: 24, thumb: 16 }
164
+ };
165
+ function Toggle({ checked, onChange, label, disabled, error = false, layout = "full", size = "md", "aria-labelledby": ariaLabelledBy, ...rest }) {
166
+ const change = useReportedChange(onChange, checked);
167
+ const dims = TOGGLE_SIZES[size];
168
+ const travel = dims.w - dims.thumb - 3;
169
+ const switchButton = /* @__PURE__ */ jsx(
170
+ "button",
171
+ {
172
+ type: "button",
173
+ role: "switch",
174
+ "aria-checked": checked,
175
+ "aria-label": !label && !ariaLabelledBy ? "Toggle" : void 0,
176
+ "aria-labelledby": ariaLabelledBy,
177
+ "aria-invalid": error || void 0,
178
+ disabled,
179
+ onClick: () => !disabled && change(!checked),
180
+ ...rest,
181
+ className: cn(
182
+ "relative inline-flex shrink-0 items-center overflow-hidden",
183
+ "border-[length:var(--ds-border-width)]",
184
+ "rounded-[var(--ds-radius-full)]",
185
+ "transition-colors duration-[var(--ds-transition-speed)]",
186
+ "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
187
+ disabled ? "cursor-not-allowed" : "cursor-pointer",
188
+ checked ? "bg-[var(--ds-accent-primary)]" : "bg-[var(--ds-bg-muted)]",
189
+ error ? "border-[var(--ds-status-error)]" : checked ? "border-transparent" : "border-[var(--ds-border-default)]"
190
+ ),
191
+ style: { width: dims.w, height: dims.h },
192
+ children: /* @__PURE__ */ jsx(
193
+ "span",
194
+ {
195
+ className: cn(
196
+ "pointer-events-none inline-block shrink-0",
197
+ "bg-[var(--ds-bg-surface)]",
198
+ "border-[length:var(--ds-border-width)] border-transparent",
199
+ "rounded-[var(--ds-radius-full)]",
200
+ "transition-transform"
201
+ ),
202
+ style: {
203
+ width: dims.thumb,
204
+ height: dims.thumb,
205
+ transform: checked ? `translateX(calc(${travel}px - var(--ds-border-width)))` : "translateX(calc(1px + var(--ds-border-width)))"
206
+ }
207
+ }
208
+ )
209
+ }
210
+ );
211
+ if (!label) {
212
+ return /* @__PURE__ */ jsx("div", { className: cn(disabled && "opacity-disabled"), children: switchButton });
213
+ }
214
+ return /* @__PURE__ */ jsxs(
215
+ "label",
216
+ {
217
+ className: cn(
218
+ layout === "full" ? "flex items-center justify-between gap-ds-4" : "inline-flex items-center gap-ds-3",
219
+ disabled ? "cursor-not-allowed opacity-disabled" : "cursor-pointer"
220
+ ),
221
+ children: [
222
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: label }),
223
+ switchButton
224
+ ]
225
+ }
226
+ );
227
+ }
228
+ function InlineFieldError({ message, id, className }) {
229
+ return /* @__PURE__ */ jsxs(
230
+ "p",
231
+ {
232
+ id,
233
+ role: "alert",
234
+ className: cn(
235
+ "flex items-center gap-ds-1.5 text-xs text-[var(--ds-status-error)]",
236
+ "mt-ds-1.5",
237
+ className
238
+ ),
239
+ children: [
240
+ /* @__PURE__ */ jsx(Icon, { name: "error", size: 14, className: "shrink-0" }),
241
+ /* @__PURE__ */ jsx("span", { children: message })
242
+ ]
243
+ }
244
+ );
245
+ }
246
+ var FieldContext = createContext(null);
247
+ function useField() {
248
+ return useContext(FieldContext);
249
+ }
250
+ function useFieldControl(own, options = {}) {
251
+ const field = useField();
252
+ const id = own.id ?? field?.controlId;
253
+ const invalid = !!own.error || own["aria-invalid"] === true || own["aria-invalid"] === "true" || !!field?.invalid;
254
+ const required = !!own.required || own["aria-required"] === true || own["aria-required"] === "true" || !!field?.required;
255
+ const noteId = options.requiredInDescription && required && id ? `${id}-required` : void 0;
256
+ const ids = [own["aria-describedby"], field?.describedBy, noteId].filter(Boolean).join(" ").split(/\s+/).filter(Boolean);
257
+ return {
258
+ id,
259
+ labelId: own["aria-labelledby"] ?? field?.labelId,
260
+ describedBy: [...new Set(ids)].join(" ") || void 0,
261
+ invalid,
262
+ required,
263
+ requiredNote: noteId ? /* @__PURE__ */ jsx("span", { id: noteId, hidden: true, children: "Required" }) : null
264
+ };
265
+ }
266
+ var AddAnotherRowCtx = createContext(false);
267
+ function Field({ label, children, hint, error, required, labelAction, hideLabel = false, id: idProp, validate, className }) {
268
+ const inGroupRow = useContext(AddAnotherRowCtx);
269
+ const generated = useId();
270
+ const id = idProp ?? `field-${generated}`;
271
+ const hintId = `${id}-hint`;
272
+ const errorId = `${id}-error`;
273
+ const form = useOptionalFormContext();
274
+ const mode = form?.validationMode ?? "onBlur";
275
+ const [localError, setLocalError] = useState(void 0);
276
+ const [left, setLeft] = useState(false);
277
+ const checkError = validate ? form ? form.errors[id] : localError : void 0;
278
+ const child = typeof children === "function" ? null : Children.only(children);
279
+ const childProps = child && isValidElement(child) ? child.props : null;
280
+ const latest = useLatest({ validate, childProps, id, mode, left, showing: !!checkError, form });
281
+ const check = useCallback(() => {
282
+ const { validate: fn, childProps: props, id: controlId } = latest.current;
283
+ if (!fn) return void 0;
284
+ const value = props && "value" in props ? props.value : props && "checked" in props ? props.checked : document.getElementById(controlId)?.value;
285
+ const message = fn(value);
286
+ return typeof message === "string" && message.length > 0 ? message : void 0;
287
+ }, [latest]);
288
+ const show = useCallback((message) => {
289
+ const { form: f, id: controlId } = latest.current;
290
+ if (f) f.setError(controlId, message);
291
+ else setLocalError(message);
292
+ }, [latest]);
293
+ const register = form?.register;
294
+ const checks = !!validate;
295
+ useEffect(() => register && checks ? register(id, check) : void 0, [register, checks, id, check]);
296
+ const leaving = useRef(null);
297
+ useEffect(() => () => {
298
+ if (leaving.current) clearTimeout(leaving.current);
299
+ }, []);
300
+ const onFocusCapture = () => {
301
+ if (leaving.current) {
302
+ clearTimeout(leaving.current);
303
+ leaving.current = null;
304
+ }
305
+ };
306
+ const onBlurCapture = () => {
307
+ if (!validate) return;
308
+ if (leaving.current) clearTimeout(leaving.current);
309
+ leaving.current = setTimeout(() => {
310
+ leaving.current = null;
311
+ setLeft(true);
312
+ if (latest.current.mode !== "onSubmit" || latest.current.showing) show(check());
313
+ }, 0);
314
+ };
315
+ const recheck = () => {
316
+ if (!validate) return;
317
+ setTimeout(() => {
318
+ const { mode: m, left: wasLeft, showing } = latest.current;
319
+ if (m === "onChange" && wasLeft || showing) show(check());
320
+ }, 0);
321
+ };
322
+ const shownError = typeof error === "string" && error.length > 0 ? error : checkError;
323
+ const hasError = typeof shownError === "string" && shownError.length > 0;
324
+ const describedBy = [hint ? hintId : null, hasError ? errorId : null].filter(Boolean).join(" ") || void 0;
325
+ const labelId = `${id}-label`;
326
+ const fieldContext = useMemo(() => ({ controlId: id, labelId, describedBy, invalid: hasError, required: !!required }), [id, labelId, describedBy, hasError, required]);
327
+ const controlProps = {
328
+ id,
329
+ "aria-describedby": describedBy,
330
+ ...hasError ? { "aria-invalid": true, error: true } : {},
331
+ ...required ? { "aria-required": true, required: true } : {}
332
+ };
333
+ let control;
334
+ if (typeof children === "function") {
335
+ control = children(controlProps);
336
+ } else {
337
+ if (!child || !isValidElement(child)) throw new Error("Field expects a single form control as its child");
338
+ const isNative = typeof child.type === "string";
339
+ const { error: errorFlag, ...ariaOnly } = controlProps;
340
+ const ownOnChange = childProps?.onChange;
341
+ const onChange = validate && !isNative ? { onChange: (...args) => {
342
+ ownOnChange?.(...args);
343
+ recheck();
344
+ } } : {};
345
+ control = cloneElement(child, isNative ? ariaOnly : { ...ariaOnly, ...errorFlag ? { error: true } : {}, ...onChange });
346
+ }
347
+ const labelNode = /* @__PURE__ */ jsxs(Label, { id: labelId, htmlFor: id, className: cn(hideLabel && "sr-only", hideLabel && inGroupRow && "@max-[30rem]:not-sr-only"), children: [
348
+ label,
349
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ml-ds-0.5 text-[var(--ds-status-error)]", children: "*" })
350
+ ] });
351
+ return (
352
+ // In a settings panel a field keeps the narrow reading width however wide the page is (--ds-form-max, Sprint 25.9 A4)
353
+ /* @__PURE__ */ jsxs(
354
+ "div",
355
+ {
356
+ className: cn("flex flex-col gap-ds-1.5 max-w-[var(--ds-form-max,none)]", className),
357
+ ...validate ? { onFocusCapture, onBlurCapture, onInputCapture: recheck } : {},
358
+ children: [
359
+ hideLabel && !labelAction ? labelNode : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-ds-2", children: [
360
+ labelNode,
361
+ labelAction
362
+ ] }),
363
+ /* @__PURE__ */ jsx(FieldContext.Provider, { value: fieldContext, children: control }),
364
+ hint && /* @__PURE__ */ jsx("p", { id: hintId, className: "text-xs text-[var(--ds-text-muted)]", children: hint }),
365
+ hasError && /* @__PURE__ */ jsx(InlineFieldError, { id: errorId, message: shownError, className: "mt-0" })
366
+ ]
367
+ }
368
+ )
369
+ );
370
+ }
371
+ var DROPDOWN_HEIGHT = {
372
+ sm: "px-ds-2 py-ds-1 text-xs h-[var(--ds-control-height-sm)]",
373
+ md: "px-ds-3 py-ds-2 text-sm h-[var(--ds-control-height-md)]",
374
+ lg: "px-ds-4 py-ds-3 text-base h-[var(--ds-control-height-lg)]"
375
+ };
376
+ function Dropdown({
377
+ options,
378
+ value,
379
+ onChange: onChangeProp,
380
+ label,
381
+ disabled,
382
+ placeholder = "Select...",
383
+ multiple,
384
+ searchable,
385
+ size: sizeProp,
386
+ className,
387
+ ref,
388
+ container,
389
+ error = false,
390
+ showChips = true,
391
+ "aria-label": ariaLabel,
392
+ ...wiring
393
+ }) {
394
+ const onChange = useReportedChange(onChangeProp, value);
395
+ const contextSize = useContext(ControlSizeContext);
396
+ const generatedId = useId();
397
+ const control = useFieldControl({ ...wiring, error: error || void 0 }, { requiredInDescription: true });
398
+ const triggerId = control.id ?? generatedId;
399
+ const labelSpanId = `${triggerId}-label`;
400
+ const labelledBy = control.labelId ? `${control.labelId} ${triggerId}` : label && !ariaLabel ? `${labelSpanId} ${triggerId}` : void 0;
401
+ const invalid = control.invalid;
402
+ const size = sizeProp ?? contextSize ?? "md";
403
+ const [open, setOpen] = useState(false);
404
+ const [search, setSearch] = useState("");
405
+ const [positioned, setPositioned] = useState(false);
406
+ const triggerRef = useRef(null);
407
+ const menuRef = useRef(null);
408
+ const searchRef = useRef(null);
409
+ const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
410
+ const selectedValues = Array.isArray(value) ? value : [value].filter(Boolean);
411
+ const updatePos = useCallback(() => {
412
+ if (!triggerRef.current) return;
413
+ const rect = triggerRef.current.getBoundingClientRect();
414
+ let top = rect.bottom + 4;
415
+ let left = rect.left;
416
+ const width = rect.width;
417
+ if (menuRef.current) {
418
+ const menuRect = menuRef.current.getBoundingClientRect();
419
+ const vw = window.innerWidth;
420
+ const vh = window.innerHeight;
421
+ if (left + menuRect.width > vw) left = Math.max(0, vw - menuRect.width - 8);
422
+ if (top + menuRect.height > vh) {
423
+ const above = rect.top - 4 - menuRect.height;
424
+ if (above >= 0) top = above;
425
+ else top = Math.max(0, vh - menuRect.height - 8);
426
+ }
427
+ }
428
+ setPos({ top, left, width });
429
+ }, []);
430
+ const [prevOpen, setPrevOpen] = useState(open);
431
+ if (open !== prevOpen) {
432
+ setPrevOpen(open);
433
+ if (open) setSearch("");
434
+ else setPositioned(false);
435
+ }
436
+ useEffect(() => {
437
+ if (!open) return;
438
+ updatePos();
439
+ const raf = requestAnimationFrame(() => {
440
+ updatePos();
441
+ setPositioned(true);
442
+ });
443
+ const onScroll = () => updatePos();
444
+ window.addEventListener("scroll", onScroll, true);
445
+ window.addEventListener("resize", updatePos);
446
+ return () => {
447
+ cancelAnimationFrame(raf);
448
+ window.removeEventListener("scroll", onScroll, true);
449
+ window.removeEventListener("resize", updatePos);
450
+ };
451
+ }, [open, updatePos]);
452
+ const moved = useRef(false);
453
+ useLayoutEffect(() => {
454
+ if (!open || !positioned) return;
455
+ moved.current = false;
456
+ if (searchRef.current) {
457
+ searchRef.current.focus();
458
+ return;
459
+ }
460
+ const els = Array.from(menuRef.current?.querySelectorAll('[role="option"]') ?? []);
461
+ (els.find((el) => el.getAttribute("aria-selected") === "true") ?? els[0])?.focus();
462
+ }, [open, positioned]);
463
+ useEffect(() => {
464
+ if (!open) return;
465
+ const handler = (e) => {
466
+ const target = e.target;
467
+ if (triggerRef.current?.contains(target) || menuRef.current?.contains(target)) return;
468
+ setOpen(false);
469
+ };
470
+ document.addEventListener("mousedown", handler);
471
+ return () => document.removeEventListener("mousedown", handler);
472
+ }, [open]);
473
+ const commitMoved = () => {
474
+ if (multiple || !moved.current) return;
475
+ const el = document.activeElement;
476
+ const id = el instanceof HTMLElement && menuRef.current?.contains(el) ? el.getAttribute("data-option-id") : null;
477
+ if (id) onChange(id);
478
+ };
479
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: menuRef, close: () => setOpen(false), tabExits: "any", onTabOut: commitMoved });
480
+ const typeahead = useRef({ buffer: "", at: 0 });
481
+ useEffect(() => {
482
+ if (!open) return;
483
+ const optionButtons = () => Array.from(menuRef.current?.querySelectorAll('[role="option"]') ?? []);
484
+ const focusOption = (idx) => {
485
+ const els = optionButtons();
486
+ if (els.length === 0) return;
487
+ els[(idx % els.length + els.length) % els.length].focus();
488
+ moved.current = true;
489
+ };
490
+ const onKey = (e) => {
491
+ const inMenu = menuRef.current?.contains(document.activeElement) || triggerRef.current === document.activeElement;
492
+ if (!inMenu) return;
493
+ const els = optionButtons();
494
+ const current = els.findIndex((el) => el === document.activeElement);
495
+ if (e.key === "Escape") {
496
+ if (e.defaultPrevented || !isTopLayer()) return;
497
+ e.preventDefault();
498
+ returnFocus();
499
+ setOpen(false);
500
+ return;
501
+ }
502
+ if (e.key === "ArrowDown") {
503
+ e.preventDefault();
504
+ focusOption(current < 0 ? 0 : current + 1);
505
+ return;
506
+ }
507
+ if (e.key === "ArrowUp") {
508
+ e.preventDefault();
509
+ focusOption(current < 0 ? els.length - 1 : current - 1);
510
+ return;
511
+ }
512
+ const inSearch = document.activeElement === searchRef.current;
513
+ if (e.key === "Home" && !inSearch) {
514
+ e.preventDefault();
515
+ focusOption(0);
516
+ return;
517
+ }
518
+ if (e.key === "End" && !inSearch) {
519
+ e.preventDefault();
520
+ focusOption(els.length - 1);
521
+ return;
522
+ }
523
+ if (!searchable && e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
524
+ const now = Date.now();
525
+ typeahead.current.buffer = now - typeahead.current.at > 700 ? e.key : typeahead.current.buffer + e.key;
526
+ typeahead.current.at = now;
527
+ const q = typeahead.current.buffer.toLowerCase();
528
+ const hit = els.findIndex((el) => (el.textContent ?? "").trim().toLowerCase().startsWith(q));
529
+ if (hit >= 0) {
530
+ e.preventDefault();
531
+ focusOption(hit);
532
+ }
533
+ }
534
+ };
535
+ document.addEventListener("keydown", onKey);
536
+ return () => document.removeEventListener("keydown", onKey);
537
+ }, [open, searchable, isTopLayer, returnFocus]);
538
+ const filtered = searchable && search ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase())) : options;
539
+ const groups = /* @__PURE__ */ new Map();
540
+ for (const opt of filtered) {
541
+ const g = opt.group ?? "";
542
+ if (!groups.has(g)) groups.set(g, []);
543
+ groups.get(g).push(opt);
544
+ }
545
+ const hasGroups = Array.from(groups.keys()).some((k) => k !== "");
546
+ const handleSelect = (id) => {
547
+ if (multiple) {
548
+ const arr = selectedValues.includes(id) ? selectedValues.filter((v) => v !== id) : [...selectedValues, id];
549
+ onChange(arr);
550
+ } else {
551
+ returnFocus();
552
+ onChange(id);
553
+ setOpen(false);
554
+ }
555
+ };
556
+ const chipsRef = useRef(null);
557
+ const pendingChipFocus = useRef(null);
558
+ useLayoutEffect(() => {
559
+ const pending = pendingChipFocus.current;
560
+ if (pending === null) return;
561
+ pendingChipFocus.current = null;
562
+ if (selectedValues.includes(pending.value)) return;
563
+ const chips = Array.from(chipsRef.current?.querySelectorAll("[data-chip-remove]") ?? []);
564
+ focusAfterRemoval(chips, pending.index, triggerRef.current);
565
+ });
566
+ const displayLabel = multiple ? selectedValues.length > 0 ? `${selectedValues.length} selected` : placeholder : options.find((o) => o.id === selectedValues[0])?.label ?? placeholder;
567
+ const { ref: overflowRef, isTruncated } = useTextOverflow({ lines: 1, content: displayLabel });
568
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("relative", className), children: [
569
+ label && /* @__PURE__ */ jsx("span", { id: labelSpanId, className: "block mb-ds-1.5 text-sm font-medium text-[var(--ds-text-secondary)]", children: label }),
570
+ control.requiredNote,
571
+ /* @__PURE__ */ jsxs(
572
+ "button",
573
+ {
574
+ ref: triggerRef,
575
+ id: triggerId,
576
+ type: "button",
577
+ onClick: () => !disabled && setOpen(!open),
578
+ onKeyDown: (e) => {
579
+ if (disabled || open) return;
580
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
581
+ e.preventDefault();
582
+ setOpen(true);
583
+ }
584
+ },
585
+ disabled,
586
+ "aria-invalid": invalid || void 0,
587
+ "aria-haspopup": "listbox",
588
+ "aria-expanded": open,
589
+ "aria-label": labelledBy ? void 0 : ariaLabel,
590
+ "aria-labelledby": labelledBy,
591
+ "aria-describedby": control.describedBy,
592
+ className: cn(
593
+ "w-full flex items-center justify-between gap-ds-2",
594
+ DROPDOWN_HEIGHT[size],
595
+ "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)]",
596
+ "border-[length:var(--ds-border-width)]",
597
+ invalid ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)]",
598
+ "rounded-[var(--ds-radius-md)]",
599
+ "shadow-[var(--ds-input-shadow)]",
600
+ "transition-colors",
601
+ !invalid && !disabled && !open && "hover:border-[var(--ds-input-border-hover)]",
602
+ open && (invalid ? ERROR_ACTIVE_RING : ACTIVE_RING),
603
+ invalid ? ERROR_FOCUS_RING : FOCUS_RING,
604
+ "cursor-pointer disabled:cursor-not-allowed disabled:opacity-disabled",
605
+ !selectedValues.length && "text-[var(--ds-text-muted)]"
606
+ ),
607
+ children: [
608
+ isTruncated ? /* @__PURE__ */ jsx(Tooltip, { content: displayLabel, children: /* @__PURE__ */ jsx("span", { ref: overflowRef, className: "truncate", children: displayLabel }) }) : /* @__PURE__ */ jsx("span", { ref: overflowRef, className: "truncate", children: displayLabel }),
609
+ /* @__PURE__ */ jsx("span", { className: cn("transition-transform", open && "rotate-180"), children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14, className: "rotate-90" }) })
610
+ ]
611
+ }
612
+ ),
613
+ open && createPortal(
614
+ /* @__PURE__ */ jsxs(
615
+ "div",
616
+ {
617
+ ref: menuRef,
618
+ tabIndex: -1,
619
+ onClick: (e) => e.stopPropagation(),
620
+ className: cn(
621
+ "fixed z-[var(--ds-z-popover)] outline-none",
622
+ "bg-[var(--ds-bg-surface-elevated)]",
623
+ "[backdrop-filter:var(--ds-glass-filter)] [-webkit-backdrop-filter:var(--ds-glass-filter)]",
624
+ "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
625
+ "rounded-[var(--ds-radius-md)] overflow-hidden",
626
+ "shadow-[var(--ds-shadow-dropdown),var(--ds-glass-inset)]"
627
+ ),
628
+ style: { top: pos.top, left: pos.left, minWidth: pos.width, width: "max-content", maxWidth: "calc(100vw - 16px)", visibility: positioned ? "visible" : "hidden" },
629
+ children: [
630
+ searchable && /* @__PURE__ */ jsx("div", { className: "border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]", style: { padding: "var(--ds-space-2)" }, children: /* @__PURE__ */ jsx(
631
+ "input",
632
+ {
633
+ ref: searchRef,
634
+ type: "text",
635
+ value: search,
636
+ onChange: (e) => setSearch(e.target.value),
637
+ placeholder: "Search\u2026",
638
+ className: cn("w-full bg-transparent text-[var(--ds-text-primary)] placeholder:text-[var(--ds-text-muted)] outline-none", size === "sm" ? "text-xs" : "text-sm")
639
+ }
640
+ ) }),
641
+ /* @__PURE__ */ jsx("div", { role: "listbox", className: "overflow-y-auto", style: { padding: "var(--ds-space-1)", maxHeight: "var(--ds-dropdown-max-height)" }, children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: cn("text-[var(--ds-text-muted)] text-center py-ds-4", size === "sm" ? "text-xs" : "text-sm"), children: "No options found" }) : hasGroups ? Array.from(groups.entries()).map(([group, opts]) => /* @__PURE__ */ jsxs("div", { children: [
642
+ group && /* @__PURE__ */ jsx("p", { className: "text-2xs font-medium uppercase tracking-wider text-[var(--ds-text-muted)] px-ds-2 pt-ds-2 pb-ds-1", children: group }),
643
+ opts.map((opt) => /* @__PURE__ */ jsx(
644
+ OptionButton,
645
+ {
646
+ opt,
647
+ isSelected: selectedValues.includes(opt.id),
648
+ multiple,
649
+ onSelect: handleSelect,
650
+ size
651
+ },
652
+ opt.id
653
+ ))
654
+ ] }, group)) : filtered.map((opt) => /* @__PURE__ */ jsx(
655
+ OptionButton,
656
+ {
657
+ opt,
658
+ isSelected: selectedValues.includes(opt.id),
659
+ multiple,
660
+ onSelect: handleSelect,
661
+ size
662
+ },
663
+ opt.id
664
+ )) })
665
+ ]
666
+ }
667
+ ),
668
+ container ?? document.body
669
+ ),
670
+ multiple && showChips && selectedValues.length > 0 && /* @__PURE__ */ jsx("div", { ref: chipsRef, className: "flex flex-wrap gap-ds-1 mt-ds-1.5", children: selectedValues.map((v, i) => {
671
+ const opt = options.find((o) => o.id === v);
672
+ return opt ? /* @__PURE__ */ jsxs(
673
+ "span",
674
+ {
675
+ className: "inline-flex items-center gap-ds-1 text-xs rounded-[var(--ds-radius-sm)] bg-[var(--ds-bg-muted)] text-[var(--ds-text-secondary)] px-ds-1.5 py-ds-0.5",
676
+ children: [
677
+ opt.label,
678
+ /* @__PURE__ */ jsx(
679
+ "button",
680
+ {
681
+ type: "button",
682
+ "data-chip-remove": "",
683
+ onClick: () => {
684
+ pendingChipFocus.current = { index: i, value: v };
685
+ handleSelect(v);
686
+ },
687
+ className: "hover:text-[var(--ds-text-primary)] cursor-pointer",
688
+ "aria-label": `Remove ${opt.label}`,
689
+ children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 10 })
690
+ }
691
+ )
692
+ ]
693
+ },
694
+ v
695
+ ) : null;
696
+ }) })
697
+ ] });
698
+ }
699
+ function OptionButton({
700
+ opt,
701
+ isSelected,
702
+ multiple,
703
+ onSelect,
704
+ size = "md"
705
+ }) {
706
+ return /* @__PURE__ */ jsxs(
707
+ "button",
708
+ {
709
+ type: "button",
710
+ role: "option",
711
+ "data-option-id": opt.id,
712
+ "aria-selected": isSelected,
713
+ tabIndex: -1,
714
+ onClick: () => onSelect(opt.id),
715
+ className: cn(
716
+ "w-full text-left cursor-pointer rounded-[var(--ds-radius-sm)]",
717
+ "transition-colors flex items-center gap-ds-2",
718
+ "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
719
+ size === "sm" ? "px-ds-2 py-ds-1 text-xs" : "px-ds-2 py-ds-1.5 text-sm",
720
+ isSelected && !multiple ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)]" : "text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-emphasis)]"
721
+ ),
722
+ children: [
723
+ multiple && /* @__PURE__ */ jsx(
724
+ "span",
725
+ {
726
+ className: cn(
727
+ "w-[var(--ds-icon-size-sm)] h-[var(--ds-icon-size-sm)] rounded-[var(--ds-radius-sm)] border-[length:var(--ds-border-width)] shrink-0 flex items-center justify-center",
728
+ isSelected ? "bg-[var(--ds-accent-primary)] border-[var(--ds-accent-primary)]" : "border-[var(--ds-border-default)]"
729
+ ),
730
+ children: isSelected && /* @__PURE__ */ jsx(Icon, { name: "check", size: 10, className: "text-[var(--ds-text-on-primary)]" })
731
+ }
732
+ ),
733
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: opt.label }),
734
+ isSelected && !multiple && /* @__PURE__ */ jsx(Icon, { name: "check", size: size === "sm" ? 12 : 14 })
735
+ ]
736
+ }
737
+ );
738
+ }
739
+ var sizes = {
740
+ sm: { box: "h-[var(--ds-control-height-sm)]", input: "text-xs self-stretch", btn: "w-7 text-xs" },
741
+ md: { box: "h-[var(--ds-control-height-md)]", input: "text-sm self-stretch", btn: "w-9 text-sm" },
742
+ lg: { box: "h-[var(--ds-control-height-lg)]", input: "text-base self-stretch", btn: "w-10 text-base" }
743
+ };
744
+ function NumberInput({
745
+ value,
746
+ onChange: onChangeProp,
747
+ min = -Infinity,
748
+ max = Infinity,
749
+ step = 1,
750
+ disabled = false,
751
+ placeholder = "0",
752
+ prefix,
753
+ suffix,
754
+ size: sizeProp,
755
+ className,
756
+ error = false,
757
+ allowDecimals,
758
+ id
759
+ }) {
760
+ const onChange = useReportedChange(onChangeProp, value);
761
+ const contextSize = useContext(ControlSizeContext);
762
+ const size = sizeProp ?? contextSize ?? "md";
763
+ const intervalRef = useRef(void 0);
764
+ const timeoutRef = useRef(void 0);
765
+ const inputRef = useRef(null);
766
+ const [focused, setFocused] = useState(false);
767
+ const [draft, setDraft] = useState(String(value));
768
+ const decimals = allowDecimals ?? step % 1 !== 0;
769
+ const [prevSync, setPrevSync] = useState({ value, focused });
770
+ if (value !== prevSync.value || focused !== prevSync.focused) {
771
+ setPrevSync({ value, focused });
772
+ if (!focused) setDraft(String(value));
773
+ }
774
+ const clamp = useCallback(
775
+ (v) => Math.min(max, Math.max(min, v)),
776
+ [min, max]
777
+ );
778
+ const valueRef = useLatest(value);
779
+ const commitValue = useCallback((v, own = false) => {
780
+ const clamped = clamp(v);
781
+ valueRef.current = clamped;
782
+ setDraft(String(clamped));
783
+ (own ? onChangeProp : onChange)(clamped);
784
+ return clamped;
785
+ }, [onChange, onChangeProp, clamp, valueRef]);
786
+ const increment = useCallback(() => commitValue(valueRef.current + step), [commitValue, step, valueRef]);
787
+ const decrement = useCallback(() => commitValue(valueRef.current - step), [commitValue, step, valueRef]);
788
+ const startRepeat = useCallback(
789
+ (fn) => {
790
+ fn();
791
+ timeoutRef.current = setTimeout(() => {
792
+ intervalRef.current = setInterval(fn, 75);
793
+ }, 400);
794
+ },
795
+ []
796
+ );
797
+ const stopRepeat = useCallback(() => {
798
+ clearTimeout(timeoutRef.current);
799
+ clearInterval(intervalRef.current);
800
+ }, []);
801
+ const handleInput = useCallback(
802
+ (e) => {
803
+ const raw = e.target.value;
804
+ if (raw === "" || raw === "-") {
805
+ setDraft(raw);
806
+ return;
807
+ }
808
+ if (!decimals && raw.includes(".")) return;
809
+ const validPattern = decimals ? /^-?\d*\.?\d*$/ : /^-?\d*$/;
810
+ if (!validPattern.test(raw)) return;
811
+ setDraft(raw);
812
+ const parsed = parseFloat(raw);
813
+ if (!isNaN(parsed) && isFinite(parsed)) {
814
+ onChange(clamp(parsed));
815
+ }
816
+ },
817
+ [onChange, clamp, decimals]
818
+ );
819
+ const handleBlur = useCallback(() => {
820
+ setFocused(false);
821
+ const parsed = parseFloat(draft);
822
+ if (!isNaN(parsed) && isFinite(parsed)) {
823
+ const committed = commitValue(parsed, draft === String(value));
824
+ setDraft(String(committed));
825
+ } else {
826
+ const fallback = isFinite(min) ? min : 0;
827
+ const committed = commitValue(value !== void 0 ? value : fallback, true);
828
+ setDraft(String(committed));
829
+ }
830
+ }, [draft, commitValue, min, value]);
831
+ const handleFocus = useCallback(() => {
832
+ setFocused(true);
833
+ }, []);
834
+ const handleKeyDown = useCallback(
835
+ (e) => {
836
+ if (e.key === "ArrowUp") {
837
+ e.preventDefault();
838
+ increment();
839
+ }
840
+ if (e.key === "ArrowDown") {
841
+ e.preventDefault();
842
+ decrement();
843
+ }
844
+ },
845
+ [increment, decrement]
846
+ );
847
+ const s = sizes[size];
848
+ const atMin = value <= min;
849
+ const atMax = value >= max;
850
+ return /* @__PURE__ */ jsxs(
851
+ "div",
852
+ {
853
+ className: cn(
854
+ "flex items-center w-full",
855
+ s.box,
856
+ "border-[length:var(--ds-border-width)]",
857
+ error ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)]",
858
+ "rounded-[var(--ds-radius-md)]",
859
+ "shadow-[var(--ds-input-shadow)]",
860
+ "bg-[var(--ds-input-bg)]",
861
+ "transition-colors duration-[var(--ds-transition-speed)]",
862
+ !error && !disabled && "hover:border-[var(--ds-input-border-hover)]",
863
+ error ? ERROR_FOCUS_WITHIN_RING : cn(FOCUS_WITHIN_RING, "focus-within:hover:border-[var(--ds-border-focus)]"),
864
+ disabled && "opacity-disabled cursor-not-allowed",
865
+ className
866
+ ),
867
+ children: [
868
+ /* @__PURE__ */ jsx(
869
+ "button",
870
+ {
871
+ type: "button",
872
+ tabIndex: -1,
873
+ disabled: disabled || atMin,
874
+ onMouseDown: (e) => {
875
+ e.preventDefault();
876
+ inputRef.current?.focus();
877
+ if (!disabled && !atMin) startRepeat(decrement);
878
+ },
879
+ onMouseUp: stopRepeat,
880
+ onMouseLeave: stopRepeat,
881
+ className: cn(
882
+ "inline-flex items-center justify-center shrink-0",
883
+ "border-r-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
884
+ "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
885
+ "transition-colors cursor-pointer",
886
+ "rounded-l-[var(--ds-radius-md)]",
887
+ s.btn,
888
+ s.input,
889
+ (disabled || atMin) && "opacity-disabled cursor-not-allowed hover:bg-transparent hover:text-[var(--ds-text-muted)]"
890
+ ),
891
+ "aria-label": "Decrease",
892
+ children: "\u2212"
893
+ }
894
+ ),
895
+ /* @__PURE__ */ jsxs("div", { className: cn("flex items-center flex-1 min-w-0 px-ds-2 gap-ds-1", s.input), children: [
896
+ prefix && /* @__PURE__ */ jsx("span", { className: "text-[var(--ds-text-muted)] shrink-0 text-xs", children: prefix }),
897
+ /* @__PURE__ */ jsx(
898
+ "input",
899
+ {
900
+ ref: inputRef,
901
+ id,
902
+ type: "text",
903
+ inputMode: decimals ? "decimal" : "numeric",
904
+ "aria-invalid": error || void 0,
905
+ value: focused ? draft : String(value),
906
+ onChange: handleInput,
907
+ onFocus: handleFocus,
908
+ onBlur: handleBlur,
909
+ onKeyDown: handleKeyDown,
910
+ disabled,
911
+ placeholder,
912
+ className: cn(
913
+ "w-full min-w-0 bg-transparent text-center outline-none",
914
+ "text-[var(--ds-text-primary)]",
915
+ "placeholder:text-[var(--ds-text-muted)]",
916
+ "font-[family-name:var(--ds-font-mono)]",
917
+ s.input,
918
+ disabled && "cursor-not-allowed"
919
+ )
920
+ }
921
+ ),
922
+ suffix && /* @__PURE__ */ jsx("span", { className: "text-[var(--ds-text-muted)] shrink-0 text-xs", children: suffix })
923
+ ] }),
924
+ /* @__PURE__ */ jsx(
925
+ "button",
926
+ {
927
+ type: "button",
928
+ tabIndex: -1,
929
+ disabled: disabled || atMax,
930
+ onMouseDown: (e) => {
931
+ e.preventDefault();
932
+ inputRef.current?.focus();
933
+ if (!disabled && !atMax) startRepeat(increment);
934
+ },
935
+ onMouseUp: stopRepeat,
936
+ onMouseLeave: stopRepeat,
937
+ className: cn(
938
+ "inline-flex items-center justify-center shrink-0",
939
+ "border-l-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
940
+ "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
941
+ "transition-colors cursor-pointer",
942
+ "rounded-r-[var(--ds-radius-md)]",
943
+ s.btn,
944
+ s.input,
945
+ (disabled || atMax) && "opacity-disabled cursor-not-allowed hover:bg-transparent hover:text-[var(--ds-text-muted)]"
946
+ ),
947
+ "aria-label": "Increase",
948
+ children: "+"
949
+ }
950
+ )
951
+ ]
952
+ }
953
+ );
954
+ }
955
+ function Popover({ open, onOpenChange, trigger, children, side = "bottom", align = "center", width, className, container }) {
956
+ const triggerRef = useRef(null);
957
+ const floatingRef = useRef(null);
958
+ const [triggerWidth, setTriggerWidth] = useState(void 0);
959
+ const setTriggerNode = useCallback((node) => {
960
+ triggerRef.current = node;
961
+ setTriggerWidth(node?.offsetWidth);
962
+ }, []);
963
+ const pos = usePosition(triggerRef, floatingRef, open, { side, align, offset: 8, flip: true });
964
+ const onOpenChangeRef = useLatest(onOpenChange);
965
+ const { props: pressMarks, marked } = usePressMarks();
966
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => onOpenChangeRef.current(false), tabExits: "edges", marked });
967
+ useEffect(() => {
968
+ if (!open) return;
969
+ const onKeyDown = (e) => {
970
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
971
+ const active = document.activeElement;
972
+ if (active && active !== document.body && !triggerRef.current?.contains(active) && !floatingRef.current?.contains(active)) return;
973
+ e.preventDefault();
974
+ returnFocus();
975
+ onOpenChangeRef.current(false);
976
+ };
977
+ const onPress = (e) => {
978
+ const target = e.target;
979
+ if (marked(e) || triggerRef.current?.contains(target) || floatingRef.current?.contains(target)) return;
980
+ onOpenChangeRef.current(false);
981
+ };
982
+ document.addEventListener("keydown", onKeyDown);
983
+ document.addEventListener("mousedown", onPress);
984
+ return () => {
985
+ document.removeEventListener("keydown", onKeyDown);
986
+ document.removeEventListener("mousedown", onPress);
987
+ };
988
+ }, [open, onOpenChangeRef, isTopLayer, returnFocus, marked]);
989
+ useEffect(() => {
990
+ if (open) focusInitial(floatingRef.current);
991
+ }, [open]);
992
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
993
+ /* @__PURE__ */ jsx(
994
+ Slot,
995
+ {
996
+ ...triggerType(trigger),
997
+ ref: setTriggerNode,
998
+ onClick: () => {
999
+ setTriggerWidth(triggerRef.current?.offsetWidth);
1000
+ onOpenChange(!open);
1001
+ },
1002
+ children: trigger
1003
+ }
1004
+ ),
1005
+ open && createPortal(
1006
+ /* @__PURE__ */ jsx(
1007
+ "div",
1008
+ {
1009
+ ref: floatingRef,
1010
+ "data-gs": "popover",
1011
+ ...pressMarks,
1012
+ onClick: (e) => e.stopPropagation(),
1013
+ className: cn(
1014
+ "fixed z-[var(--ds-z-popover)]",
1015
+ "bg-[var(--ds-bg-surface-elevated)]",
1016
+ GLASS_DROPDOWN,
1017
+ "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
1018
+ "rounded-[var(--ds-radius-md)]",
1019
+ className
1020
+ ),
1021
+ style: {
1022
+ top: pos.top,
1023
+ left: pos.left,
1024
+ width: width === "trigger" ? triggerWidth : width === "auto" ? void 0 : width
1025
+ },
1026
+ children
1027
+ }
1028
+ ),
1029
+ container ?? document.body
1030
+ )
1031
+ ] });
1032
+ }
1033
+
1034
+ // ../../showcase/src/lib/dev-warn.ts
1035
+ var warned = /* @__PURE__ */ new Set();
1036
+ function warnOnce(component, message) {
1037
+ let dev = false;
1038
+ try {
1039
+ dev = process.env.NODE_ENV !== "production";
1040
+ } catch {
1041
+ dev = false;
1042
+ }
1043
+ const text = `${component}: ${message}`;
1044
+ if (!dev || warned.has(text)) return;
1045
+ warned.add(text);
1046
+ console.warn(text);
1047
+ }
1048
+ var controlsDrawn = /* @__PURE__ */ new WeakMap();
1049
+ function Pagination(props) {
1050
+ const { pagerToken } = props;
1051
+ useEffect(() => {
1052
+ if (!pagerToken) return;
1053
+ const count = (controlsDrawn.get(pagerToken) ?? 0) + 1;
1054
+ controlsDrawn.set(pagerToken, count);
1055
+ if (count > 1) warnOnce("Pagination", "this list's pager already draws its control: a list given the pager (<Table pager={pager}>) draws it beneath the rows, so remove the separate <Pagination {...pager.props} />.");
1056
+ return () => {
1057
+ controlsDrawn.set(pagerToken, (controlsDrawn.get(pagerToken) ?? 1) - 1);
1058
+ };
1059
+ }, [pagerToken]);
1060
+ if ((props.mode === "load-more" || props.mode === "infinite-scroll") && props.onLoadMore) return /* @__PURE__ */ jsx(LoadMore, { ...props, onLoadMore: props.onLoadMore, scroll: props.mode === "infinite-scroll" });
1061
+ return /* @__PURE__ */ jsx(PageNumbers, { ...props });
1062
+ }
1063
+ function scrollerOf(el) {
1064
+ for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
1065
+ const { overflowY } = getComputedStyle(n);
1066
+ if ((overflowY === "auto" || overflowY === "scroll") && n.scrollHeight > n.clientHeight) return n;
1067
+ }
1068
+ return null;
1069
+ }
1070
+ function contentTop(el) {
1071
+ let top = el.getBoundingClientRect().top + window.scrollY;
1072
+ for (let n = el.parentElement; n && n !== document.documentElement; n = n.parentElement) top += n.scrollTop;
1073
+ return top;
1074
+ }
1075
+ function LoadMore({ hasMore, onLoadMore, loadMoreLabel, currentPage, scroll, className }) {
1076
+ const marker = useRef(null);
1077
+ const load = useLatest(onLoadMore);
1078
+ const loadedAt = useRef(null);
1079
+ useEffect(() => {
1080
+ const el = marker.current;
1081
+ if (!scroll || !hasMore || !el || typeof IntersectionObserver === "undefined") return;
1082
+ const observer = new IntersectionObserver((entries) => {
1083
+ if (!entries.some((e) => e.isIntersecting)) return;
1084
+ const at = contentTop(el);
1085
+ if (loadedAt.current !== null && Math.abs(at - loadedAt.current) < 1) return;
1086
+ loadedAt.current = at;
1087
+ load.current();
1088
+ }, { root: scrollerOf(el), rootMargin: "200px 0px" });
1089
+ observer.observe(el);
1090
+ const rearm = () => {
1091
+ loadedAt.current = null;
1092
+ };
1093
+ window.addEventListener("scroll", rearm, { capture: true, passive: true });
1094
+ return () => {
1095
+ observer.disconnect();
1096
+ window.removeEventListener("scroll", rearm, { capture: true });
1097
+ };
1098
+ }, [scroll, hasMore, currentPage, load]);
1099
+ if (!hasMore) return null;
1100
+ return /* @__PURE__ */ jsxs("div", { "data-ds-pager": "", "data-pagination-mode": scroll ? "infinite-scroll" : "load-more", className: cn("flex flex-col items-center", className), children: [
1101
+ scroll && /* @__PURE__ */ jsx("div", { ref: marker, "data-load-more-trigger": "", "aria-hidden": "true", className: "h-px w-full" }),
1102
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: onLoadMore, children: loadMoreLabel ?? "Load more" })
1103
+ ] });
1104
+ }
1105
+ function PageNumbers({ currentPage, totalPages, onPageChange, pageSize: pageSizeProp, pageSizeOptions, onPageSizeChange, className }) {
1106
+ const pages = buildPageRange(currentPage, totalPages);
1107
+ const preferences = useShellPreferences();
1108
+ const pageSize = pageSizeProp ?? preferences.itemsPerPage;
1109
+ const sizeChoice = !!(pageSizeOptions && pageSize != null && onPageSizeChange);
1110
+ if (totalPages <= 1 && !sizeChoice) return null;
1111
+ return /* @__PURE__ */ jsxs("nav", { "aria-label": "Pagination", "data-ds-pager": "", className: cn("flex flex-wrap items-center gap-ds-3", className), children: [
1112
+ sizeChoice && pageSizeOptions && pageSize != null && onPageSizeChange && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
1113
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-[var(--ds-text-muted)] whitespace-nowrap", children: "Rows per page" }),
1114
+ /* @__PURE__ */ jsx(FormChangeCtx, { value: null, children: /* @__PURE__ */ jsx(
1115
+ Dropdown,
1116
+ {
1117
+ "aria-label": `Rows per page, ${pageSize}`,
1118
+ options: pageSizeOptions.map((n) => ({ id: String(n), label: String(n) })),
1119
+ value: String(pageSize),
1120
+ onChange: (v) => onPageSizeChange(Number(v)),
1121
+ size: "sm",
1122
+ className: "w-[4.5rem]"
1123
+ }
1124
+ ) })
1125
+ ] }),
1126
+ totalPages > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-1", children: [
1127
+ /* @__PURE__ */ jsx(
1128
+ "button",
1129
+ {
1130
+ type: "button",
1131
+ onClick: () => onPageChange(currentPage - 1),
1132
+ disabled: currentPage <= 1,
1133
+ "aria-label": "Previous page",
1134
+ className: cn(
1135
+ "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-sm cursor-pointer",
1136
+ "rounded-[var(--ds-radius-md)]",
1137
+ "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
1138
+ "bg-[var(--ds-bg-surface)] text-[var(--ds-text-secondary)]",
1139
+ "transition-colors duration-[var(--ds-transition-speed)]",
1140
+ "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]",
1141
+ "disabled:opacity-disabled disabled:cursor-not-allowed"
1142
+ ),
1143
+ children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 16, className: "rotate-180" })
1144
+ }
1145
+ ),
1146
+ pages.map(
1147
+ (p, i) => p === "..." ? /* @__PURE__ */ jsx(EllipsisJumper, { totalPages, onPageChange }, `ellipsis-${i}`) : /* @__PURE__ */ jsx(
1148
+ "button",
1149
+ {
1150
+ type: "button",
1151
+ onClick: () => onPageChange(p),
1152
+ "aria-current": p === currentPage ? "page" : void 0,
1153
+ className: cn(
1154
+ "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-sm font-medium cursor-pointer",
1155
+ "rounded-[var(--ds-radius-md)]",
1156
+ "border-[length:var(--ds-border-width)]",
1157
+ "transition-colors duration-[var(--ds-transition-speed)]",
1158
+ p === currentPage ? "bg-[var(--ds-accent-primary)] [background-image:var(--ds-gradient)] text-[var(--ds-text-on-primary)] border-[var(--ds-accent-primary)]" : "bg-[var(--ds-bg-surface)] text-[var(--ds-text-secondary)] border-[var(--ds-border-default)] hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]"
1159
+ ),
1160
+ children: p
1161
+ },
1162
+ p
1163
+ )
1164
+ ),
1165
+ /* @__PURE__ */ jsx(
1166
+ "button",
1167
+ {
1168
+ type: "button",
1169
+ onClick: () => onPageChange(currentPage + 1),
1170
+ disabled: currentPage >= totalPages,
1171
+ "aria-label": "Next page",
1172
+ className: cn(
1173
+ "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-sm cursor-pointer",
1174
+ "rounded-[var(--ds-radius-md)]",
1175
+ "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
1176
+ "bg-[var(--ds-bg-surface)] text-[var(--ds-text-secondary)]",
1177
+ "transition-colors duration-[var(--ds-transition-speed)]",
1178
+ "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]",
1179
+ "disabled:opacity-disabled disabled:cursor-not-allowed"
1180
+ ),
1181
+ children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 16 })
1182
+ }
1183
+ )
1184
+ ] })
1185
+ ] });
1186
+ }
1187
+ function buildPageRange(current, total) {
1188
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
1189
+ const pages = [1];
1190
+ if (current > 3) pages.push("...");
1191
+ const start = Math.max(2, current - 1);
1192
+ const end = Math.min(total - 1, current + 1);
1193
+ for (let i = start; i <= end; i++) pages.push(i);
1194
+ if (current < total - 2) pages.push("...");
1195
+ pages.push(total);
1196
+ return pages;
1197
+ }
1198
+ function EllipsisJumper({ totalPages, onPageChange }) {
1199
+ const [open, setOpen] = useState(false);
1200
+ const [jumpPage, setJumpPage] = useState(1);
1201
+ const handleOpenChange = (next) => {
1202
+ if (next) setJumpPage(1);
1203
+ setOpen(next);
1204
+ };
1205
+ const handleGo = () => {
1206
+ const clamped = Math.min(totalPages, Math.max(1, jumpPage));
1207
+ onPageChange(clamped);
1208
+ setOpen(false);
1209
+ };
1210
+ return /* @__PURE__ */ jsx(
1211
+ Popover,
1212
+ {
1213
+ open,
1214
+ onOpenChange: handleOpenChange,
1215
+ trigger: /* @__PURE__ */ jsx(
1216
+ "button",
1217
+ {
1218
+ type: "button",
1219
+ "aria-label": "Go to page",
1220
+ onClick: () => handleOpenChange(true),
1221
+ className: cn(
1222
+ "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-sm cursor-pointer",
1223
+ "rounded-[var(--ds-radius-md)]",
1224
+ "text-[var(--ds-text-muted)]",
1225
+ "transition-colors duration-[var(--ds-transition-speed)]",
1226
+ "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]"
1227
+ ),
1228
+ children: "\u2026"
1229
+ }
1230
+ ),
1231
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-ds-2 p-ds-3", children: [
1232
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-[var(--ds-text-secondary)]", children: "Go to page" }),
1233
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
1234
+ /* @__PURE__ */ jsx(
1235
+ NumberInput,
1236
+ {
1237
+ value: jumpPage,
1238
+ onChange: setJumpPage,
1239
+ min: 1,
1240
+ max: totalPages,
1241
+ step: 1,
1242
+ size: "sm",
1243
+ className: "w-[5rem]"
1244
+ }
1245
+ ),
1246
+ /* @__PURE__ */ jsx(
1247
+ "button",
1248
+ {
1249
+ type: "button",
1250
+ onClick: handleGo,
1251
+ className: cn(
1252
+ "px-ds-2.5 h-[var(--ds-control-height-sm)] text-xs font-medium cursor-pointer",
1253
+ "rounded-[var(--ds-radius-md)]",
1254
+ "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)]",
1255
+ "transition-colors duration-[var(--ds-transition-speed)]",
1256
+ "hover:opacity-90"
1257
+ ),
1258
+ children: "Go"
1259
+ }
1260
+ )
1261
+ ] })
1262
+ ] })
1263
+ }
1264
+ );
1265
+ }
1266
+ function firstKey(item) {
1267
+ if (item !== null && typeof item === "object") return "id" in item ? item.id : void 0;
1268
+ return item;
1269
+ }
1270
+ function resetKey(resetOn) {
1271
+ try {
1272
+ return JSON.stringify(resetOn ?? null);
1273
+ } catch {
1274
+ return String(resetOn);
1275
+ }
1276
+ }
1277
+ function signature(list, resetOn) {
1278
+ let first = "";
1279
+ try {
1280
+ first = JSON.stringify(firstKey(list[0]) ?? null);
1281
+ } catch {
1282
+ first = "";
1283
+ }
1284
+ return `${list.length}|${first}|${resetKey(resetOn)}`;
1285
+ }
1286
+ function grewAtAnEnd(seen, list, reset) {
1287
+ if (reset !== seen.reset || list.length <= seen.length || seen.first === void 0 || seen.length === 0) return false;
1288
+ return firstKey(list[0]) === seen.first || firstKey(list[list.length - seen.length]) === seen.first;
1289
+ }
1290
+ function usePagination(list, options = {}) {
1291
+ const shell = useShellPreferences();
1292
+ const style = options.style ?? shell.paginationStyle ?? PREFERENCE_HOUSE_DEFAULTS.paginationStyle;
1293
+ const [chosenSize, setChosenSize] = useState(null);
1294
+ const [pagerToken] = useState(() => ({}));
1295
+ const pageSize = Math.max(1, Math.floor(chosenSize ?? options.pageSize ?? shell.itemsPerPage ?? PREFERENCE_HOUSE_DEFAULTS.itemsPerPage));
1296
+ const [page, setPageState] = useState(1);
1297
+ const sig = signature(list, options.resetOn);
1298
+ const reset = resetKey(options.resetOn);
1299
+ const [seen, setSeen] = useState(() => ({ sig, length: list.length, first: firstKey(list[0]), reset }));
1300
+ const changed = seen.sig !== sig;
1301
+ const startsAgain = changed && !grewAtAnEnd(seen, list, reset);
1302
+ if (changed) {
1303
+ setSeen({ sig, length: list.length, first: firstKey(list[0]), reset });
1304
+ if (startsAgain && page !== 1) setPageState(1);
1305
+ }
1306
+ const totalItems = list.length;
1307
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
1308
+ const current = Math.min(Math.max(1, startsAgain ? 1 : page), totalPages);
1309
+ const grows = style !== "pagination";
1310
+ const start = grows ? 0 : (current - 1) * pageSize;
1311
+ const items = list.slice(start, current * pageSize);
1312
+ const range = items.length === 0 ? [0, 0] : [start + 1, start + items.length];
1313
+ const hasMore = current < totalPages;
1314
+ const setPage = (next) => setPageState(Math.min(Math.max(1, Math.floor(next)), totalPages));
1315
+ const loadMore = () => setPageState((p) => Math.min(p + 1, totalPages));
1316
+ const setPageSize = (size) => {
1317
+ setChosenSize(Math.max(1, Math.floor(size)));
1318
+ setPageState(1);
1319
+ };
1320
+ return {
1321
+ style,
1322
+ items,
1323
+ page: current,
1324
+ pageSize,
1325
+ totalItems,
1326
+ totalPages,
1327
+ range,
1328
+ shown: items.length,
1329
+ hasMore,
1330
+ loadMore,
1331
+ setPage,
1332
+ setPageSize,
1333
+ props: { currentPage: current, totalPages, onPageChange: setPage, pageSize, onPageSizeChange: setPageSize, mode: style, hasMore, onLoadMore: loadMore, pagerToken, ...options.pageSizeOptions ? { pageSizeOptions: options.pageSizeOptions } : {} }
1334
+ };
1335
+ }
1336
+ function pagerPart(list, pager) {
1337
+ if (!pager) return list;
1338
+ return pager.range[0] === 0 ? [] : list.slice(pager.range[0] - 1, pager.range[1]);
1339
+ }
1340
+ function useWholeListCheck(component, length, pager) {
1341
+ const total = pager?.totalItems;
1342
+ useEffect(() => {
1343
+ if (total !== void 0 && length !== total) warnOnce(component, `the pager counts ${total} items but the list holds ${length}: give the list and usePagination the same items, every item of the list, and the list draws the pager's part (Sprint 25.13 A3).`);
1344
+ }, [component, length, total]);
1345
+ }
1346
+
1347
+ export { AddAnotherRowCtx, Dropdown, Field, InlineFieldError, Input, Label, NumberInput, Pagination, Popover, Textarea, Toggle, pagerPart, useField, useFieldControl, usePagination, useWholeListCheck, warnOnce };
1348
+ //# sourceMappingURL=chunk-2RV23OBK.js.map
1349
+ //# sourceMappingURL=chunk-2RV23OBK.js.map