@squadbase/vantage 0.6.2-dev1.b332e5a → 0.6.2-dev1.d7c0911

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 (212) hide show
  1. package/dist/add-4GHF2TX2.js +6 -0
  2. package/dist/add-4GHF2TX2.js.map +1 -0
  3. package/dist/{build-TVKFIVFQ.js → build-X5NVCQKT.js} +7 -7
  4. package/dist/{build-TVKFIVFQ.js.map → build-X5NVCQKT.js.map} +1 -1
  5. package/dist/{check-3KHZXZMQ.js → check-ZBIH422T.js} +5 -5
  6. package/dist/{check-3KHZXZMQ.js.map → check-ZBIH422T.js.map} +1 -1
  7. package/dist/chunk-2ICNHUG6.js +1820 -0
  8. package/dist/chunk-2ICNHUG6.js.map +1 -0
  9. package/dist/{chunk-5ZOP4SQR.js → chunk-2N72FKLB.js} +12 -7
  10. package/dist/chunk-2N72FKLB.js.map +1 -0
  11. package/dist/{chunk-P7W4LR3Z.js → chunk-7DZGJQ3P.js} +3 -3
  12. package/dist/{chunk-P7W4LR3Z.js.map → chunk-7DZGJQ3P.js.map} +1 -1
  13. package/dist/{chunk-U2M6HGKQ.js → chunk-AABMKHEU.js} +4 -4
  14. package/dist/{chunk-U2M6HGKQ.js.map → chunk-AABMKHEU.js.map} +1 -1
  15. package/dist/{chunk-QI3XQKHS.js → chunk-GNXCX4WE.js} +10 -3
  16. package/dist/chunk-GNXCX4WE.js.map +1 -0
  17. package/dist/{chunk-PXRX5PEC.js → chunk-PDOBHMUQ.js} +3 -3
  18. package/dist/chunk-PDOBHMUQ.js.map +1 -0
  19. package/dist/{chunk-ZVANIULZ.js → chunk-RNTDBX6S.js} +4 -4
  20. package/dist/{chunk-ZVANIULZ.js.map → chunk-RNTDBX6S.js.map} +1 -1
  21. package/dist/{chunk-A2UUGASH.js → chunk-U736PVHY.js} +2 -2
  22. package/dist/chunk-U736PVHY.js.map +1 -0
  23. package/dist/{chunk-PVTHHV7X.js → chunk-V3LF7BE7.js} +27 -5
  24. package/dist/chunk-V3LF7BE7.js.map +1 -0
  25. package/dist/{chunk-LMUACBMA.js → chunk-WAMZWZJB.js} +53 -8
  26. package/dist/chunk-WAMZWZJB.js.map +1 -0
  27. package/dist/chunk-WRSX2MDK.js +7866 -0
  28. package/dist/chunk-WRSX2MDK.js.map +1 -0
  29. package/dist/{chunk-RPZAALCX.js → chunk-XPQSTZUU.js} +254 -164
  30. package/dist/chunk-XPQSTZUU.js.map +1 -0
  31. package/dist/{chunk-DTDVSFRY.js → chunk-XQUSGIDG.js} +17 -3
  32. package/dist/chunk-XQUSGIDG.js.map +1 -0
  33. package/dist/cli.js +12 -11
  34. package/dist/cli.js.map +1 -1
  35. package/dist/client/index.d.ts +32 -3
  36. package/dist/client/index.js +205 -12
  37. package/dist/client/index.js.map +1 -1
  38. package/dist/components/index.d.ts +1975 -28
  39. package/dist/components/index.js +4 -4
  40. package/dist/{dev-GDKATCC7.js → dev-4XEDTELV.js} +8 -8
  41. package/dist/dev-4XEDTELV.js.map +1 -0
  42. package/dist/doc/index.js +2 -2
  43. package/dist/doc/index.js.map +1 -1
  44. package/dist/{docs-EJJ3BECT.js → docs-JVP4RPKT.js} +3 -3
  45. package/dist/{docs-EJJ3BECT.js.map → docs-JVP4RPKT.js.map} +1 -1
  46. package/dist/{doctor-WQXPXP6K.js → doctor-5E2VEZPK.js} +5 -5
  47. package/dist/{doctor-WQXPXP6K.js.map → doctor-5E2VEZPK.js.map} +1 -1
  48. package/dist/markdown/index.js +1 -1
  49. package/dist/markdown/index.js.map +1 -1
  50. package/dist/{preview-OGMCNQUO.js → preview-T6CLSQVX.js} +3 -3
  51. package/dist/{preview-OGMCNQUO.js.map → preview-T6CLSQVX.js.map} +1 -1
  52. package/dist/query/index.d.ts +0 -11
  53. package/dist/query/index.js +2 -2
  54. package/dist/{routes-MWBWWOJ2.js → routes-3SGAFDMC.js} +4 -4
  55. package/dist/{routes-MWBWWOJ2.js.map → routes-3SGAFDMC.js.map} +1 -1
  56. package/dist/{search-6Y422SV6.js → search-5LC4XPB5.js} +3 -3
  57. package/dist/{search-6Y422SV6.js.map → search-5LC4XPB5.js.map} +1 -1
  58. package/dist/table/index.js +10 -9
  59. package/dist/table/index.js.map +1 -1
  60. package/dist/ui/index.d.ts +144 -11
  61. package/dist/ui/index.js +1339 -87
  62. package/dist/ui/index.js.map +1 -1
  63. package/dist/{upgrade-HCANX4GQ.js → upgrade-JHXZBGQS.js} +6 -6
  64. package/dist/{upgrade-HCANX4GQ.js.map → upgrade-JHXZBGQS.js.map} +1 -1
  65. package/dist/vite/index.d.ts +10 -0
  66. package/dist/vite/index.js +3 -3
  67. package/docs/en/components.md +29 -15
  68. package/docs/en/parts/data-table.md +22 -0
  69. package/docs/en/parts/delta.md +50 -0
  70. package/docs/en/parts/format.md +43 -0
  71. package/docs/en/parts/kpi-card.md +57 -0
  72. package/docs/en/parts/metric-value.md +3 -2
  73. package/docs/en/parts/sparkline.md +1 -1
  74. package/docs/en/parts/trend-indicator.md +17 -1
  75. package/docs/en/ui-and-theming.md +1 -1
  76. package/docs/index.json +366 -76
  77. package/docs/ja/changelog.md +31 -0
  78. package/docs/ja/components.md +57 -101
  79. package/docs/ja/page-types/csv.md +83 -0
  80. package/docs/ja/page-types/doc.md +83 -0
  81. package/docs/ja/page-types/excel.md +82 -0
  82. package/docs/ja/page-types/slide.md +132 -0
  83. package/docs/ja/page-types.md +47 -0
  84. package/docs/ja/parts/area-chart.md +61 -0
  85. package/docs/ja/parts/bar-chart.md +69 -0
  86. package/docs/ja/parts/bar-list.md +35 -0
  87. package/docs/ja/parts/category-bar.md +36 -0
  88. package/docs/ja/parts/data-table.md +22 -0
  89. package/docs/ja/parts/delta.md +50 -0
  90. package/docs/ja/parts/donut-chart.md +47 -0
  91. package/docs/ja/parts/echart.md +3 -0
  92. package/docs/ja/parts/format.md +44 -0
  93. package/docs/ja/parts/kpi-card.md +55 -0
  94. package/docs/ja/parts/line-chart.md +58 -0
  95. package/docs/ja/parts/metric-value.md +3 -2
  96. package/docs/ja/parts/progress-circle.md +34 -0
  97. package/docs/ja/parts/sparkline.md +2 -2
  98. package/docs/ja/parts/tracker.md +32 -0
  99. package/docs/ja/parts/trend-indicator.md +17 -1
  100. package/docs/ja/routing.md +8 -86
  101. package/docs/ja/ui/alert-dialog.md +41 -0
  102. package/docs/ja/ui/avatar.md +31 -0
  103. package/docs/ja/ui/combobox.md +74 -0
  104. package/docs/ja/ui/drawer.md +41 -0
  105. package/docs/ja/ui/field.md +42 -0
  106. package/docs/ja/ui/hover-card.md +21 -0
  107. package/docs/ja/ui/kbd.md +16 -0
  108. package/docs/ja/ui/pagination.md +36 -0
  109. package/docs/ja/ui/radio-group.md +21 -0
  110. package/docs/ja/ui/toast.md +46 -0
  111. package/docs/ja/ui-and-theming.md +14 -7
  112. package/package.json +13 -4
  113. package/registry/ui/accordion.tsx +76 -0
  114. package/registry/ui/alert-dialog.tsx +189 -0
  115. package/registry/ui/alert.tsx +80 -0
  116. package/registry/ui/app-shell.tsx +287 -0
  117. package/registry/ui/area-chart.tsx +3639 -0
  118. package/registry/ui/avatar.tsx +110 -0
  119. package/registry/ui/badge.tsx +56 -0
  120. package/registry/ui/bar-chart.tsx +3636 -0
  121. package/registry/ui/bar-list.tsx +3680 -0
  122. package/registry/ui/breadcrumb.tsx +128 -0
  123. package/registry/ui/button.tsx +62 -0
  124. package/registry/ui/calendar.tsx +223 -0
  125. package/registry/ui/card.tsx +107 -0
  126. package/registry/ui/category-bar.tsx +1255 -0
  127. package/registry/ui/chart-frame.tsx +1358 -0
  128. package/registry/ui/checkbox.tsx +30 -0
  129. package/registry/ui/collapsible.tsx +23 -0
  130. package/registry/ui/combobox.tsx +310 -0
  131. package/registry/ui/command.tsx +198 -0
  132. package/registry/ui/dashboard-card.tsx +217 -0
  133. package/registry/ui/data-table.tsx +693 -146
  134. package/registry/ui/date-range-picker.tsx +390 -0
  135. package/registry/ui/delta.tsx +188 -0
  136. package/registry/ui/dialog.tsx +161 -0
  137. package/registry/ui/donut-chart.tsx +3688 -0
  138. package/registry/ui/drawer.tsx +229 -0
  139. package/registry/ui/dropdown-menu.tsx +274 -0
  140. package/registry/ui/echart.tsx +890 -0
  141. package/registry/ui/field.tsx +240 -0
  142. package/registry/ui/filter-bar.tsx +392 -0
  143. package/registry/ui/funnel-steps.tsx +3541 -0
  144. package/registry/ui/heatmap.tsx +1752 -0
  145. package/registry/ui/hover-card.tsx +52 -0
  146. package/registry/ui/input-group.tsx +160 -0
  147. package/registry/ui/input.tsx +24 -0
  148. package/registry/ui/kbd.tsx +30 -0
  149. package/registry/ui/kpi-card.tsx +677 -0
  150. package/registry/ui/label.tsx +22 -0
  151. package/registry/ui/legend.tsx +3500 -0
  152. package/registry/ui/line-chart.tsx +3637 -0
  153. package/registry/ui/metric-value.tsx +62 -0
  154. package/registry/ui/multi-select.tsx +232 -0
  155. package/registry/ui/page-shell.tsx +282 -0
  156. package/registry/ui/pagination.tsx +140 -0
  157. package/registry/ui/placeholder.tsx +36 -0
  158. package/registry/ui/popover.tsx +92 -0
  159. package/registry/ui/progress-circle.tsx +867 -0
  160. package/registry/ui/progress.tsx +85 -0
  161. package/registry/ui/radio-group.tsx +39 -0
  162. package/registry/ui/sample-data.tsx +86 -0
  163. package/registry/ui/scatter-chart.tsx +4150 -0
  164. package/registry/ui/scroll-area.tsx +56 -0
  165. package/registry/ui/searchable-select.tsx +214 -0
  166. package/registry/ui/section-header.tsx +43 -0
  167. package/registry/ui/segmented-control.tsx +76 -0
  168. package/registry/ui/select.tsx +255 -0
  169. package/registry/ui/separator.tsx +27 -0
  170. package/registry/ui/sheet.tsx +139 -0
  171. package/registry/ui/sidebar.tsx +725 -0
  172. package/registry/ui/skeleton.tsx +17 -0
  173. package/registry/ui/slider.tsx +56 -0
  174. package/registry/ui/sparkline.tsx +230 -0
  175. package/registry/ui/spinner.tsx +14 -0
  176. package/registry/ui/states.tsx +90 -0
  177. package/registry/ui/status-badge.tsx +121 -0
  178. package/registry/ui/switch.tsx +34 -0
  179. package/registry/ui/table.tsx +118 -0
  180. package/registry/ui/tabs.tsx +84 -0
  181. package/registry/ui/textarea.tsx +22 -0
  182. package/registry/ui/toast.tsx +250 -0
  183. package/registry/ui/toggle-group.tsx +91 -0
  184. package/registry/ui/toggle.tsx +47 -0
  185. package/registry/ui/tooltip.tsx +68 -0
  186. package/registry/ui/tracker.tsx +843 -0
  187. package/registry/ui/trend-indicator.tsx +74 -0
  188. package/registry/ui/updated-at.tsx +122 -0
  189. package/registry/ui/waterfall-chart.tsx +4186 -0
  190. package/skills/vantage-add-feature/SKILL.md +9 -7
  191. package/skills/vantage-pitfalls/SKILL.md +5 -1
  192. package/templates/AGENTS.md +22 -1
  193. package/templates/agents/csv.md +2 -1
  194. package/templates/agents/doc.md +4 -3
  195. package/templates/agents/page.md +20 -6
  196. package/templates/agents/xlsx.md +2 -1
  197. package/theme.css +237 -136
  198. package/dist/add-DY4AXANK.js +0 -6
  199. package/dist/add-DY4AXANK.js.map +0 -1
  200. package/dist/chunk-5ZOP4SQR.js.map +0 -1
  201. package/dist/chunk-A2UUGASH.js.map +0 -1
  202. package/dist/chunk-DTDVSFRY.js.map +0 -1
  203. package/dist/chunk-LMUACBMA.js.map +0 -1
  204. package/dist/chunk-NKFBFDRZ.js +0 -2241
  205. package/dist/chunk-NKFBFDRZ.js.map +0 -1
  206. package/dist/chunk-PVTHHV7X.js.map +0 -1
  207. package/dist/chunk-PXRX5PEC.js.map +0 -1
  208. package/dist/chunk-QI3XQKHS.js.map +0 -1
  209. package/dist/chunk-RPZAALCX.js.map +0 -1
  210. package/dist/chunk-VA776YE5.js +0 -316
  211. package/dist/chunk-VA776YE5.js.map +0 -1
  212. package/dist/dev-GDKATCC7.js.map +0 -1
