@burojs/ui-shadcn 0.4.0 → 0.6.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 (131) hide show
  1. package/README.md +22 -5
  2. package/dist/bulk-summary-CwSd6Yjb.cjs +108 -0
  3. package/dist/bulk-summary-CwSd6Yjb.cjs.map +1 -0
  4. package/dist/bulk-summary-GxMDP4bw.js +108 -0
  5. package/dist/bulk-summary-GxMDP4bw.js.map +1 -0
  6. package/dist/button-BxnnE28H.cjs +55 -0
  7. package/dist/button-BxnnE28H.cjs.map +1 -0
  8. package/dist/button-MWVeZaqj.js +44 -0
  9. package/dist/button-MWVeZaqj.js.map +1 -0
  10. package/dist/candidates-gate-Bb70LPAy.js +135 -0
  11. package/dist/candidates-gate-Bb70LPAy.js.map +1 -0
  12. package/dist/candidates-gate-D33jXLHO.cjs +153 -0
  13. package/dist/candidates-gate-D33jXLHO.cjs.map +1 -0
  14. package/dist/candidates-relation-picker-B116Bmqi.js +31 -0
  15. package/dist/candidates-relation-picker-B116Bmqi.js.map +1 -0
  16. package/dist/candidates-relation-picker-Cy0rTxvI.cjs +31 -0
  17. package/dist/candidates-relation-picker-Cy0rTxvI.cjs.map +1 -0
  18. package/dist/check-confirm-dialog--gPFnEeg.cjs +1547 -0
  19. package/dist/check-confirm-dialog--gPFnEeg.cjs.map +1 -0
  20. package/dist/check-confirm-dialog-bRT8lt0d.js +1245 -0
  21. package/dist/check-confirm-dialog-bRT8lt0d.js.map +1 -0
  22. package/dist/dialog-B_rt9S1c.js +63 -0
  23. package/dist/dialog-B_rt9S1c.js.map +1 -0
  24. package/dist/dialog-DRNUq9EA.cjs +112 -0
  25. package/dist/dialog-DRNUq9EA.cjs.map +1 -0
  26. package/dist/filter-builder-Bhn-dhZV.js +1621 -0
  27. package/dist/filter-builder-Bhn-dhZV.js.map +1 -0
  28. package/dist/filter-builder-nXqtM_h4.cjs +1789 -0
  29. package/dist/filter-builder-nXqtM_h4.cjs.map +1 -0
  30. package/dist/flow-runtime-D3avVH0I.cjs +255 -0
  31. package/dist/flow-runtime-D3avVH0I.cjs.map +1 -0
  32. package/dist/flow-runtime-s6lkRPKF.js +252 -0
  33. package/dist/flow-runtime-s6lkRPKF.js.map +1 -0
  34. package/dist/flow-screen-CCvgDI9G.js +704 -0
  35. package/dist/flow-screen-CCvgDI9G.js.map +1 -0
  36. package/dist/flow-screen-Ci_Sik4Y.cjs +708 -0
  37. package/dist/flow-screen-Ci_Sik4Y.cjs.map +1 -0
  38. package/dist/index.cjs +3420 -11986
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +1843 -2015
  41. package/dist/index.d.ts +1849 -2021
  42. package/dist/index.js +3090 -11675
  43. package/dist/index.js.map +1 -1
  44. package/dist/inline-flow-host-B6FI0ctQ.js +112 -0
  45. package/dist/inline-flow-host-B6FI0ctQ.js.map +1 -0
  46. package/dist/inline-flow-host-DV1et3c6.cjs +112 -0
  47. package/dist/inline-flow-host-DV1et3c6.cjs.map +1 -0
  48. package/dist/input-BI0czGyI.cjs +23 -0
  49. package/dist/input-BI0czGyI.cjs.map +1 -0
  50. package/dist/input-CUPStzZO.js +17 -0
  51. package/dist/input-CUPStzZO.js.map +1 -0
  52. package/dist/label-66W1qbke.js +29 -0
  53. package/dist/label-66W1qbke.js.map +1 -0
  54. package/dist/label-w6JYf2cF.cjs +36 -0
  55. package/dist/label-w6JYf2cF.cjs.map +1 -0
  56. package/dist/markdown/index.cjs +1 -1
  57. package/dist/markdown/index.d.cts +2 -3
  58. package/dist/markdown/index.d.ts +2 -3
  59. package/dist/markdown/index.js +1 -1
  60. package/dist/page-flow-route-hdnULWqd.cjs +277 -0
  61. package/dist/page-flow-route-hdnULWqd.cjs.map +1 -0
  62. package/dist/page-flow-route-i3ZfJ1sd.js +277 -0
  63. package/dist/page-flow-route-i3ZfJ1sd.js.map +1 -0
  64. package/dist/place-node-C5SmIAYU.cjs +8139 -0
  65. package/dist/place-node-C5SmIAYU.cjs.map +1 -0
  66. package/dist/place-node-NraNYKPx.js +7597 -0
  67. package/dist/place-node-NraNYKPx.js.map +1 -0
  68. package/dist/related-list-cards-BWZ9GfOG.js +42 -0
  69. package/dist/related-list-cards-BWZ9GfOG.js.map +1 -0
  70. package/dist/related-list-cards-CFq3meUn.cjs +42 -0
  71. package/dist/related-list-cards-CFq3meUn.cjs.map +1 -0
  72. package/dist/related-list-timeline-CCzl71aA.cjs +142 -0
  73. package/dist/related-list-timeline-CCzl71aA.cjs.map +1 -0
  74. package/dist/related-list-timeline-yrPsjA3F.js +142 -0
  75. package/dist/related-list-timeline-yrPsjA3F.js.map +1 -0
  76. package/dist/related-rows-ea43qhhy.cjs +208 -0
  77. package/dist/related-rows-ea43qhhy.cjs.map +1 -0
  78. package/dist/related-rows-o0_t-Yce.js +185 -0
  79. package/dist/related-rows-o0_t-Yce.js.map +1 -0
  80. package/dist/relation-create-IA40t1hi.cjs +364 -0
  81. package/dist/relation-create-IA40t1hi.cjs.map +1 -0
  82. package/dist/relation-create-h6F9B4C4.js +339 -0
  83. package/dist/relation-create-h6F9B4C4.js.map +1 -0
  84. package/dist/relation-many-picker-C8zFEjwd.cjs +467 -0
  85. package/dist/relation-many-picker-C8zFEjwd.cjs.map +1 -0
  86. package/dist/relation-many-picker-bvkjkv-k.js +466 -0
  87. package/dist/relation-many-picker-bvkjkv-k.js.map +1 -0
  88. package/dist/relation-picker-Z0ZJJ5uS.js +493 -0
  89. package/dist/relation-picker-Z0ZJJ5uS.js.map +1 -0
  90. package/dist/relation-picker-dialog-0IzkjmiA.cjs +304 -0
  91. package/dist/relation-picker-dialog-0IzkjmiA.cjs.map +1 -0
  92. package/dist/relation-picker-dialog-B6nIiJBf.js +303 -0
  93. package/dist/relation-picker-dialog-B6nIiJBf.js.map +1 -0
  94. package/dist/relation-picker-gag0qSR5.cjs +516 -0
  95. package/dist/relation-picker-gag0qSR5.cjs.map +1 -0
  96. package/dist/resource-page-router-3RUf9d1j.js +1588 -0
  97. package/dist/resource-page-router-3RUf9d1j.js.map +1 -0
  98. package/dist/resource-page-router-PXfMW9nh.cjs +1785 -0
  99. package/dist/resource-page-router-PXfMW9nh.cjs.map +1 -0
  100. package/dist/{rich-text-IDFvyvRE.js → rich-text-DFerywgz.js} +7 -4
  101. package/dist/{rich-text-IDFvyvRE.js.map → rich-text-DFerywgz.js.map} +1 -1
  102. package/dist/{rich-text-lndbYqzR.cjs → rich-text-vTyVjDoa.cjs} +25 -22
  103. package/dist/{rich-text-lndbYqzR.cjs.map → rich-text-vTyVjDoa.cjs.map} +1 -1
  104. package/dist/search-access-CPPmfuvy.js +654 -0
  105. package/dist/search-access-CPPmfuvy.js.map +1 -0
  106. package/dist/search-access-Dugvttlg.cjs +791 -0
  107. package/dist/search-access-Dugvttlg.cjs.map +1 -0
  108. package/dist/shell-BPlk8hEa.js +205 -0
  109. package/dist/shell-BPlk8hEa.js.map +1 -0
  110. package/dist/shell-DxhlsPwm.cjs +271 -0
  111. package/dist/shell-DxhlsPwm.cjs.map +1 -0
  112. package/dist/skeleton-U2noefj5.js +13 -0
  113. package/dist/skeleton-U2noefj5.js.map +1 -0
  114. package/dist/skeleton-_1gP7wan.cjs +18 -0
  115. package/dist/skeleton-_1gP7wan.cjs.map +1 -0
  116. package/dist/styles.css +241 -9
  117. package/dist/toaster-C02XbB9r.js +135 -0
  118. package/dist/toaster-C02XbB9r.js.map +1 -0
  119. package/dist/toaster-Cr51X7bz.cjs +195 -0
  120. package/dist/toaster-Cr51X7bz.cjs.map +1 -0
  121. package/dist/use-dialog-filter-state-C--oMAZG.js +87 -0
  122. package/dist/use-dialog-filter-state-C--oMAZG.js.map +1 -0
  123. package/dist/use-dialog-filter-state-Dhe8PcB5.cjs +92 -0
  124. package/dist/use-dialog-filter-state-Dhe8PcB5.cjs.map +1 -0
  125. package/dist/{use-theme-BwPWinB2.cjs → use-theme-DFlpXils.cjs} +12 -1
  126. package/dist/use-theme-DFlpXils.cjs.map +1 -0
  127. package/dist/{use-theme-IHmtrhEt.js → use-theme-dnE3Od3I.js} +7 -2
  128. package/dist/use-theme-dnE3Od3I.js.map +1 -0
  129. package/package.json +16 -14
  130. package/dist/use-theme-BwPWinB2.cjs.map +0 -1
  131. package/dist/use-theme-IHmtrhEt.js.map +0 -1
