@officina/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,2008 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { cva } from 'class-variance-authority';
4
+ import { forwardRef, createContext, useEffect, lazy, useState, useRef, isValidElement, cloneElement, useId, useContext, useMemo, Suspense } from 'react';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+ import { useFloating, autoUpdate, offset, flip, shift, arrow, useHover, useFocus, useDismiss, useRole, useInteractions, FloatingPortal, FloatingArrow, useClick, FloatingFocusManager } from '@floating-ui/react';
7
+ import { FormProvider, useFormContext } from 'react-hook-form';
8
+ import { Checkbox as Checkbox$1, Field as Field$1, Label, Description, RadioGroup as RadioGroup$1, Radio, Switch as Switch$1 } from '@headlessui/react';
9
+ import { ChevronUp, ChevronDown, Search, X, Minus, Check, CheckCircle2, Loader2, Filter } from 'lucide-react';
10
+ import { matchSorter } from 'match-sorter';
11
+ import { useDebouncedCallback } from 'use-debounce';
12
+
13
+ function cn(...inputs) {
14
+ return twMerge(clsx(inputs));
15
+ }
16
+ var buttonVariants = cva(
17
+ [
18
+ "inline-flex items-center justify-center gap-2 rounded-md font-semibold",
19
+ "whitespace-nowrap transition-[background-color,border-color,color,box-shadow,transform,opacity] duration-[var(--motion-fast)] ease-[var(--ease-standard)]",
20
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-offset-2",
21
+ "active:scale-[0.985] disabled:cursor-not-allowed disabled:opacity-45"
22
+ ],
23
+ {
24
+ variants: {
25
+ variant: {
26
+ primary: [
27
+ "bg-[var(--color-accent)] text-[var(--color-accent-contrast)] border border-[var(--color-accent-hover)]",
28
+ "hover:bg-[var(--color-accent-hover)]"
29
+ ],
30
+ secondary: [
31
+ "bg-[var(--color-bg-base)] text-[var(--color-fg-base)] border border-[var(--color-border)]",
32
+ "hover:bg-[var(--color-bg-muted)]"
33
+ ],
34
+ outline: [
35
+ "bg-[var(--color-bg-base)] text-[var(--color-fg-base)] border border-[var(--color-border)]",
36
+ "hover:bg-[var(--color-bg-muted)]"
37
+ ],
38
+ ghost: [
39
+ "bg-transparent text-[var(--color-fg-base)] border border-transparent",
40
+ "hover:bg-[var(--color-bg-muted)]"
41
+ ],
42
+ soft: [
43
+ "bg-[var(--color-accent-muted)] text-[var(--color-accent-fg)] border border-transparent",
44
+ "hover:bg-[var(--color-accent-subtle)]"
45
+ ],
46
+ danger: [
47
+ "bg-[var(--color-danger)] text-[var(--color-danger-contrast)] border border-[var(--color-danger-hover)]",
48
+ "hover:bg-[var(--color-danger-hover)]"
49
+ ],
50
+ success: [
51
+ "bg-[var(--color-success)] text-[var(--color-success-contrast)] border border-[var(--color-success-hover)]",
52
+ "hover:bg-[var(--color-success-hover)]"
53
+ ]
54
+ },
55
+ size: {
56
+ xs: "h-7 px-2.5 text-xs",
57
+ sm: "h-8 px-3 text-xs",
58
+ md: "h-9 px-3.5 text-sm",
59
+ lg: "h-11 px-5 text-sm",
60
+ icon: "size-9",
61
+ "icon-sm": "size-7"
62
+ }
63
+ },
64
+ defaultVariants: { variant: "primary", size: "md" }
65
+ }
66
+ );
67
+ var Button = forwardRef(function Button2({ className, variant, size, isLoading, disabled, children, type = "button", ...rest }, ref) {
68
+ const resolvedSize = size ?? "md";
69
+ return /* @__PURE__ */ jsxs(
70
+ "button",
71
+ {
72
+ ref,
73
+ type,
74
+ disabled: disabled || isLoading,
75
+ "aria-busy": isLoading,
76
+ "data-density-control": "button",
77
+ "data-density-size": resolvedSize,
78
+ ...rest,
79
+ className: cn(buttonVariants({ variant, size: resolvedSize }), className),
80
+ children: [
81
+ isLoading ? /* @__PURE__ */ jsx(
82
+ "span",
83
+ {
84
+ "aria-hidden": "true",
85
+ className: "size-4 animate-spin rounded-full border-2 border-current border-t-transparent"
86
+ }
87
+ ) : null,
88
+ children
89
+ ]
90
+ }
91
+ );
92
+ });
93
+ function toPlacement(side = "bottom", align = "center") {
94
+ return `${side}${align === "center" ? "" : `-${align}`}`;
95
+ }
96
+ function assignRef(ref, value) {
97
+ if (!ref) return;
98
+ if (typeof ref === "function") {
99
+ ref(value);
100
+ return;
101
+ }
102
+ ref.current = value;
103
+ }
104
+ function composeRefs(...refs) {
105
+ return (node) => {
106
+ refs.forEach((ref) => assignRef(ref, node));
107
+ };
108
+ }
109
+ function Tooltip({
110
+ content,
111
+ children,
112
+ side = "top",
113
+ align = "center",
114
+ delayMs = 200,
115
+ sideOffset = 6,
116
+ disabled
117
+ }) {
118
+ const [open, setOpen] = useState(false);
119
+ const arrowRef = useRef(null);
120
+ const floating = useFloating({
121
+ open: disabled ? false : open,
122
+ onOpenChange: setOpen,
123
+ placement: toPlacement(side, align),
124
+ whileElementsMounted: autoUpdate,
125
+ middleware: [offset(sideOffset), flip(), shift({ padding: 8 }), arrow({ element: arrowRef })]
126
+ });
127
+ const hover = useHover(floating.context, {
128
+ delay: { open: delayMs, close: 60 },
129
+ enabled: !disabled
130
+ });
131
+ const focus = useFocus(floating.context, { enabled: !disabled });
132
+ const dismiss = useDismiss(floating.context);
133
+ const role = useRole(floating.context, { role: "tooltip" });
134
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]);
135
+ const trigger = isValidElement(children) ? (() => {
136
+ const childProps = children.props;
137
+ return cloneElement(
138
+ children,
139
+ getReferenceProps({
140
+ ...childProps,
141
+ ref: composeRefs(childProps.ref, floating.refs.setReference)
142
+ })
143
+ );
144
+ })() : children;
145
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
146
+ trigger,
147
+ open && !disabled ? /* @__PURE__ */ jsx(FloatingPortal, { children: /* @__PURE__ */ jsxs(
148
+ "div",
149
+ {
150
+ ref: floating.refs.setFloating,
151
+ style: floating.floatingStyles,
152
+ ...getFloatingProps({
153
+ className: cn(
154
+ "z-[9999] max-w-xs rounded-[var(--radius-sm)] border border-[var(--color-border-strong)] bg-[var(--color-tooltip-bg)] px-2 py-1",
155
+ "text-xs font-medium text-[var(--color-tooltip-fg)] shadow-[var(--shadow-lg)]",
156
+ "transition-opacity duration-[var(--motion-fast)] ease-[var(--ease-standard)]"
157
+ )
158
+ }),
159
+ children: [
160
+ content,
161
+ /* @__PURE__ */ jsx(
162
+ FloatingArrow,
163
+ {
164
+ ref: arrowRef,
165
+ context: floating.context,
166
+ className: "fill-[var(--color-tooltip-bg)]"
167
+ }
168
+ )
169
+ ]
170
+ }
171
+ ) }) : null
172
+ ] });
173
+ }
174
+ var IconButton = forwardRef(function IconButton2({ icon, size = "md", tooltip, tooltipSide = "top", className, ...props }, ref) {
175
+ const button = /* @__PURE__ */ jsx(
176
+ Button,
177
+ {
178
+ ref,
179
+ size: size === "sm" ? "icon-sm" : "icon",
180
+ className: cn("shrink-0", className),
181
+ ...props,
182
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: size === "sm" ? "[&>svg]:size-3.5" : "[&>svg]:size-4", children: icon })
183
+ }
184
+ );
185
+ if (tooltip) {
186
+ return /* @__PURE__ */ jsx(Tooltip, { content: tooltip, side: tooltipSide, children: button });
187
+ }
188
+ return button;
189
+ });
190
+ function ButtonGroup({
191
+ children,
192
+ orientation = "horizontal",
193
+ attached = true,
194
+ className
195
+ }) {
196
+ return /* @__PURE__ */ jsx(
197
+ "div",
198
+ {
199
+ role: "group",
200
+ className: cn(
201
+ "inline-flex",
202
+ orientation === "vertical" && "flex-col",
203
+ attached && orientation === "horizontal" && "[&>*:first-child]:rounded-r-none [&>*:last-child]:rounded-l-none [&>*:not(:first-child):not(:last-child)]:rounded-none [&>*:not(:first-child)]:-ml-px",
204
+ attached && orientation === "vertical" && "[&>*:first-child]:rounded-b-none [&>*:last-child]:rounded-t-none [&>*:not(:first-child):not(:last-child)]:rounded-none [&>*:not(:first-child)]:-mt-px",
205
+ className
206
+ ),
207
+ children
208
+ }
209
+ );
210
+ }
211
+ var badgeVariants = cva("inline-flex items-center gap-1 rounded-full font-semibold", {
212
+ variants: {
213
+ variant: {
214
+ neutral: "bg-[var(--color-bg-muted)] text-[var(--color-fg-base)]",
215
+ success: "bg-[var(--color-success-subtle)] text-[var(--color-success-fg)] dark:bg-[var(--color-success-subtle)] dark:text-[var(--color-success-fg)]",
216
+ warning: "bg-[var(--color-warning-subtle)] text-[var(--color-warning-fg)] dark:bg-[var(--color-warning-subtle)] dark:text-[var(--color-warning-fg)]",
217
+ danger: "bg-[var(--color-danger-subtle)] text-[var(--color-danger-fg)] dark:bg-[var(--color-danger-subtle)] dark:text-[var(--color-danger-fg)]",
218
+ info: "bg-[var(--color-info-subtle)] text-[var(--color-info-fg)] dark:bg-[var(--color-info-subtle)] dark:text-[var(--color-info-fg)]",
219
+ accent: "bg-[var(--color-accent-muted)] text-[var(--color-accent-fg)]"
220
+ },
221
+ size: {
222
+ sm: "px-2 py-0.5 text-[10px]",
223
+ md: "px-2.5 py-0.5 text-xs"
224
+ }
225
+ },
226
+ defaultVariants: { variant: "neutral", size: "md" }
227
+ });
228
+ function Badge({ className, variant, tone, size, dot, children, ...props }) {
229
+ const requestedVariant = variant ?? tone;
230
+ const resolvedVariant = requestedVariant === "error" ? "danger" : requestedVariant;
231
+ return /* @__PURE__ */ jsxs("span", { ...props, className: cn(badgeVariants({ variant: resolvedVariant, size }), className), children: [
232
+ dot ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "size-1.5 rounded-full bg-current" }) : null,
233
+ children
234
+ ] });
235
+ }
236
+ function Form({
237
+ form,
238
+ onSubmit,
239
+ children,
240
+ className,
241
+ ...rest
242
+ }) {
243
+ return /* @__PURE__ */ jsx(FormProvider, { ...form, children: /* @__PURE__ */ jsx(
244
+ "form",
245
+ {
246
+ noValidate: true,
247
+ ...rest,
248
+ onSubmit: (e) => {
249
+ void form.handleSubmit(onSubmit)(e);
250
+ },
251
+ className: cn("space-y-4", className),
252
+ children
253
+ }
254
+ ) });
255
+ }
256
+ function Field({
257
+ id: providedId,
258
+ name,
259
+ label,
260
+ hint,
261
+ error: manualError,
262
+ required,
263
+ className,
264
+ children
265
+ }) {
266
+ const generatedId = useId();
267
+ const id = providedId ?? generatedId;
268
+ const errorId = `${id}-error`;
269
+ const hintId = `${id}-hint`;
270
+ const ctx = useFormContext();
271
+ const error = name ? ctx?.formState.errors[name] : void 0;
272
+ const errorMessage = manualError ?? (typeof error?.message === "string" ? error.message : void 0);
273
+ const describedBy = [hint ? hintId : null, errorMessage ? errorId : null].filter(Boolean).join(" ");
274
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-1.5", className), children: [
275
+ /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "text-sm font-medium text-[var(--color-fg-base)]", children: [
276
+ label,
277
+ required ? /* @__PURE__ */ jsx("span", { className: "ml-1 text-[var(--color-danger)]", "aria-hidden": "true", children: "*" }) : null
278
+ ] }),
279
+ typeof children === "function" ? children({ id, "aria-invalid": Boolean(errorMessage), "aria-describedby": describedBy }) : isValidElement(children) ? cloneElement(children, {
280
+ id,
281
+ "aria-invalid": Boolean(errorMessage),
282
+ "aria-describedby": describedBy || void 0
283
+ }) : children,
284
+ hint && !errorMessage ? /* @__PURE__ */ jsx("p", { id: hintId, className: "text-xs text-[var(--color-fg-muted)]", children: hint }) : null,
285
+ errorMessage ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-xs text-[var(--color-danger)]", children: errorMessage }) : null
286
+ ] });
287
+ }
288
+ var Input = forwardRef(function Input2({ className, ...props }, ref) {
289
+ return /* @__PURE__ */ jsx(
290
+ "input",
291
+ {
292
+ ref,
293
+ "data-density-control": "input",
294
+ ...props,
295
+ className: cn(
296
+ "block h-9 w-full rounded-md border border-[var(--color-border-strong)]",
297
+ "bg-[var(--color-bg-base)] px-3 text-sm text-[var(--color-fg-base)]",
298
+ "placeholder:text-[var(--color-fg-subtle)]",
299
+ "transition-[border-color,box-shadow] duration-[var(--duration-fast)]",
300
+ "focus:ring-3 focus:ring-[var(--color-accent)]/15 focus:border-[var(--color-accent)] focus:outline-none",
301
+ "disabled:cursor-not-allowed disabled:opacity-50",
302
+ "aria-[invalid=true]:focus:ring-[var(--color-danger)]/15 aria-[invalid=true]:border-[var(--color-danger)]",
303
+ className
304
+ )
305
+ }
306
+ );
307
+ });
308
+ var gridColumns = {
309
+ 1: "grid-cols-1",
310
+ 2: "grid-cols-1 md:grid-cols-2",
311
+ 3: "grid-cols-1 md:grid-cols-3",
312
+ 4: "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4"
313
+ };
314
+ var actionAlign = {
315
+ between: "justify-between",
316
+ center: "justify-center",
317
+ end: "justify-end",
318
+ start: "justify-start"
319
+ };
320
+ function FormGrid({ children, className, columns = 2, ...props }) {
321
+ return /* @__PURE__ */ jsx("div", { ...props, className: cn("grid gap-4", gridColumns[columns], className), children });
322
+ }
323
+ function Fieldset({
324
+ children,
325
+ className,
326
+ columns = 1,
327
+ description,
328
+ legend,
329
+ ...props
330
+ }) {
331
+ return /* @__PURE__ */ jsxs(
332
+ "fieldset",
333
+ {
334
+ ...props,
335
+ className: cn(
336
+ "min-w-0 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-base)] p-4",
337
+ className
338
+ ),
339
+ children: [
340
+ legend || description ? /* @__PURE__ */ jsxs(Fragment, { children: [
341
+ legend ? /* @__PURE__ */ jsx("legend", { className: "text-sm font-semibold text-[var(--color-fg-base)]", children: legend }) : null,
342
+ description ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs leading-5 text-[var(--color-fg-muted)]", children: description }) : null
343
+ ] }) : null,
344
+ /* @__PURE__ */ jsx(FormGrid, { columns, className: legend || description ? "mt-4" : void 0, children })
345
+ ]
346
+ }
347
+ );
348
+ }
349
+ function FormActions({
350
+ align = "end",
351
+ children,
352
+ className,
353
+ sticky,
354
+ ...props
355
+ }) {
356
+ return /* @__PURE__ */ jsx(
357
+ "div",
358
+ {
359
+ ...props,
360
+ className: cn(
361
+ "flex flex-wrap items-center gap-2 border-t border-[var(--color-border)] pt-4",
362
+ actionAlign[align],
363
+ sticky ? "bg-[var(--color-bg-base)]/95 sticky bottom-0 z-10 -mx-4 px-4 pb-4 backdrop-blur" : void 0,
364
+ className
365
+ ),
366
+ children
367
+ }
368
+ );
369
+ }
370
+ var FormControlContext = createContext(null);
371
+ function useFormControlContext() {
372
+ return useContext(FormControlContext);
373
+ }
374
+ function useFormControl() {
375
+ const ctx = useFormControlContext();
376
+ if (!ctx) return {};
377
+ return {
378
+ id: ctx.id,
379
+ "aria-describedby": ctx["aria-describedby"],
380
+ "aria-invalid": ctx["aria-invalid"],
381
+ "aria-required": ctx["aria-required"],
382
+ required: ctx.required,
383
+ disabled: ctx.disabled
384
+ };
385
+ }
386
+ function FormControl({
387
+ id,
388
+ invalid = false,
389
+ required = false,
390
+ disabled = false,
391
+ className,
392
+ children,
393
+ ...props
394
+ }) {
395
+ const generatedId = useId();
396
+ const controlId = id ?? generatedId;
397
+ const descriptionId = `${controlId}-description`;
398
+ const errorId = `${controlId}-error`;
399
+ const describedBy = [invalid ? errorId : null, descriptionId].filter(Boolean).join(" ") || void 0;
400
+ const field = {
401
+ id: controlId,
402
+ "aria-describedby": describedBy,
403
+ "aria-invalid": invalid ? true : void 0,
404
+ "aria-required": required ? true : void 0,
405
+ required,
406
+ disabled
407
+ };
408
+ const value = { ...field, descriptionId, errorId, invalid };
409
+ return /* @__PURE__ */ jsx(FormControlContext.Provider, { value, children: /* @__PURE__ */ jsx(
410
+ "div",
411
+ {
412
+ ...props,
413
+ "data-invalid": invalid ? "" : void 0,
414
+ "data-disabled": disabled ? "" : void 0,
415
+ className: cn("flex flex-col gap-1.5", className),
416
+ children: typeof children === "function" ? children(field) : children
417
+ }
418
+ ) });
419
+ }
420
+ function FormLabel({ className, children, required, htmlFor, ...props }) {
421
+ const ctx = useFormControlContext();
422
+ const isRequired = required ?? ctx?.required ?? false;
423
+ const target = htmlFor ?? ctx?.id;
424
+ return /* @__PURE__ */ jsxs(
425
+ "label",
426
+ {
427
+ ...props,
428
+ ...target ? { htmlFor: target } : {},
429
+ className: cn(
430
+ "flex select-none items-center gap-1 text-sm font-medium text-[var(--color-fg-base)]",
431
+ ctx?.disabled && "opacity-60",
432
+ className
433
+ ),
434
+ children: [
435
+ children,
436
+ isRequired ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-[var(--color-danger-fg)]", children: "*" }) : null
437
+ ]
438
+ }
439
+ );
440
+ }
441
+ var helperTone = {
442
+ muted: "text-[var(--color-fg-muted)]",
443
+ danger: "text-[var(--color-danger-fg)]",
444
+ success: "text-[var(--color-success-fg)]"
445
+ };
446
+ function FormHelperText({
447
+ className,
448
+ tone = "muted",
449
+ children,
450
+ ...props
451
+ }) {
452
+ const ctx = useFormControlContext();
453
+ return /* @__PURE__ */ jsx(
454
+ "p",
455
+ {
456
+ ...props,
457
+ ...ctx?.descriptionId ? { id: ctx.descriptionId } : {},
458
+ className: cn("text-xs leading-5", helperTone[tone], className),
459
+ children
460
+ }
461
+ );
462
+ }
463
+ function FormError({ className, children, ...props }) {
464
+ const ctx = useFormControlContext();
465
+ if (ctx && !ctx.invalid) return null;
466
+ if (!children) return null;
467
+ return /* @__PURE__ */ jsx(
468
+ "p",
469
+ {
470
+ ...props,
471
+ ...ctx?.errorId ? { id: ctx.errorId } : {},
472
+ role: "alert",
473
+ className: cn("text-xs leading-5 text-[var(--color-danger-fg)]", className),
474
+ children
475
+ }
476
+ );
477
+ }
478
+ function Checkbox({
479
+ checked,
480
+ defaultChecked,
481
+ onChange,
482
+ indeterminate,
483
+ disabled,
484
+ label,
485
+ description,
486
+ className,
487
+ id
488
+ }) {
489
+ const checkboxProps = {
490
+ ...id !== void 0 ? { id } : {},
491
+ ...checked !== void 0 ? { checked } : {},
492
+ ...defaultChecked !== void 0 ? { defaultChecked } : {},
493
+ ...onChange !== void 0 ? { onChange } : {},
494
+ ...disabled !== void 0 ? { disabled } : {}
495
+ };
496
+ const control = /* @__PURE__ */ jsx(
497
+ Checkbox$1,
498
+ {
499
+ ...checkboxProps,
500
+ className: cn(
501
+ "group relative flex size-4 shrink-0 items-center justify-center rounded",
502
+ "border border-[var(--color-border-strong)] bg-[var(--color-bg-base)]",
503
+ "transition-colors duration-[var(--duration-fast)]",
504
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-offset-2",
505
+ "data-[checked]:border-[var(--color-accent)] data-[checked]:bg-[var(--color-accent)]",
506
+ "data-[indeterminate]:border-[var(--color-accent)] data-[indeterminate]:bg-[var(--color-accent)]",
507
+ "disabled:cursor-not-allowed disabled:opacity-50",
508
+ className
509
+ ),
510
+ ...indeterminate ? { "data-indeterminate": true } : {},
511
+ children: indeterminate ? /* @__PURE__ */ jsx(Minus, { className: "size-2.5 text-white", strokeWidth: 3, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
512
+ Check,
513
+ {
514
+ className: "hidden size-2.5 text-white group-data-[checked]:block",
515
+ strokeWidth: 3,
516
+ "aria-hidden": "true"
517
+ }
518
+ )
519
+ }
520
+ );
521
+ if (!label) return control;
522
+ return /* @__PURE__ */ jsxs(Field$1, { className: "flex items-start gap-2.5", children: [
523
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5", children: control }),
524
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
525
+ /* @__PURE__ */ jsx(
526
+ Label,
527
+ {
528
+ ...id !== void 0 ? { htmlFor: id } : {},
529
+ className: "block cursor-pointer select-none text-sm font-medium text-[var(--color-fg-base)]",
530
+ children: label
531
+ }
532
+ ),
533
+ description && /* @__PURE__ */ jsx(Description, { className: "mt-0.5 text-xs text-[var(--color-fg-subtle)]", children: description })
534
+ ] })
535
+ ] });
536
+ }
537
+ function RadioGroup({
538
+ options,
539
+ value,
540
+ onChange,
541
+ orientation = "vertical",
542
+ variant = "plain",
543
+ name,
544
+ className
545
+ }) {
546
+ return /* @__PURE__ */ jsx(
547
+ RadioGroup$1,
548
+ {
549
+ value,
550
+ onChange,
551
+ ...name ? { name } : {},
552
+ className: cn(
553
+ "flex gap-2",
554
+ orientation === "vertical" ? "flex-col" : "flex-row flex-wrap",
555
+ className
556
+ ),
557
+ children: options.map((option) => /* @__PURE__ */ jsxs(
558
+ Radio,
559
+ {
560
+ value: option.value,
561
+ ...option.disabled !== void 0 ? { disabled: option.disabled } : {},
562
+ className: cn(
563
+ "group flex min-h-16 cursor-pointer items-start gap-2 rounded-[var(--radius-md)] text-sm outline-none transition-[background-color,border-color,box-shadow,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)] disabled:cursor-not-allowed disabled:opacity-45",
564
+ variant === "card" ? "data-[checked]:bg-[var(--color-accent)]/5 data-[checked]:ring-[var(--color-accent)]/20 min-w-48 border border-[var(--color-border-strong)] bg-[var(--color-bg-base)] p-3 hover:border-[var(--color-border)] data-[checked]:border-[var(--color-accent)] data-[checked]:ring-2" : "p-1"
565
+ ),
566
+ children: [
567
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 flex size-4 items-center justify-center rounded-full border border-[var(--color-border-strong)] group-data-[checked]:border-[var(--color-accent)] group-data-[checked]:bg-[var(--color-accent)]", children: /* @__PURE__ */ jsx("span", { className: "hidden size-1.5 rounded-full bg-[var(--color-bg-base)] group-data-[checked]:block" }) }),
568
+ option.icon ? /* @__PURE__ */ jsx("span", { className: "size-4 shrink-0", children: option.icon }) : null,
569
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
570
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 font-medium text-[var(--color-fg-base)]", children: [
571
+ option.label,
572
+ variant === "card" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "hidden size-4 text-[var(--color-accent)] group-data-[checked]:block" }) : null
573
+ ] }),
574
+ option.description ? /* @__PURE__ */ jsx("span", { className: "block text-xs text-[var(--color-fg-subtle)]", children: option.description }) : null
575
+ ] })
576
+ ]
577
+ },
578
+ String(option.value)
579
+ ))
580
+ }
581
+ );
582
+ }
583
+ function Switch({
584
+ checked,
585
+ defaultChecked,
586
+ onChange,
587
+ disabled,
588
+ label,
589
+ description,
590
+ className,
591
+ id,
592
+ "aria-label": ariaLabel,
593
+ "aria-labelledby": ariaLabelledby
594
+ }) {
595
+ const switchProps = {
596
+ ...id !== void 0 ? { id } : {},
597
+ ...checked !== void 0 ? { checked } : {},
598
+ ...defaultChecked !== void 0 ? { defaultChecked } : {},
599
+ ...onChange !== void 0 ? { onChange } : {},
600
+ ...disabled !== void 0 ? { disabled } : {},
601
+ ...ariaLabelledby !== void 0 ? { "aria-labelledby": ariaLabelledby } : ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {}
602
+ };
603
+ const control = /* @__PURE__ */ jsx(
604
+ Switch$1,
605
+ {
606
+ ...switchProps,
607
+ className: cn(
608
+ "group relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full",
609
+ "border-2 border-transparent bg-[var(--color-bg-muted)]",
610
+ "transition-colors duration-[var(--duration-base)] ease-[var(--ease-standard)]",
611
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-offset-2",
612
+ "data-[checked]:bg-[var(--color-accent)]",
613
+ "disabled:cursor-not-allowed disabled:opacity-50",
614
+ className
615
+ ),
616
+ children: /* @__PURE__ */ jsx(
617
+ "span",
618
+ {
619
+ "aria-hidden": "true",
620
+ className: cn(
621
+ "pointer-events-none inline-block size-4 rounded-full bg-[var(--color-bg-base)] shadow-sm",
622
+ "ring-0 transition-transform duration-[var(--duration-base)] ease-[var(--ease-standard)]",
623
+ "translate-x-0.5 group-data-[checked]:translate-x-5"
624
+ )
625
+ }
626
+ )
627
+ }
628
+ );
629
+ if (!label) return control;
630
+ return /* @__PURE__ */ jsxs(Field$1, { className: "flex items-start gap-3", children: [
631
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5", children: control }),
632
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
633
+ /* @__PURE__ */ jsx(
634
+ Label,
635
+ {
636
+ ...id !== void 0 ? { htmlFor: id } : {},
637
+ className: "block cursor-pointer select-none text-sm font-medium text-[var(--color-fg-base)]",
638
+ children: label
639
+ }
640
+ ),
641
+ description && /* @__PURE__ */ jsx(Description, { className: "mt-0.5 text-xs text-[var(--color-fg-subtle)]", children: description })
642
+ ] })
643
+ ] });
644
+ }
645
+ function CheckboxGroup({
646
+ options,
647
+ value,
648
+ onChange,
649
+ orientation = "vertical",
650
+ disabled,
651
+ invalid,
652
+ name,
653
+ className,
654
+ ...aria
655
+ }) {
656
+ const toggle = (optionValue, checked) => {
657
+ if (checked) {
658
+ if (!value.includes(optionValue)) onChange([...value, optionValue]);
659
+ } else {
660
+ onChange(value.filter((v) => v !== optionValue));
661
+ }
662
+ };
663
+ return /* @__PURE__ */ jsx(
664
+ "div",
665
+ {
666
+ ...aria,
667
+ role: "group",
668
+ "data-invalid": invalid ? "" : void 0,
669
+ className: cn(
670
+ "flex gap-3",
671
+ orientation === "vertical" ? "flex-col" : "flex-row flex-wrap gap-x-6",
672
+ className
673
+ ),
674
+ children: options.map((option) => /* @__PURE__ */ jsx(
675
+ Checkbox,
676
+ {
677
+ checked: value.includes(option.value),
678
+ onChange: (checked) => toggle(option.value, checked),
679
+ disabled: Boolean(disabled || option.disabled),
680
+ label: option.label,
681
+ ...option.description ? { description: option.description } : {},
682
+ ...name ? { id: `${name}-${option.value}` } : {}
683
+ },
684
+ option.value
685
+ ))
686
+ }
687
+ );
688
+ }
689
+ function SwitchGroup({
690
+ options,
691
+ value,
692
+ onChange,
693
+ disabled,
694
+ bordered = true,
695
+ className,
696
+ ...aria
697
+ }) {
698
+ const toggle = (optionValue, checked) => {
699
+ if (checked) {
700
+ if (!value.includes(optionValue)) onChange([...value, optionValue]);
701
+ } else {
702
+ onChange(value.filter((v) => v !== optionValue));
703
+ }
704
+ };
705
+ return /* @__PURE__ */ jsx(
706
+ "div",
707
+ {
708
+ ...aria,
709
+ role: "group",
710
+ className: cn(
711
+ "flex flex-col",
712
+ bordered ? "divide-y divide-[var(--color-border)] overflow-hidden rounded-lg border border-[var(--color-border)]" : "gap-3",
713
+ className
714
+ ),
715
+ children: options.map((option) => {
716
+ const id = `switch-${option.value}`;
717
+ return /* @__PURE__ */ jsxs(
718
+ "div",
719
+ {
720
+ className: cn(
721
+ "flex items-start justify-between gap-4",
722
+ bordered ? "px-3.5 py-3 hover:bg-[var(--color-bg-muted)]" : "",
723
+ (disabled || option.disabled) && "opacity-60"
724
+ ),
725
+ children: [
726
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
727
+ /* @__PURE__ */ jsx("span", { className: "block text-sm font-medium text-[var(--color-fg-base)]", children: option.label }),
728
+ option.description ? /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-xs text-[var(--color-fg-subtle)]", children: option.description }) : null
729
+ ] }),
730
+ /* @__PURE__ */ jsx(
731
+ Switch,
732
+ {
733
+ id,
734
+ "aria-label": option.label,
735
+ checked: value.includes(option.value),
736
+ onChange: (checked) => toggle(option.value, checked),
737
+ disabled: Boolean(disabled || option.disabled)
738
+ }
739
+ )
740
+ ]
741
+ },
742
+ option.value
743
+ );
744
+ })
745
+ }
746
+ );
747
+ }
748
+ var cardColumns = {
749
+ 1: "grid-cols-1",
750
+ 2: "grid-cols-1 sm:grid-cols-2",
751
+ 3: "grid-cols-1 sm:grid-cols-3",
752
+ 4: "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4"
753
+ };
754
+ function RadioCardGroup({
755
+ columns,
756
+ className,
757
+ ...props
758
+ }) {
759
+ const gridClass = columns ? cn("!grid !flex-none", cardColumns[columns]) : void 0;
760
+ return /* @__PURE__ */ jsx(
761
+ RadioGroup,
762
+ {
763
+ ...props,
764
+ variant: "card",
765
+ orientation: props.orientation ?? "horizontal",
766
+ className: cn(gridClass, className)
767
+ }
768
+ );
769
+ }
770
+ var Select = forwardRef(function Select2({ className, ...props }, ref) {
771
+ return /* @__PURE__ */ jsx(
772
+ "select",
773
+ {
774
+ ref,
775
+ "data-density-control": "input",
776
+ ...props,
777
+ className: cn(
778
+ "block h-9 w-full rounded-md border border-[var(--color-border-strong)]",
779
+ "bg-[var(--color-bg-base)] text-sm text-[var(--color-fg-base)]",
780
+ "appearance-none pl-3 pr-8",
781
+ "transition-[border-color,box-shadow] duration-[var(--duration-fast)]",
782
+ "focus:ring-3 focus:ring-[var(--color-accent)]/15 focus:border-[var(--color-accent)] focus:outline-none",
783
+ "disabled:cursor-not-allowed disabled:opacity-50",
784
+ "bg-[length:16px] bg-[right_8px_center] bg-no-repeat",
785
+ className
786
+ )
787
+ }
788
+ );
789
+ });
790
+ var Textarea = forwardRef(function Textarea2({ className, ...props }, ref) {
791
+ return /* @__PURE__ */ jsx(
792
+ "textarea",
793
+ {
794
+ ref,
795
+ "data-density-control": "textarea",
796
+ ...props,
797
+ className: cn(
798
+ "block min-h-[80px] w-full rounded-md border border-[var(--color-border-strong)]",
799
+ "bg-[var(--color-bg-base)] text-sm text-[var(--color-fg-base)]",
800
+ "resize-vertical px-3 py-2",
801
+ "placeholder:text-[var(--color-fg-subtle)]",
802
+ "transition-[border-color,box-shadow] duration-[var(--duration-fast)]",
803
+ "focus:ring-3 focus:ring-[var(--color-accent)]/15 focus:border-[var(--color-accent)] focus:outline-none",
804
+ "disabled:cursor-not-allowed disabled:opacity-50",
805
+ "aria-[invalid=true]:focus:ring-[var(--color-danger)]/15 aria-[invalid=true]:border-[var(--color-danger)]",
806
+ className
807
+ )
808
+ }
809
+ );
810
+ });
811
+ function ToggleGroup(props) {
812
+ const active = (value) => props.type === "single" ? props.value === value : props.value.includes(value);
813
+ const toggle = (value) => {
814
+ if (props.type === "single") props.onChange(value);
815
+ else
816
+ props.onChange(
817
+ active(value) ? props.value.filter((item) => item !== value) : [...props.value, value]
818
+ );
819
+ };
820
+ return /* @__PURE__ */ jsx(
821
+ "div",
822
+ {
823
+ role: props.type === "single" ? "radiogroup" : "group",
824
+ "aria-label": props.ariaLabel,
825
+ className: cn(
826
+ "inline-flex overflow-hidden rounded-[var(--radius-md)] border border-[var(--color-border-strong)] bg-[var(--color-bg-base)]",
827
+ props.className
828
+ ),
829
+ children: props.options.map((option) => /* @__PURE__ */ jsxs(
830
+ "button",
831
+ {
832
+ type: "button",
833
+ disabled: option.disabled,
834
+ "aria-pressed": props.type === "multiple" ? active(option.value) : void 0,
835
+ "aria-checked": props.type === "single" ? active(option.value) : void 0,
836
+ role: props.type === "single" ? "radio" : void 0,
837
+ onClick: () => toggle(option.value),
838
+ className: cn(
839
+ "inline-flex h-9 items-center gap-2 border-r border-[var(--color-border)] px-3 text-sm font-medium text-[var(--color-fg-muted)] last:border-r-0",
840
+ "transition-[background-color,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)] hover:bg-[var(--color-bg-muted)] hover:text-[var(--color-fg-base)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] disabled:opacity-45",
841
+ active(option.value) && "bg-[var(--color-accent)] text-[var(--color-accent-contrast)] hover:bg-[var(--color-accent-hover)] hover:text-[var(--color-accent-contrast)]"
842
+ ),
843
+ children: [
844
+ option.icon,
845
+ option.label
846
+ ]
847
+ },
848
+ option.value
849
+ ))
850
+ }
851
+ );
852
+ }
853
+ function isSameValue(a, b) {
854
+ return Object.is(a, b);
855
+ }
856
+ function Combobox({
857
+ options,
858
+ value,
859
+ onChange,
860
+ placeholder,
861
+ emptyMessage = "No results",
862
+ loading,
863
+ onInputChange,
864
+ renderOption,
865
+ size = "md",
866
+ disabled,
867
+ clearable = true,
868
+ name,
869
+ id,
870
+ className,
871
+ inputRef
872
+ }) {
873
+ const [open, setOpen] = useState(false);
874
+ const [query, setQuery] = useState("");
875
+ const [activeIndex, setActiveIndex] = useState(-1);
876
+ const rootRef = useRef(null);
877
+ const selected = options.find((option) => isSameValue(value, option.value));
878
+ const filtered = useMemo(
879
+ () => query ? matchSorter(options, query, { keys: ["label", "description"] }) : options,
880
+ [options, query]
881
+ );
882
+ const inputValue = open ? query : selected?.label ?? "";
883
+ const listboxId = id ? `${id}-listbox` : void 0;
884
+ const optionId = (index) => id ? `${id}-option-${String(index)}` : void 0;
885
+ useEffect(() => {
886
+ const onPointerDown = (event) => {
887
+ if (!rootRef.current?.contains(event.target)) {
888
+ setOpen(false);
889
+ setActiveIndex(-1);
890
+ setQuery("");
891
+ }
892
+ };
893
+ document.addEventListener("mousedown", onPointerDown);
894
+ return () => document.removeEventListener("mousedown", onPointerDown);
895
+ }, []);
896
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("relative", className), children: [
897
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
898
+ /* @__PURE__ */ jsx(
899
+ "input",
900
+ {
901
+ ref: inputRef,
902
+ id,
903
+ name,
904
+ value: inputValue,
905
+ disabled,
906
+ placeholder,
907
+ role: "combobox",
908
+ "aria-expanded": open,
909
+ "aria-controls": listboxId,
910
+ "aria-autocomplete": "list",
911
+ "aria-activedescendant": open && activeIndex >= 0 ? optionId(activeIndex) : void 0,
912
+ onFocus: () => {
913
+ if (!disabled) setOpen(true);
914
+ },
915
+ onChange: (event) => {
916
+ if (disabled) return;
917
+ setQuery(event.target.value);
918
+ onInputChange?.(event.target.value);
919
+ setOpen(true);
920
+ },
921
+ onKeyDown: (event) => {
922
+ if (disabled) return;
923
+ if (event.key === "ArrowDown") {
924
+ event.preventDefault();
925
+ setOpen(true);
926
+ setActiveIndex((index) => Math.min(index + 1, filtered.length - 1));
927
+ }
928
+ if (event.key === "ArrowUp") {
929
+ event.preventDefault();
930
+ setOpen(true);
931
+ setActiveIndex((index) => Math.max(index - 1, 0));
932
+ }
933
+ if (event.key === "Enter" && open && activeIndex >= 0 && filtered[activeIndex]) {
934
+ event.preventDefault();
935
+ onChange(filtered[activeIndex].value);
936
+ setOpen(false);
937
+ setQuery("");
938
+ setActiveIndex(-1);
939
+ }
940
+ if (event.key === "Escape") {
941
+ setOpen(false);
942
+ setQuery("");
943
+ setActiveIndex(-1);
944
+ }
945
+ },
946
+ className: cn(
947
+ "block w-full rounded-md border border-[var(--color-border-strong)] bg-[var(--color-bg-base)] pr-16 text-[var(--color-fg-base)]",
948
+ "focus:ring-3 focus:ring-[var(--color-accent)]/15 placeholder:text-[var(--color-fg-subtle)] focus:border-[var(--color-accent)] focus:outline-none",
949
+ size === "sm" ? "h-8 px-2 text-xs" : "h-9 px-3 text-sm"
950
+ )
951
+ }
952
+ ),
953
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-y-0 right-1 flex items-center gap-1", children: [
954
+ loading ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin text-[var(--color-fg-subtle)]" }) : null,
955
+ clearable && value !== null ? /* @__PURE__ */ jsx(
956
+ "button",
957
+ {
958
+ type: "button",
959
+ "aria-label": "Clear",
960
+ onClick: () => {
961
+ onChange(null);
962
+ setQuery("");
963
+ },
964
+ className: "rounded p-1 text-[var(--color-fg-subtle)] hover:bg-[var(--color-bg-muted)] hover:text-[var(--color-fg-base)]",
965
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
966
+ }
967
+ ) : null,
968
+ /* @__PURE__ */ jsx(
969
+ "button",
970
+ {
971
+ type: "button",
972
+ "aria-label": "Toggle",
973
+ disabled,
974
+ onClick: () => {
975
+ setOpen((next) => !next);
976
+ setActiveIndex(-1);
977
+ },
978
+ className: "rounded p-1 text-[var(--color-fg-subtle)] hover:text-[var(--color-fg-base)]",
979
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
980
+ }
981
+ )
982
+ ] })
983
+ ] }),
984
+ open ? /* @__PURE__ */ jsx(
985
+ "div",
986
+ {
987
+ id: listboxId,
988
+ role: "listbox",
989
+ className: "absolute z-[9997] mt-1 max-h-64 w-full overflow-auto rounded-[var(--radius-md)] border border-[var(--color-border-strong)] bg-[var(--color-bg-base)] p-1 shadow-[var(--shadow-xl)] transition-opacity duration-[var(--motion-base)] ease-[var(--ease-standard)]",
990
+ children: filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-sm text-[var(--color-fg-subtle)]", children: emptyMessage }) : filtered.map((option, index) => /* @__PURE__ */ jsxs(
991
+ "button",
992
+ {
993
+ id: optionId(index),
994
+ type: "button",
995
+ role: "option",
996
+ disabled: option.disabled,
997
+ "aria-selected": isSameValue(value, option.value),
998
+ className: cn(
999
+ "flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm disabled:opacity-45",
1000
+ isSameValue(value, option.value) && "bg-[var(--color-accent)]/10",
1001
+ index === activeIndex ? "bg-[var(--color-bg-muted)]" : "hover:bg-[var(--color-bg-muted)]"
1002
+ ),
1003
+ onMouseDown: (event) => event.preventDefault(),
1004
+ onMouseEnter: () => setActiveIndex(index),
1005
+ onClick: () => {
1006
+ onChange(option.value);
1007
+ setQuery("");
1008
+ setOpen(false);
1009
+ setActiveIndex(-1);
1010
+ },
1011
+ children: [
1012
+ option.icon,
1013
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
1014
+ renderOption ? renderOption(option) : /* @__PURE__ */ jsx("span", { className: "block truncate", children: option.label }),
1015
+ option.description ? /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-[var(--color-fg-subtle)]", children: option.description }) : null
1016
+ ] }),
1017
+ isSameValue(value, option.value) ? /* @__PURE__ */ jsx(Check, { className: "size-4 text-[var(--color-accent)]" }) : null
1018
+ ]
1019
+ },
1020
+ String(option.value)
1021
+ ))
1022
+ }
1023
+ ) : null
1024
+ ] });
1025
+ }
1026
+ function MultiColumnCombobox({
1027
+ items,
1028
+ columns,
1029
+ value,
1030
+ onChange,
1031
+ displayKey,
1032
+ label,
1033
+ placeholder = "Search\u2026",
1034
+ emptyMessage = "No results",
1035
+ disabled = false,
1036
+ clearable = true,
1037
+ className
1038
+ }) {
1039
+ const id = useId();
1040
+ const [open, setOpen] = useState(false);
1041
+ const [query, setQuery] = useState("");
1042
+ const [activeIndex, setActiveIndex] = useState(-1);
1043
+ const rootRef = useRef(null);
1044
+ const display = displayKey ?? columns[0]?.key ?? "id";
1045
+ const selected = items.find((item) => item.id === value) ?? null;
1046
+ const searchKeys = useMemo(() => columns.map((c) => c.key), [columns]);
1047
+ const filtered = useMemo(
1048
+ () => query ? matchSorter(items, query, { keys: searchKeys }) : items,
1049
+ [items, query, searchKeys]
1050
+ );
1051
+ useEffect(() => {
1052
+ const onPointerDown = (event) => {
1053
+ if (!rootRef.current?.contains(event.target)) {
1054
+ setOpen(false);
1055
+ setActiveIndex(-1);
1056
+ setQuery("");
1057
+ }
1058
+ };
1059
+ document.addEventListener("mousedown", onPointerDown);
1060
+ return () => {
1061
+ document.removeEventListener("mousedown", onPointerDown);
1062
+ };
1063
+ }, []);
1064
+ const commit = (item) => {
1065
+ onChange(item.id);
1066
+ setOpen(false);
1067
+ setQuery("");
1068
+ setActiveIndex(-1);
1069
+ };
1070
+ const inputValue = open ? query : selected?.[display] ?? "";
1071
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("relative flex flex-col gap-1.5", className), children: [
1072
+ label ? /* @__PURE__ */ jsx("label", { htmlFor: id, className: "text-sm font-medium text-[var(--color-fg-base)]", children: label }) : null,
1073
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1074
+ /* @__PURE__ */ jsx(
1075
+ "input",
1076
+ {
1077
+ id,
1078
+ type: "text",
1079
+ role: "combobox",
1080
+ "aria-expanded": open,
1081
+ "aria-controls": `${id}-grid`,
1082
+ "aria-autocomplete": "list",
1083
+ value: inputValue,
1084
+ placeholder: selected ? selected[display] ?? placeholder : placeholder,
1085
+ disabled,
1086
+ onFocus: () => {
1087
+ setOpen(true);
1088
+ },
1089
+ onChange: (event) => {
1090
+ setQuery(event.target.value);
1091
+ setOpen(true);
1092
+ setActiveIndex(0);
1093
+ },
1094
+ onKeyDown: (event) => {
1095
+ if (event.key === "ArrowDown") {
1096
+ event.preventDefault();
1097
+ setOpen(true);
1098
+ setActiveIndex((i) => Math.min(i + 1, filtered.length - 1));
1099
+ } else if (event.key === "ArrowUp") {
1100
+ event.preventDefault();
1101
+ setActiveIndex((i) => Math.max(i - 1, 0));
1102
+ } else if (event.key === "Enter" && open && activeIndex >= 0) {
1103
+ event.preventDefault();
1104
+ const item = filtered[activeIndex];
1105
+ if (item) commit(item);
1106
+ } else if (event.key === "Escape") {
1107
+ setOpen(false);
1108
+ setQuery("");
1109
+ }
1110
+ },
1111
+ className: "focus-visible:ring-[var(--color-accent)]/20 block h-9 w-full rounded-md border border-[var(--color-border-strong)] bg-[var(--color-bg-base)] px-3 py-2 pr-14 text-sm text-[var(--color-fg-base)] transition-[border-color,box-shadow] duration-[var(--motion-fast)] placeholder:text-[var(--color-fg-subtle)] focus:outline-none focus-visible:border-[var(--color-accent)] focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50"
1112
+ }
1113
+ ),
1114
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-y-0 right-2 flex items-center gap-0.5", children: [
1115
+ clearable && selected && !disabled ? /* @__PURE__ */ jsx(
1116
+ "button",
1117
+ {
1118
+ type: "button",
1119
+ "aria-label": "Clear selection",
1120
+ onClick: () => {
1121
+ onChange(null);
1122
+ setQuery("");
1123
+ },
1124
+ className: "focus-visible:ring-[var(--color-accent)]/40 rounded p-0.5 text-[var(--color-fg-subtle)] hover:text-[var(--color-fg-base)] focus-visible:outline-none focus-visible:ring-2",
1125
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
1126
+ }
1127
+ ) : null,
1128
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-4 text-[var(--color-fg-subtle)]", "aria-hidden": "true" })
1129
+ ] })
1130
+ ] }),
1131
+ open ? /* @__PURE__ */ jsx(
1132
+ "div",
1133
+ {
1134
+ id: `${id}-grid`,
1135
+ role: "grid",
1136
+ "aria-label": label ?? "Options",
1137
+ className: "absolute left-0 right-0 top-full z-30 mt-1 max-h-72 overflow-auto rounded-md border border-[var(--color-border)] bg-[var(--color-bg-base)] shadow-[var(--shadow-md)]",
1138
+ children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-sm", children: [
1139
+ /* @__PURE__ */ jsx("thead", { className: "sticky top-0 bg-[var(--color-bg-subtle)]", children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsx(
1140
+ "th",
1141
+ {
1142
+ scope: "col",
1143
+ style: column.width ? { width: column.width } : void 0,
1144
+ className: "border-b border-[var(--color-border)] px-3 py-1.5 text-left text-[11px] font-semibold uppercase tracking-wide text-[var(--color-fg-muted)]",
1145
+ children: column.header
1146
+ },
1147
+ column.key
1148
+ )) }) }),
1149
+ /* @__PURE__ */ jsx("tbody", { children: filtered.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
1150
+ "td",
1151
+ {
1152
+ colSpan: columns.length,
1153
+ className: "px-3 py-4 text-center text-[var(--color-fg-subtle)]",
1154
+ children: emptyMessage
1155
+ }
1156
+ ) }) : filtered.map((item, index) => /* @__PURE__ */ jsx(
1157
+ "tr",
1158
+ {
1159
+ "aria-selected": item.id === value,
1160
+ onClick: () => {
1161
+ commit(item);
1162
+ },
1163
+ onMouseEnter: () => {
1164
+ setActiveIndex(index);
1165
+ },
1166
+ className: cn(
1167
+ "cursor-pointer transition-colors duration-[var(--motion-fast)]",
1168
+ index === activeIndex && "bg-[var(--color-bg-subtle)]",
1169
+ item.id === value && "bg-[var(--color-accent)]/8"
1170
+ ),
1171
+ children: columns.map((column) => /* @__PURE__ */ jsx(
1172
+ "td",
1173
+ {
1174
+ className: "border-b border-[var(--color-border-subtle)] px-3 py-2 text-[var(--color-fg-base)]",
1175
+ children: item[column.key]
1176
+ },
1177
+ column.key
1178
+ ))
1179
+ },
1180
+ item.id
1181
+ )) })
1182
+ ] })
1183
+ }
1184
+ ) : null
1185
+ ] });
1186
+ }
1187
+ function TagInput({
1188
+ value,
1189
+ onChange,
1190
+ placeholder,
1191
+ maxTags,
1192
+ delimiter = /[,;\n\t]/,
1193
+ validate,
1194
+ suggestions = [],
1195
+ className
1196
+ }) {
1197
+ const [input, setInput] = useState("");
1198
+ const [error, setError] = useState(null);
1199
+ const add = (raw) => {
1200
+ const tag = raw.trim();
1201
+ if (!tag || value.includes(tag) || maxTags && value.length >= maxTags) return;
1202
+ const nextError = validate?.(tag) ?? null;
1203
+ setError(nextError);
1204
+ if (!nextError) {
1205
+ onChange([...value, tag]);
1206
+ setInput("");
1207
+ }
1208
+ };
1209
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1210
+ /* @__PURE__ */ jsxs("div", { className: "focus-within:ring-3 focus-within:ring-[var(--color-accent)]/15 flex min-h-9 w-full flex-wrap items-center gap-1 rounded-md border border-[var(--color-border-strong)] bg-[var(--color-bg-base)] px-2 py-1 focus-within:border-[var(--color-accent)]", children: [
1211
+ value.map((tag) => /* @__PURE__ */ jsxs(
1212
+ "span",
1213
+ {
1214
+ className: "inline-flex h-7 items-center gap-1.5 rounded-full bg-[var(--color-bg-muted)] px-2.5 text-sm text-[var(--color-fg-base)]",
1215
+ children: [
1216
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: tag }),
1217
+ /* @__PURE__ */ jsx(
1218
+ "button",
1219
+ {
1220
+ type: "button",
1221
+ "aria-label": `Remove ${tag}`,
1222
+ onClick: () => onChange(value.filter((item) => item !== tag)),
1223
+ className: "inline-flex size-5 items-center justify-center rounded-full text-[var(--color-fg-muted)] transition-[background-color,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)] hover:bg-black/10 hover:text-[var(--color-fg-base)]",
1224
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
1225
+ }
1226
+ )
1227
+ ]
1228
+ },
1229
+ tag
1230
+ )),
1231
+ /* @__PURE__ */ jsx(
1232
+ "input",
1233
+ {
1234
+ value: input,
1235
+ placeholder: value.length ? void 0 : placeholder,
1236
+ onChange: (event) => {
1237
+ const next = event.target.value;
1238
+ if (delimiter.test(next)) add(next.replace(delimiter, ""));
1239
+ else setInput(next);
1240
+ },
1241
+ onKeyDown: (event) => {
1242
+ if (event.key === "Enter") {
1243
+ event.preventDefault();
1244
+ add(input);
1245
+ }
1246
+ if (event.key === "Backspace" && !input) onChange(value.slice(0, -1));
1247
+ },
1248
+ className: "min-w-24 flex-1 bg-transparent text-sm outline-none placeholder:text-[var(--color-fg-subtle)]"
1249
+ }
1250
+ )
1251
+ ] }),
1252
+ suggestions.length > 0 && input ? /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: suggestions.filter((item) => item.toLowerCase().includes(input.toLowerCase())).slice(0, 5).map((item) => /* @__PURE__ */ jsx(
1253
+ "button",
1254
+ {
1255
+ type: "button",
1256
+ onClick: () => add(item),
1257
+ className: "inline-flex h-6 cursor-pointer items-center rounded-full bg-[var(--color-bg-muted)] px-2 text-xs font-medium text-[var(--color-fg-base)] transition-[background-color,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)] hover:bg-[var(--color-accent-subtle)] hover:text-[var(--color-accent)]",
1258
+ children: item
1259
+ },
1260
+ item
1261
+ )) }) : null,
1262
+ error ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-[var(--color-danger-fg)]", children: error }) : null
1263
+ ] });
1264
+ }
1265
+ function clamp(value, min, max) {
1266
+ return Math.min(
1267
+ max ?? Number.POSITIVE_INFINITY,
1268
+ Math.max(min ?? Number.NEGATIVE_INFINITY, value)
1269
+ );
1270
+ }
1271
+ var NumberInput = forwardRef(function NumberInput2({
1272
+ value,
1273
+ onChange,
1274
+ min,
1275
+ max,
1276
+ step = 1,
1277
+ precision,
1278
+ placeholder,
1279
+ suffix,
1280
+ prefix,
1281
+ showSteppers = true,
1282
+ size = "md",
1283
+ disabled,
1284
+ className,
1285
+ ariaLabel
1286
+ }, ref) {
1287
+ const commit = (next) => onChange(next === null ? null : Number(clamp(next, min, max).toFixed(precision ?? 10)));
1288
+ const bump = (factor) => commit((value ?? 0) + step * factor);
1289
+ const keyDown = (event) => {
1290
+ if (event.key === "ArrowUp") bump(1);
1291
+ if (event.key === "ArrowDown") bump(-1);
1292
+ if (event.key === "PageUp") bump(10);
1293
+ if (event.key === "PageDown") bump(-10);
1294
+ if (event.key === "Home" && min !== void 0) commit(min);
1295
+ if (event.key === "End" && max !== void 0) commit(max);
1296
+ };
1297
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
1298
+ prefix ? /* @__PURE__ */ jsx("span", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-sm text-[var(--color-fg-subtle)]", children: prefix }) : null,
1299
+ /* @__PURE__ */ jsx(
1300
+ "input",
1301
+ {
1302
+ ref,
1303
+ type: "number",
1304
+ role: "spinbutton",
1305
+ "aria-label": ariaLabel,
1306
+ "aria-valuemin": min,
1307
+ "aria-valuemax": max,
1308
+ "aria-valuenow": value ?? void 0,
1309
+ value: value ?? "",
1310
+ min,
1311
+ max,
1312
+ step,
1313
+ disabled,
1314
+ placeholder,
1315
+ "data-density-control": "input",
1316
+ "data-density-size": size,
1317
+ onKeyDown: keyDown,
1318
+ onChange: (event) => commit(event.target.value === "" ? null : Number(event.target.value)),
1319
+ className: cn(
1320
+ "number-input block w-full appearance-none rounded-md border border-[var(--color-border-strong)] bg-[var(--color-bg-base)] text-[var(--color-fg-base)] outline-none",
1321
+ "focus:ring-3 focus:ring-[var(--color-accent)]/15 focus:border-[var(--color-accent)] disabled:opacity-50",
1322
+ size === "sm" ? "h-8 px-2 text-xs" : "h-9 px-3 text-sm",
1323
+ prefix && "pl-7",
1324
+ showSteppers ? "pr-14" : suffix ? "pr-10" : void 0
1325
+ )
1326
+ }
1327
+ ),
1328
+ suffix ? /* @__PURE__ */ jsx(
1329
+ "span",
1330
+ {
1331
+ className: cn(
1332
+ "absolute top-1/2 -translate-y-1/2 text-sm text-[var(--color-fg-subtle)]",
1333
+ showSteppers ? "right-10" : "right-3"
1334
+ ),
1335
+ children: suffix
1336
+ }
1337
+ ) : null,
1338
+ showSteppers ? /* @__PURE__ */ jsxs("span", { className: "absolute bottom-1 right-1 top-1 flex w-7 flex-col overflow-hidden rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-bg-subtle)]", children: [
1339
+ /* @__PURE__ */ jsx(
1340
+ "button",
1341
+ {
1342
+ type: "button",
1343
+ "aria-label": "Increment",
1344
+ disabled,
1345
+ onClick: () => bump(1),
1346
+ className: "flex flex-1 cursor-pointer items-center justify-center text-[var(--color-fg-muted)] transition-[background-color,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)] hover:bg-[var(--color-bg-muted)] hover:text-[var(--color-fg-base)] disabled:cursor-not-allowed disabled:opacity-50",
1347
+ children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-3" })
1348
+ }
1349
+ ),
1350
+ /* @__PURE__ */ jsx(
1351
+ "button",
1352
+ {
1353
+ type: "button",
1354
+ "aria-label": "Decrement",
1355
+ disabled,
1356
+ onClick: () => bump(-1),
1357
+ className: "flex flex-1 cursor-pointer items-center justify-center border-t border-[var(--color-border)] text-[var(--color-fg-muted)] transition-[background-color,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)] hover:bg-[var(--color-bg-muted)] hover:text-[var(--color-fg-base)] disabled:cursor-not-allowed disabled:opacity-50",
1358
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-3" })
1359
+ }
1360
+ )
1361
+ ] }) : null,
1362
+ /* @__PURE__ */ jsx("style", { children: `
1363
+ .number-input::-webkit-outer-spin-button,
1364
+ .number-input::-webkit-inner-spin-button {
1365
+ -webkit-appearance: none;
1366
+ margin: 0;
1367
+ }
1368
+ .number-input[type='number'] {
1369
+ -moz-appearance: textfield;
1370
+ }
1371
+ ` })
1372
+ ] });
1373
+ });
1374
+ var SearchInput = forwardRef(function SearchInput2({
1375
+ value,
1376
+ onChange,
1377
+ onDebouncedChange,
1378
+ debounceMs = 200,
1379
+ showClear = true,
1380
+ clearLabel = "Clear",
1381
+ className,
1382
+ ...props
1383
+ }, ref) {
1384
+ const debounced = useDebouncedCallback((next) => onDebouncedChange?.(next), debounceMs);
1385
+ useEffect(() => {
1386
+ debounced(value);
1387
+ }, [debounced, value]);
1388
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
1389
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[var(--color-fg-subtle)]" }),
1390
+ /* @__PURE__ */ jsx(
1391
+ "input",
1392
+ {
1393
+ ref,
1394
+ "data-density-control": "input",
1395
+ value,
1396
+ onChange: (event) => onChange(event.target.value),
1397
+ className: "focus:ring-[var(--color-accent)]/20 block h-9 w-full rounded-md border border-[var(--color-border-strong)] bg-[var(--color-bg-base)] pl-9 pr-9 text-sm text-[var(--color-fg-base)] outline-none transition-[border-color,box-shadow] duration-[var(--motion-fast)] ease-[var(--ease-standard)] placeholder:text-[var(--color-fg-subtle)] focus:border-[var(--color-accent)] focus:ring-2",
1398
+ ...props
1399
+ }
1400
+ ),
1401
+ showClear && value ? /* @__PURE__ */ jsx(
1402
+ "button",
1403
+ {
1404
+ type: "button",
1405
+ "aria-label": clearLabel,
1406
+ onClick: () => onChange(""),
1407
+ className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--color-fg-subtle)] transition-[background-color,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)] hover:bg-[var(--color-bg-muted)] hover:text-[var(--color-fg-base)]",
1408
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
1409
+ }
1410
+ ) : null
1411
+ ] });
1412
+ });
1413
+ function Popover({
1414
+ trigger,
1415
+ children,
1416
+ side = "bottom",
1417
+ align = "start",
1418
+ sideOffset = 8,
1419
+ open,
1420
+ defaultOpen = false,
1421
+ onOpenChange,
1422
+ modal = false,
1423
+ className
1424
+ }) {
1425
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
1426
+ const actualOpen = open ?? uncontrolledOpen;
1427
+ const setOpen = (next) => {
1428
+ setUncontrolledOpen(next);
1429
+ onOpenChange?.(next);
1430
+ };
1431
+ useEffect(() => {
1432
+ if (!actualOpen) return;
1433
+ const onKeyDown = (event) => {
1434
+ if (event.key === "Escape") {
1435
+ setUncontrolledOpen(false);
1436
+ onOpenChange?.(false);
1437
+ }
1438
+ };
1439
+ window.addEventListener("keydown", onKeyDown);
1440
+ return () => window.removeEventListener("keydown", onKeyDown);
1441
+ }, [actualOpen, onOpenChange]);
1442
+ const floating = useFloating({
1443
+ open: actualOpen,
1444
+ onOpenChange: setOpen,
1445
+ strategy: "fixed",
1446
+ placement: toPlacement(side, align),
1447
+ whileElementsMounted: autoUpdate,
1448
+ middleware: [offset(sideOffset), flip(), shift({ padding: 8 })]
1449
+ });
1450
+ const { getReferenceProps, getFloatingProps } = useInteractions([
1451
+ useClick(floating.context),
1452
+ useDismiss(floating.context),
1453
+ useRole(floating.context, { role: modal ? "dialog" : "menu" })
1454
+ ]);
1455
+ const renderedTrigger = isValidElement(trigger) ? (() => {
1456
+ const triggerProps = trigger.props;
1457
+ return cloneElement(
1458
+ trigger,
1459
+ getReferenceProps({
1460
+ ...triggerProps,
1461
+ ref: composeRefs(triggerProps.ref, floating.refs.setReference)
1462
+ })
1463
+ );
1464
+ })() : trigger;
1465
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1466
+ renderedTrigger,
1467
+ actualOpen ? /* @__PURE__ */ jsx(FloatingPortal, { children: /* @__PURE__ */ jsx(FloatingFocusManager, { context: floating.context, modal, children: /* @__PURE__ */ jsx(
1468
+ "div",
1469
+ {
1470
+ ref: floating.refs.setFloating,
1471
+ style: floating.floatingStyles,
1472
+ ...getFloatingProps({
1473
+ className: cn(
1474
+ "z-[9998] min-w-52 max-w-[min(24rem,calc(100vw-1rem))] rounded-[var(--radius-md)] border border-[var(--color-border-strong)]",
1475
+ "bg-[var(--color-bg-base)] p-3.5 text-sm text-[var(--color-fg-base)] shadow-[var(--shadow-xl)] outline-none focus:outline-none focus-visible:outline-none",
1476
+ "transition-opacity duration-[var(--motion-fast)] ease-[var(--ease-standard)]",
1477
+ className
1478
+ )
1479
+ }),
1480
+ children: typeof children === "function" ? children({ close: () => setOpen(false) }) : children
1481
+ }
1482
+ ) }) }) : null
1483
+ ] });
1484
+ }
1485
+
1486
+ // src/lib/filters/types.ts
1487
+ var isGroup = (node) => typeof node.combinator === "string" && Array.isArray(node.conditions);
1488
+
1489
+ // src/lib/filters/operators.ts
1490
+ var OPERATORS_BY_TYPE = {
1491
+ string: [
1492
+ "contains",
1493
+ "notContains",
1494
+ "eq",
1495
+ "neq",
1496
+ "startsWith",
1497
+ "endsWith",
1498
+ "in",
1499
+ "isEmpty",
1500
+ "isNotEmpty"
1501
+ ],
1502
+ number: ["eq", "neq", "gt", "gte", "lt", "lte", "between", "isEmpty", "isNotEmpty"],
1503
+ date: ["before", "after", "between", "relative", "isEmpty", "isNotEmpty"],
1504
+ enum: ["in", "notIn", "eq", "neq", "isEmpty", "isNotEmpty"],
1505
+ boolean: ["is", "isEmpty", "isNotEmpty"]
1506
+ };
1507
+ var DEFAULT_OPERATOR = {
1508
+ string: "contains",
1509
+ number: "eq",
1510
+ date: "after",
1511
+ enum: "in",
1512
+ boolean: "is"
1513
+ };
1514
+ var NULLARY = /* @__PURE__ */ new Set(["isEmpty", "isNotEmpty"]);
1515
+ var BINARY_RANGE = /* @__PURE__ */ new Set(["between"]);
1516
+ var MULTI = /* @__PURE__ */ new Set(["in", "notIn"]);
1517
+ var isNullaryOp = (op) => NULLARY.has(op);
1518
+ var isRangeOp = (op) => BINARY_RANGE.has(op);
1519
+ var isMultiValueOp = (op) => MULTI.has(op);
1520
+ var operatorsFor = (type, override) => {
1521
+ if (override && override.length > 0) {
1522
+ const valid = new Set(OPERATORS_BY_TYPE[type]);
1523
+ return override.filter((op) => valid.has(op));
1524
+ }
1525
+ return OPERATORS_BY_TYPE[type];
1526
+ };
1527
+ var FiltersContext = createContext(null);
1528
+ function FiltersProvider({ value, children }) {
1529
+ return /* @__PURE__ */ jsx(FiltersContext.Provider, { value, children });
1530
+ }
1531
+ function useFiltersContext() {
1532
+ const ctx = useContext(FiltersContext);
1533
+ if (!ctx) {
1534
+ throw new Error("useFiltersContext must be used within FiltersProvider");
1535
+ }
1536
+ return ctx;
1537
+ }
1538
+ function useOptionalFiltersContext() {
1539
+ return useContext(FiltersContext);
1540
+ }
1541
+ function FilterPopover({
1542
+ label,
1543
+ activeCount = 0,
1544
+ onApply,
1545
+ onClear,
1546
+ children,
1547
+ icon,
1548
+ applyLabel = "Apply",
1549
+ clearLabel = "Clear"
1550
+ }) {
1551
+ return /* @__PURE__ */ jsx(
1552
+ Popover,
1553
+ {
1554
+ trigger: /* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "sm", className: "max-w-60 justify-start", children: [
1555
+ icon ?? /* @__PURE__ */ jsx(Filter, { className: "size-4" }),
1556
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
1557
+ activeCount > 0 ? /* @__PURE__ */ jsx(Badge, { tone: "info", children: activeCount }) : null
1558
+ ] }),
1559
+ className: "w-[min(28rem,94vw)]",
1560
+ children: ({ close }) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1561
+ children,
1562
+ onApply || onClear ? /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 border-t border-[var(--color-border-subtle)] pt-3", children: [
1563
+ onClear ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: onClear, children: clearLabel }) : null,
1564
+ onApply ? /* @__PURE__ */ jsx(
1565
+ Button,
1566
+ {
1567
+ size: "sm",
1568
+ onClick: () => {
1569
+ onApply();
1570
+ close();
1571
+ },
1572
+ children: applyLabel
1573
+ }
1574
+ ) : null
1575
+ ] }) : null
1576
+ ] })
1577
+ }
1578
+ );
1579
+ }
1580
+ var LazyDateRangeFilterCalendar = lazy(
1581
+ () => import('./DateRangeFilterCalendar-D3ZM42SZ.js').then((module) => ({
1582
+ default: module.DateRangeFilterCalendar
1583
+ }))
1584
+ );
1585
+ function DateRangeFilter({
1586
+ label,
1587
+ value,
1588
+ onChange,
1589
+ inline = false,
1590
+ onClear,
1591
+ placeholder
1592
+ }) {
1593
+ const dateFormatter = new Intl.DateTimeFormat(void 0, { day: "numeric", month: "short" });
1594
+ const text = value.from ? `${dateFormatter.format(value.from)} - ${value.to ? dateFormatter.format(value.to) : ""}` : label;
1595
+ const picker = /* @__PURE__ */ jsx(
1596
+ Suspense,
1597
+ {
1598
+ fallback: /* @__PURE__ */ jsx(
1599
+ "div",
1600
+ {
1601
+ "aria-busy": "true",
1602
+ className: "h-80 rounded-md border border-dashed border-[var(--color-border)] bg-[var(--color-bg-subtle)]"
1603
+ }
1604
+ ),
1605
+ children: /* @__PURE__ */ jsx(LazyDateRangeFilterCalendar, { value, onChange })
1606
+ }
1607
+ );
1608
+ if (inline) return picker;
1609
+ return /* @__PURE__ */ jsx(
1610
+ FilterPopover,
1611
+ {
1612
+ label: text || placeholder || label,
1613
+ activeCount: value.from ? 1 : 0,
1614
+ ...onClear ? { onClear } : {},
1615
+ children: picker
1616
+ }
1617
+ );
1618
+ }
1619
+ function FacetedFilter({
1620
+ label,
1621
+ options,
1622
+ value,
1623
+ onChange,
1624
+ inline = false,
1625
+ showSearch,
1626
+ searchPlaceholder
1627
+ }) {
1628
+ const [query, setQuery] = useState("");
1629
+ const filtered = useMemo(
1630
+ () => options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase())),
1631
+ [options, query]
1632
+ );
1633
+ const toggle = (option) => onChange(
1634
+ value.some((item) => Object.is(item, option.value)) ? value.filter((item) => !Object.is(item, option.value)) : [...value, option.value]
1635
+ );
1636
+ const inlineContent = /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-2", children: [
1637
+ showSearch ? /* @__PURE__ */ jsx(SearchInput, { value: query, onChange: setQuery, placeholder: searchPlaceholder }) : null,
1638
+ /* @__PURE__ */ jsxs("div", { className: "flex max-h-24 min-w-0 flex-wrap gap-1.5 overflow-auto pr-1", children: [
1639
+ filtered.map((option) => {
1640
+ const active = value.some((item) => Object.is(item, option.value));
1641
+ return /* @__PURE__ */ jsxs(
1642
+ "button",
1643
+ {
1644
+ type: "button",
1645
+ onClick: () => toggle(option),
1646
+ "aria-pressed": active,
1647
+ className: cn(
1648
+ "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium",
1649
+ "transition-[background-color,border-color,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)]",
1650
+ active ? "border-[var(--color-accent)] bg-[var(--color-accent-muted)] text-[var(--color-accent-fg)]" : "border-[var(--color-border)] bg-[var(--color-bg-base)] text-[var(--color-fg-muted)] hover:bg-[var(--color-bg-muted)] hover:text-[var(--color-fg-base)]"
1651
+ ),
1652
+ children: [
1653
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: option.label }),
1654
+ option.count !== void 0 ? /* @__PURE__ */ jsx("span", { className: "text-[var(--color-fg-subtle)]", children: option.count }) : null
1655
+ ]
1656
+ },
1657
+ String(option.value)
1658
+ );
1659
+ }),
1660
+ filtered.length === 0 ? /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--color-fg-subtle)]", children: "No options" }) : null
1661
+ ] })
1662
+ ] });
1663
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
1664
+ showSearch ? /* @__PURE__ */ jsx(SearchInput, { value: query, onChange: setQuery, placeholder: searchPlaceholder }) : null,
1665
+ /* @__PURE__ */ jsx("div", { className: "max-h-60 space-y-0.5 overflow-auto", children: filtered.map((option) => {
1666
+ const active = value.some((item) => Object.is(item, option.value));
1667
+ return /* @__PURE__ */ jsxs(
1668
+ "button",
1669
+ {
1670
+ type: "button",
1671
+ onClick: () => toggle(option),
1672
+ className: "flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm transition-[background-color,color] duration-[var(--motion-fast)] ease-[var(--ease-standard)] hover:bg-[var(--color-bg-muted)]",
1673
+ children: [
1674
+ /* @__PURE__ */ jsx("span", { className: "flex size-4 items-center justify-center rounded border border-[var(--color-border)] bg-[var(--color-bg-base)]", children: active ? /* @__PURE__ */ jsx(Check, { className: "size-3 text-[var(--color-accent)]" }) : null }),
1675
+ /* @__PURE__ */ jsx(
1676
+ "span",
1677
+ {
1678
+ className: active ? "flex-1 text-[var(--color-fg-base)]" : "flex-1 text-[var(--color-fg-muted)]",
1679
+ children: option.label
1680
+ }
1681
+ ),
1682
+ option.count !== void 0 ? /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--color-fg-subtle)]", children: option.count }) : null
1683
+ ]
1684
+ },
1685
+ String(option.value)
1686
+ );
1687
+ }) })
1688
+ ] });
1689
+ if (inline) return inlineContent;
1690
+ return /* @__PURE__ */ jsx(FilterPopover, { label, activeCount: value.length, children: content });
1691
+ }
1692
+ var OPERATOR_LABEL = {
1693
+ eq: "is",
1694
+ neq: "is not",
1695
+ contains: "contains",
1696
+ notContains: "does not contain",
1697
+ startsWith: "starts with",
1698
+ endsWith: "ends with",
1699
+ in: "is any of",
1700
+ notIn: "is none of",
1701
+ gt: "greater than",
1702
+ gte: "greater or equal",
1703
+ lt: "less than",
1704
+ lte: "less or equal",
1705
+ between: "between",
1706
+ before: "before",
1707
+ after: "after",
1708
+ relative: "within",
1709
+ is: "is",
1710
+ isEmpty: "is empty",
1711
+ isNotEmpty: "is not empty"
1712
+ };
1713
+ var RELATIVE_OPTIONS = [
1714
+ { label: "Today", value: "today" },
1715
+ { label: "7 days", value: "7d" },
1716
+ { label: "30 days", value: "30d" },
1717
+ { label: "90 days", value: "90d" },
1718
+ { label: "1 year", value: "1y" }
1719
+ ];
1720
+ var toInputDate = (value) => {
1721
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
1722
+ if (typeof value === "string") return value.slice(0, 10);
1723
+ if (typeof value === "number") return new Date(value).toISOString().slice(0, 10);
1724
+ return "";
1725
+ };
1726
+ var dateFromInput = (value) => {
1727
+ const input = toInputDate(value);
1728
+ if (!input) return void 0;
1729
+ const date = /* @__PURE__ */ new Date(`${input}T00:00:00`);
1730
+ return Number.isNaN(date.getTime()) ? void 0 : date;
1731
+ };
1732
+ var asArray = (value) => Array.isArray(value) ? value : [];
1733
+ var asStringArray = (value) => asArray(value).filter((item) => typeof item === "string");
1734
+ var asNumber = (value) => {
1735
+ if (typeof value === "number") return value;
1736
+ if (typeof value === "string" && value !== "") {
1737
+ const n = Number(value);
1738
+ return Number.isNaN(n) ? null : n;
1739
+ }
1740
+ return null;
1741
+ };
1742
+ var valuesEqual = (a, b) => Object.is(a, b);
1743
+ var defaultValueForOperator = (op, field) => {
1744
+ if (isNullaryOp(op)) return void 0;
1745
+ if (isRangeOp(op)) return [];
1746
+ if (isMultiValueOp(op)) return [];
1747
+ if (field?.type === "boolean") return "";
1748
+ if (field?.type === "date" && op === "relative") return "7d";
1749
+ return "";
1750
+ };
1751
+ var optionsForField = (field) => field.options ?? [];
1752
+ function EnumValueEditor({
1753
+ condition,
1754
+ field,
1755
+ onChange
1756
+ }) {
1757
+ const options = optionsForField(field);
1758
+ if (isMultiValueOp(condition.op)) {
1759
+ return /* @__PURE__ */ jsx(
1760
+ FacetedFilter,
1761
+ {
1762
+ label: field.label,
1763
+ options,
1764
+ value: asArray(condition.value),
1765
+ onChange: (value) => onChange({ value }),
1766
+ inline: true,
1767
+ showSearch: options.length > 8
1768
+ }
1769
+ );
1770
+ }
1771
+ const comboOptions = options.map((option) => ({
1772
+ value: option.value,
1773
+ label: option.label
1774
+ }));
1775
+ return /* @__PURE__ */ jsx(
1776
+ Combobox,
1777
+ {
1778
+ options: comboOptions,
1779
+ value: comboOptions.find((option) => valuesEqual(option.value, condition.value))?.value ?? null,
1780
+ onChange: (value) => onChange({ value: value ?? "" }),
1781
+ placeholder: "Value",
1782
+ size: "sm"
1783
+ }
1784
+ );
1785
+ }
1786
+ function DateValueEditor({
1787
+ condition,
1788
+ field,
1789
+ onChange
1790
+ }) {
1791
+ if (condition.op === "relative") {
1792
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: RELATIVE_OPTIONS.map((option) => /* @__PURE__ */ jsx(
1793
+ Button,
1794
+ {
1795
+ variant: condition.value === option.value ? "soft" : "ghost",
1796
+ size: "xs",
1797
+ onClick: () => onChange({ value: option.value }),
1798
+ children: option.label
1799
+ },
1800
+ option.value
1801
+ )) });
1802
+ }
1803
+ if (condition.op === "between") {
1804
+ const [from, to] = asArray(condition.value);
1805
+ const range = {
1806
+ from: dateFromInput(from),
1807
+ to: dateFromInput(to)
1808
+ };
1809
+ return /* @__PURE__ */ jsx(
1810
+ DateRangeFilter,
1811
+ {
1812
+ label: field.label,
1813
+ value: range,
1814
+ onChange: (next) => onChange({
1815
+ value: [toInputDate(next.from), toInputDate(next.to)]
1816
+ }),
1817
+ inline: true
1818
+ }
1819
+ );
1820
+ }
1821
+ return /* @__PURE__ */ jsx(
1822
+ Input,
1823
+ {
1824
+ type: "date",
1825
+ value: toInputDate(condition.value),
1826
+ onChange: (event) => onChange({ value: event.target.value }),
1827
+ "aria-label": `${field.label} value`,
1828
+ className: "h-8 text-xs"
1829
+ }
1830
+ );
1831
+ }
1832
+ function NumberValueEditor({
1833
+ condition,
1834
+ field,
1835
+ onChange
1836
+ }) {
1837
+ if (condition.op === "between") {
1838
+ const [from, to] = asArray(condition.value);
1839
+ return /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1840
+ /* @__PURE__ */ jsx(
1841
+ NumberInput,
1842
+ {
1843
+ value: asNumber(from),
1844
+ onChange: (value) => onChange({ value: [value, to ?? ""] }),
1845
+ size: "sm",
1846
+ showSteppers: false,
1847
+ ariaLabel: `${field.label} from`
1848
+ }
1849
+ ),
1850
+ /* @__PURE__ */ jsx(
1851
+ NumberInput,
1852
+ {
1853
+ value: asNumber(to),
1854
+ onChange: (value) => onChange({ value: [from ?? "", value] }),
1855
+ size: "sm",
1856
+ showSteppers: false,
1857
+ ariaLabel: `${field.label} to`
1858
+ }
1859
+ )
1860
+ ] });
1861
+ }
1862
+ return /* @__PURE__ */ jsx(
1863
+ NumberInput,
1864
+ {
1865
+ value: asNumber(condition.value),
1866
+ onChange: (value) => onChange({ value: value ?? "" }),
1867
+ size: "sm",
1868
+ showSteppers: false,
1869
+ ariaLabel: `${field.label} value`
1870
+ }
1871
+ );
1872
+ }
1873
+ function ValueEditor({
1874
+ condition,
1875
+ field,
1876
+ onChange
1877
+ }) {
1878
+ if (isNullaryOp(condition.op)) {
1879
+ return /* @__PURE__ */ jsx("div", { className: "flex h-8 items-center rounded-md border border-[var(--color-border)] bg-[var(--color-bg-subtle)] px-3 text-xs text-[var(--color-fg-subtle)]", children: "No value" });
1880
+ }
1881
+ if (field.type === "enum") {
1882
+ return /* @__PURE__ */ jsx(EnumValueEditor, { condition, field, onChange });
1883
+ }
1884
+ if (field.type === "boolean") {
1885
+ return /* @__PURE__ */ jsxs(
1886
+ Select,
1887
+ {
1888
+ value: typeof condition.value === "boolean" ? String(condition.value) : typeof condition.value === "string" ? condition.value : "",
1889
+ onChange: (event) => {
1890
+ const { value } = event.target;
1891
+ onChange({ value: value === "" ? "" : value === "true" });
1892
+ },
1893
+ "aria-label": `${field.label} value`,
1894
+ className: "h-8 text-xs",
1895
+ children: [
1896
+ /* @__PURE__ */ jsx("option", { value: "", children: "Any" }),
1897
+ /* @__PURE__ */ jsx("option", { value: "true", children: "Yes" }),
1898
+ /* @__PURE__ */ jsx("option", { value: "false", children: "No" })
1899
+ ]
1900
+ }
1901
+ );
1902
+ }
1903
+ if (field.type === "number") {
1904
+ return /* @__PURE__ */ jsx(NumberValueEditor, { condition, field, onChange });
1905
+ }
1906
+ if (field.type === "date") {
1907
+ return /* @__PURE__ */ jsx(DateValueEditor, { condition, field, onChange });
1908
+ }
1909
+ if (isMultiValueOp(condition.op)) {
1910
+ return /* @__PURE__ */ jsx(
1911
+ TagInput,
1912
+ {
1913
+ value: asStringArray(condition.value),
1914
+ onChange: (value) => onChange({ value }),
1915
+ placeholder: "Add values"
1916
+ }
1917
+ );
1918
+ }
1919
+ return /* @__PURE__ */ jsx(
1920
+ Input,
1921
+ {
1922
+ value: typeof condition.value === "string" ? condition.value : "",
1923
+ onChange: (event) => onChange({ value: event.target.value }),
1924
+ placeholder: "Value",
1925
+ "aria-label": `${field.label} value`,
1926
+ className: "h-8 text-xs"
1927
+ }
1928
+ );
1929
+ }
1930
+ function FilterBuilder({
1931
+ condition,
1932
+ fields,
1933
+ onChange,
1934
+ onRemove
1935
+ }) {
1936
+ const field = fields.find((item) => item.id === condition.field) ?? fields[0];
1937
+ const fieldOptions = useMemo(
1938
+ () => fields.map((item) => ({ value: item.id, label: item.label })),
1939
+ [fields]
1940
+ );
1941
+ const operatorOptions = useMemo(() => {
1942
+ if (!field) return [];
1943
+ return operatorsFor(field.type, field.operators).map((op) => ({
1944
+ value: op,
1945
+ label: OPERATOR_LABEL[op]
1946
+ }));
1947
+ }, [field]);
1948
+ if (!field) return null;
1949
+ return /* @__PURE__ */ jsxs("div", { className: "grid min-w-0 gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-bg-subtle)] p-2 md:grid-cols-[minmax(9rem,1fr)_minmax(10rem,1fr)_minmax(12rem,1.4fr)_auto]", children: [
1950
+ /* @__PURE__ */ jsx(
1951
+ Combobox,
1952
+ {
1953
+ className: "min-w-0",
1954
+ options: fieldOptions,
1955
+ value: condition.field,
1956
+ onChange: (fieldId) => {
1957
+ const nextField = fields.find((item) => item.id === fieldId);
1958
+ if (!nextField) return;
1959
+ const op = nextField.operators?.[0] ?? DEFAULT_OPERATOR[nextField.type];
1960
+ onChange(condition.id, {
1961
+ field: nextField.id,
1962
+ op,
1963
+ value: defaultValueForOperator(op, nextField)
1964
+ });
1965
+ },
1966
+ placeholder: "Field",
1967
+ size: "sm",
1968
+ clearable: false
1969
+ }
1970
+ ),
1971
+ /* @__PURE__ */ jsx(
1972
+ Combobox,
1973
+ {
1974
+ className: "min-w-0",
1975
+ options: operatorOptions,
1976
+ value: operatorOptions.some((option) => option.value === condition.op) ? condition.op : field.operators?.[0] ?? DEFAULT_OPERATOR[field.type],
1977
+ onChange: (op) => {
1978
+ if (!op) return;
1979
+ onChange(condition.id, { op, value: defaultValueForOperator(op, field) });
1980
+ },
1981
+ placeholder: "Operator",
1982
+ size: "sm",
1983
+ clearable: false
1984
+ }
1985
+ ),
1986
+ /* @__PURE__ */ jsx("div", { className: "min-w-0", children: /* @__PURE__ */ jsx(
1987
+ ValueEditor,
1988
+ {
1989
+ condition,
1990
+ field,
1991
+ onChange: (patch) => onChange(condition.id, patch)
1992
+ }
1993
+ ) }),
1994
+ /* @__PURE__ */ jsx(
1995
+ Button,
1996
+ {
1997
+ variant: "ghost",
1998
+ size: "icon-sm",
1999
+ "aria-label": `Remove filter: ${field.label}`,
2000
+ onClick: () => onRemove(condition.id),
2001
+ className: "self-start",
2002
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
2003
+ }
2004
+ )
2005
+ ] });
2006
+ }
2007
+
2008
+ export { Badge, Button, ButtonGroup, Checkbox, CheckboxGroup, Combobox, DEFAULT_OPERATOR, DateRangeFilter, FacetedFilter, Field, Fieldset, FilterBuilder, FilterPopover, FiltersProvider, Form, FormActions, FormControl, FormError, FormGrid, FormHelperText, FormLabel, IconButton, Input, MultiColumnCombobox, NumberInput, Popover, RadioCardGroup, RadioGroup, SearchInput, Select, Switch, SwitchGroup, TagInput, Textarea, ToggleGroup, Tooltip, cn, defaultValueForOperator, isGroup, isMultiValueOp, isNullaryOp, isRangeOp, operatorsFor, useFiltersContext, useFormControl, useFormControlContext, useOptionalFiltersContext };