@squadbase/vantage 0.6.2-dev1.a7c4a3e → 0.6.2-dev1.ac99e02

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 (186) hide show
  1. package/dist/add-4GHF2TX2.js +6 -0
  2. package/dist/add-4GHF2TX2.js.map +1 -0
  3. package/dist/{build-JB4VOMUI.js → build-MFWRPYKW.js} +9 -9
  4. package/dist/{build-JB4VOMUI.js.map → build-MFWRPYKW.js.map} +1 -1
  5. package/dist/{check-ZJYK6RLG.js → check-ZBIH422T.js} +7 -7
  6. package/dist/{check-ZJYK6RLG.js.map → check-ZBIH422T.js.map} +1 -1
  7. package/dist/{chunk-BCE22JSM.js → chunk-7DZGJQ3P.js} +33 -14
  8. package/dist/chunk-7DZGJQ3P.js.map +1 -0
  9. package/dist/{chunk-U2M6HGKQ.js → chunk-AABMKHEU.js} +4 -4
  10. package/dist/{chunk-U2M6HGKQ.js.map → chunk-AABMKHEU.js.map} +1 -1
  11. package/dist/{chunk-MP3QSOXL.js → chunk-AG6RZ2RR.js} +992 -16
  12. package/dist/chunk-AG6RZ2RR.js.map +1 -0
  13. package/dist/{chunk-KLEIVDUV.js → chunk-CEXAQQ5W.js} +150 -11
  14. package/dist/chunk-CEXAQQ5W.js.map +1 -0
  15. package/dist/{chunk-QI3XQKHS.js → chunk-GNXCX4WE.js} +10 -3
  16. package/dist/chunk-GNXCX4WE.js.map +1 -0
  17. package/dist/chunk-IBGOCWJB.js +1716 -0
  18. package/dist/chunk-IBGOCWJB.js.map +1 -0
  19. package/dist/{chunk-JGRMRXXZ.js → chunk-JKLLFVEU.js} +3 -3
  20. package/dist/{chunk-JGRMRXXZ.js.map → chunk-JKLLFVEU.js.map} +1 -1
  21. package/dist/{chunk-RPZAALCX.js → chunk-OB6UBJXF.js} +153 -153
  22. package/dist/{chunk-RPZAALCX.js.map → chunk-OB6UBJXF.js.map} +1 -1
  23. package/dist/{chunk-EP6JHXSR.js → chunk-QEWCYW3I.js} +44 -9
  24. package/dist/chunk-QEWCYW3I.js.map +1 -0
  25. package/dist/{chunk-ZVANIULZ.js → chunk-RNTDBX6S.js} +4 -4
  26. package/dist/{chunk-ZVANIULZ.js.map → chunk-RNTDBX6S.js.map} +1 -1
  27. package/dist/{chunk-PVTHHV7X.js → chunk-V3LF7BE7.js} +27 -5
  28. package/dist/chunk-V3LF7BE7.js.map +1 -0
  29. package/dist/{chunk-DTDVSFRY.js → chunk-XQUSGIDG.js} +17 -3
  30. package/dist/chunk-XQUSGIDG.js.map +1 -0
  31. package/dist/cli.js +12 -11
  32. package/dist/cli.js.map +1 -1
  33. package/dist/client/index.d.ts +32 -3
  34. package/dist/client/index.js +205 -12
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/components/index.d.ts +216 -1
  37. package/dist/components/index.js +2 -2
  38. package/dist/{dev-FCEC3UKQ.js → dev-DCTOHQ6W.js} +10 -10
  39. package/dist/dev-DCTOHQ6W.js.map +1 -0
  40. package/dist/{docs-EJJ3BECT.js → docs-JVP4RPKT.js} +3 -3
  41. package/dist/{docs-EJJ3BECT.js.map → docs-JVP4RPKT.js.map} +1 -1
  42. package/dist/{doctor-WHZXPERA.js → doctor-5E2VEZPK.js} +7 -7
  43. package/dist/{doctor-WHZXPERA.js.map → doctor-5E2VEZPK.js.map} +1 -1
  44. package/dist/{preview-OGMCNQUO.js → preview-T6CLSQVX.js} +3 -3
  45. package/dist/{preview-OGMCNQUO.js.map → preview-T6CLSQVX.js.map} +1 -1
  46. package/dist/query/index.d.ts +0 -11
  47. package/dist/query/index.js +2 -2
  48. package/dist/{routes-GTFO5PUF.js → routes-3SGAFDMC.js} +6 -6
  49. package/dist/{routes-GTFO5PUF.js.map → routes-3SGAFDMC.js.map} +1 -1
  50. package/dist/{search-6Y422SV6.js → search-5LC4XPB5.js} +3 -3
  51. package/dist/{search-6Y422SV6.js.map → search-5LC4XPB5.js.map} +1 -1
  52. package/dist/slide/index.js +47 -10
  53. package/dist/slide/index.js.map +1 -1
  54. package/dist/table/index.js +8 -7
  55. package/dist/table/index.js.map +1 -1
  56. package/dist/ui/index.d.ts +140 -10
  57. package/dist/ui/index.js +1405 -63
  58. package/dist/ui/index.js.map +1 -1
  59. package/dist/{upgrade-AL4KZZR4.js → upgrade-DBBE6GIR.js} +7 -7
  60. package/dist/{upgrade-AL4KZZR4.js.map → upgrade-DBBE6GIR.js.map} +1 -1
  61. package/dist/vite/index.d.ts +10 -0
  62. package/dist/vite/index.js +4 -4
  63. package/docs/index.json +291 -61
  64. package/docs/ja/changelog.md +26 -0
  65. package/docs/ja/components.md +33 -89
  66. package/docs/ja/page-types/csv.md +83 -0
  67. package/docs/ja/page-types/doc.md +83 -0
  68. package/docs/ja/page-types/excel.md +82 -0
  69. package/docs/ja/page-types/slide.md +132 -0
  70. package/docs/ja/page-types.md +47 -0
  71. package/docs/ja/parts/area-chart.md +61 -0
  72. package/docs/ja/parts/bar-chart.md +69 -0
  73. package/docs/ja/parts/bar-list.md +35 -0
  74. package/docs/ja/parts/category-bar.md +36 -0
  75. package/docs/ja/parts/donut-chart.md +47 -0
  76. package/docs/ja/parts/echart.md +3 -0
  77. package/docs/ja/parts/line-chart.md +58 -0
  78. package/docs/ja/parts/progress-circle.md +34 -0
  79. package/docs/ja/parts/sparkline.md +1 -1
  80. package/docs/ja/parts/tracker.md +32 -0
  81. package/docs/ja/routing.md +8 -86
  82. package/docs/ja/ui/alert-dialog.md +41 -0
  83. package/docs/ja/ui/avatar.md +31 -0
  84. package/docs/ja/ui/combobox.md +74 -0
  85. package/docs/ja/ui/drawer.md +41 -0
  86. package/docs/ja/ui/field.md +42 -0
  87. package/docs/ja/ui/hover-card.md +21 -0
  88. package/docs/ja/ui/kbd.md +16 -0
  89. package/docs/ja/ui/pagination.md +36 -0
  90. package/docs/ja/ui/radio-group.md +21 -0
  91. package/docs/ja/ui/toast.md +46 -0
  92. package/docs/ja/ui-and-theming.md +14 -7
  93. package/package.json +25 -13
  94. package/registry/ui/accordion.tsx +74 -0
  95. package/registry/ui/alert-dialog.tsx +189 -0
  96. package/registry/ui/alert.tsx +78 -0
  97. package/registry/ui/app-shell.tsx +281 -0
  98. package/registry/ui/area-chart.tsx +1015 -0
  99. package/registry/ui/avatar.tsx +110 -0
  100. package/registry/ui/badge.tsx +54 -0
  101. package/registry/ui/bar-chart.tsx +1013 -0
  102. package/registry/ui/bar-list.tsx +302 -0
  103. package/registry/ui/breadcrumb.tsx +126 -0
  104. package/registry/ui/button.tsx +60 -0
  105. package/registry/ui/calendar.tsx +221 -0
  106. package/registry/ui/card.tsx +105 -0
  107. package/registry/ui/category-bar.tsx +281 -0
  108. package/registry/ui/checkbox.tsx +28 -0
  109. package/registry/ui/collapsible.tsx +21 -0
  110. package/registry/ui/combobox.tsx +310 -0
  111. package/registry/ui/command.tsx +196 -0
  112. package/registry/ui/dashboard-card.tsx +174 -0
  113. package/registry/ui/data-table.tsx +16 -91
  114. package/registry/ui/date-range-picker.tsx +209 -0
  115. package/registry/ui/dialog.tsx +159 -0
  116. package/registry/ui/donut-chart.tsx +1004 -0
  117. package/registry/ui/drawer.tsx +229 -0
  118. package/registry/ui/dropdown-menu.tsx +272 -0
  119. package/registry/ui/echart.tsx +486 -0
  120. package/registry/ui/field.tsx +240 -0
  121. package/registry/ui/filter-bar.tsx +365 -0
  122. package/registry/ui/funnel-steps.tsx +274 -0
  123. package/registry/ui/hover-card.tsx +52 -0
  124. package/registry/ui/input-group.tsx +158 -0
  125. package/registry/ui/input.tsx +22 -0
  126. package/registry/ui/kbd.tsx +28 -0
  127. package/registry/ui/label.tsx +20 -0
  128. package/registry/ui/line-chart.tsx +1007 -0
  129. package/registry/ui/metric-value.tsx +54 -0
  130. package/registry/ui/multi-select.tsx +226 -0
  131. package/registry/ui/page-shell.tsx +227 -0
  132. package/registry/ui/pagination.tsx +138 -0
  133. package/registry/ui/placeholder.tsx +30 -0
  134. package/registry/ui/popover.tsx +90 -0
  135. package/registry/ui/progress-circle.tsx +272 -0
  136. package/registry/ui/progress.tsx +83 -0
  137. package/registry/ui/radio-group.tsx +39 -0
  138. package/registry/ui/scroll-area.tsx +55 -0
  139. package/registry/ui/searchable-select.tsx +209 -0
  140. package/registry/ui/section-header.tsx +41 -0
  141. package/registry/ui/segmented-control.tsx +71 -0
  142. package/registry/ui/select.tsx +253 -0
  143. package/registry/ui/separator.tsx +25 -0
  144. package/registry/ui/sheet.tsx +137 -0
  145. package/registry/ui/sidebar.tsx +723 -0
  146. package/registry/ui/skeleton.tsx +15 -0
  147. package/registry/ui/slider.tsx +54 -0
  148. package/registry/ui/sparkline.tsx +155 -0
  149. package/registry/ui/spinner.tsx +12 -0
  150. package/registry/ui/states.tsx +61 -0
  151. package/registry/ui/status-badge.tsx +79 -0
  152. package/registry/ui/switch.tsx +32 -0
  153. package/registry/ui/table.tsx +116 -0
  154. package/registry/ui/tabs.tsx +82 -0
  155. package/registry/ui/textarea.tsx +20 -0
  156. package/registry/ui/toast.tsx +250 -0
  157. package/registry/ui/toggle-group.tsx +89 -0
  158. package/registry/ui/toggle.tsx +45 -0
  159. package/registry/ui/tooltip.tsx +66 -0
  160. package/registry/ui/tracker.tsx +236 -0
  161. package/registry/ui/trend-indicator.tsx +62 -0
  162. package/skills/vantage-add-feature/SKILL.md +9 -7
  163. package/skills/vantage-design/SKILL.md +3 -1
  164. package/skills/vantage-design/references/format.md +1 -1
  165. package/skills/vantage-pitfalls/SKILL.md +5 -1
  166. package/skills/vantage-slide/SKILL.md +17 -0
  167. package/templates/AGENTS.md +10 -1
  168. package/templates/agents/api.md +29 -0
  169. package/templates/agents/csv.md +2 -1
  170. package/templates/agents/doc.md +4 -3
  171. package/templates/agents/page.md +22 -8
  172. package/templates/agents/slide.md +13 -1
  173. package/templates/agents/xlsx.md +2 -1
  174. package/theme.css +7 -1
  175. package/dist/add-DY4AXANK.js +0 -6
  176. package/dist/add-DY4AXANK.js.map +0 -1
  177. package/dist/chunk-BCE22JSM.js.map +0 -1
  178. package/dist/chunk-DTDVSFRY.js.map +0 -1
  179. package/dist/chunk-EP6JHXSR.js.map +0 -1
  180. package/dist/chunk-KLEIVDUV.js.map +0 -1
  181. package/dist/chunk-MP3QSOXL.js.map +0 -1
  182. package/dist/chunk-PVTHHV7X.js.map +0 -1
  183. package/dist/chunk-QI3XQKHS.js.map +0 -1
  184. package/dist/chunk-T3FLAGI6.js +0 -316
  185. package/dist/chunk-T3FLAGI6.js.map +0 -1
  186. package/dist/dev-FCEC3UKQ.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/vantage src/ui/field.tsx by `npm run registry` (in Vantage, edit src/, 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,365 @@
1
+ // Copied by `vantage add ui filter-bar`. This file is yours to edit.
2
+ // Generated from @squadbase/vantage src/components/filter-bar.tsx by `npm run registry` (in Vantage, edit src/, not registry/).
3
+ /**
4
+ * FilterBar — a compound filter row. `<FilterBar>` holds the (always
5
+ * controlled) filter values on context; the select, multi-select, active-chip
6
+ * and clear parts read and update them. `FilterBarPreset` builds the whole row
7
+ * from a `FilterConfig[]`.
8
+ */
9
+ import {
10
+ createContext,
11
+ forwardRef,
12
+ useContext,
13
+ type HTMLAttributes,
14
+ type ReactNode,
15
+ } from "react"
16
+ import { ChevronDown, X } from "lucide-react"
17
+
18
+ import { cn } from "@squadbase/vantage/ui"
19
+ import { Badge } from "@squadbase/vantage/ui"
20
+ import { Button } from "@squadbase/vantage/ui"
21
+ import { Checkbox } from "@squadbase/vantage/ui"
22
+ import { Popover, PopoverContent, PopoverTrigger } from "@squadbase/vantage/ui"
23
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@squadbase/vantage/ui"
24
+ import type { FilterConfig, FilterOption, FilterValues } from "@squadbase/vantage/components"
25
+
26
+ /* --------------------------------------------------------------- context */
27
+
28
+ interface FilterBarContextValue {
29
+ /** Current filter values (controlled from outside) */
30
+ value: FilterValues
31
+ /** Notify the parent of a value change */
32
+ onChange: (next: FilterValues) => void
33
+ /** Optional clear callback — undefined means no clear button */
34
+ onClear?: () => void
35
+ }
36
+
37
+ const FilterBarContext = createContext<FilterBarContextValue | null>(null)
38
+
39
+ function useFilterBar(): FilterBarContextValue {
40
+ const ctx = useContext(FilterBarContext)
41
+ if (!ctx) throw new Error("useFilterBar must be used within <FilterBar>")
42
+ return ctx
43
+ }
44
+
45
+ /* ------------------------------------------------------------------ root */
46
+
47
+ export interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
48
+ value: FilterValues
49
+ onChange: (values: FilterValues) => void
50
+ onClear?: () => void
51
+ className?: string
52
+ children?: ReactNode
53
+ }
54
+
55
+ export const FilterBar = forwardRef<HTMLDivElement, FilterBarProps>(
56
+ ({ value, onChange, onClear, className, children, ...props }, ref) => (
57
+ <FilterBarContext.Provider value={{ value, onChange, onClear }}>
58
+ <div
59
+ ref={ref}
60
+ role="search"
61
+ aria-label="フィルター"
62
+ data-slot="filter-bar"
63
+ className={cn("flex flex-wrap items-center gap-2", className)}
64
+ {...props}
65
+ >
66
+ {children}
67
+ </div>
68
+ </FilterBarContext.Provider>
69
+ ),
70
+ )
71
+ FilterBar.displayName = "FilterBar"
72
+
73
+ /* ---------------------------------------------------------------- select */
74
+
75
+ export interface FilterBarSelectProps {
76
+ filterKey: string
77
+ label: string
78
+ options: FilterOption[]
79
+ placeholder?: string
80
+ className?: string
81
+ }
82
+
83
+ export function FilterBarSelect({
84
+ filterKey,
85
+ label,
86
+ options,
87
+ placeholder,
88
+ className,
89
+ }: FilterBarSelectProps) {
90
+ const { value, onChange } = useFilterBar()
91
+
92
+ // Base UI reports `null` when the selection is cleared — drop the key so
93
+ // "no filter" reads as absent rather than as an empty string.
94
+ const handleChange = (selected: string | null) =>
95
+ onChange({ ...value, [filterKey]: selected ?? undefined })
96
+
97
+ return (
98
+ <Select value={(value[filterKey] as string) ?? ""} onValueChange={handleChange}>
99
+ <SelectTrigger size="sm" aria-label={label} data-slot="filter-bar-select" className={className}>
100
+ <SelectValue placeholder={placeholder ?? label} />
101
+ </SelectTrigger>
102
+ <SelectContent>
103
+ {options.map((option) => (
104
+ <SelectItem key={option.value} value={option.value} disabled={option.disabled}>
105
+ {option.label}
106
+ </SelectItem>
107
+ ))}
108
+ </SelectContent>
109
+ </Select>
110
+ )
111
+ }
112
+ FilterBarSelect.displayName = "FilterBarSelect"
113
+
114
+ /* ---------------------------------------------------------- multi-select */
115
+
116
+ export interface FilterBarMultiSelectProps {
117
+ filterKey: string
118
+ label: string
119
+ options: FilterOption[]
120
+ placeholder?: string
121
+ className?: string
122
+ }
123
+
124
+ export function FilterBarMultiSelect({
125
+ filterKey,
126
+ label,
127
+ options,
128
+ placeholder,
129
+ className,
130
+ }: FilterBarMultiSelectProps) {
131
+ const { value, onChange } = useFilterBar()
132
+ const selected = (value[filterKey] as string[] | undefined) ?? []
133
+
134
+ const handleToggle = (option: string) => {
135
+ const next = selected.includes(option)
136
+ ? selected.filter((v) => v !== option)
137
+ : [...selected, option]
138
+ // Drop the key entirely when nothing is left, so "no filter" reads as absent.
139
+ onChange({ ...value, [filterKey]: next.length > 0 ? next : undefined })
140
+ }
141
+
142
+ return (
143
+ <Popover>
144
+ <PopoverTrigger
145
+ render={<Button variant="outline" size="sm" className={cn("h-8 gap-1", className)} />}
146
+ aria-label={`${placeholder ?? label}${selected.length > 0 ? ` (${selected.length}件選択中)` : ""}`}
147
+ data-slot="filter-bar-multi-select"
148
+ >
149
+ {placeholder ?? label}
150
+ {selected.length > 0 && (
151
+ <Badge variant="secondary" className="ml-1 px-1.5 py-0 text-xs">
152
+ {selected.length}
153
+ </Badge>
154
+ )}
155
+ <ChevronDown className="size-3.5 opacity-50" aria-hidden="true" />
156
+ </PopoverTrigger>
157
+ <PopoverContent align="start" className="w-56 p-2">
158
+ <div className="flex flex-col gap-1" role="group" aria-label={label}>
159
+ {options.map((option) => (
160
+ <label
161
+ key={option.value}
162
+ className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
163
+ >
164
+ <Checkbox
165
+ checked={selected.includes(option.value)}
166
+ disabled={option.disabled}
167
+ onCheckedChange={() => handleToggle(option.value)}
168
+ />
169
+ {option.label}
170
+ </label>
171
+ ))}
172
+ </div>
173
+ </PopoverContent>
174
+ </Popover>
175
+ )
176
+ }
177
+ FilterBarMultiSelect.displayName = "FilterBarMultiSelect"
178
+
179
+ /* ---------------------------------------------------------- active chips */
180
+
181
+ export interface FilterBarActiveChipsProps {
182
+ /** Filter configs used to resolve option labels. Without them, raw values show. */
183
+ filters?: FilterConfig[]
184
+ className?: string
185
+ }
186
+
187
+ interface ActiveChip {
188
+ key: string
189
+ filterLabel: string
190
+ optionLabel: string
191
+ optionValue?: string
192
+ isMulti: boolean
193
+ }
194
+
195
+ export function FilterBarActiveChips({ filters, className }: FilterBarActiveChipsProps) {
196
+ const { value, onChange } = useFilterBar()
197
+
198
+ const getActiveChips = (): ActiveChip[] => {
199
+ const chips: ActiveChip[] = []
200
+
201
+ const pushRaw = (key: string, v: string | string[]) => {
202
+ if (typeof v === "string") {
203
+ chips.push({ key, filterLabel: key, optionLabel: v, isMulti: false })
204
+ } else {
205
+ for (const val of v) {
206
+ chips.push({ key, filterLabel: key, optionLabel: val, optionValue: val, isMulti: true })
207
+ }
208
+ }
209
+ }
210
+
211
+ for (const [key, v] of Object.entries(value)) {
212
+ if (v === undefined || v === "") continue
213
+
214
+ const filter = filters?.find((f) => f.key === key)
215
+ if (!filter) {
216
+ pushRaw(key, v)
217
+ continue
218
+ }
219
+
220
+ if (filter.type === "select" && typeof v === "string") {
221
+ const option = filter.options?.find((o) => o.value === v)
222
+ chips.push({
223
+ key,
224
+ filterLabel: filter.label,
225
+ optionLabel: option?.label ?? v,
226
+ isMulti: false,
227
+ })
228
+ } else if (filter.type === "multi-select" && Array.isArray(v)) {
229
+ for (const val of v) {
230
+ const option = filter.options?.find((o) => o.value === val)
231
+ chips.push({
232
+ key,
233
+ filterLabel: filter.label,
234
+ optionLabel: option?.label ?? val,
235
+ optionValue: val,
236
+ isMulti: true,
237
+ })
238
+ }
239
+ }
240
+ }
241
+
242
+ return chips
243
+ }
244
+
245
+ const handleRemoveFilter = (key: string, optionValue?: string) => {
246
+ if (optionValue !== undefined) {
247
+ const current = (value[key] as string[] | undefined) ?? []
248
+ const next = current.filter((v) => v !== optionValue)
249
+ onChange({ ...value, [key]: next.length > 0 ? next : undefined })
250
+ } else {
251
+ onChange({ ...value, [key]: undefined })
252
+ }
253
+ }
254
+
255
+ const activeChips = getActiveChips()
256
+
257
+ if (activeChips.length === 0) return null
258
+
259
+ return (
260
+ <>
261
+ {activeChips.map((chip) => (
262
+ <Badge
263
+ key={`${chip.key}-${chip.optionValue ?? "single"}`}
264
+ variant="secondary"
265
+ className={cn("gap-1 pr-1", className)}
266
+ data-slot="filter-bar-chip"
267
+ >
268
+ <span className="text-xs text-muted-foreground">{chip.filterLabel}:</span>
269
+ {chip.optionLabel}
270
+ <button
271
+ type="button"
272
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted-foreground/20"
273
+ onClick={() => handleRemoveFilter(chip.key, chip.optionValue)}
274
+ aria-label={`${chip.filterLabel}: ${chip.optionLabel} を削除`}
275
+ >
276
+ <X className="size-3" aria-hidden="true" />
277
+ </button>
278
+ </Badge>
279
+ ))}
280
+ </>
281
+ )
282
+ }
283
+ FilterBarActiveChips.displayName = "FilterBarActiveChips"
284
+
285
+ /* ----------------------------------------------------------- clear button */
286
+
287
+ export interface FilterBarClearButtonProps {
288
+ /** Override the button label. Defaults to "クリア". */
289
+ label?: string
290
+ className?: string
291
+ }
292
+
293
+ export function FilterBarClearButton({ label = "クリア", className }: FilterBarClearButtonProps) {
294
+ const { value, onClear } = useFilterBar()
295
+ const hasActiveFilters = Object.values(value).some((v) =>
296
+ Array.isArray(v) ? v.length > 0 : v !== undefined && v !== "",
297
+ )
298
+ if (!hasActiveFilters || !onClear) return null
299
+
300
+ return (
301
+ <Button
302
+ variant="ghost"
303
+ size="sm"
304
+ className={cn("h-7 px-2 text-xs", className)}
305
+ onClick={onClear}
306
+ aria-label="すべてのフィルターをクリア"
307
+ data-slot="filter-bar-clear-button"
308
+ >
309
+ {label}
310
+ </Button>
311
+ )
312
+ }
313
+ FilterBarClearButton.displayName = "FilterBarClearButton"
314
+
315
+ /* ------------------------------------------------------------------ preset */
316
+
317
+ export interface FilterBarPresetProps {
318
+ filters: FilterConfig[]
319
+ value: FilterValues
320
+ onChange: (values: FilterValues) => void
321
+ onClear?: () => void
322
+ className?: string
323
+ }
324
+
325
+ /** The whole row built from a `FilterConfig[]`. */
326
+ export function FilterBarPreset({
327
+ filters,
328
+ value,
329
+ onChange,
330
+ onClear,
331
+ className,
332
+ }: FilterBarPresetProps) {
333
+ return (
334
+ <FilterBar value={value} onChange={onChange} onClear={onClear} className={className}>
335
+ {filters.map((filter) => {
336
+ if (filter.type === "select") {
337
+ return (
338
+ <FilterBarSelect
339
+ key={filter.key}
340
+ filterKey={filter.key}
341
+ label={filter.label}
342
+ placeholder={filter.placeholder}
343
+ options={filter.options ?? []}
344
+ />
345
+ )
346
+ }
347
+ if (filter.type === "multi-select") {
348
+ return (
349
+ <FilterBarMultiSelect
350
+ key={filter.key}
351
+ filterKey={filter.key}
352
+ label={filter.label}
353
+ placeholder={filter.placeholder}
354
+ options={filter.options ?? []}
355
+ />
356
+ )
357
+ }
358
+ return null
359
+ })}
360
+ <FilterBarActiveChips filters={filters} />
361
+ {onClear && <FilterBarClearButton />}
362
+ </FilterBar>
363
+ )
364
+ }
365
+ FilterBarPreset.displayName = "FilterBarPreset"