@squadbase/vantage 0.6.1-dev2.5f947f1 → 0.6.2-dev1.31accb1
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.
- package/dist/add-DY4AXANK.js +6 -0
- package/dist/add-DY4AXANK.js.map +1 -0
- package/dist/{build-O5TVNH4E.js → build-TVKFIVFQ.js} +8 -8
- package/dist/{build-O5TVNH4E.js.map → build-TVKFIVFQ.js.map} +1 -1
- package/dist/{check-WFBEDHDR.js → check-3KHZXZMQ.js} +6 -6
- package/dist/{check-WFBEDHDR.js.map → check-3KHZXZMQ.js.map} +1 -1
- package/dist/{chunk-LSRMRIXA.js → chunk-AG6RZ2RR.js} +1004 -23
- package/dist/chunk-AG6RZ2RR.js.map +1 -0
- package/dist/{chunk-KLEIVDUV.js → chunk-CEXAQQ5W.js} +150 -11
- package/dist/chunk-CEXAQQ5W.js.map +1 -0
- package/dist/{chunk-JGRMRXXZ.js → chunk-JKLLFVEU.js} +3 -3
- package/dist/{chunk-JGRMRXXZ.js.map → chunk-JKLLFVEU.js.map} +1 -1
- package/dist/{chunk-EP6JHXSR.js → chunk-LMUACBMA.js} +3 -3
- package/dist/{chunk-EP6JHXSR.js.map → chunk-LMUACBMA.js.map} +1 -1
- package/dist/{chunk-RPZAALCX.js → chunk-OB6UBJXF.js} +153 -153
- package/dist/{chunk-RPZAALCX.js.map → chunk-OB6UBJXF.js.map} +1 -1
- package/dist/{chunk-BCE22JSM.js → chunk-P7W4LR3Z.js} +32 -13
- package/dist/chunk-P7W4LR3Z.js.map +1 -0
- package/dist/{chunk-3G43C6OL.js → chunk-QI3XQKHS.js} +39 -3
- package/dist/chunk-QI3XQKHS.js.map +1 -0
- package/dist/{chunk-QDZA5JSL.js → chunk-U2M6HGKQ.js} +3 -3
- package/dist/{chunk-QDZA5JSL.js.map → chunk-U2M6HGKQ.js.map} +1 -1
- package/dist/{chunk-T3FLAGI6.js → chunk-VA776YE5.js} +3 -3
- package/dist/{chunk-T3FLAGI6.js.map → chunk-VA776YE5.js.map} +1 -1
- package/dist/cli.js +14 -11
- package/dist/cli.js.map +1 -1
- package/dist/components/index.d.ts +223 -1
- package/dist/components/index.js +2 -2
- package/dist/{dev-NZRLOQCL.js → dev-GDKATCC7.js} +22 -11
- package/dist/dev-GDKATCC7.js.map +1 -0
- package/dist/doc/index.js +2 -0
- package/dist/doc/index.js.map +1 -1
- package/dist/{docs-ZGDJUHJI.js → docs-EJJ3BECT.js} +3 -3
- package/dist/{docs-ZGDJUHJI.js.map → docs-EJJ3BECT.js.map} +1 -1
- package/dist/{doctor-VFKEMR54.js → doctor-WQXPXP6K.js} +7 -7
- package/dist/doctor-WQXPXP6K.js.map +1 -0
- package/dist/{preview-QDCIVVSR.js → preview-OGMCNQUO.js} +12 -4
- package/dist/preview-OGMCNQUO.js.map +1 -0
- package/dist/{routes-RHP5FVGD.js → routes-MWBWWOJ2.js} +5 -5
- package/dist/{routes-RHP5FVGD.js.map → routes-MWBWWOJ2.js.map} +1 -1
- package/dist/{search-RFBELGY7.js → search-6Y422SV6.js} +3 -3
- package/dist/{search-RFBELGY7.js.map → search-6Y422SV6.js.map} +1 -1
- package/dist/slide/index.js +61 -15
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +6 -5
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +140 -10
- package/dist/ui/index.js +1405 -63
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-GF2P67M6.js → upgrade-HCANX4GQ.js} +6 -6
- package/dist/{upgrade-GF2P67M6.js.map → upgrade-HCANX4GQ.js.map} +1 -1
- package/dist/vite/index.js +3 -3
- package/docs/en/changelog.md +24 -0
- package/docs/en/cli-reference.md +30 -0
- package/docs/en/parts/page-shell.md +1 -1
- package/docs/index.json +291 -61
- package/docs/ja/changelog.md +50 -0
- package/docs/ja/cli-reference.md +29 -0
- package/docs/ja/components.md +33 -89
- package/docs/ja/page-types/csv.md +83 -0
- package/docs/ja/page-types/doc.md +83 -0
- package/docs/ja/page-types/excel.md +82 -0
- package/docs/ja/page-types/slide.md +132 -0
- package/docs/ja/page-types.md +47 -0
- package/docs/ja/parts/area-chart.md +61 -0
- package/docs/ja/parts/bar-chart.md +69 -0
- package/docs/ja/parts/bar-list.md +35 -0
- package/docs/ja/parts/category-bar.md +36 -0
- package/docs/ja/parts/donut-chart.md +47 -0
- package/docs/ja/parts/echart.md +3 -0
- package/docs/ja/parts/line-chart.md +58 -0
- package/docs/ja/parts/page-shell.md +1 -1
- package/docs/ja/parts/progress-circle.md +34 -0
- package/docs/ja/parts/sparkline.md +1 -1
- package/docs/ja/parts/tracker.md +32 -0
- package/docs/ja/routing.md +8 -86
- package/docs/ja/ui/alert-dialog.md +41 -0
- package/docs/ja/ui/avatar.md +31 -0
- package/docs/ja/ui/combobox.md +74 -0
- package/docs/ja/ui/drawer.md +41 -0
- package/docs/ja/ui/field.md +42 -0
- package/docs/ja/ui/hover-card.md +21 -0
- package/docs/ja/ui/kbd.md +16 -0
- package/docs/ja/ui/pagination.md +36 -0
- package/docs/ja/ui/radio-group.md +21 -0
- package/docs/ja/ui/toast.md +46 -0
- package/docs/ja/ui-and-theming.md +14 -7
- package/package.json +18 -12
- package/registry/ui/accordion.tsx +74 -0
- package/registry/ui/alert-dialog.tsx +189 -0
- package/registry/ui/alert.tsx +78 -0
- package/registry/ui/app-shell.tsx +281 -0
- package/registry/ui/area-chart.tsx +1015 -0
- package/registry/ui/avatar.tsx +110 -0
- package/registry/ui/badge.tsx +54 -0
- package/registry/ui/bar-chart.tsx +1013 -0
- package/registry/ui/bar-list.tsx +302 -0
- package/registry/ui/breadcrumb.tsx +126 -0
- package/registry/ui/button.tsx +60 -0
- package/registry/ui/calendar.tsx +221 -0
- package/registry/ui/card.tsx +105 -0
- package/registry/ui/category-bar.tsx +281 -0
- package/registry/ui/checkbox.tsx +28 -0
- package/registry/ui/collapsible.tsx +21 -0
- package/registry/ui/combobox.tsx +310 -0
- package/registry/ui/command.tsx +196 -0
- package/registry/ui/dashboard-card.tsx +174 -0
- package/registry/ui/data-table.tsx +16 -91
- package/registry/ui/date-range-picker.tsx +209 -0
- package/registry/ui/dialog.tsx +159 -0
- package/registry/ui/donut-chart.tsx +1004 -0
- package/registry/ui/drawer.tsx +229 -0
- package/registry/ui/dropdown-menu.tsx +272 -0
- package/registry/ui/echart.tsx +486 -0
- package/registry/ui/field.tsx +240 -0
- package/registry/ui/filter-bar.tsx +365 -0
- package/registry/ui/funnel-steps.tsx +274 -0
- package/registry/ui/hover-card.tsx +52 -0
- package/registry/ui/input-group.tsx +158 -0
- package/registry/ui/input.tsx +22 -0
- package/registry/ui/kbd.tsx +28 -0
- package/registry/ui/label.tsx +20 -0
- package/registry/ui/line-chart.tsx +1007 -0
- package/registry/ui/metric-value.tsx +54 -0
- package/registry/ui/multi-select.tsx +226 -0
- package/registry/ui/page-shell.tsx +227 -0
- package/registry/ui/pagination.tsx +138 -0
- package/registry/ui/placeholder.tsx +30 -0
- package/registry/ui/popover.tsx +90 -0
- package/registry/ui/progress-circle.tsx +272 -0
- package/registry/ui/progress.tsx +83 -0
- package/registry/ui/radio-group.tsx +39 -0
- package/registry/ui/scroll-area.tsx +55 -0
- package/registry/ui/searchable-select.tsx +209 -0
- package/registry/ui/section-header.tsx +41 -0
- package/registry/ui/segmented-control.tsx +71 -0
- package/registry/ui/select.tsx +253 -0
- package/registry/ui/separator.tsx +25 -0
- package/registry/ui/sheet.tsx +137 -0
- package/registry/ui/sidebar.tsx +723 -0
- package/registry/ui/skeleton.tsx +15 -0
- package/registry/ui/slider.tsx +54 -0
- package/registry/ui/sparkline.tsx +155 -0
- package/registry/ui/spinner.tsx +12 -0
- package/registry/ui/states.tsx +61 -0
- package/registry/ui/status-badge.tsx +79 -0
- package/registry/ui/switch.tsx +32 -0
- package/registry/ui/table.tsx +116 -0
- package/registry/ui/tabs.tsx +82 -0
- package/registry/ui/textarea.tsx +20 -0
- package/registry/ui/toast.tsx +250 -0
- package/registry/ui/toggle-group.tsx +89 -0
- package/registry/ui/toggle.tsx +45 -0
- package/registry/ui/tooltip.tsx +66 -0
- package/registry/ui/tracker.tsx +236 -0
- package/registry/ui/trend-indicator.tsx +62 -0
- package/skills/vantage-add-feature/SKILL.md +9 -7
- package/skills/vantage-design/SKILL.md +3 -1
- package/skills/vantage-design/references/format.md +1 -1
- package/skills/vantage-pitfalls/SKILL.md +5 -1
- package/skills/vantage-slide/SKILL.md +17 -0
- package/templates/AGENTS.md +10 -1
- package/templates/agents/api.md +29 -0
- package/templates/agents/csv.md +2 -1
- package/templates/agents/doc.md +4 -3
- package/templates/agents/page.md +22 -8
- package/templates/agents/slide.md +13 -1
- package/templates/agents/xlsx.md +2 -1
- package/theme.css +15 -1
- package/dist/add-EISGSZAH.js +0 -6
- package/dist/add-EISGSZAH.js.map +0 -1
- package/dist/chunk-3G43C6OL.js.map +0 -1
- package/dist/chunk-BCE22JSM.js.map +0 -1
- package/dist/chunk-KLEIVDUV.js.map +0 -1
- package/dist/chunk-LSRMRIXA.js.map +0 -1
- package/dist/dev-NZRLOQCL.js.map +0 -1
- package/dist/doctor-VFKEMR54.js.map +0 -1
- package/dist/preview-QDCIVVSR.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"
|