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