azamat-ui-kit-cli 0.2.2
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/README.md +8 -0
- package/dist/index.js +432 -0
- package/package.json +34 -0
- package/vendor/package.json +4 -0
- package/vendor/src/components/actions/action-bar.tsx +35 -0
- package/vendor/src/components/actions/action-menu.tsx +120 -0
- package/vendor/src/components/actions/button-group.tsx +47 -0
- package/vendor/src/components/actions/copy-button.tsx +91 -0
- package/vendor/src/components/actions/copy-field.tsx +31 -0
- package/vendor/src/components/actions/floating-action-button.tsx +33 -0
- package/vendor/src/components/actions/index.ts +7 -0
- package/vendor/src/components/actions/public.ts +5 -0
- package/vendor/src/components/actions/quick-action-grid.tsx +162 -0
- package/vendor/src/components/calendar/calendar.tsx +328 -0
- package/vendor/src/components/calendar/date-picker.tsx +78 -0
- package/vendor/src/components/calendar/date-range-picker.tsx +96 -0
- package/vendor/src/components/calendar/date-utils.ts +89 -0
- package/vendor/src/components/calendar/index.ts +4 -0
- package/vendor/src/components/charts/charts.tsx +275 -0
- package/vendor/src/components/charts/horizontal-bar-chart.tsx +46 -0
- package/vendor/src/components/charts/index.ts +4 -0
- package/vendor/src/components/charts/kpi.tsx +68 -0
- package/vendor/src/components/charts/progress-ring.tsx +45 -0
- package/vendor/src/components/charts/public.ts +1 -0
- package/vendor/src/components/command/command-palette.tsx +375 -0
- package/vendor/src/components/command/index.ts +1 -0
- package/vendor/src/components/data-table/data-table-actions-column.tsx +58 -0
- package/vendor/src/components/data-table/data-table-bulk-actions.tsx +84 -0
- package/vendor/src/components/data-table/data-table-column-visibility-menu.tsx +79 -0
- package/vendor/src/components/data-table/data-table-pagination.tsx +91 -0
- package/vendor/src/components/data-table/data-table-row-actions.tsx +48 -0
- package/vendor/src/components/data-table/data-table-select-column.tsx +59 -0
- package/vendor/src/components/data-table/data-table-sortable-header.tsx +45 -0
- package/vendor/src/components/data-table/data-table-toolbar.tsx +76 -0
- package/vendor/src/components/data-table/data-table-view-presets.tsx +128 -0
- package/vendor/src/components/data-table/data-table.tsx +507 -0
- package/vendor/src/components/data-table/index.ts +12 -0
- package/vendor/src/components/data-table/public.ts +10 -0
- package/vendor/src/components/data-table/table-export-menu.tsx +56 -0
- package/vendor/src/components/data-table/table-import-button.tsx +43 -0
- package/vendor/src/components/display/activity-feed.tsx +97 -0
- package/vendor/src/components/display/avatar.tsx +131 -0
- package/vendor/src/components/display/code-block.tsx +33 -0
- package/vendor/src/components/display/data-state.tsx +63 -0
- package/vendor/src/components/display/description-list.tsx +119 -0
- package/vendor/src/components/display/descriptions.tsx +83 -0
- package/vendor/src/components/display/entity-card.tsx +53 -0
- package/vendor/src/components/display/file-card.tsx +54 -0
- package/vendor/src/components/display/index.ts +30 -0
- package/vendor/src/components/display/kanban.tsx +104 -0
- package/vendor/src/components/display/keyboard-shortcut.tsx +31 -0
- package/vendor/src/components/display/list.tsx +100 -0
- package/vendor/src/components/display/metric-grid.tsx +86 -0
- package/vendor/src/components/display/progress.tsx +162 -0
- package/vendor/src/components/display/property-grid.tsx +54 -0
- package/vendor/src/components/display/result.tsx +90 -0
- package/vendor/src/components/display/smart-card.tsx +168 -0
- package/vendor/src/components/display/statistic.tsx +107 -0
- package/vendor/src/components/display/status-legend.tsx +108 -0
- package/vendor/src/components/display/tag-list.tsx +52 -0
- package/vendor/src/components/display/timeline.tsx +132 -0
- package/vendor/src/components/display/tree-view.tsx +116 -0
- package/vendor/src/components/feedback/alert.tsx +69 -0
- package/vendor/src/components/feedback/empty-state.tsx +56 -0
- package/vendor/src/components/feedback/index.ts +5 -0
- package/vendor/src/components/feedback/loading-state.tsx +39 -0
- package/vendor/src/components/feedback/page-state.tsx +69 -0
- package/vendor/src/components/feedback/status-badge.tsx +62 -0
- package/vendor/src/components/filters/filter-bar.tsx +89 -0
- package/vendor/src/components/filters/filter-chips.tsx +69 -0
- package/vendor/src/components/filters/index.ts +2 -0
- package/vendor/src/components/form/form-actions.tsx +53 -0
- package/vendor/src/components/form/form-async-select.tsx +26 -0
- package/vendor/src/components/form/form-date-input.tsx +19 -0
- package/vendor/src/components/form/form-date-picker.tsx +54 -0
- package/vendor/src/components/form/form-date-range-input.tsx +79 -0
- package/vendor/src/components/form/form-date-range-picker.tsx +57 -0
- package/vendor/src/components/form/form-field-shell.tsx +191 -0
- package/vendor/src/components/form/form-input.tsx +480 -0
- package/vendor/src/components/form/form-number-input.tsx +19 -0
- package/vendor/src/components/form/form-password-input.tsx +19 -0
- package/vendor/src/components/form/form-phone-input.tsx +22 -0
- package/vendor/src/components/form/form-search-input.tsx +19 -0
- package/vendor/src/components/form/form-section.tsx +29 -0
- package/vendor/src/components/form/form-select.tsx +194 -0
- package/vendor/src/components/form/form-switch.tsx +145 -0
- package/vendor/src/components/form/form-textarea.tsx +103 -0
- package/vendor/src/components/form/index.ts +17 -0
- package/vendor/src/components/form/public.ts +14 -0
- package/vendor/src/components/form/smart-form-shell.tsx +59 -0
- package/vendor/src/components/inputs/async-select.tsx +1143 -0
- package/vendor/src/components/inputs/clearable-input.tsx +78 -0
- package/vendor/src/components/inputs/color-input.tsx +47 -0
- package/vendor/src/components/inputs/combobox.tsx +89 -0
- package/vendor/src/components/inputs/date-input.tsx +32 -0
- package/vendor/src/components/inputs/date-range-input.tsx +67 -0
- package/vendor/src/components/inputs/index.ts +19 -0
- package/vendor/src/components/inputs/input-chrome.tsx +37 -0
- package/vendor/src/components/inputs/input-decorator.tsx +64 -0
- package/vendor/src/components/inputs/input-value.ts +42 -0
- package/vendor/src/components/inputs/masked-input.tsx +51 -0
- package/vendor/src/components/inputs/money-input.tsx +73 -0
- package/vendor/src/components/inputs/number-input.tsx +87 -0
- package/vendor/src/components/inputs/numeric-value.ts +39 -0
- package/vendor/src/components/inputs/otp-input.tsx +102 -0
- package/vendor/src/components/inputs/password-input.tsx +85 -0
- package/vendor/src/components/inputs/phone-input.tsx +46 -0
- package/vendor/src/components/inputs/quantity-input.tsx +116 -0
- package/vendor/src/components/inputs/quantity-stepper.tsx +49 -0
- package/vendor/src/components/inputs/rating.tsx +98 -0
- package/vendor/src/components/inputs/search-input.tsx +26 -0
- package/vendor/src/components/inputs/simple-select.tsx +72 -0
- package/vendor/src/components/inputs/slider.tsx +149 -0
- package/vendor/src/components/inputs/tag-input.tsx +104 -0
- package/vendor/src/components/layout/app-header.tsx +46 -0
- package/vendor/src/components/layout/app-shell.tsx +243 -0
- package/vendor/src/components/layout/app-sidebar.tsx +179 -0
- package/vendor/src/components/layout/breadcrumbs.tsx +72 -0
- package/vendor/src/components/layout/index.ts +11 -0
- package/vendor/src/components/layout/page-container.tsx +30 -0
- package/vendor/src/components/layout/page-header.tsx +60 -0
- package/vendor/src/components/layout/public.ts +10 -0
- package/vendor/src/components/layout/section.tsx +76 -0
- package/vendor/src/components/layout/sidebar-nav.tsx +147 -0
- package/vendor/src/components/layout/stat-card.tsx +88 -0
- package/vendor/src/components/layout/sticky-footer-bar.tsx +23 -0
- package/vendor/src/components/layout/workspace-shell.tsx +50 -0
- package/vendor/src/components/navigation/anchor-nav.tsx +44 -0
- package/vendor/src/components/navigation/index.ts +4 -0
- package/vendor/src/components/navigation/page-tabs.tsx +67 -0
- package/vendor/src/components/navigation/pagination.tsx +179 -0
- package/vendor/src/components/navigation/stepper-tabs.tsx +67 -0
- package/vendor/src/components/notifications/index.ts +1 -0
- package/vendor/src/components/notifications/toast.tsx +259 -0
- package/vendor/src/components/overlay/confirm-dialog.tsx +66 -0
- package/vendor/src/components/overlay/dialog-actions.tsx +68 -0
- package/vendor/src/components/overlay/index.ts +4 -0
- package/vendor/src/components/overlay/modal-shell.tsx +93 -0
- package/vendor/src/components/overlay/sheet-shell.tsx +212 -0
- package/vendor/src/components/patterns/action-system.tsx +116 -0
- package/vendor/src/components/patterns/crud-system.tsx +53 -0
- package/vendor/src/components/patterns/data-view.tsx +84 -0
- package/vendor/src/components/patterns/entity-details.tsx +66 -0
- package/vendor/src/components/patterns/filter-builder.tsx +113 -0
- package/vendor/src/components/patterns/form-builder-presets.ts +131 -0
- package/vendor/src/components/patterns/form-builder.tsx +334 -0
- package/vendor/src/components/patterns/index.ts +12 -0
- package/vendor/src/components/patterns/public.ts +4 -0
- package/vendor/src/components/patterns/resource-detail-page.tsx +160 -0
- package/vendor/src/components/patterns/resource-page.tsx +159 -0
- package/vendor/src/components/patterns/resource-system.tsx +61 -0
- package/vendor/src/components/patterns/settings-section.tsx +46 -0
- package/vendor/src/components/patterns/status-system.tsx +89 -0
- package/vendor/src/components/theme-provider.tsx +51 -0
- package/vendor/src/components/ui/badge.tsx +52 -0
- package/vendor/src/components/ui/button.tsx +61 -0
- package/vendor/src/components/ui/card.tsx +103 -0
- package/vendor/src/components/ui/checkbox.tsx +82 -0
- package/vendor/src/components/ui/collapse.tsx +126 -0
- package/vendor/src/components/ui/command.tsx +194 -0
- package/vendor/src/components/ui/dialog.tsx +160 -0
- package/vendor/src/components/ui/divider.tsx +46 -0
- package/vendor/src/components/ui/dropdown-menu.tsx +266 -0
- package/vendor/src/components/ui/input-group.tsx +158 -0
- package/vendor/src/components/ui/input.tsx +20 -0
- package/vendor/src/components/ui/popover.tsx +90 -0
- package/vendor/src/components/ui/segmented-control.tsx +78 -0
- package/vendor/src/components/ui/select.tsx +201 -0
- package/vendor/src/components/ui/skeleton.tsx +75 -0
- package/vendor/src/components/ui/spinner.tsx +50 -0
- package/vendor/src/components/ui/switch.tsx +71 -0
- package/vendor/src/components/ui/table.tsx +114 -0
- package/vendor/src/components/ui/tabs.tsx +55 -0
- package/vendor/src/components/ui/textarea.tsx +18 -0
- package/vendor/src/components/ui/tooltip.tsx +38 -0
- package/vendor/src/components/upload/file-upload.tsx +483 -0
- package/vendor/src/components/upload/image-upload.tsx +118 -0
- package/vendor/src/components/upload/index.ts +2 -0
- package/vendor/src/components/wizard/index.ts +2 -0
- package/vendor/src/components/wizard/stepper.tsx +53 -0
- package/vendor/src/components/wizard/wizard.tsx +60 -0
- package/vendor/src/families/card-family.ts +28 -0
- package/vendor/src/families/catalog.ts +96 -0
- package/vendor/src/families/data-table-family.ts +31 -0
- package/vendor/src/families/docs-adoption.ts +103 -0
- package/vendor/src/families/docs-groups.ts +209 -0
- package/vendor/src/families/docs-queries.ts +84 -0
- package/vendor/src/families/docs-routing.ts +89 -0
- package/vendor/src/families/form-family.ts +45 -0
- package/vendor/src/families/index.ts +17 -0
- package/vendor/src/families/input-family.ts +61 -0
- package/vendor/src/families/member-metadata.ts +466 -0
- package/vendor/src/families/member-queries.ts +28 -0
- package/vendor/src/families/member-snippet-queries.ts +54 -0
- package/vendor/src/families/member-snippets.ts +673 -0
- package/vendor/src/families/migration-map.ts +79 -0
- package/vendor/src/families/queries.ts +63 -0
- package/vendor/src/families/select-family.ts +33 -0
- package/vendor/src/families/views.ts +81 -0
- package/vendor/src/hooks/index.ts +6 -0
- package/vendor/src/hooks/use-before-unload-when-dirty.ts +21 -0
- package/vendor/src/hooks/use-data-table-view-state.ts +122 -0
- package/vendor/src/hooks/use-debounce.ts +52 -0
- package/vendor/src/hooks/use-disclosure.ts +38 -0
- package/vendor/src/hooks/use-is-mobile.ts +28 -0
- package/vendor/src/hooks/use-session-storage-state.ts +85 -0
- package/vendor/src/index.ts +38 -0
- package/vendor/src/lib/utils.ts +6 -0
- package/vendor/templates/components/button.tsx +0 -0
- package/vendor/templates/components/data-table.tsx +0 -0
- package/vendor/templates/components/input.tsx +0 -0
- package/vendor/templates/lib/utils.ts +0 -0
- package/vendor/templates/styles/globals.css +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { DataView, type DataViewProps } from "@/components/patterns/data-view"
|
|
4
|
+
import { EntityDetails, type EntityDetailsProps } from "@/components/patterns/entity-details"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
export type ResourceSystemMode = "list" | "detail" | "split"
|
|
8
|
+
|
|
9
|
+
export type ResourceSystemProps<TItem = unknown> = React.ComponentProps<"div"> & {
|
|
10
|
+
mode?: ResourceSystemMode
|
|
11
|
+
title?: React.ReactNode
|
|
12
|
+
description?: React.ReactNode
|
|
13
|
+
actions?: React.ReactNode
|
|
14
|
+
list: DataViewProps<TItem>
|
|
15
|
+
detail?: React.ReactNode | EntityDetailsProps
|
|
16
|
+
aside?: React.ReactNode
|
|
17
|
+
splitSize?: "sm" | "md" | "lg"
|
|
18
|
+
renderHeader?: () => React.ReactNode
|
|
19
|
+
listClassName?: string
|
|
20
|
+
detailClassName?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const splitClassName = {
|
|
24
|
+
sm: "lg:grid-cols-[minmax(0,1fr)_320px]",
|
|
25
|
+
md: "lg:grid-cols-[minmax(0,1fr)_420px]",
|
|
26
|
+
lg: "lg:grid-cols-[minmax(0,1fr)_520px]",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ResourceSystem<TItem = unknown>({ mode = "list", title, description, actions, list, detail, aside, splitSize = "md", renderHeader, listClassName, detailClassName, className, ...props }: ResourceSystemProps<TItem>) {
|
|
30
|
+
const detailNode = isEntityDetailsProps(detail) ? <EntityDetails {...detail} /> : detail
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div data-slot="resource-system" data-mode={mode} className={cn("grid gap-4", className)} {...props}>
|
|
34
|
+
{renderHeader?.() ?? ((title || description || actions) && (
|
|
35
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
36
|
+
<div className="grid gap-1">
|
|
37
|
+
{title && <h1 className="text-2xl font-semibold tracking-tight text-foreground">{title}</h1>}
|
|
38
|
+
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
|
39
|
+
</div>
|
|
40
|
+
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
{mode === "split" ? (
|
|
44
|
+
<div className={cn("grid gap-4", splitClassName[splitSize])}>
|
|
45
|
+
<DataView {...list} className={cn(list.className, listClassName)} />
|
|
46
|
+
<div className={cn("min-w-0", detailClassName)}>{detailNode ?? aside}</div>
|
|
47
|
+
</div>
|
|
48
|
+
) : mode === "detail" ? (
|
|
49
|
+
<div className={detailClassName}>{detailNode ?? aside}</div>
|
|
50
|
+
) : (
|
|
51
|
+
<DataView {...list} className={cn(list.className, listClassName)} />
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isEntityDetailsProps(value: unknown): value is EntityDetailsProps {
|
|
58
|
+
return Boolean(value && typeof value === "object" && "title" in value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { ResourceSystem }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
export type SettingsSectionProps = React.ComponentProps<"section"> & {
|
|
6
|
+
title?: React.ReactNode
|
|
7
|
+
description?: React.ReactNode
|
|
8
|
+
actions?: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function SettingsSection({ title, description, actions, className, children, ...props }: SettingsSectionProps) {
|
|
12
|
+
return (
|
|
13
|
+
<section data-slot="settings-section" className={cn("grid gap-4 rounded-lg border bg-card p-4", className)} {...props}>
|
|
14
|
+
{(title || description || actions) && (
|
|
15
|
+
<div className="flex items-start justify-between gap-3">
|
|
16
|
+
<div className="grid gap-1">
|
|
17
|
+
{title && <h3 className="text-base font-semibold text-foreground">{title}</h3>}
|
|
18
|
+
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
|
19
|
+
</div>
|
|
20
|
+
{actions}
|
|
21
|
+
</div>
|
|
22
|
+
)}
|
|
23
|
+
<div className="grid divide-y">{children}</div>
|
|
24
|
+
</section>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SettingsRowProps = React.ComponentProps<"div"> & {
|
|
29
|
+
label: React.ReactNode
|
|
30
|
+
description?: React.ReactNode
|
|
31
|
+
control?: React.ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function SettingsRow({ label, description, control, className, children, ...props }: SettingsRowProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div data-slot="settings-row" className={cn("flex flex-wrap items-center justify-between gap-3 py-3 first:pt-0 last:pb-0", className)} {...props}>
|
|
37
|
+
<div className="grid min-w-0 gap-1">
|
|
38
|
+
<div className="text-sm font-medium text-foreground">{label}</div>
|
|
39
|
+
{description && <div className="text-sm text-muted-foreground">{description}</div>}
|
|
40
|
+
</div>
|
|
41
|
+
<div className="shrink-0">{control ?? children}</div>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { SettingsRow, SettingsSection }
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@/components/ui/badge"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
export type StatusTone = "neutral" | "success" | "warning" | "danger" | "info"
|
|
7
|
+
|
|
8
|
+
export type StatusState<TValue extends string = string> = {
|
|
9
|
+
value: TValue
|
|
10
|
+
label: React.ReactNode
|
|
11
|
+
icon?: React.ReactNode
|
|
12
|
+
tone?: StatusTone
|
|
13
|
+
next?: TValue[]
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type StatusSystemProps<TValue extends string = string> = React.ComponentProps<"div"> & {
|
|
18
|
+
value: TValue
|
|
19
|
+
states: StatusState<TValue>[]
|
|
20
|
+
mode?: "badge" | "select" | "steps"
|
|
21
|
+
allowAllTransitions?: boolean
|
|
22
|
+
onValueChange?: (value: TValue, state: StatusState<TValue>) => void
|
|
23
|
+
renderState?: (state: StatusState<TValue>, meta: { active: boolean; passed: boolean; disabled: boolean }) => React.ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const toneClassName: Record<StatusTone, string> = {
|
|
27
|
+
neutral: "border-border bg-muted text-muted-foreground",
|
|
28
|
+
success: "border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
|
|
29
|
+
warning: "border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-400",
|
|
30
|
+
danger: "border-destructive/20 bg-destructive/10 text-destructive",
|
|
31
|
+
info: "border-primary/20 bg-primary/10 text-primary",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function StatusSystem<TValue extends string = string>({ value, states, mode = "badge", allowAllTransitions = false, onValueChange, renderState, className, ...props }: StatusSystemProps<TValue>) {
|
|
35
|
+
const activeIndex = states.findIndex((state) => state.value === value)
|
|
36
|
+
const activeState = states[activeIndex]
|
|
37
|
+
const nextValues = new Set(allowAllTransitions ? states.map((state) => state.value) : activeState?.next ?? [])
|
|
38
|
+
|
|
39
|
+
if (!activeState) return null
|
|
40
|
+
|
|
41
|
+
if (mode === "select") {
|
|
42
|
+
return (
|
|
43
|
+
<div data-slot="status-system" className={cn("inline-flex", className)} {...props}>
|
|
44
|
+
<select
|
|
45
|
+
value={value}
|
|
46
|
+
className="h-8 rounded-md border bg-background px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
47
|
+
onChange={(event) => {
|
|
48
|
+
const state = states.find((item) => item.value === event.currentTarget.value)
|
|
49
|
+
if (state) onValueChange?.(state.value, state)
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{states.map((state) => {
|
|
53
|
+
const active = state.value === value
|
|
54
|
+
const disabled = state.disabled || (!active && !nextValues.has(state.value))
|
|
55
|
+
return <option key={state.value} value={state.value} disabled={disabled}>{typeof state.label === "string" ? state.label : state.value}</option>
|
|
56
|
+
})}
|
|
57
|
+
</select>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (mode === "steps") {
|
|
63
|
+
return (
|
|
64
|
+
<div data-slot="status-system" className={cn("flex flex-wrap items-center gap-3", className)} {...props}>
|
|
65
|
+
{states.map((state, index) => {
|
|
66
|
+
const active = state.value === value
|
|
67
|
+
const passed = activeIndex >= index
|
|
68
|
+
const disabled = state.disabled || (!active && !nextValues.has(state.value))
|
|
69
|
+
return renderState?.(state, { active, passed, disabled }) ?? (
|
|
70
|
+
<button key={state.value} type="button" disabled={disabled} className="flex items-center gap-2 disabled:opacity-50" onClick={() => onValueChange?.(state.value, state)}>
|
|
71
|
+
<span className={cn("flex size-6 items-center justify-center rounded-full border text-xs", passed ? "border-primary bg-primary text-primary-foreground" : "bg-muted text-muted-foreground")}>{index + 1}</span>
|
|
72
|
+
<span className={cn("text-sm", active ? "font-medium text-foreground" : "text-muted-foreground")}>{state.label}</span>
|
|
73
|
+
</button>
|
|
74
|
+
)
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div data-slot="status-system" className={className} {...props}>
|
|
82
|
+
{renderState?.(activeState, { active: true, passed: true, disabled: Boolean(activeState.disabled) }) ?? (
|
|
83
|
+
<Badge variant="outline" className={cn("gap-1", toneClassName[activeState.tone ?? "neutral"])}>{activeState.icon}{activeState.label}</Badge>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { StatusSystem }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
type ThemeMode = "light" | "dark"
|
|
4
|
+
|
|
5
|
+
type ThemeContextValue = {
|
|
6
|
+
theme: ThemeMode
|
|
7
|
+
setTheme: (theme: ThemeMode) => void
|
|
8
|
+
toggleTheme: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const THEME_STORAGE_KEY = "azamat-ui-theme"
|
|
12
|
+
|
|
13
|
+
const ThemeContext = React.createContext<ThemeContextValue | null>(null)
|
|
14
|
+
|
|
15
|
+
function resolveInitialTheme(): ThemeMode {
|
|
16
|
+
if (typeof window === "undefined") return "light"
|
|
17
|
+
|
|
18
|
+
const stored = window.localStorage.getItem(THEME_STORAGE_KEY)
|
|
19
|
+
if (stored === "light" || stored === "dark") return stored
|
|
20
|
+
|
|
21
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
25
|
+
const [theme, setThemeState] = React.useState<ThemeMode>(resolveInitialTheme)
|
|
26
|
+
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
const root = window.document.documentElement
|
|
29
|
+
root.classList.toggle("dark", theme === "dark")
|
|
30
|
+
root.style.colorScheme = theme
|
|
31
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, theme)
|
|
32
|
+
}, [theme])
|
|
33
|
+
|
|
34
|
+
const setTheme = React.useCallback((nextTheme: ThemeMode) => {
|
|
35
|
+
setThemeState(nextTheme)
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const toggleTheme = React.useCallback(() => {
|
|
39
|
+
setThemeState((current) => (current === "dark" ? "light" : "dark"))
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
const value = React.useMemo(() => ({ theme, setTheme, toggleTheme }), [theme, setTheme, toggleTheme])
|
|
43
|
+
|
|
44
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useTheme() {
|
|
48
|
+
const context = React.useContext(ThemeContext)
|
|
49
|
+
if (!context) throw new Error("useTheme must be used within ThemeProvider")
|
|
50
|
+
return context
|
|
51
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props"
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
13
|
+
secondary:
|
|
14
|
+
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
15
|
+
destructive:
|
|
16
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
17
|
+
outline:
|
|
18
|
+
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "default",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
function Badge({
|
|
31
|
+
className,
|
|
32
|
+
variant = "default",
|
|
33
|
+
render,
|
|
34
|
+
...props
|
|
35
|
+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
36
|
+
return useRender({
|
|
37
|
+
defaultTagName: "span",
|
|
38
|
+
props: mergeProps<"span">(
|
|
39
|
+
{
|
|
40
|
+
className: cn(badgeVariants({ variant }), className),
|
|
41
|
+
},
|
|
42
|
+
props
|
|
43
|
+
),
|
|
44
|
+
render,
|
|
45
|
+
state: {
|
|
46
|
+
slot: "badge",
|
|
47
|
+
variant,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[transform,background-color,border-color,color,box-shadow] outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
"border-primary/90 bg-primary text-primary-foreground shadow-[0_10px_24px_color-mix(in_oklch,var(--primary),transparent_78%)] hover:bg-[color-mix(in_oklch,var(--primary),white_8%)] hover:shadow-[0_14px_30px_color-mix(in_oklch,var(--primary),transparent_72%)]",
|
|
13
|
+
outline:
|
|
14
|
+
"border-border/80 bg-background/92 text-foreground shadow-sm hover:border-border hover:bg-accent/70 hover:text-foreground aria-expanded:bg-accent/70 aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/55",
|
|
15
|
+
secondary:
|
|
16
|
+
"border-transparent bg-secondary text-secondary-foreground shadow-sm hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_4%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
17
|
+
ghost:
|
|
18
|
+
"border-transparent bg-transparent hover:bg-accent/70 hover:text-foreground aria-expanded:bg-accent/70 aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
19
|
+
destructive:
|
|
20
|
+
"border-destructive/15 bg-destructive text-white shadow-[0_10px_24px_color-mix(in_oklch,var(--destructive),transparent_78%)] hover:bg-[color-mix(in_oklch,var(--destructive),black_6%)] focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default:
|
|
25
|
+
"h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
|
|
26
|
+
xs: "h-7 gap-1 rounded-[min(var(--radius-lg),12px)] px-2.5 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
|
|
27
|
+
sm: "h-8 gap-1 rounded-[min(var(--radius-lg),14px)] px-3 text-[0.82rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3.5",
|
|
28
|
+
lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
29
|
+
icon: "size-9",
|
|
30
|
+
"icon-xs":
|
|
31
|
+
"size-7 rounded-[min(var(--radius-lg),12px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
32
|
+
"icon-sm":
|
|
33
|
+
"size-8 rounded-[min(var(--radius-lg),14px)] in-data-[slot=button-group]:rounded-lg",
|
|
34
|
+
"icon-lg": "size-10",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
variant: "default",
|
|
39
|
+
size: "default",
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export type ButtonProps = ButtonPrimitive.Props & VariantProps<typeof buttonVariants>
|
|
45
|
+
|
|
46
|
+
function Button({
|
|
47
|
+
className,
|
|
48
|
+
variant = "default",
|
|
49
|
+
size = "default",
|
|
50
|
+
...props
|
|
51
|
+
}: ButtonProps) {
|
|
52
|
+
return (
|
|
53
|
+
<ButtonPrimitive
|
|
54
|
+
data-slot="button"
|
|
55
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Card({
|
|
6
|
+
className,
|
|
7
|
+
size = "default",
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
data-slot="card"
|
|
13
|
+
data-size={size}
|
|
14
|
+
className={cn(
|
|
15
|
+
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-[var(--radius-2xl)] border border-border/75 bg-card/98 py-(--card-spacing) text-sm text-card-foreground shadow-sm [--card-spacing:--spacing(5)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-[var(--radius-2xl)] *:[img:last-child]:rounded-b-[var(--radius-2xl)]",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-slot="card-header"
|
|
27
|
+
className={cn(
|
|
28
|
+
"group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-t-[var(--radius-2xl)] px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-slot="card-title"
|
|
40
|
+
className={cn(
|
|
41
|
+
"font-heading text-[1.05rem] leading-snug font-semibold tracking-tight group-data-[size=sm]/card:text-sm",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
data-slot="card-description"
|
|
53
|
+
className={cn("text-sm leading-6 text-muted-foreground", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
data-slot="card-action"
|
|
63
|
+
className={cn(
|
|
64
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="card-content"
|
|
76
|
+
className={cn("px-(--card-spacing)", className)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
data-slot="card-footer"
|
|
86
|
+
className={cn(
|
|
87
|
+
"flex items-center rounded-b-[var(--radius-2xl)] border-t border-border/70 bg-muted/45 p-(--card-spacing)",
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
Card,
|
|
97
|
+
CardHeader,
|
|
98
|
+
CardFooter,
|
|
99
|
+
CardTitle,
|
|
100
|
+
CardAction,
|
|
101
|
+
CardDescription,
|
|
102
|
+
CardContent,
|
|
103
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { CheckIcon, MinusIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
export type CheckboxCheckedState = boolean | "indeterminate"
|
|
7
|
+
|
|
8
|
+
export type CheckboxProps = Omit<
|
|
9
|
+
React.ComponentPropsWithoutRef<"button">,
|
|
10
|
+
"checked" | "defaultChecked" | "onChange" | "value"
|
|
11
|
+
> & {
|
|
12
|
+
checked?: CheckboxCheckedState
|
|
13
|
+
defaultChecked?: CheckboxCheckedState
|
|
14
|
+
onCheckedChange?: (checked: boolean) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getNextCheckedState(checked: CheckboxCheckedState) {
|
|
18
|
+
return checked === true ? false : true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
className,
|
|
25
|
+
checked,
|
|
26
|
+
defaultChecked = false,
|
|
27
|
+
onCheckedChange,
|
|
28
|
+
disabled,
|
|
29
|
+
onClick,
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
33
|
+
ref
|
|
34
|
+
) => {
|
|
35
|
+
const isControlled = checked !== undefined
|
|
36
|
+
const [internalChecked, setInternalChecked] = React.useState<CheckboxCheckedState>(defaultChecked)
|
|
37
|
+
const currentChecked = isControlled ? checked : internalChecked
|
|
38
|
+
const dataState = currentChecked === "indeterminate" ? "indeterminate" : currentChecked ? "checked" : "unchecked"
|
|
39
|
+
|
|
40
|
+
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
|
|
41
|
+
const nextChecked = getNextCheckedState(currentChecked)
|
|
42
|
+
|
|
43
|
+
if (!isControlled) {
|
|
44
|
+
setInternalChecked(nextChecked)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onCheckedChange?.(nextChecked)
|
|
48
|
+
onClick?.(event)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
ref={ref}
|
|
54
|
+
type="button"
|
|
55
|
+
role="checkbox"
|
|
56
|
+
aria-checked={currentChecked === "indeterminate" ? "mixed" : currentChecked}
|
|
57
|
+
data-state={dataState}
|
|
58
|
+
data-slot="checkbox"
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
className={cn(
|
|
61
|
+
"peer flex size-5 shrink-0 items-center justify-center rounded-[min(var(--radius-lg),8px)] border border-input/90 bg-background/92 text-primary-foreground shadow-sm outline-none transition-[background-color,border-color,box-shadow,color] hover:border-border focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/45 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:shadow-[0_10px_24px_color-mix(in_oklch,var(--primary),transparent_80%)] data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:shadow-[0_10px_24px_color-mix(in_oklch,var(--primary),transparent_80%)]",
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
onClick={handleClick}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
{children ?? (
|
|
68
|
+
<span className="flex items-center justify-center">
|
|
69
|
+
{currentChecked === "indeterminate" ? (
|
|
70
|
+
<MinusIcon className="size-3.5" />
|
|
71
|
+
) : currentChecked ? (
|
|
72
|
+
<CheckIcon className="size-3.5" />
|
|
73
|
+
) : null}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
</button>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
Checkbox.displayName = "Checkbox"
|
|
81
|
+
|
|
82
|
+
export { Checkbox }
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ChevronDownIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
export type CollapseProps = React.ComponentProps<"details"> & {
|
|
7
|
+
defaultOpen?: boolean
|
|
8
|
+
open?: boolean
|
|
9
|
+
onOpenChange?: (open: boolean) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function Collapse({ open, defaultOpen, onOpenChange, onToggle, className, children, ...props }: CollapseProps) {
|
|
13
|
+
const controlledProps = open === undefined ? { defaultOpen } : { open }
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<details
|
|
17
|
+
data-slot="collapse"
|
|
18
|
+
className={cn("group rounded-lg border bg-card text-card-foreground", className)}
|
|
19
|
+
onToggle={(event) => {
|
|
20
|
+
onToggle?.(event)
|
|
21
|
+
onOpenChange?.(event.currentTarget.open)
|
|
22
|
+
}}
|
|
23
|
+
{...controlledProps}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</details>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CollapseTriggerProps = React.ComponentProps<"summary"> & {
|
|
32
|
+
icon?: React.ReactNode
|
|
33
|
+
hideIcon?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CollapseTrigger({ icon, hideIcon = false, className, children, ...props }: CollapseTriggerProps) {
|
|
37
|
+
return (
|
|
38
|
+
<summary
|
|
39
|
+
data-slot="collapse-trigger"
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex cursor-pointer list-none items-center justify-between gap-3 rounded-lg px-4 py-3 text-sm font-medium outline-none transition-colors hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-details-marker]:hidden",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<span className="min-w-0 flex-1">{children}</span>
|
|
47
|
+
{!hideIcon && (
|
|
48
|
+
<span className="shrink-0 text-muted-foreground transition-transform group-open:rotate-180">
|
|
49
|
+
{icon ?? <ChevronDownIcon className="size-4" />}
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
</summary>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function CollapseContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
data-slot="collapse-content"
|
|
60
|
+
className={cn("border-t px-4 py-3 text-sm text-muted-foreground", className)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type CollapseItem = {
|
|
67
|
+
key: string
|
|
68
|
+
title: React.ReactNode
|
|
69
|
+
content: React.ReactNode
|
|
70
|
+
description?: React.ReactNode
|
|
71
|
+
disabled?: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type CollapseGroupProps = React.ComponentProps<"div"> & {
|
|
75
|
+
items: CollapseItem[]
|
|
76
|
+
type?: "single" | "multiple"
|
|
77
|
+
value?: string | string[]
|
|
78
|
+
defaultValue?: string | string[]
|
|
79
|
+
onValueChange?: (value: string | string[]) => void
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function CollapseGroup({ items, type = "multiple", value, defaultValue, onValueChange, className, ...props }: CollapseGroupProps) {
|
|
83
|
+
const initialValue = React.useMemo(() => {
|
|
84
|
+
if (value !== undefined) return value
|
|
85
|
+
if (defaultValue !== undefined) return defaultValue
|
|
86
|
+
return type === "single" ? "" : []
|
|
87
|
+
}, [defaultValue, type, value])
|
|
88
|
+
const [internalValue, setInternalValue] = React.useState<string | string[]>(initialValue)
|
|
89
|
+
const currentValue = value ?? internalValue
|
|
90
|
+
|
|
91
|
+
const isOpen = (key: string) => Array.isArray(currentValue) ? currentValue.includes(key) : currentValue === key
|
|
92
|
+
|
|
93
|
+
const updateValue = (key: string, nextOpen: boolean) => {
|
|
94
|
+
const nextValue = type === "single"
|
|
95
|
+
? nextOpen ? key : ""
|
|
96
|
+
: nextOpen
|
|
97
|
+
? Array.from(new Set([...(Array.isArray(currentValue) ? currentValue : []), key]))
|
|
98
|
+
: (Array.isArray(currentValue) ? currentValue.filter((item) => item !== key) : [])
|
|
99
|
+
|
|
100
|
+
if (value === undefined) setInternalValue(nextValue)
|
|
101
|
+
onValueChange?.(nextValue)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div data-slot="collapse-group" className={cn("grid gap-2", className)} {...props}>
|
|
106
|
+
{items.map((item) => (
|
|
107
|
+
<Collapse
|
|
108
|
+
key={item.key}
|
|
109
|
+
open={isOpen(item.key)}
|
|
110
|
+
onOpenChange={(open) => updateValue(item.key, open)}
|
|
111
|
+
className={cn(item.disabled && "pointer-events-none opacity-60")}
|
|
112
|
+
>
|
|
113
|
+
<CollapseTrigger>
|
|
114
|
+
<span className="grid gap-0.5">
|
|
115
|
+
<span>{item.title}</span>
|
|
116
|
+
{item.description && <span className="text-xs font-normal text-muted-foreground">{item.description}</span>}
|
|
117
|
+
</span>
|
|
118
|
+
</CollapseTrigger>
|
|
119
|
+
<CollapseContent>{item.content}</CollapseContent>
|
|
120
|
+
</Collapse>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { Collapse, CollapseContent, CollapseGroup, CollapseTrigger }
|