@@ -0,0 +1,240 @@
1
+ // Copied by `vantage add ui field`. This file is yours to edit.
2
+ // Generated from @squadbase/squadkit registry/ui/field.tsx by `npm run registry` (in Vantage, edit packages/squadkit, not registry/).
3
+ "use client"
4
+
5
+ import { useMemo } from "react"
6
+ import { cva, type VariantProps } from "class-variance-authority"
7
+ import { cn } from "@squadbase/vantage/ui"
8
+
9
+ import { Label } from "@squadbase/vantage/ui"
10
+ import { Separator } from "@squadbase/vantage/ui"
11
+
12
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
13
+ return (
14
+ <fieldset
15
+ data-slot="field-set"
16
+ className={cn(
17
+ "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ function FieldLegend({
26
+ className,
27
+ variant = "legend",
28
+ ...props
29
+ }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
30
+ return (
31
+ <legend
32
+ data-slot="field-legend"
33
+ data-variant={variant}
34
+ className={cn(
35
+ "mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
44
+ return (
45
+ <div
46
+ data-slot="field-group"
47
+ className={cn(
48
+ "group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ )
54
+ }
55
+
56
+ const fieldVariants = cva(
57
+ "group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
58
+ {
59
+ variants: {
60
+ orientation: {
61
+ vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
62
+ horizontal:
63
+ "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
64
+ responsive:
65
+ "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
66
+ },
67
+ },
68
+ defaultVariants: {
69
+ orientation: "vertical",
70
+ },
71
+ }
72
+ )
73
+
74
+ function Field({
75
+ className,
76
+ orientation = "vertical",
77
+ ...props
78
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
79
+ return (
80
+ <div
81
+ role="group"
82
+ data-slot="field"
83
+ data-orientation={orientation}
84
+ className={cn(fieldVariants({ orientation }), className)}
85
+ {...props}
86
+ />
87
+ )
88
+ }
89
+
90
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
91
+ return (
92
+ <div
93
+ data-slot="field-content"
94
+ className={cn(
95
+ "group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ function FieldLabel({
104
+ className,
105
+ ...props
106
+ }: React.ComponentProps<typeof Label>) {
107
+ return (
108
+ <Label
109
+ data-slot="field-label"
110
+ className={cn(
111
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted/50 has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:ring-3 has-[>[data-slot=field]]:has-[:focus-visible]:ring-ring/50 *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
112
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
113
+ className
114
+ )}
115
+ {...props}
116
+ />
117
+ )
118
+ }
119
+
120
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
121
+ return (
122
+ <div
123
+ data-slot="field-label"
124
+ className={cn(
125
+ "flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
126
+ className
127
+ )}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
134
+ return (
135
+ <p
136
+ data-slot="field-description"
137
+ className={cn(
138
+ "text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
139
+ "last:mt-0 nth-last-2:-mt-1",
140
+ "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
141
+ className
142
+ )}
143
+ {...props}
144
+ />
145
+ )
146
+ }
147
+
148
+ function FieldSeparator({
149
+ children,
150
+ className,
151
+ ...props
152
+ }: React.ComponentProps<"div"> & {
153
+ children?: React.ReactNode
154
+ }) {
155
+ return (
156
+ <div
157
+ data-slot="field-separator"
158
+ data-content={!!children}
159
+ className={cn(
160
+ "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
161
+ className
162
+ )}
163
+ {...props}
164
+ >
165
+ <Separator className="absolute inset-0 top-1/2" />
166
+ {children && (
167
+ <span
168
+ className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
169
+ data-slot="field-separator-content"
170
+ >
171
+ {children}
172
+ </span>
173
+ )}
174
+ </div>
175
+ )
176
+ }
177
+
178
+ function FieldError({
179
+ className,
180
+ children,
181
+ errors,
182
+ ...props
183
+ }: React.ComponentProps<"div"> & {
184
+ errors?: Array<{ message?: string } | undefined>
185
+ }) {
186
+ const content = useMemo(() => {
187
+ if (children) {
188
+ return children
189
+ }
190
+
191
+ if (!errors?.length) {
192
+ return null
193
+ }
194
+
195
+ const uniqueErrors = [
196
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
197
+ ]
198
+
199
+ if (uniqueErrors?.length == 1) {
200
+ return uniqueErrors[0]?.message
201
+ }
202
+
203
+ return (
204
+ <ul className="ml-4 flex list-disc flex-col gap-1">
205
+ {uniqueErrors.map(
206
+ (error, index) =>
207
+ error?.message && <li key={index}>{error.message}</li>
208
+ )}
209
+ </ul>
210
+ )
211
+ }, [children, errors])
212
+
213
+ if (!content) {
214
+ return null
215
+ }
216
+
217
+ return (
218
+ <div
219
+ role="alert"
220
+ data-slot="field-error"
221
+ className={cn("text-sm font-normal text-destructive", className)}
222
+ {...props}
223
+ >
224
+ {content}
225
+ </div>
226
+ )
227
+ }
228
+
229
+ export {
230
+ Field,
231
+ FieldLabel,
232
+ FieldDescription,
233
+ FieldError,
234
+ FieldGroup,
235
+ FieldLegend,
236
+ FieldSeparator,
237
+ FieldSet,
238
+ FieldContent,
239
+ FieldTitle,
240
+ }
@@ -0,0 +1,392 @@
1
+ // Copied by `vantage add ui filter-bar`. This file is yours to edit.
2
+ // Generated from @squadbase/squadkit registry/components/filter-bar.tsx by `npm run registry` (in Vantage, edit packages/squadkit, not registry/).
3
+ "use client"
4
+
5
+ /**
6
+ * FilterBar — a compound filter row. `<FilterBar>` holds the (always
7
+ * controlled) filter values on context; the select, multi-select, active-chip
8
+ * and clear parts read and update them. `FilterBarPreset` builds the whole row
9
+ * from a `FilterConfig[]`.
10
+ */
11
+ import {
12
+ createContext,
13
+ forwardRef,
14
+ useContext,
15
+ type HTMLAttributes,
16
+ type ReactNode,
17
+ } from "react"
18
+ import { ChevronDown, X } from "lucide-react"
19
+
20
+ import { cn } from "@squadbase/vantage/ui"
21
+ import { Badge } from "@squadbase/vantage/ui"
22
+ import { Button } from "@squadbase/vantage/ui"
23
+ import { Checkbox } from "@squadbase/vantage/ui"
24
+ import { Popover, PopoverContent, PopoverTrigger } from "@squadbase/vantage/ui"
25
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@squadbase/vantage/ui"
26
+ import type { FilterConfig, FilterOption, FilterValues } from "@squadbase/vantage/components"
27
+
28
+ /* --------------------------------------------------------------- context */
29
+
30
+ interface FilterBarContextValue {
31
+ /** Current filter values (controlled from outside) */
32
+ value: FilterValues
33
+ /** Notify the parent of a value change */
34
+ onChange: (next: FilterValues) => void
35
+ /** Optional clear callback — undefined means no clear button */
36
+ onClear?: () => void
37
+ }
38
+
39
+ const FilterBarContext = createContext<FilterBarContextValue | null>(null)
40
+
41
+ function useFilterBar(): FilterBarContextValue {
42
+ const ctx = useContext(FilterBarContext)
43
+ if (!ctx) throw new Error("useFilterBar must be used within <FilterBar>")
44
+ return ctx
45
+ }
46
+
47
+ /* ------------------------------------------------------------------ root */
48
+
49
+ export interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
50
+ value: FilterValues
51
+ onChange: (values: FilterValues) => void
52
+ onClear?: () => void
53
+ className?: string
54
+ children?: ReactNode
55
+ }
56
+
57
+ export const FilterBar = forwardRef<HTMLDivElement, FilterBarProps>(
58
+ ({ value, onChange, onClear, className, children, ...props }, ref) => (
59
+ <FilterBarContext.Provider value={{ value, onChange, onClear }}>
60
+ <div
61
+ ref={ref}
62
+ role="search"
63
+ aria-label="フィルター"
64
+ data-slot="filter-bar"
65
+ className={cn("flex flex-wrap items-center gap-2", className)}
66
+ {...props}
67
+ >
68
+ {children}
69
+ </div>
70
+ </FilterBarContext.Provider>
71
+ ),
72
+ )
73
+ FilterBar.displayName = "FilterBar"
74
+
75
+ /* ---------------------------------------------------------------- select */
76
+
77
+ export interface FilterBarSelectProps {
78
+ filterKey: string
79
+ label: string
80
+ options: FilterOption[]
81
+ placeholder?: string
82
+ /**
83
+ * 渡すと、選択肢の先頭に「絞り込まない」項目をこの文言で足す(例: `"すべて"`)。選ぶとこのキーを値から外す(`undefined`)ので、
84
+ * アクティブチップにも出ない。絞り込んでいないときのトリガーは `placeholder`(既定は `label`)のまま。既定は足さない。
85
+ * @example allLabel="すべて"
86
+ */
87
+ allLabel?: string
88
+ className?: string
89
+ }
90
+
91
+ /** 「すべて」項目の値。実際の選択肢の値と衝突しない内部用の印で、`onChange` には渡らない。 */
92
+ const ALL_VALUE = "__squadkit_all__"
93
+
94
+ export function FilterBarSelect({
95
+ filterKey,
96
+ label,
97
+ options,
98
+ placeholder,
99
+ allLabel,
100
+ className,
101
+ }: FilterBarSelectProps) {
102
+ const { value, onChange } = useFilterBar()
103
+ const hasAll = allLabel !== undefined
104
+ const items = hasAll ? [{ value: ALL_VALUE, label: allLabel }, ...options] : options
105
+ const current = value[filterKey] as string | undefined
106
+
107
+ // Base UI reports `null` when the selection is cleared — drop the key so
108
+ // "no filter" reads as absent rather than as an empty string. "すべて" does the same.
109
+ const handleChange = (selected: string | null) =>
110
+ onChange({ ...value, [filterKey]: selected === null || selected === ALL_VALUE ? undefined : selected })
111
+
112
+ return (
113
+ <Select
114
+ // FilterOption is already `{ value, label }` — Base UI uses it to show the
115
+ // label (not the raw value) in the trigger.
116
+ items={items}
117
+ value={current ?? ""}
118
+ onValueChange={handleChange}
119
+ >
120
+ <SelectTrigger size="sm" aria-label={label} data-slot="filter-bar-select" className={className}>
121
+ <SelectValue placeholder={placeholder ?? label} />
122
+ </SelectTrigger>
123
+ <SelectContent>
124
+ {hasAll && <SelectItem value={ALL_VALUE}>{allLabel}</SelectItem>}
125
+ {options.map((option) => (
126
+ <SelectItem key={option.value} value={option.value} disabled={option.disabled}>
127
+ {option.label}
128
+ </SelectItem>
129
+ ))}
130
+ </SelectContent>
131
+ </Select>
132
+ )
133
+ }
134
+ FilterBarSelect.displayName = "FilterBarSelect"
135
+
136
+ /* ---------------------------------------------------------- multi-select */
137
+
138
+ export interface FilterBarMultiSelectProps {
139
+ filterKey: string
140
+ label: string
141
+ options: FilterOption[]
142
+ placeholder?: string
143
+ className?: string
144
+ }
145
+
146
+ export function FilterBarMultiSelect({
147
+ filterKey,
148
+ label,
149
+ options,
150
+ placeholder,
151
+ className,
152
+ }: FilterBarMultiSelectProps) {
153
+ const { value, onChange } = useFilterBar()
154
+ const selected = (value[filterKey] as string[] | undefined) ?? []
155
+
156
+ const handleToggle = (option: string) => {
157
+ const next = selected.includes(option)
158
+ ? selected.filter((v) => v !== option)
159
+ : [...selected, option]
160
+ // Drop the key entirely when nothing is left, so "no filter" reads as absent.
161
+ onChange({ ...value, [filterKey]: next.length > 0 ? next : undefined })
162
+ }
163
+
164
+ return (
165
+ <Popover>
166
+ <PopoverTrigger
167
+ render={<Button variant="outline" size="sm" className={cn("h-8 gap-1", className)} />}
168
+ aria-label={`${placeholder ?? label}${selected.length > 0 ? ` (${selected.length}件選択中)` : ""}`}
169
+ data-slot="filter-bar-multi-select"
170
+ >
171
+ {placeholder ?? label}
172
+ {selected.length > 0 && (
173
+ <Badge variant="secondary" className="ml-1 px-1.5 py-0 text-xs">
174
+ {selected.length}
175
+ </Badge>
176
+ )}
177
+ <ChevronDown className="size-3.5 opacity-50" aria-hidden="true" />
178
+ </PopoverTrigger>
179
+ <PopoverContent align="start" className="w-56 p-2">
180
+ <div className="flex flex-col gap-1" role="group" aria-label={label}>
181
+ {options.map((option) => (
182
+ <label
183
+ key={option.value}
184
+ className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
185
+ >
186
+ <Checkbox
187
+ checked={selected.includes(option.value)}
188
+ disabled={option.disabled}
189
+ onCheckedChange={() => handleToggle(option.value)}
190
+ />
191
+ {option.label}
192
+ </label>
193
+ ))}
194
+ </div>
195
+ </PopoverContent>
196
+ </Popover>
197
+ )
198
+ }
199
+ FilterBarMultiSelect.displayName = "FilterBarMultiSelect"
200
+
201
+ /* ---------------------------------------------------------- active chips */
202
+
203
+ export interface FilterBarActiveChipsProps {
204
+ /** Filter configs used to resolve option labels. Without them, raw values show. */
205
+ filters?: FilterConfig[]
206
+ className?: string
207
+ }
208
+
209
+ interface ActiveChip {
210
+ key: string
211
+ filterLabel: string
212
+ optionLabel: string
213
+ optionValue?: string
214
+ isMulti: boolean
215
+ }
216
+
217
+ export function FilterBarActiveChips({ filters, className }: FilterBarActiveChipsProps) {
218
+ const { value, onChange } = useFilterBar()
219
+
220
+ const getActiveChips = (): ActiveChip[] => {
221
+ const chips: ActiveChip[] = []
222
+
223
+ const pushRaw = (key: string, v: string | string[]) => {
224
+ if (typeof v === "string") {
225
+ chips.push({ key, filterLabel: key, optionLabel: v, isMulti: false })
226
+ } else {
227
+ for (const val of v) {
228
+ chips.push({ key, filterLabel: key, optionLabel: val, optionValue: val, isMulti: true })
229
+ }
230
+ }
231
+ }
232
+
233
+ for (const [key, v] of Object.entries(value)) {
234
+ if (v === undefined || v === "") continue
235
+
236
+ const filter = filters?.find((f) => f.key === key)
237
+ if (!filter) {
238
+ pushRaw(key, v)
239
+ continue
240
+ }
241
+
242
+ if (filter.type === "select" && typeof v === "string") {
243
+ const option = filter.options?.find((o) => o.value === v)
244
+ chips.push({
245
+ key,
246
+ filterLabel: filter.label,
247
+ optionLabel: option?.label ?? v,
248
+ isMulti: false,
249
+ })
250
+ } else if (filter.type === "multi-select" && Array.isArray(v)) {
251
+ for (const val of v) {
252
+ const option = filter.options?.find((o) => o.value === val)
253
+ chips.push({
254
+ key,
255
+ filterLabel: filter.label,
256
+ optionLabel: option?.label ?? val,
257
+ optionValue: val,
258
+ isMulti: true,
259
+ })
260
+ }
261
+ }
262
+ }
263
+
264
+ return chips
265
+ }
266
+
267
+ const handleRemoveFilter = (key: string, optionValue?: string) => {
268
+ if (optionValue !== undefined) {
269
+ const current = (value[key] as string[] | undefined) ?? []
270
+ const next = current.filter((v) => v !== optionValue)
271
+ onChange({ ...value, [key]: next.length > 0 ? next : undefined })
272
+ } else {
273
+ onChange({ ...value, [key]: undefined })
274
+ }
275
+ }
276
+
277
+ const activeChips = getActiveChips()
278
+
279
+ if (activeChips.length === 0) return null
280
+
281
+ return (
282
+ <>
283
+ {activeChips.map((chip) => (
284
+ <Badge
285
+ key={`${chip.key}-${chip.optionValue ?? "single"}`}
286
+ variant="secondary"
287
+ className={cn("gap-1 pr-1", className)}
288
+ data-slot="filter-bar-chip"
289
+ >
290
+ <span className="text-xs text-muted-foreground">{chip.filterLabel}:</span>
291
+ {chip.optionLabel}
292
+ <button
293
+ type="button"
294
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted-foreground/20"
295
+ onClick={() => handleRemoveFilter(chip.key, chip.optionValue)}
296
+ aria-label={`${chip.filterLabel}: ${chip.optionLabel} を削除`}
297
+ >
298
+ <X className="size-3" aria-hidden="true" />
299
+ </button>
300
+ </Badge>
301
+ ))}
302
+ </>
303
+ )
304
+ }
305
+ FilterBarActiveChips.displayName = "FilterBarActiveChips"
306
+
307
+ /* ----------------------------------------------------------- clear button */
308
+
309
+ export interface FilterBarClearButtonProps {
310
+ /** Override the button label. Defaults to "クリア". */
311
+ label?: string
312
+ className?: string
313
+ }
314
+
315
+ export function FilterBarClearButton({ label = "クリア", className }: FilterBarClearButtonProps) {
316
+ const { value, onClear } = useFilterBar()
317
+ const hasActiveFilters = Object.values(value).some((v) =>
318
+ Array.isArray(v) ? v.length > 0 : v !== undefined && v !== "",
319
+ )
320
+ if (!hasActiveFilters || !onClear) return null
321
+
322
+ return (
323
+ <Button
324
+ variant="ghost"
325
+ size="sm"
326
+ className={cn("h-7 px-2 text-xs", className)}
327
+ onClick={onClear}
328
+ aria-label="すべてのフィルターをクリア"
329
+ data-slot="filter-bar-clear-button"
330
+ >
331
+ {label}
332
+ </Button>
333
+ )
334
+ }
335
+ FilterBarClearButton.displayName = "FilterBarClearButton"
336
+
337
+ /* ------------------------------------------------------------------ preset */
338
+
339
+ export interface FilterBarPresetProps {
340
+ filters: FilterConfig[]
341
+ value: FilterValues
342
+ onChange: (values: FilterValues) => void
343
+ onClear?: () => void
344
+ className?: string
345
+ }
346
+
347
+ /** Not given yet (a draft fills props in steps): render empty, keep the same value between renders. */
348
+ const NO_FILTERS: never[] = []
349
+ const NO_FILTER_VALUES: FilterValues = {}
350
+
351
+ /** The whole row built from a `FilterConfig[]`. */
352
+ export function FilterBarPreset({
353
+ filters = NO_FILTERS,
354
+ value = NO_FILTER_VALUES,
355
+ onChange,
356
+ onClear,
357
+ className,
358
+ }: FilterBarPresetProps) {
359
+ return (
360
+ <FilterBar value={value} onChange={onChange} onClear={onClear} className={className}>
361
+ {filters.map((filter) => {
362
+ if (filter.type === "select") {
363
+ return (
364
+ <FilterBarSelect
365
+ key={filter.key}
366
+ filterKey={filter.key}
367
+ label={filter.label}
368
+ placeholder={filter.placeholder}
369
+ allLabel={filter.allLabel}
370
+ options={filter.options ?? []}
371
+ />
372
+ )
373
+ }
374
+ if (filter.type === "multi-select") {
375
+ return (
376
+ <FilterBarMultiSelect
377
+ key={filter.key}
378
+ filterKey={filter.key}
379
+ label={filter.label}
380
+ placeholder={filter.placeholder}
381
+ options={filter.options ?? []}
382
+ />
383
+ )
384
+ }
385
+ return null
386
+ })}
387
+ <FilterBarActiveChips filters={filters} />
388
+ {onClear && <FilterBarClearButton />}
389
+ </FilterBar>
390
+ )
391
+ }
392
+ FilterBarPreset.displayName = "FilterBarPreset"