@@ -0,0 +1,1621 @@
1
+ import { l as cn } from "./use-theme-dnE3Od3I.js";
2
+ import { d as PopoverTrigger, s as Popover, u as PopoverContent } from "./toaster-C02XbB9r.js";
3
+ import { t as Button } from "./button-MWVeZaqj.js";
4
+ import { t as Input } from "./input-CUPStzZO.js";
5
+ import { _ as CommandEmpty, b as CommandItem, d as useRowTitle, h as Command, n as narrowSearchDecl, v as CommandGroup, x as CommandList, y as CommandInput } from "./search-access-CPPmfuvy.js";
6
+ import { EMPTY_FILTER_GROUP, convertValueForOp, enumValueText, isCompletePredicate, isFilterGroup, opArity, resolveEnumLabel } from "@burojs/core";
7
+ import { DEFAULT_LANGUAGE_STORAGE_KEY, useApp, useEnumValues, useI18nCaps, useList, useListAccess, useOptionalT, useRelatedRecord } from "@burojs/react";
8
+ import { IconCheck, IconChevronDown, IconChevronUp, IconFilter, IconFolderPlus, IconLoader2, IconPlus, IconX } from "@tabler/icons-react";
9
+ import * as React from "react";
10
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
11
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
12
+ import { cva } from "class-variance-authority";
13
+ import * as SelectPrimitive from "@radix-ui/react-select";
14
+ //#region src/layout/language-storage.ts
15
+ function loadLocale(storageKey = DEFAULT_LANGUAGE_STORAGE_KEY) {
16
+ if (typeof window === "undefined") return void 0;
17
+ try {
18
+ return window.localStorage.getItem(storageKey) ?? void 0;
19
+ } catch {
20
+ return;
21
+ }
22
+ }
23
+ function saveLocale(code, storageKey = DEFAULT_LANGUAGE_STORAGE_KEY) {
24
+ if (typeof window === "undefined") return;
25
+ try {
26
+ window.localStorage.setItem(storageKey, code);
27
+ } catch {}
28
+ }
29
+ //#endregion
30
+ //#region src/primitives/badge.tsx
31
+ const badgeVariants = cva("inline-flex max-w-full items-center truncate rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", {
32
+ variants: { variant: {
33
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
34
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
35
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
36
+ outline: "text-foreground",
37
+ success: "border-success/20 bg-success/15 text-success-emphasis hover:bg-success/20",
38
+ warning: "border-warning/20 bg-warning/15 text-warning-emphasis hover:bg-warning/20",
39
+ info: "border-info/20 bg-info/15 text-info-emphasis hover:bg-info/20"
40
+ } },
41
+ defaultVariants: { variant: "default" }
42
+ });
43
+ function Badge({ className, variant, children, ...props }) {
44
+ return jsx("div", {
45
+ className: cn(badgeVariants({ variant }), className),
46
+ ...props,
47
+ children: jsx("span", {
48
+ className: "min-w-0 truncate",
49
+ children
50
+ })
51
+ });
52
+ }
53
+ //#endregion
54
+ //#region src/primitives/table.tsx
55
+ const Table = React.forwardRef(({ className, ...props }, ref) => jsx("div", {
56
+ className: "relative w-full overflow-auto",
57
+ children: jsx("table", {
58
+ ref,
59
+ className: cn("w-full caption-bottom text-sm", className),
60
+ ...props
61
+ })
62
+ }));
63
+ Table.displayName = "Table";
64
+ const TableHeader = React.forwardRef(({ className, ...props }, ref) => jsx("thead", {
65
+ ref,
66
+ className: cn("[&_tr]:border-b", className),
67
+ ...props
68
+ }));
69
+ TableHeader.displayName = "TableHeader";
70
+ const TableBody = React.forwardRef(({ className, ...props }, ref) => jsx("tbody", {
71
+ ref,
72
+ className: cn("[&_tr:last-child]:border-0", className),
73
+ ...props
74
+ }));
75
+ TableBody.displayName = "TableBody";
76
+ const TableRow = React.forwardRef(({ className, ...props }, ref) => jsx("tr", {
77
+ ref,
78
+ className: cn("group/row border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-primary/10 data-[state=selected]:hover:bg-primary/15", className),
79
+ ...props
80
+ }));
81
+ TableRow.displayName = "TableRow";
82
+ const TableHead = React.forwardRef(({ className, ...props }, ref) => jsx("th", {
83
+ ref,
84
+ className: cn("h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className),
85
+ ...props
86
+ }));
87
+ TableHead.displayName = "TableHead";
88
+ const TableCell = React.forwardRef(({ className, ...props }, ref) => jsx("td", {
89
+ ref,
90
+ className: cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", className),
91
+ ...props
92
+ }));
93
+ TableCell.displayName = "TableCell";
94
+ const TableCaption = React.forwardRef(({ className, ...props }, ref) => jsx("caption", {
95
+ ref,
96
+ className: cn("mt-4 text-sm text-muted-foreground", className),
97
+ ...props
98
+ }));
99
+ TableCaption.displayName = "TableCaption";
100
+ //#endregion
101
+ //#region src/primitives/select.tsx
102
+ const Select = SelectPrimitive.Root;
103
+ const SelectGroup = SelectPrimitive.Group;
104
+ const SelectValue = SelectPrimitive.Value;
105
+ const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => jsxs(SelectPrimitive.Trigger, {
106
+ ref,
107
+ className: cn("flex h-9 w-full items-center justify-between rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-input-disabled [&>span]:line-clamp-1", className),
108
+ ...props,
109
+ children: [children, jsx(SelectPrimitive.Icon, {
110
+ asChild: true,
111
+ children: jsx(IconChevronDown, { className: "h-4 w-4 opacity-50" })
112
+ })]
113
+ }));
114
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
115
+ const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => jsx(SelectPrimitive.ScrollUpButton, {
116
+ ref,
117
+ className: cn("flex cursor-default items-center justify-center py-1", className),
118
+ ...props,
119
+ children: jsx(IconChevronUp, { className: "h-4 w-4" })
120
+ }));
121
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
122
+ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => jsx(SelectPrimitive.ScrollDownButton, {
123
+ ref,
124
+ className: cn("flex cursor-default items-center justify-center py-1", className),
125
+ ...props,
126
+ children: jsx(IconChevronDown, { className: "h-4 w-4" })
127
+ }));
128
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
129
+ const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => jsx(SelectPrimitive.Portal, { children: jsxs(SelectPrimitive.Content, {
130
+ ref,
131
+ className: cn("relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md duration-[var(--duration-overlay)] ease-[var(--ease-overlay)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className),
132
+ position,
133
+ ...props,
134
+ children: [
135
+ jsx(SelectScrollUpButton, {}),
136
+ jsx(SelectPrimitive.Viewport, {
137
+ className: cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"),
138
+ children
139
+ }),
140
+ jsx(SelectScrollDownButton, {})
141
+ ]
142
+ }) }));
143
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
144
+ const SelectLabel = React.forwardRef(({ className, ...props }, ref) => jsx(SelectPrimitive.Label, {
145
+ ref,
146
+ className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className),
147
+ ...props
148
+ }));
149
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
150
+ const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => jsxs(SelectPrimitive.Item, {
151
+ ref,
152
+ className: cn("relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className),
153
+ ...props,
154
+ children: [jsx("span", {
155
+ className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center",
156
+ children: jsx(SelectPrimitive.ItemIndicator, { children: jsx(IconCheck, { className: "h-4 w-4" }) })
157
+ }), jsx(SelectPrimitive.ItemText, { children })]
158
+ }));
159
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
160
+ const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => jsx(SelectPrimitive.Separator, {
161
+ ref,
162
+ className: cn("-mx-1 my-1 h-px bg-muted", className),
163
+ ...props
164
+ }));
165
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
166
+ //#endregion
167
+ //#region src/fields/format.ts
168
+ function localeTag(locale) {
169
+ return locale || loadLocale() || "en-US";
170
+ }
171
+ const dateCache = new Map();
172
+ const dateTimeCache = new Map();
173
+ const numberCache = new Map();
174
+ function cached(cache, key, make) {
175
+ const hit = cache.get(key);
176
+ if (hit) return hit;
177
+ let made;
178
+ try {
179
+ made = make(key);
180
+ } catch {
181
+ made = make("en-US");
182
+ }
183
+ cache.set(key, made);
184
+ return made;
185
+ }
186
+ function formatDate(d, locale) {
187
+ const tag = localeTag(locale);
188
+ return cached(dateCache, tag, (t) => new Intl.DateTimeFormat(t, {
189
+ year: "numeric",
190
+ month: "short",
191
+ day: "numeric"
192
+ })).format(d);
193
+ }
194
+ function formatDateTime(d, locale) {
195
+ const tag = localeTag(locale);
196
+ return cached(dateTimeCache, tag, (t) => new Intl.DateTimeFormat(t, {
197
+ year: "numeric",
198
+ month: "short",
199
+ day: "numeric",
200
+ hour: "2-digit",
201
+ minute: "2-digit"
202
+ })).format(d);
203
+ }
204
+ function formatNumber(n, locale) {
205
+ const tag = localeTag(locale);
206
+ return cached(numberCache, tag, (t) => new Intl.NumberFormat(t)).format(n);
207
+ }
208
+ //#endregion
209
+ //#region src/list-kit/filter-builder/describe-row.ts
210
+ const MAX_DESCRIBE_DEPTH = 2;
211
+ function describeValue(value, t, spec, formatValue) {
212
+ if (value === void 0) return null;
213
+ if (Array.isArray(value)) return t("filters.selectedCount", { count: value.length }, `${value.length} selected`);
214
+ const optionLabel = spec?.options?.find((o) => o.value === String(value))?.label;
215
+ if (optionLabel !== void 0) return optionLabel;
216
+ if (spec && formatValue) {
217
+ const formatted = formatValue(spec.kind, value);
218
+ if (formatted !== void 0) return formatted;
219
+ }
220
+ return String(value);
221
+ }
222
+ function describeAt(row, available, t, depth, formatValue) {
223
+ if (isFilterGroup(row)) {
224
+ if (depth > MAX_DESCRIBE_DEPTH) return "…";
225
+ const joinLabel = t(`filters.${row.join}`, void 0, row.join);
226
+ return `(${row.rows.map((child) => describeAt(child, available, t, depth + 1, formatValue)).join(` ${joinLabel} `)})`;
227
+ }
228
+ const spec = available.find((s) => s.field === row.field);
229
+ const label = spec?.label ?? row.field;
230
+ const opLabel = t(`filters.operators.${row.op}`, void 0, row.op);
231
+ const valueText = opArity(row.op) === "none" ? null : describeValue(row.value, t, spec, formatValue);
232
+ return valueText === null ? `${label} ${opLabel}` : `${label} ${opLabel} ${valueText}`;
233
+ }
234
+ function describeRow(row, available, t, formatValue) {
235
+ return describeAt(row, available, t, 1, formatValue);
236
+ }
237
+ function relationLabelKey(to, by, value) {
238
+ return `${by === void 0 ? to : `${to}#${by}`}::${value}`;
239
+ }
240
+ function collectFormatRefs(rows, available) {
241
+ const enumNames = new Set();
242
+ const relationRefs = new Map();
243
+ const walk = (nodes) => {
244
+ for (const row of nodes) {
245
+ if (isFilterGroup(row)) {
246
+ walk(row.rows);
247
+ continue;
248
+ }
249
+ if (row.value === void 0 || Array.isArray(row.value)) continue;
250
+ const spec = available.find((s) => s.field === row.field);
251
+ if (!spec) continue;
252
+ if (spec.kind.kind === "enum") enumNames.add(spec.kind.enumName);
253
+ else if (spec.kind.kind === "relation") {
254
+ const id = String(row.value);
255
+ const { to, by } = spec.kind;
256
+ relationRefs.set(relationLabelKey(to, by, id), by === void 0 ? {
257
+ to,
258
+ id
259
+ } : {
260
+ to,
261
+ id,
262
+ by
263
+ });
264
+ }
265
+ }
266
+ };
267
+ walk(rows);
268
+ return {
269
+ enumNames: Array.from(enumNames),
270
+ relationRefs: Array.from(relationRefs.values())
271
+ };
272
+ }
273
+ //#endregion
274
+ //#region src/list-kit/filter-builder/describe-value-resolvers.tsx
275
+ function EnumLabelResolver(props) {
276
+ const { enumName, onResolve } = props;
277
+ const { values } = useEnumValues(enumName);
278
+ const caps = useI18nCaps();
279
+ const def = useApp().registry.enums.get(enumName);
280
+ useEffect(() => {
281
+ if (!values) return;
282
+ const labels = {};
283
+ for (const entry of values) labels[entry.value] = resolveEnumLabel(caps, enumName, entry.value, enumValueText(def, entry.value) ?? entry.label);
284
+ onResolve(enumName, labels);
285
+ }, [
286
+ values,
287
+ enumName,
288
+ caps,
289
+ def,
290
+ onResolve
291
+ ]);
292
+ return null;
293
+ }
294
+ function RelationLabelResolver(props) {
295
+ const { to, id, by, onResolve } = props;
296
+ const targetDef = useApp().registry.get(to);
297
+ const { record } = useRelatedRecord(to, id, by === void 0 ? void 0 : { by });
298
+ const rowTitle = useRowTitle(to, targetDef?.displayField);
299
+ useEffect(() => {
300
+ if (!record) return;
301
+ onResolve(relationLabelKey(to, by, id), rowTitle(record));
302
+ }, [
303
+ record,
304
+ to,
305
+ by,
306
+ id,
307
+ rowTitle,
308
+ onResolve
309
+ ]);
310
+ return null;
311
+ }
312
+ function FormatResolvers(props) {
313
+ const { refs, onEnumResolved, onRelationResolved } = props;
314
+ return jsxs(Fragment$1, { children: [refs.enumNames.map((name) => jsx(EnumLabelResolver, {
315
+ enumName: name,
316
+ onResolve: onEnumResolved
317
+ }, `enum:${name}`)), refs.relationRefs.map(({ to, id, by }) => jsx(RelationLabelResolver, {
318
+ to,
319
+ id,
320
+ ...by === void 0 ? {} : { by },
321
+ onResolve: onRelationResolved
322
+ }, `relation:${relationLabelKey(to, by, id)}`))] });
323
+ }
324
+ function useFormatValueMaps() {
325
+ const [enumLabels, setEnumLabels] = useState({});
326
+ const [relationLabels, setRelationLabels] = useState({});
327
+ return {
328
+ enumLabels,
329
+ relationLabels,
330
+ onEnumResolved: useCallback((enumName, labels) => {
331
+ setEnumLabels((prev) => prev[enumName] === labels ? prev : {
332
+ ...prev,
333
+ [enumName]: labels
334
+ });
335
+ }, []),
336
+ onRelationResolved: useCallback((key, label) => {
337
+ setRelationLabels((prev) => prev[key] === label ? prev : {
338
+ ...prev,
339
+ [key]: label
340
+ });
341
+ }, [])
342
+ };
343
+ }
344
+ //#endregion
345
+ //#region src/list-kit/filter-builder/pinned-ownership.ts
346
+ function isFilterPredicateExpr(expr) {
347
+ return "field" in expr;
348
+ }
349
+ function findOwnedIndex(value, spec) {
350
+ const op = spec.operators[0] ?? "eq";
351
+ return value.findIndex((expr) => isFilterPredicateExpr(expr) && expr.field === spec.field && expr.op === op);
352
+ }
353
+ function claimedPinnedIndices(value, pinnedFields) {
354
+ const claimed = new Set();
355
+ for (const spec of pinnedFields) {
356
+ const op = spec.operators[0] ?? "eq";
357
+ const index = value.findIndex((expr, i) => !claimed.has(i) && isFilterPredicateExpr(expr) && expr.field === spec.field && expr.op === op);
358
+ if (index >= 0) claimed.add(index);
359
+ }
360
+ return claimed;
361
+ }
362
+ //#endregion
363
+ //#region src/list-kit/filter-builder/filter-badges.tsx
364
+ function badgeKey(row, index) {
365
+ if (isFilterGroup(row)) return `${index}:group:${row.join}`;
366
+ return `${index}:${row.field}:${row.op}`;
367
+ }
368
+ function FilterBadges(props) {
369
+ const { group, setGroup, available, fixed, testIdBase, onOpenAt, onRemoved, pinnedFields = [], pinnedValue = [], setPinnedValue } = props;
370
+ const t = useOptionalT();
371
+ const tt = (key, params, fallback) => t(key, {
372
+ params,
373
+ ...typeof params?.count === "number" ? { count: params.count } : {},
374
+ defaultValue: fallback
375
+ });
376
+ const removeLabel = t("filters.removeRow", { defaultValue: "Remove filter" });
377
+ const { enumLabels, relationLabels, onEnumResolved, onRelationResolved } = useFormatValueMaps();
378
+ const formatValue = (kind, value) => {
379
+ if (kind.kind === "enum") return enumLabels[kind.enumName]?.[String(value)];
380
+ if (kind.kind === "relation") return relationLabels[relationLabelKey(kind.to, kind.by, String(value))];
381
+ if (kind.kind === "date") {
382
+ const d = new Date(String(value));
383
+ if (Number.isNaN(d.getTime())) return void 0;
384
+ return kind.time ? formatDateTime(d) : formatDate(d);
385
+ }
386
+ };
387
+ const orphanPinned = useMemo(() => {
388
+ const claimed = claimedPinnedIndices(pinnedValue, pinnedFields);
389
+ return pinnedValue.map((expr, index) => ({
390
+ expr,
391
+ index
392
+ })).filter(({ expr, index }) => !claimed.has(index) && isFilterPredicateExpr(expr));
393
+ }, [pinnedValue, pinnedFields]);
394
+ const refs = useMemo(() => collectFormatRefs([...group.rows, ...orphanPinned.map(({ expr }) => expr)], available), [
395
+ group,
396
+ orphanPinned,
397
+ available
398
+ ]);
399
+ const handleRemove = (index) => {
400
+ setGroup({
401
+ ...group,
402
+ rows: group.rows.filter((_, i) => i !== index)
403
+ });
404
+ onRemoved?.(index);
405
+ };
406
+ const handleRemovePinned = (index) => {
407
+ setPinnedValue?.(pinnedValue.filter((_, i) => i !== index));
408
+ };
409
+ const labelledFixed = fixed.filter((entry) => entry.label !== void 0);
410
+ return jsxs("div", {
411
+ className: "flex flex-wrap items-center gap-1",
412
+ children: [
413
+ jsx(FormatResolvers, {
414
+ refs,
415
+ onEnumResolved,
416
+ onRelationResolved
417
+ }),
418
+ group.rows.map((row, index) => jsxs(Badge, {
419
+ variant: "secondary",
420
+ className: "gap-1 py-1 pr-1",
421
+ children: [jsx("button", {
422
+ type: "button",
423
+ "data-testid": `${testIdBase}:badge:${index}`,
424
+ onClick: () => onOpenAt([index]),
425
+ className: "max-w-full cursor-pointer truncate bg-transparent p-0 text-left",
426
+ children: describeRow(row, available, tt, formatValue)
427
+ }), jsx("button", {
428
+ type: "button",
429
+ "data-testid": `${testIdBase}:badge:${index}:remove`,
430
+ "aria-label": removeLabel,
431
+ onClick: () => handleRemove(index),
432
+ className: "rounded-sm hover:bg-accent",
433
+ children: jsx(IconX, {
434
+ className: "h-3 w-3",
435
+ "aria-hidden": "true"
436
+ })
437
+ })]
438
+ }, badgeKey(row, index))),
439
+ labelledFixed.map((entry, index) => jsx(Badge, {
440
+ variant: "outline",
441
+ "data-testid": `${testIdBase}:badge:fixed:${index}`,
442
+ children: entry.label
443
+ }, `fixed-${index}`)),
444
+ orphanPinned.map(({ expr, index }) => jsxs(Badge, {
445
+ variant: "secondary",
446
+ className: "gap-1 py-1",
447
+ children: [jsx("span", {
448
+ "data-testid": `${testIdBase}:badge:pinned:${index}`,
449
+ children: describeRow(expr, available, tt, formatValue)
450
+ }), jsx("button", {
451
+ type: "button",
452
+ "data-testid": `${testIdBase}:badge:pinned:${index}:remove`,
453
+ "aria-label": removeLabel,
454
+ onClick: () => handleRemovePinned(index),
455
+ className: "rounded-sm hover:bg-accent",
456
+ children: jsx(IconX, {
457
+ className: "h-3 w-3",
458
+ "aria-hidden": "true"
459
+ })
460
+ })]
461
+ }, `pinned-${index}`))
462
+ ]
463
+ });
464
+ }
465
+ let counter = 0;
466
+ function nextId() {
467
+ counter += 1;
468
+ return `r${counter}`;
469
+ }
470
+ function isDraftGroup(node) {
471
+ return Object.hasOwn(node, "rows");
472
+ }
473
+ function toDraft(group) {
474
+ return {
475
+ rowId: nextId(),
476
+ join: group.join,
477
+ rows: group.rows.map(toDraftNode)
478
+ };
479
+ }
480
+ function toDraftNode(row) {
481
+ if (isFilterGroup(row)) return toDraft(row);
482
+ const predicate = row;
483
+ return {
484
+ rowId: nextId(),
485
+ field: predicate.field,
486
+ op: predicate.op,
487
+ ...Object.hasOwn(predicate, "value") ? { value: predicate.value } : {}
488
+ };
489
+ }
490
+ function fromDraft(draft) {
491
+ return {
492
+ join: draft.join,
493
+ rows: draft.rows.map((node) => isDraftGroup(node) ? fromDraft(node) : {
494
+ field: node.field,
495
+ op: node.op,
496
+ ...Object.hasOwn(node, "value") ? { value: node.value } : {}
497
+ })
498
+ };
499
+ }
500
+ function pruneIncompleteRows(group) {
501
+ const rows = [];
502
+ for (const row of group.rows) if (isFilterGroup(row)) {
503
+ const pruned = pruneIncompleteRows(row);
504
+ if (pruned.rows.length > 0) rows.push(pruned);
505
+ } else if (isCompletePredicate(row)) rows.push(row);
506
+ return {
507
+ join: group.join,
508
+ rows
509
+ };
510
+ }
511
+ function mapGroupAt(draft, path, replace) {
512
+ if (path.length === 0) return replace(draft);
513
+ const [head, ...rest] = path;
514
+ const child = draft.rows[head];
515
+ if (child === void 0 || !isDraftGroup(child)) return draft;
516
+ const next = mapGroupAt(child, rest, replace);
517
+ if (next === child) return draft;
518
+ const rows = draft.rows.slice();
519
+ rows[head] = next;
520
+ return {
521
+ ...draft,
522
+ rows
523
+ };
524
+ }
525
+ function depthOf(path) {
526
+ return path.length + 1;
527
+ }
528
+ function defaultValueForOp(op) {
529
+ const arity = opArity(op);
530
+ if (arity === "none") return void 0;
531
+ if (arity === "multi") return [];
532
+ return "";
533
+ }
534
+ function addRow(draft, path, available) {
535
+ const spec = available[0];
536
+ if (!spec) return draft;
537
+ const op = spec.operators[0] ?? "eq";
538
+ const value = defaultValueForOp(op);
539
+ const row = {
540
+ rowId: nextId(),
541
+ field: spec.field,
542
+ op,
543
+ ...value === void 0 ? {} : { value }
544
+ };
545
+ return mapGroupAt(draft, path, (group) => ({
546
+ ...group,
547
+ rows: [...group.rows, row]
548
+ }));
549
+ }
550
+ function addGroup(draft, path) {
551
+ if (depthOf(path) >= 3) return draft;
552
+ const group = {
553
+ rowId: nextId(),
554
+ join: "and",
555
+ rows: []
556
+ };
557
+ return mapGroupAt(draft, path, (parent) => ({
558
+ ...parent,
559
+ rows: [...parent.rows, group]
560
+ }));
561
+ }
562
+ function removeAt(draft, path) {
563
+ if (path.length === 0) return draft;
564
+ const parentPath = path.slice(0, -1);
565
+ const index = path[path.length - 1];
566
+ return mapGroupAt(draft, parentPath, (group) => {
567
+ if (group.rows[index] === void 0) return group;
568
+ const rows = group.rows.slice();
569
+ rows.splice(index, 1);
570
+ return {
571
+ ...group,
572
+ rows
573
+ };
574
+ });
575
+ }
576
+ function setJoin(draft, path, join) {
577
+ return mapGroupAt(draft, path, (group) => group.join === join ? group : {
578
+ ...group,
579
+ join
580
+ });
581
+ }
582
+ function updateRow(draft, path, patch) {
583
+ if (path.length === 0) return draft;
584
+ const parentPath = path.slice(0, -1);
585
+ const index = path[path.length - 1];
586
+ return mapGroupAt(draft, parentPath, (group) => {
587
+ const current = group.rows[index];
588
+ if (current === void 0 || isDraftGroup(current)) return group;
589
+ const fieldChanged = patch.field !== void 0 && patch.field !== current.field;
590
+ const op = patch.op ?? current.op;
591
+ let value;
592
+ if (fieldChanged) value = defaultValueForOp(op);
593
+ else if (patch.op !== void 0 && patch.op !== current.op) value = convertValueForOp(current.value, current.op, patch.op);
594
+ else value = Object.hasOwn(patch, "value") ? patch.value : current.value;
595
+ const next = {
596
+ rowId: current.rowId,
597
+ field: patch.field ?? current.field,
598
+ op,
599
+ ...value === void 0 ? {} : { value }
600
+ };
601
+ const rows = group.rows.slice();
602
+ rows[index] = next;
603
+ return {
604
+ ...group,
605
+ rows
606
+ };
607
+ });
608
+ }
609
+ const EMPTY_DRAFT = () => toDraft(EMPTY_FILTER_GROUP);
610
+ //#endregion
611
+ //#region src/list-kit/filter-builder/values/use-selectable-value-control.ts
612
+ function useSelectableValueControl(options) {
613
+ const { value, arity, onChange, emitSingle = (token) => token } = options;
614
+ const [open, setOpen] = useState(false);
615
+ const selected = arity === "multi" ? new Set(Array.isArray(value) ? value.map(String) : []) : new Set(value === void 0 || value === null ? [] : [String(value)]);
616
+ const select = (token) => {
617
+ if (arity === "single") {
618
+ onChange(emitSingle(token));
619
+ setOpen(false);
620
+ return;
621
+ }
622
+ const next = new Set(selected);
623
+ if (next.has(token)) next.delete(token);
624
+ else next.add(token);
625
+ onChange(next.size === 0 ? void 0 : Array.from(next));
626
+ };
627
+ const triggerLabel = (labelOf, placeholder) => {
628
+ const picked = Array.from(selected);
629
+ return picked.length === 0 ? placeholder : picked.map(labelOf).join(", ");
630
+ };
631
+ const clear = () => {
632
+ onChange(void 0);
633
+ setOpen(false);
634
+ };
635
+ return {
636
+ open,
637
+ setOpen,
638
+ selected,
639
+ select,
640
+ triggerLabel,
641
+ clear
642
+ };
643
+ }
644
+ //#endregion
645
+ //#region src/list-kit/filter-builder/values/boolean-value-control.tsx
646
+ const OPTIONS = ["true", "false"];
647
+ function BooleanValueControl(props) {
648
+ const { value, onChange, testId, arity, autoFocus, fieldLabel, fill } = props;
649
+ const t = useOptionalT();
650
+ const ariaLabel = fieldLabel ? t("filters.valueFor", {
651
+ defaultValue: `Value for ${fieldLabel}`,
652
+ params: { field: fieldLabel }
653
+ }) : t("filters.value", { defaultValue: "Value" });
654
+ const yesLabel = t("boolean.yes", { defaultValue: "Yes" });
655
+ const noLabel = t("boolean.no", { defaultValue: "No" });
656
+ const labelOf = (token) => token === "true" ? yesLabel : noLabel;
657
+ const clearLabel = t("filters.clear", { defaultValue: "Clear" });
658
+ const { open, setOpen, selected, select, triggerLabel, clear } = useSelectableValueControl({
659
+ value,
660
+ arity,
661
+ onChange,
662
+ emitSingle: (token) => token === "true"
663
+ });
664
+ return jsxs(Popover, {
665
+ open,
666
+ onOpenChange: setOpen,
667
+ children: [jsx(PopoverTrigger, {
668
+ asChild: true,
669
+ children: jsx(Button, {
670
+ type: "button",
671
+ variant: "outline",
672
+ size: "sm",
673
+ autoFocus,
674
+ "data-testid": testId,
675
+ "aria-label": ariaLabel,
676
+ className: cn("h-8 justify-between", fill && "w-full"),
677
+ children: triggerLabel(labelOf, ariaLabel)
678
+ })
679
+ }), jsx(PopoverContent, {
680
+ className: "w-[140px] p-0",
681
+ align: "start",
682
+ children: jsx(Command, { children: jsx(CommandList, { children: jsxs(CommandGroup, { children: [arity === "single" && selected.size > 0 && jsxs(CommandItem, {
683
+ "data-testid": `${testId}:clear`,
684
+ value: "__clear__",
685
+ onSelect: () => clear(),
686
+ children: [jsx(IconX, {
687
+ className: "mr-2 h-4 w-4",
688
+ "aria-hidden": "true"
689
+ }), clearLabel]
690
+ }), OPTIONS.map((token) => {
691
+ const isSelected = selected.has(token);
692
+ return jsxs(CommandItem, {
693
+ value: labelOf(token),
694
+ "data-testid": `${testId}:option:${token}`,
695
+ onSelect: () => select(token),
696
+ children: [jsx(IconCheck, {
697
+ className: cn("mr-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0"),
698
+ "aria-hidden": "true"
699
+ }), labelOf(token)]
700
+ }, token);
701
+ })] }) }) })
702
+ })]
703
+ });
704
+ }
705
+ //#endregion
706
+ //#region src/list-kit/filter-builder/values/select-list-value-control.tsx
707
+ function SelectListValueControl(props) {
708
+ const { options, value, onChange, testId, arity, autoFocus, fieldLabel, isLoading = false } = props;
709
+ const t = useOptionalT();
710
+ const ariaLabel = fieldLabel ? t("filters.valueFor", {
711
+ defaultValue: `Value for ${fieldLabel}`,
712
+ params: { field: fieldLabel }
713
+ }) : t("filters.value", { defaultValue: "Value" });
714
+ const loadingLabel = t("table.loading", { defaultValue: "Loading…" });
715
+ const clearLabel = t("filters.clear", { defaultValue: "Clear" });
716
+ const labelFor = props.labelFor ?? ((raw) => options.find((o) => o.value === raw)?.label ?? raw);
717
+ const { open, setOpen, selected, select, triggerLabel, clear } = useSelectableValueControl({
718
+ value,
719
+ arity,
720
+ onChange
721
+ });
722
+ return jsxs(Popover, {
723
+ open,
724
+ onOpenChange: setOpen,
725
+ children: [jsx(PopoverTrigger, {
726
+ asChild: true,
727
+ children: jsx(Button, {
728
+ type: "button",
729
+ variant: "outline",
730
+ size: "sm",
731
+ autoFocus,
732
+ "data-testid": testId,
733
+ "aria-label": ariaLabel,
734
+ className: cn("h-8 justify-between truncate", props.fill ? "w-full" : "max-w-[200px]"),
735
+ children: jsx("span", {
736
+ className: "truncate",
737
+ children: isLoading ? loadingLabel : triggerLabel(labelFor, ariaLabel)
738
+ })
739
+ })
740
+ }), jsx(PopoverContent, {
741
+ className: "w-[220px] p-0",
742
+ align: "start",
743
+ children: jsxs(Command, { children: [jsx(CommandInput, {
744
+ placeholder: ariaLabel,
745
+ "data-testid": `${testId}:search`
746
+ }), jsxs(CommandList, { children: [
747
+ isLoading && jsxs("div", {
748
+ className: "flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground",
749
+ "data-testid": `${testId}:loading`,
750
+ children: [jsx(IconLoader2, {
751
+ className: "h-4 w-4 animate-spin",
752
+ "aria-hidden": "true"
753
+ }), loadingLabel]
754
+ }),
755
+ !isLoading && jsx(CommandEmpty, { children: t("relation.noResults", { defaultValue: "No results." }) }),
756
+ !isLoading && jsxs(CommandGroup, { children: [arity === "single" && selected.size > 0 && jsxs(CommandItem, {
757
+ "data-testid": `${testId}:clear`,
758
+ value: "__clear__",
759
+ onSelect: () => clear(),
760
+ children: [jsx(IconX, {
761
+ className: "mr-2 h-4 w-4",
762
+ "aria-hidden": "true"
763
+ }), clearLabel]
764
+ }), options.map((entry) => {
765
+ const isSelected = selected.has(entry.value);
766
+ const label = labelFor(entry.value);
767
+ return jsxs(CommandItem, {
768
+ value: label,
769
+ "data-testid": `${testId}:option:${entry.value}`,
770
+ onSelect: () => select(entry.value),
771
+ children: [jsx(IconCheck, {
772
+ className: cn("mr-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0"),
773
+ "aria-hidden": "true"
774
+ }), label]
775
+ }, entry.value);
776
+ })] })
777
+ ] })] })
778
+ })]
779
+ });
780
+ }
781
+ //#endregion
782
+ //#region src/list-kit/filter-builder/values/enum-value-control.tsx
783
+ function EnumValueControl(props) {
784
+ const { enumName, value, onChange, testId, arity, autoFocus, fieldLabel, fill } = props;
785
+ const caps = useI18nCaps();
786
+ const def = useApp().registry.enums.get(enumName);
787
+ const { values, isLoading } = useEnumValues(enumName);
788
+ const labelFor = (raw) => resolveEnumLabel(caps, enumName, raw, enumValueText(def, raw) ?? values?.find((v) => v.value === raw)?.label);
789
+ const options = (values ?? []).map((entry) => ({
790
+ value: entry.value,
791
+ label: entry.value
792
+ }));
793
+ return jsx(SelectListValueControl, {
794
+ value,
795
+ onChange,
796
+ testId,
797
+ arity,
798
+ autoFocus,
799
+ fieldLabel,
800
+ options,
801
+ labelFor,
802
+ isLoading,
803
+ fill
804
+ });
805
+ }
806
+ //#endregion
807
+ //#region src/list-kit/use-related-labels.ts
808
+ function useRelatedLabels(to, ids, by) {
809
+ const targetDef = useApp().registry.get(to);
810
+ const idField = (targetDef?.data)?.idField;
811
+ const rowTitle = useRowTitle(to, targetDef?.displayField);
812
+ const field = by ?? (typeof idField === "string" && idField.length > 0 ? idField : "id");
813
+ const query = useList(to, {
814
+ pagination: {
815
+ kind: "offset",
816
+ page: 1,
817
+ pageSize: Math.min(Math.max(1, ids.length), 100)
818
+ },
819
+ ...ids.length ? { filter: {
820
+ field,
821
+ op: "in",
822
+ value: ids
823
+ } } : {},
824
+ scope: "none",
825
+ enabled: ids.length > 0
826
+ });
827
+ return useMemo(() => {
828
+ const out = {};
829
+ for (const row of query.data?.data ?? []) {
830
+ const key = row[field];
831
+ if (key !== null && key !== void 0) out[String(key)] = rowTitle(row);
832
+ }
833
+ return out;
834
+ }, [
835
+ query.data,
836
+ rowTitle,
837
+ field
838
+ ]);
839
+ }
840
+ //#endregion
841
+ //#region src/list-kit/filter-builder/values/relation-value-control.tsx
842
+ function RelationValueControl(props) {
843
+ const { to, by, value, onChange, testId, arity, autoFocus, fieldLabel } = props;
844
+ const app = useApp();
845
+ const t = useOptionalT();
846
+ const targetDef = app.registry.get(to);
847
+ const displayField = targetDef?.displayField ?? "name";
848
+ const [search, setSearch] = useState("");
849
+ const { open, setOpen, selected, select, triggerLabel, clear } = useSelectableValueControl({
850
+ value,
851
+ arity,
852
+ onChange
853
+ });
854
+ const rowTitle = useRowTitle(to, targetDef?.displayField);
855
+ const access = useListAccess(to);
856
+ const searchable = useMemo(() => access.status === "ready" && narrowSearchDecl({ fields: [displayField] }, (name) => access.fieldVisible(name)) !== void 0, [access, displayField]);
857
+ const listQuery = useList(to, {
858
+ pagination: {
859
+ kind: "offset",
860
+ page: 1,
861
+ pageSize: 10
862
+ },
863
+ ...searchable && search.trim() ? { filter: {
864
+ field: displayField,
865
+ op: "contains",
866
+ value: search.trim()
867
+ } } : {},
868
+ scope: "session",
869
+ enabled: open && access.status === "ready"
870
+ });
871
+ const rows = listQuery.data?.data ?? [];
872
+ const fallbackLabels = useRelatedLabels(to, Array.from(selected), by);
873
+ const idField = (targetDef?.data)?.idField;
874
+ const valueField = by ?? (typeof idField === "string" && idField.length > 0 ? idField : "id");
875
+ const keyOf = (row) => {
876
+ const key = row[valueField];
877
+ return key === null || key === void 0 || key === "" ? void 0 : String(key);
878
+ };
879
+ const labelFor = (id) => {
880
+ const fromRows = rows.find((r) => keyOf(r) === id);
881
+ if (fromRows) return rowTitle(fromRows);
882
+ return fallbackLabels[id] ?? id;
883
+ };
884
+ const ariaLabel = fieldLabel ? t("filters.valueFor", {
885
+ defaultValue: `Value for ${fieldLabel}`,
886
+ params: { field: fieldLabel }
887
+ }) : t("filters.value", { defaultValue: "Value" });
888
+ const clearLabel = t("filters.clear", { defaultValue: "Clear" });
889
+ return jsxs(Popover, {
890
+ open,
891
+ onOpenChange: setOpen,
892
+ children: [jsx(PopoverTrigger, {
893
+ asChild: true,
894
+ children: jsx(Button, {
895
+ type: "button",
896
+ variant: "outline",
897
+ size: "sm",
898
+ autoFocus,
899
+ "data-testid": testId,
900
+ "aria-label": ariaLabel,
901
+ className: cn("h-8 justify-between truncate", props.fill ? "w-full" : "max-w-[220px]"),
902
+ children: jsx("span", {
903
+ className: "truncate",
904
+ children: triggerLabel(labelFor, ariaLabel)
905
+ })
906
+ })
907
+ }), jsx(PopoverContent, {
908
+ className: "w-[240px] p-0",
909
+ align: "start",
910
+ children: jsxs(Command, {
911
+ shouldFilter: false,
912
+ children: [searchable && jsx(CommandInput, {
913
+ value: search,
914
+ onValueChange: setSearch,
915
+ placeholder: ariaLabel,
916
+ "data-testid": `${testId}:search`
917
+ }), jsxs(CommandList, { children: [
918
+ listQuery.isLoading && jsxs("div", {
919
+ className: "flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground",
920
+ "data-testid": `${testId}:loading`,
921
+ children: [jsx(IconLoader2, {
922
+ className: "h-4 w-4 animate-spin",
923
+ "aria-hidden": "true"
924
+ }), t("table.loading", { defaultValue: "Loading…" })]
925
+ }),
926
+ !listQuery.isLoading && rows.length === 0 && jsx(CommandEmpty, { children: t("relation.noResults", { defaultValue: "No results." }) }),
927
+ !listQuery.isLoading && arity === "single" && selected.size > 0 && jsxs(CommandItem, {
928
+ "data-testid": `${testId}:clear`,
929
+ value: "__clear__",
930
+ onSelect: () => clear(),
931
+ children: [jsx(IconX, {
932
+ className: "mr-2 h-4 w-4",
933
+ "aria-hidden": "true"
934
+ }), clearLabel]
935
+ }),
936
+ !listQuery.isLoading && rows.map((row) => {
937
+ const id = keyOf(row);
938
+ if (id === void 0) return null;
939
+ const label = rowTitle(row);
940
+ const isSelected = selected.has(id);
941
+ return jsxs(CommandItem, {
942
+ value: `${label} ${id}`,
943
+ "data-testid": `${testId}:option:${id}`,
944
+ onSelect: () => select(id),
945
+ children: [jsx(IconCheck, {
946
+ className: cn("mr-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0"),
947
+ "aria-hidden": "true"
948
+ }), jsx("span", {
949
+ className: "truncate",
950
+ children: label
951
+ })]
952
+ }, id);
953
+ })
954
+ ] })]
955
+ })
956
+ })]
957
+ });
958
+ }
959
+ //#endregion
960
+ //#region src/list-kit/filter-builder/values/scalar-value-input.tsx
961
+ function isDateTime(kind) {
962
+ return kind.kind === "date" && kind.time === true;
963
+ }
964
+ function inputTypeFor(kind) {
965
+ switch (kind.kind) {
966
+ case "number": return "number";
967
+ case "date": return isDateTime(kind) ? "datetime-local" : "date";
968
+ default: return "text";
969
+ }
970
+ }
971
+ function toText(kind, value) {
972
+ if (value === void 0 || value === null || value === "") return "";
973
+ if (!isDateTime(kind)) return String(value);
974
+ const d = new Date(String(value));
975
+ if (Number.isNaN(d.getTime())) return "";
976
+ const pad = (n) => String(n).padStart(2, "0");
977
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
978
+ }
979
+ function parseOne(kind, raw) {
980
+ if (raw === "") return void 0;
981
+ if (kind.kind === "number") {
982
+ const n = Number(raw);
983
+ return Number.isFinite(n) ? n : void 0;
984
+ }
985
+ if (isDateTime(kind)) {
986
+ const d = new Date(raw);
987
+ return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
988
+ }
989
+ return raw;
990
+ }
991
+ function ScalarValueInput(props) {
992
+ const t = useOptionalT();
993
+ const inputType = inputTypeFor(props.kind);
994
+ const ariaLabel = props.fieldLabel ? t("filters.valueFor", {
995
+ defaultValue: `Value for ${props.fieldLabel}`,
996
+ params: { field: props.fieldLabel }
997
+ }) : t("filters.value", { defaultValue: "Value" });
998
+ if (props.arity === "multi") return jsx(ScalarMultiInput, {
999
+ ...props,
1000
+ inputType,
1001
+ ariaLabel
1002
+ });
1003
+ return jsx(ScalarSingleInput, {
1004
+ ...props,
1005
+ inputType,
1006
+ ariaLabel
1007
+ });
1008
+ }
1009
+ function ScalarSingleInput(props) {
1010
+ const { value, onChange, kind, testId, autoFocus, inputType, ariaLabel, debounceMs = 300 } = props;
1011
+ const storedText = toText(kind, value);
1012
+ const [text, setText] = useState(storedText);
1013
+ const lastPushedRef = useRef(storedText);
1014
+ const timerRef = useRef(null);
1015
+ const pendingRawRef = useRef(null);
1016
+ const commitRef = useRef(() => {});
1017
+ useEffect(() => {
1018
+ commitRef.current = (raw) => {
1019
+ const parsed = parseOne(kind, raw);
1020
+ lastPushedRef.current = toText(kind, parsed);
1021
+ pendingRawRef.current = null;
1022
+ onChange(parsed);
1023
+ };
1024
+ });
1025
+ useEffect(() => {
1026
+ if (storedText !== lastPushedRef.current) {
1027
+ setText(storedText);
1028
+ lastPushedRef.current = storedText;
1029
+ if (timerRef.current) {
1030
+ clearTimeout(timerRef.current);
1031
+ timerRef.current = null;
1032
+ }
1033
+ pendingRawRef.current = null;
1034
+ }
1035
+ }, [storedText]);
1036
+ useEffect(() => () => {
1037
+ if (timerRef.current) clearTimeout(timerRef.current);
1038
+ if (pendingRawRef.current !== null) commitRef.current(pendingRawRef.current);
1039
+ }, []);
1040
+ const handleChange = (raw) => {
1041
+ setText(raw);
1042
+ if (timerRef.current) {
1043
+ clearTimeout(timerRef.current);
1044
+ timerRef.current = null;
1045
+ }
1046
+ if (debounceMs <= 0) {
1047
+ pendingRawRef.current = null;
1048
+ commitRef.current(raw);
1049
+ return;
1050
+ }
1051
+ pendingRawRef.current = raw;
1052
+ timerRef.current = setTimeout(() => commitRef.current(raw), debounceMs);
1053
+ };
1054
+ return jsx(Input, {
1055
+ type: inputType,
1056
+ value: text,
1057
+ autoFocus,
1058
+ "aria-label": ariaLabel,
1059
+ placeholder: props.placeholder,
1060
+ "data-testid": testId,
1061
+ onChange: (event) => handleChange(event.target.value),
1062
+ className: cn("h-8", props.fill ? "w-full" : "w-[150px]")
1063
+ });
1064
+ }
1065
+ function ScalarMultiInput(props) {
1066
+ const { value, onChange, kind, testId, autoFocus, inputType, ariaLabel } = props;
1067
+ const chips = Array.isArray(value) ? value.map((v) => String(v)) : [];
1068
+ const [draft, setDraft] = useState("");
1069
+ const addChip = () => {
1070
+ const parsed = parseOne(kind, draft.trim());
1071
+ if (parsed === void 0) return;
1072
+ const text = String(parsed);
1073
+ setDraft("");
1074
+ if (chips.includes(text)) return;
1075
+ onChange([...chips, text]);
1076
+ };
1077
+ const removeChip = (chip) => {
1078
+ const next = chips.filter((c) => c !== chip);
1079
+ onChange(next.length === 0 ? void 0 : next);
1080
+ };
1081
+ return jsxs("div", {
1082
+ className: cn("flex flex-wrap items-center gap-1", props.fill && "w-full"),
1083
+ children: [chips.map((chip) => jsxs(Badge, {
1084
+ variant: "secondary",
1085
+ "data-testid": `${testId}:chip:${chip}`,
1086
+ className: "gap-1 rounded-sm px-1 font-normal",
1087
+ children: [chip, jsx("button", {
1088
+ type: "button",
1089
+ "data-testid": `${testId}:chip:${chip}:remove`,
1090
+ "aria-label": `Remove ${chip}`,
1091
+ onClick: () => removeChip(chip),
1092
+ className: "rounded-sm hover:bg-accent",
1093
+ children: jsx(IconX, {
1094
+ className: "h-3 w-3",
1095
+ "aria-hidden": "true"
1096
+ })
1097
+ })]
1098
+ }, chip)), jsx(Input, {
1099
+ type: inputType,
1100
+ value: draft,
1101
+ autoFocus,
1102
+ "aria-label": ariaLabel,
1103
+ placeholder: props.placeholder,
1104
+ "data-testid": testId,
1105
+ onChange: (event) => setDraft(event.target.value),
1106
+ onKeyDown: (event) => {
1107
+ if (event.key !== "Enter") return;
1108
+ event.preventDefault();
1109
+ addChip();
1110
+ },
1111
+ className: cn("h-8", props.fill ? "min-w-[8rem] flex-1" : "w-[120px]")
1112
+ })]
1113
+ });
1114
+ }
1115
+ //#endregion
1116
+ //#region src/list-kit/filter-builder/values/filter-value-control.tsx
1117
+ function FilterValueControl(props) {
1118
+ const arity = opArity(props.op);
1119
+ if (arity === "none") return null;
1120
+ if (props.options && props.options.length > 0) return jsx(SelectListValueControl, {
1121
+ value: props.value,
1122
+ onChange: props.onChange,
1123
+ testId: props.testId,
1124
+ arity,
1125
+ autoFocus: props.autoFocus,
1126
+ fieldLabel: props.fieldLabel,
1127
+ options: props.options,
1128
+ fill: props.fill
1129
+ });
1130
+ switch (props.kind.kind) {
1131
+ case "enum": return jsx(EnumValueControl, {
1132
+ ...props,
1133
+ enumName: props.kind.enumName,
1134
+ arity
1135
+ });
1136
+ case "enum-values": return jsx(SelectListValueControl, {
1137
+ value: props.value,
1138
+ onChange: props.onChange,
1139
+ testId: props.testId,
1140
+ arity,
1141
+ autoFocus: props.autoFocus,
1142
+ fieldLabel: props.fieldLabel,
1143
+ options: props.kind.values.map((entry) => ({
1144
+ value: entry,
1145
+ label: entry
1146
+ }))
1147
+ });
1148
+ case "relation": return jsx(RelationValueControl, {
1149
+ ...props,
1150
+ to: props.kind.to,
1151
+ ...props.kind.by === void 0 ? {} : { by: props.kind.by },
1152
+ arity
1153
+ });
1154
+ case "boolean": return jsx(BooleanValueControl, {
1155
+ ...props,
1156
+ arity
1157
+ });
1158
+ default: return jsx(ScalarValueInput, {
1159
+ ...props,
1160
+ arity
1161
+ });
1162
+ }
1163
+ }
1164
+ //#endregion
1165
+ //#region src/list-kit/filter-builder/filter-row.tsx
1166
+ const ROW_CLASS = "flex min-w-0 items-center gap-2";
1167
+ const FIELD_CLASS = "h-8 min-w-0 shrink basis-40";
1168
+ const OPERATOR_CLASS = "h-8 min-w-0 shrink basis-[9.5rem]";
1169
+ function FilterRow(props) {
1170
+ const { row, path, available, onPatch, onRemove, testIdBase, debounceMs } = props;
1171
+ const t = useOptionalT();
1172
+ const base = `${testIdBase}:row:${path.join("-")}`;
1173
+ const spec = available.find((s) => s.field === row.field);
1174
+ const removeLabel = t("filters.removeRow", { defaultValue: "Remove filter" });
1175
+ if (!spec) return jsxs("div", {
1176
+ className: ROW_CLASS,
1177
+ children: [
1178
+ jsx(Button, {
1179
+ type: "button",
1180
+ variant: "outline",
1181
+ size: "sm",
1182
+ disabled: true,
1183
+ "data-testid": `${base}:field`,
1184
+ className: cn(FIELD_CLASS, "justify-start truncate opacity-60"),
1185
+ children: row.field
1186
+ }),
1187
+ jsx("div", { className: "flex-1" }),
1188
+ jsx(Button, {
1189
+ type: "button",
1190
+ variant: "ghost",
1191
+ size: "icon",
1192
+ "aria-label": removeLabel,
1193
+ "data-testid": `${base}:remove`,
1194
+ onClick: () => onRemove(path),
1195
+ className: "shrink-0",
1196
+ children: jsx(IconX, {
1197
+ className: "h-4 w-4",
1198
+ "aria-hidden": "true"
1199
+ })
1200
+ })
1201
+ ]
1202
+ });
1203
+ const fieldTestId = `${base}:field`;
1204
+ const opTestId = `${base}:op`;
1205
+ const handleFieldChange = (field) => {
1206
+ const op = available.find((s) => s.field === field)?.operators[0] ?? row.op;
1207
+ onPatch(path, {
1208
+ field,
1209
+ op
1210
+ });
1211
+ };
1212
+ const handleOpChange = (op) => onPatch(path, { op });
1213
+ const handleValueChange = (value) => onPatch(path, { value });
1214
+ const fieldAriaLabel = t("filters.field", { defaultValue: "Field" });
1215
+ const operatorAriaLabel = t("filters.operator", { defaultValue: "Operator" });
1216
+ return jsxs("div", {
1217
+ className: ROW_CLASS,
1218
+ children: [
1219
+ jsxs(Select, {
1220
+ value: row.field,
1221
+ onValueChange: handleFieldChange,
1222
+ children: [jsx(SelectTrigger, {
1223
+ "data-testid": fieldTestId,
1224
+ "aria-label": fieldAriaLabel,
1225
+ className: FIELD_CLASS,
1226
+ children: jsx(SelectValue, {})
1227
+ }), jsx(SelectContent, { children: available.map((s) => jsx(SelectItem, {
1228
+ value: s.field,
1229
+ "data-testid": `${fieldTestId}:option:${s.field}`,
1230
+ children: s.label
1231
+ }, s.field)) })]
1232
+ }),
1233
+ jsxs(Select, {
1234
+ value: row.op,
1235
+ onValueChange: handleOpChange,
1236
+ children: [jsx(SelectTrigger, {
1237
+ "data-testid": opTestId,
1238
+ "aria-label": operatorAriaLabel,
1239
+ className: OPERATOR_CLASS,
1240
+ children: jsx(SelectValue, {})
1241
+ }), jsx(SelectContent, { children: spec.operators.map((op) => jsx(SelectItem, {
1242
+ value: op,
1243
+ "data-testid": `${opTestId}:option:${op}`,
1244
+ children: t(`filters.operators.${op}`, { defaultValue: op })
1245
+ }, op)) })]
1246
+ }),
1247
+ jsx("div", {
1248
+ className: "min-w-0 flex-1",
1249
+ children: jsx(FilterValueControl, {
1250
+ kind: spec.kind,
1251
+ op: row.op,
1252
+ value: row.value,
1253
+ onChange: handleValueChange,
1254
+ testId: `${base}:value`,
1255
+ debounceMs,
1256
+ fieldLabel: spec.label,
1257
+ options: spec.options,
1258
+ fill: true
1259
+ })
1260
+ }),
1261
+ jsx(Button, {
1262
+ type: "button",
1263
+ variant: "ghost",
1264
+ size: "icon",
1265
+ "aria-label": removeLabel,
1266
+ "data-testid": `${base}:remove`,
1267
+ onClick: () => onRemove(path),
1268
+ className: "shrink-0",
1269
+ children: jsx(IconX, {
1270
+ className: "h-4 w-4",
1271
+ "aria-hidden": "true"
1272
+ })
1273
+ })
1274
+ ]
1275
+ });
1276
+ }
1277
+ //#endregion
1278
+ //#region src/list-kit/filter-builder/filter-group-editor.tsx
1279
+ const JOIN_VALUES = ["and", "or"];
1280
+ const GROUP_CLASS = "grid grid-cols-[auto_minmax(0,1fr)] items-start gap-2";
1281
+ const CONNECTOR_CLASS = "pt-1.5 text-right text-sm text-muted-foreground";
1282
+ function FilterGroupEditor(props) {
1283
+ const { group, onChange } = props;
1284
+ const addGroupAt = (targetPath) => onChange(addGroup(group, targetPath));
1285
+ return jsx(GroupEditorInner, {
1286
+ ...props,
1287
+ addGroupAt
1288
+ });
1289
+ }
1290
+ function GroupEditorInner(props) {
1291
+ const { group, path, available, onChange, testIdBase, onRemove, addGroupAt, debounceMs } = props;
1292
+ const t = useOptionalT();
1293
+ const base = `${testIdBase}:group:${path.join("-")}`;
1294
+ const canNestGroup = depthOf(path) < 3;
1295
+ const joinLabel = (join) => t(`filters.${join}`, { defaultValue: join });
1296
+ const handlePatch = (rowPath, patch) => {
1297
+ const index = rowPath[rowPath.length - 1];
1298
+ if (index === void 0) return;
1299
+ onChange(updateRow(group, [index], patch));
1300
+ };
1301
+ const handleRemove = (nodePath) => {
1302
+ const index = nodePath[nodePath.length - 1];
1303
+ if (index === void 0) return;
1304
+ onChange(removeAt(group, [index]));
1305
+ };
1306
+ const handleChildGroupChange = (index, next) => {
1307
+ if (next === group.rows[index]) return;
1308
+ const rows = group.rows.slice();
1309
+ rows[index] = next;
1310
+ onChange({
1311
+ ...group,
1312
+ rows
1313
+ });
1314
+ };
1315
+ const handleAddRow = () => onChange(addRow(group, [], available));
1316
+ const handleAddGroup = () => addGroupAt(path);
1317
+ const handleSetJoin = (join) => onChange(setJoin(group, [], join));
1318
+ const otherJoin = group.join === "and" ? "or" : "and";
1319
+ return jsxs("div", {
1320
+ className: GROUP_CLASS,
1321
+ "data-testid": base,
1322
+ children: [
1323
+ group.rows.map((node, index) => {
1324
+ const childPath = [...path, index];
1325
+ return jsxs(Fragment, { children: [jsxs("div", {
1326
+ className: CONNECTOR_CLASS,
1327
+ children: [
1328
+ index === 0 && t("filters.where", { defaultValue: "Where" }),
1329
+ index === 1 && jsx("button", {
1330
+ type: "button",
1331
+ "data-testid": `${base}:join`,
1332
+ "aria-label": t("filters.toggleJoin", {
1333
+ defaultValue: `Join: ${joinLabel(group.join)} — activate to switch to ${joinLabel(otherJoin)}`,
1334
+ params: {
1335
+ current: joinLabel(group.join),
1336
+ next: joinLabel(otherJoin)
1337
+ }
1338
+ }),
1339
+ "aria-pressed": group.join === "or",
1340
+ onClick: () => handleSetJoin(otherJoin),
1341
+ className: "inline-grid justify-items-end rounded-xs font-medium text-foreground underline underline-offset-2 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
1342
+ children: JOIN_VALUES.map((j) => jsx("span", {
1343
+ className: cn("col-start-1 row-start-1", j !== group.join && "invisible"),
1344
+ children: joinLabel(j)
1345
+ }, j))
1346
+ }),
1347
+ index >= 2 && joinLabel(group.join)
1348
+ ]
1349
+ }), isDraftGroup(node) ? jsx("div", {
1350
+ className: "min-w-0 rounded-md border bg-muted/30 p-2",
1351
+ children: jsx(GroupEditorInner, {
1352
+ group: node,
1353
+ path: childPath,
1354
+ available,
1355
+ onChange: (next) => handleChildGroupChange(index, next),
1356
+ testIdBase,
1357
+ onRemove: handleRemove,
1358
+ addGroupAt,
1359
+ debounceMs
1360
+ })
1361
+ }) : jsx(FilterRow, {
1362
+ row: node,
1363
+ path: childPath,
1364
+ available,
1365
+ onPatch: handlePatch,
1366
+ onRemove: handleRemove,
1367
+ testIdBase,
1368
+ debounceMs
1369
+ })] }, node.rowId);
1370
+ }),
1371
+ jsx("div", { "aria-hidden": "true" }),
1372
+ jsxs("div", {
1373
+ className: "flex items-center gap-2",
1374
+ children: [
1375
+ jsxs(Button, {
1376
+ type: "button",
1377
+ variant: "ghost",
1378
+ size: "sm",
1379
+ "data-testid": `${base}:add-row`,
1380
+ onClick: handleAddRow,
1381
+ children: [jsx(IconPlus, {
1382
+ className: "h-4 w-4",
1383
+ "aria-hidden": "true"
1384
+ }), t("filters.addRow", { defaultValue: "Add filter" })]
1385
+ }),
1386
+ canNestGroup && jsxs(Button, {
1387
+ type: "button",
1388
+ variant: "ghost",
1389
+ size: "sm",
1390
+ "data-testid": `${base}:add-group`,
1391
+ onClick: handleAddGroup,
1392
+ children: [jsx(IconFolderPlus, {
1393
+ className: "h-4 w-4",
1394
+ "aria-hidden": "true"
1395
+ }), t("filters.addGroup", { defaultValue: "Add group" })]
1396
+ }),
1397
+ onRemove && jsxs(Button, {
1398
+ type: "button",
1399
+ variant: "ghost",
1400
+ size: "sm",
1401
+ "data-testid": `${base}:remove`,
1402
+ onClick: () => onRemove(path),
1403
+ children: [jsx(IconX, {
1404
+ className: "h-4 w-4",
1405
+ "aria-hidden": "true"
1406
+ }), t("filters.removeGroup", { defaultValue: "Remove group" })]
1407
+ })
1408
+ ]
1409
+ })
1410
+ ]
1411
+ });
1412
+ }
1413
+ //#endregion
1414
+ //#region src/list-kit/filter-builder/filter-panel.tsx
1415
+ function countGroupRows(group) {
1416
+ let total = 0;
1417
+ for (const row of group.rows) total += "rows" in row ? countGroupRows(row) : 1;
1418
+ return total;
1419
+ }
1420
+ function FilterPanel(props) {
1421
+ const { group, setGroup, available, setPage, testIdBase, open, onOpenChange, focusPath, externalRemoval } = props;
1422
+ const t = useOptionalT();
1423
+ const [draft, setDraft] = useState(() => toDraft(group));
1424
+ const wasOpenRef = useRef(open);
1425
+ const topLevelIdsRef = useRef([]);
1426
+ const panelRef = useRef(null);
1427
+ useEffect(() => {
1428
+ if (open && !wasOpenRef.current) {
1429
+ const seeded = toDraft(group);
1430
+ setDraft(seeded);
1431
+ topLevelIdsRef.current = seeded.rows.map((node) => node.rowId);
1432
+ if (focusPath && focusPath.length > 0) {
1433
+ const targetTestId = `${testIdBase}:row:${focusPath.join("-")}:field`;
1434
+ requestAnimationFrame(() => {
1435
+ panelRef.current?.querySelector(`[data-testid="${targetTestId}"]`)?.focus();
1436
+ });
1437
+ }
1438
+ }
1439
+ wasOpenRef.current = open;
1440
+ }, [open]);
1441
+ useEffect(() => {
1442
+ if (!externalRemoval) return;
1443
+ const { index } = externalRemoval;
1444
+ const rowId = topLevelIdsRef.current[index];
1445
+ topLevelIdsRef.current = topLevelIdsRef.current.filter((_, i) => i !== index);
1446
+ if (rowId === void 0) return;
1447
+ setDraft((current) => {
1448
+ const foundIndex = current.rows.findIndex((node) => node.rowId === rowId);
1449
+ return foundIndex < 0 ? current : removeAt(current, [foundIndex]);
1450
+ });
1451
+ }, [externalRemoval]);
1452
+ const handleApply = () => {
1453
+ setGroup(pruneIncompleteRows(fromDraft(draft)));
1454
+ setPage(1);
1455
+ onOpenChange(false);
1456
+ };
1457
+ const handleReset = () => setDraft(EMPTY_DRAFT());
1458
+ const appliedCount = countGroupRows(group);
1459
+ return jsxs(Popover, {
1460
+ open,
1461
+ onOpenChange,
1462
+ children: [jsx(PopoverTrigger, {
1463
+ asChild: true,
1464
+ children: jsxs(Button, {
1465
+ type: "button",
1466
+ variant: "outline",
1467
+ size: "sm",
1468
+ "data-testid": `${testIdBase}:button`,
1469
+ children: [
1470
+ jsx(IconFilter, {
1471
+ className: "h-4 w-4",
1472
+ "aria-hidden": "true"
1473
+ }),
1474
+ t("filters.title", { defaultValue: "Filters" }),
1475
+ appliedCount > 0 && jsx("span", {
1476
+ "data-testid": `${testIdBase}:count`,
1477
+ className: "ml-1 rounded-full bg-primary px-1.5 text-xs text-primary-foreground",
1478
+ children: appliedCount
1479
+ })
1480
+ ]
1481
+ })
1482
+ }), jsxs(PopoverContent, {
1483
+ ref: panelRef,
1484
+ "data-testid": `${testIdBase}:panel`,
1485
+ align: "start",
1486
+ className: "w-[min(46rem,calc(100vw-2rem))]",
1487
+ children: [jsx(FilterGroupEditor, {
1488
+ group: draft,
1489
+ path: [],
1490
+ available,
1491
+ onChange: setDraft,
1492
+ testIdBase,
1493
+ debounceMs: 0
1494
+ }), jsxs("div", {
1495
+ className: "mt-3 flex justify-end gap-2 border-t pt-3",
1496
+ children: [jsx(Button, {
1497
+ type: "button",
1498
+ variant: "ghost",
1499
+ size: "sm",
1500
+ "data-testid": `${testIdBase}:reset`,
1501
+ onClick: handleReset,
1502
+ children: t("filters.reset", { defaultValue: "Reset" })
1503
+ }), jsx(Button, {
1504
+ type: "button",
1505
+ size: "sm",
1506
+ "data-testid": `${testIdBase}:apply`,
1507
+ onClick: handleApply,
1508
+ children: t("filters.apply", { defaultValue: "Apply" })
1509
+ })]
1510
+ })]
1511
+ })]
1512
+ });
1513
+ }
1514
+ //#endregion
1515
+ //#region src/list-kit/filter-builder/pinned-filter-controls.tsx
1516
+ function isEmptyValue(value) {
1517
+ return value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
1518
+ }
1519
+ function PinnedFilterControls(props) {
1520
+ const { pinnedFields, value, set, testIdBase } = props;
1521
+ return jsx(Fragment$1, { children: pinnedFields.map((spec) => jsx(PinnedControl, {
1522
+ spec,
1523
+ value,
1524
+ set,
1525
+ testIdBase
1526
+ }, spec.field)) });
1527
+ }
1528
+ function PinnedControl(props) {
1529
+ const { spec, value, set, testIdBase } = props;
1530
+ const op = spec.operators[0] ?? "eq";
1531
+ const index = findOwnedIndex(value, spec);
1532
+ const owned = index >= 0 ? value[index] : void 0;
1533
+ const handleChange = (next) => {
1534
+ if (isEmptyValue(next)) {
1535
+ if (index < 0) return;
1536
+ set(value.filter((_, i) => i !== index));
1537
+ return;
1538
+ }
1539
+ const predicate = {
1540
+ field: spec.field,
1541
+ op,
1542
+ value: next
1543
+ };
1544
+ if (index < 0) set([...value, predicate]);
1545
+ else {
1546
+ const nextValue = value.slice();
1547
+ nextValue[index] = predicate;
1548
+ set(nextValue);
1549
+ }
1550
+ };
1551
+ return jsx(FilterValueControl, {
1552
+ kind: spec.kind,
1553
+ op,
1554
+ value: owned?.value,
1555
+ onChange: handleChange,
1556
+ testId: `${testIdBase}:pinned:${spec.field}`,
1557
+ fieldLabel: spec.label,
1558
+ placeholder: spec.label,
1559
+ options: spec.options
1560
+ });
1561
+ }
1562
+ //#endregion
1563
+ //#region src/list-kit/filter-builder/filter-builder.tsx
1564
+ const EMPTY_PINNED_FIELDS = Object.freeze([]);
1565
+ const EMPTY_PINNED_VALUE = Object.freeze([]);
1566
+ function FilterBuilder(props) {
1567
+ const { group, setGroup, available, fixed, pinnedFields = EMPTY_PINNED_FIELDS, pinnedValue = EMPTY_PINNED_VALUE, setPinnedValue, setPage, testIdBase } = props;
1568
+ const [open, setOpen] = useState(false);
1569
+ const [focusPath, setFocusPath] = useState(void 0);
1570
+ const [externalRemoval, setExternalRemoval] = useState(void 0);
1571
+ const handleOpenChange = (next) => {
1572
+ setOpen(next);
1573
+ if (!next) setFocusPath(void 0);
1574
+ };
1575
+ const setGroupResettingPage = (next) => {
1576
+ setPage(1);
1577
+ setGroup(next);
1578
+ };
1579
+ const setPinnedResettingPage = (next) => {
1580
+ setPage(1);
1581
+ setPinnedValue?.(next);
1582
+ };
1583
+ return jsxs(Fragment$1, { children: [
1584
+ jsx(PinnedFilterControls, {
1585
+ pinnedFields,
1586
+ value: pinnedValue,
1587
+ set: setPinnedResettingPage,
1588
+ testIdBase
1589
+ }),
1590
+ available.length > 0 && jsx(FilterPanel, {
1591
+ group,
1592
+ setGroup,
1593
+ available,
1594
+ setPage,
1595
+ testIdBase,
1596
+ open,
1597
+ onOpenChange: handleOpenChange,
1598
+ focusPath,
1599
+ externalRemoval
1600
+ }),
1601
+ jsx(FilterBadges, {
1602
+ group,
1603
+ setGroup: setGroupResettingPage,
1604
+ available,
1605
+ fixed,
1606
+ pinnedFields,
1607
+ pinnedValue,
1608
+ setPinnedValue: setPinnedResettingPage,
1609
+ testIdBase,
1610
+ onOpenAt: (path) => {
1611
+ setFocusPath(path);
1612
+ setOpen(true);
1613
+ },
1614
+ onRemoved: (index) => setExternalRemoval({ index })
1615
+ })
1616
+ ] });
1617
+ }
1618
+ //#endregion
1619
+ export { TableHeader as C, DEFAULT_LANGUAGE_STORAGE_KEY as D, badgeVariants as E, loadLocale as O, TableHead as S, Badge as T, SelectValue as _, FilterBadges as a, TableCaption as b, Select as c, SelectItem as d, SelectLabel as f, SelectTrigger as g, SelectSeparator as h, FilterValueControl as i, saveLocale as k, SelectContent as l, SelectScrollUpButton as m, PinnedFilterControls as n, formatDate as o, SelectScrollDownButton as p, FilterPanel as r, formatNumber as s, FilterBuilder as t, SelectGroup as u, Table as v, TableRow as w, TableCell as x, TableBody as y };
1620
+
1621
+ //# sourceMappingURL=filter-builder-Bhn-dhZV.js.map