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,168 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { Card } from "@/components/ui/card"
|
|
4
|
+
import { Skeleton, SkeletonText } from "@/components/ui/skeleton"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
/** @deprecated Prefer `InfoCardVariant` via `InfoCard` or `CardFamily.Info` for new public usage. */
|
|
8
|
+
export type SmartCardVariant = "default" | "outline" | "elevated" | "ghost"
|
|
9
|
+
/** @deprecated Prefer `InfoCardSize` via `InfoCard` or `CardFamily.Info` for new public usage. */
|
|
10
|
+
export type SmartCardSize = "sm" | "md" | "lg"
|
|
11
|
+
/** @deprecated Prefer `InfoCardDensity` via `InfoCard` or `CardFamily.Info` for new public usage. */
|
|
12
|
+
export type SmartCardDensity = "compact" | "default" | "comfortable"
|
|
13
|
+
/** @deprecated Prefer `InfoCardOrientation` via `InfoCard` or `CardFamily.Info` for new public usage. */
|
|
14
|
+
export type SmartCardOrientation = "vertical" | "horizontal"
|
|
15
|
+
|
|
16
|
+
/** @deprecated Prefer `InfoCardClassNames` via `InfoCard` or `CardFamily.Info` for new public usage. */
|
|
17
|
+
export type SmartCardClassNames = {
|
|
18
|
+
root?: string
|
|
19
|
+
media?: string
|
|
20
|
+
header?: string
|
|
21
|
+
icon?: string
|
|
22
|
+
body?: string
|
|
23
|
+
eyebrow?: string
|
|
24
|
+
title?: string
|
|
25
|
+
description?: string
|
|
26
|
+
content?: string
|
|
27
|
+
footer?: string
|
|
28
|
+
actions?: string
|
|
29
|
+
meta?: string
|
|
30
|
+
status?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @deprecated Prefer `InfoCardRenderContext` via `InfoCard` or `CardFamily.Info` for new public usage. */
|
|
34
|
+
export type SmartCardRenderContext = {
|
|
35
|
+
title?: React.ReactNode
|
|
36
|
+
description?: React.ReactNode
|
|
37
|
+
eyebrow?: React.ReactNode
|
|
38
|
+
media?: React.ReactNode
|
|
39
|
+
icon?: React.ReactNode
|
|
40
|
+
status?: React.ReactNode
|
|
41
|
+
actions?: React.ReactNode
|
|
42
|
+
meta?: React.ReactNode
|
|
43
|
+
content?: React.ReactNode
|
|
44
|
+
footer?: React.ReactNode
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @deprecated Prefer `InfoCardProps` via `InfoCard` or `CardFamily.Info` for new public usage. */
|
|
48
|
+
export type SmartCardProps = Omit<React.ComponentProps<typeof Card>, "title" | "content" | "size"> & SmartCardRenderContext & {
|
|
49
|
+
orientation?: SmartCardOrientation
|
|
50
|
+
variant?: SmartCardVariant
|
|
51
|
+
size?: SmartCardSize
|
|
52
|
+
density?: SmartCardDensity
|
|
53
|
+
loading?: boolean
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
selected?: boolean
|
|
56
|
+
interactive?: boolean
|
|
57
|
+
classNames?: SmartCardClassNames
|
|
58
|
+
renderHeader?: (ctx: SmartCardRenderContext) => React.ReactNode
|
|
59
|
+
renderMedia?: (ctx: SmartCardRenderContext) => React.ReactNode
|
|
60
|
+
renderContent?: (ctx: SmartCardRenderContext) => React.ReactNode
|
|
61
|
+
renderFooter?: (ctx: SmartCardRenderContext) => React.ReactNode
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const variantClassName: Record<SmartCardVariant, string> = {
|
|
65
|
+
default: "bg-card",
|
|
66
|
+
outline: "border bg-card",
|
|
67
|
+
elevated: "border bg-card shadow-md",
|
|
68
|
+
ghost: "border-transparent bg-transparent shadow-none",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const densityClassName: Record<SmartCardDensity, string> = {
|
|
72
|
+
compact: "p-3",
|
|
73
|
+
default: "p-4",
|
|
74
|
+
comfortable: "p-5",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const titleClassName: Record<SmartCardSize, string> = {
|
|
78
|
+
sm: "text-sm",
|
|
79
|
+
md: "text-base",
|
|
80
|
+
lg: "text-lg",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @deprecated Prefer `InfoCard` or `CardFamily.Info` for new public usage. */
|
|
84
|
+
function SmartCard({
|
|
85
|
+
eyebrow,
|
|
86
|
+
title,
|
|
87
|
+
description,
|
|
88
|
+
media,
|
|
89
|
+
icon,
|
|
90
|
+
status,
|
|
91
|
+
actions,
|
|
92
|
+
meta,
|
|
93
|
+
content,
|
|
94
|
+
footer,
|
|
95
|
+
orientation = "vertical",
|
|
96
|
+
variant = "outline",
|
|
97
|
+
size = "md",
|
|
98
|
+
density = "default",
|
|
99
|
+
loading = false,
|
|
100
|
+
disabled = false,
|
|
101
|
+
selected = false,
|
|
102
|
+
interactive,
|
|
103
|
+
className,
|
|
104
|
+
classNames,
|
|
105
|
+
renderHeader,
|
|
106
|
+
renderMedia,
|
|
107
|
+
renderContent,
|
|
108
|
+
renderFooter,
|
|
109
|
+
children,
|
|
110
|
+
onClick,
|
|
111
|
+
...props
|
|
112
|
+
}: SmartCardProps) {
|
|
113
|
+
const ctx: SmartCardRenderContext = { eyebrow, title, description, media, icon, status, actions, meta, content, footer }
|
|
114
|
+
const clickable = Boolean(onClick || interactive)
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Card
|
|
118
|
+
data-slot="smart-card"
|
|
119
|
+
data-selected={selected || undefined}
|
|
120
|
+
data-disabled={disabled || undefined}
|
|
121
|
+
data-loading={loading || undefined}
|
|
122
|
+
className={cn(
|
|
123
|
+
"overflow-hidden transition-colors data-[selected=true]:border-primary data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-55",
|
|
124
|
+
variantClassName[variant],
|
|
125
|
+
clickable && "cursor-pointer hover:bg-muted/35",
|
|
126
|
+
orientation === "horizontal" && "flex",
|
|
127
|
+
className,
|
|
128
|
+
classNames?.root
|
|
129
|
+
)}
|
|
130
|
+
onClick={disabled ? undefined : onClick}
|
|
131
|
+
{...props}
|
|
132
|
+
>
|
|
133
|
+
{loading ? (
|
|
134
|
+
<div className={cn("grid gap-3", densityClassName[density])}>
|
|
135
|
+
<Skeleton className="h-5 w-1/2" />
|
|
136
|
+
<SkeletonText rows={3} />
|
|
137
|
+
</div>
|
|
138
|
+
) : (
|
|
139
|
+
<>
|
|
140
|
+
{media && (renderMedia?.(ctx) ?? <div data-slot="smart-card-media" className={cn("bg-muted", orientation === "horizontal" ? "w-40 shrink-0" : "aspect-video", classNames?.media)}>{media}</div>)}
|
|
141
|
+
<div data-slot="smart-card-body" className={cn("grid min-w-0 flex-1 gap-3", densityClassName[density], classNames?.body)}>
|
|
142
|
+
{renderHeader?.(ctx) ?? (
|
|
143
|
+
<div data-slot="smart-card-header" className={cn("flex items-start justify-between gap-3", classNames?.header)}>
|
|
144
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
145
|
+
{icon && <div data-slot="smart-card-icon" className={cn("flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground", classNames?.icon)}>{icon}</div>}
|
|
146
|
+
<div className="grid min-w-0 gap-1">
|
|
147
|
+
{eyebrow && <div data-slot="smart-card-eyebrow" className={cn("text-xs font-medium uppercase tracking-wide text-muted-foreground", classNames?.eyebrow)}>{eyebrow}</div>}
|
|
148
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
149
|
+
{title && <div data-slot="smart-card-title" className={cn("truncate font-semibold text-foreground", titleClassName[size], classNames?.title)}>{title}</div>}
|
|
150
|
+
{status && <div data-slot="smart-card-status" className={classNames?.status}>{status}</div>}
|
|
151
|
+
</div>
|
|
152
|
+
{description && <div data-slot="smart-card-description" className={cn("line-clamp-2 text-sm text-muted-foreground", classNames?.description)}>{description}</div>}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
{actions && <div data-slot="smart-card-actions" className={cn("shrink-0", classNames?.actions)} onClick={(event) => event.stopPropagation()}>{actions}</div>}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{meta && <div data-slot="smart-card-meta" className={cn("text-xs text-muted-foreground", classNames?.meta)}>{meta}</div>}
|
|
159
|
+
{(content || children) && (renderContent?.(ctx) ?? <div data-slot="smart-card-content" className={classNames?.content}>{content ?? children}</div>)}
|
|
160
|
+
{footer && (renderFooter?.(ctx) ?? <div data-slot="smart-card-footer" className={cn("border-t pt-3 text-sm text-muted-foreground", classNames?.footer)}>{footer}</div>)}
|
|
161
|
+
</div>
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
</Card>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { SmartCard }
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ArrowDownIcon, ArrowUpIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
export type StatisticTrend = "up" | "down" | "neutral"
|
|
8
|
+
|
|
9
|
+
export type StatisticProps = React.ComponentProps<"div"> & {
|
|
10
|
+
label: React.ReactNode
|
|
11
|
+
value: React.ReactNode
|
|
12
|
+
prefix?: React.ReactNode
|
|
13
|
+
suffix?: React.ReactNode
|
|
14
|
+
description?: React.ReactNode
|
|
15
|
+
trend?: StatisticTrend
|
|
16
|
+
change?: React.ReactNode
|
|
17
|
+
loading?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Statistic({ label, value, prefix, suffix, description, trend = "neutral", change, loading = false, className, ...props }: StatisticProps) {
|
|
21
|
+
const trendIcon = trend === "up" ? <ArrowUpIcon className="size-3" /> : trend === "down" ? <ArrowDownIcon className="size-3" /> : null
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div data-slot="statistic" className={cn("grid gap-1", className)} {...props}>
|
|
25
|
+
<div className="text-sm text-muted-foreground">{label}</div>
|
|
26
|
+
<div className="flex flex-wrap items-baseline gap-1.5">
|
|
27
|
+
{loading ? (
|
|
28
|
+
<div className="h-8 w-28 animate-pulse rounded-md bg-muted" />
|
|
29
|
+
) : (
|
|
30
|
+
<>
|
|
31
|
+
{prefix && <span className="text-base text-muted-foreground">{prefix}</span>}
|
|
32
|
+
<span className="text-2xl font-semibold tracking-tight">{value}</span>
|
|
33
|
+
{suffix && <span className="text-sm text-muted-foreground">{suffix}</span>}
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
{(description || change) && (
|
|
38
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
39
|
+
{change && (
|
|
40
|
+
<span
|
|
41
|
+
className={cn(
|
|
42
|
+
"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 font-medium",
|
|
43
|
+
trend === "up" && "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
|
44
|
+
trend === "down" && "bg-destructive/10 text-destructive",
|
|
45
|
+
trend === "neutral" && "bg-muted text-muted-foreground"
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{trendIcon}
|
|
49
|
+
{change}
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
{description && <span>{description}</span>}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type StatisticCardProps = React.ComponentProps<typeof Card> & StatisticProps & {
|
|
60
|
+
action?: React.ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function StatisticCard({ action, label, value, prefix, suffix, description, trend, change, loading, className, ...props }: StatisticCardProps) {
|
|
64
|
+
return (
|
|
65
|
+
<Card data-slot="statistic-card" className={className} {...props}>
|
|
66
|
+
<CardHeader className="flex flex-row items-center justify-between gap-3 pb-2">
|
|
67
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
|
|
68
|
+
{action}
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent>
|
|
71
|
+
<Statistic
|
|
72
|
+
label={<span className="sr-only">{label}</span>}
|
|
73
|
+
value={value}
|
|
74
|
+
prefix={prefix}
|
|
75
|
+
suffix={suffix}
|
|
76
|
+
description={description}
|
|
77
|
+
trend={trend}
|
|
78
|
+
change={change}
|
|
79
|
+
loading={loading}
|
|
80
|
+
/>
|
|
81
|
+
</CardContent>
|
|
82
|
+
</Card>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type StatisticGridProps = React.ComponentProps<"div"> & {
|
|
87
|
+
columns?: 1 | 2 | 3 | 4
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function StatisticGrid({ columns = 4, className, ...props }: StatisticGridProps) {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
data-slot="statistic-grid"
|
|
94
|
+
className={cn(
|
|
95
|
+
"grid gap-4",
|
|
96
|
+
columns === 1 && "grid-cols-1",
|
|
97
|
+
columns === 2 && "grid-cols-1 sm:grid-cols-2",
|
|
98
|
+
columns === 3 && "grid-cols-1 sm:grid-cols-2 xl:grid-cols-3",
|
|
99
|
+
columns === 4 && "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4",
|
|
100
|
+
className
|
|
101
|
+
)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { Statistic, StatisticCard, StatisticGrid }
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
4
|
+
import { Badge } from "@/components/ui/badge"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
export type StatusLegendTone = "default" | "success" | "warning" | "danger" | "info" | "muted"
|
|
8
|
+
export type StatusLegendOrientation = "vertical" | "horizontal" | "grid"
|
|
9
|
+
|
|
10
|
+
export type StatusLegendItem = {
|
|
11
|
+
key: string
|
|
12
|
+
label: React.ReactNode
|
|
13
|
+
description?: React.ReactNode
|
|
14
|
+
count?: React.ReactNode
|
|
15
|
+
tone?: StatusLegendTone
|
|
16
|
+
icon?: React.ReactNode
|
|
17
|
+
hidden?: boolean
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type StatusLegendProps = React.ComponentProps<typeof Card> & {
|
|
22
|
+
title?: React.ReactNode
|
|
23
|
+
description?: React.ReactNode
|
|
24
|
+
actions?: React.ReactNode
|
|
25
|
+
items: StatusLegendItem[]
|
|
26
|
+
orientation?: StatusLegendOrientation
|
|
27
|
+
compact?: boolean
|
|
28
|
+
showCounts?: boolean
|
|
29
|
+
contentClassName?: string
|
|
30
|
+
itemClassName?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const dotClassName: Record<StatusLegendTone, string> = {
|
|
34
|
+
default: "bg-primary",
|
|
35
|
+
success: "bg-emerald-500",
|
|
36
|
+
warning: "bg-amber-500",
|
|
37
|
+
danger: "bg-destructive",
|
|
38
|
+
info: "bg-blue-500",
|
|
39
|
+
muted: "bg-muted-foreground",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function StatusLegend({
|
|
43
|
+
title,
|
|
44
|
+
description,
|
|
45
|
+
actions,
|
|
46
|
+
items,
|
|
47
|
+
orientation = "vertical",
|
|
48
|
+
compact = false,
|
|
49
|
+
showCounts = true,
|
|
50
|
+
contentClassName,
|
|
51
|
+
itemClassName,
|
|
52
|
+
className,
|
|
53
|
+
...props
|
|
54
|
+
}: StatusLegendProps) {
|
|
55
|
+
const visibleItems = items.filter((item) => !item.hidden)
|
|
56
|
+
const hasHeader = Boolean(title || description || actions)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Card data-slot="status-legend" className={cn("min-w-0", className)} {...props}>
|
|
60
|
+
{hasHeader && (
|
|
61
|
+
<CardHeader className={cn(compact && "p-4 pb-2")}>
|
|
62
|
+
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
63
|
+
<div className="min-w-0 space-y-1">
|
|
64
|
+
{title && <CardTitle>{title}</CardTitle>}
|
|
65
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
66
|
+
</div>
|
|
67
|
+
{actions && <div className="shrink-0">{actions}</div>}
|
|
68
|
+
</div>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
<CardContent
|
|
73
|
+
className={cn(
|
|
74
|
+
"gap-2",
|
|
75
|
+
orientation === "vertical" && "grid",
|
|
76
|
+
orientation === "horizontal" && "flex flex-wrap",
|
|
77
|
+
orientation === "grid" && "grid sm:grid-cols-2",
|
|
78
|
+
compact && "p-4 pt-2",
|
|
79
|
+
contentClassName
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{visibleItems.map((item) => (
|
|
83
|
+
<div
|
|
84
|
+
key={item.key}
|
|
85
|
+
data-slot="status-legend-item"
|
|
86
|
+
data-tone={item.tone ?? "default"}
|
|
87
|
+
className={cn("flex min-w-0 items-start justify-between gap-3 rounded-lg border bg-muted/20 p-3", compact && "p-2", itemClassName, item.className)}
|
|
88
|
+
>
|
|
89
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
90
|
+
{item.icon ? (
|
|
91
|
+
<span className="mt-0.5 shrink-0 text-muted-foreground [&_svg]:size-4">{item.icon}</span>
|
|
92
|
+
) : (
|
|
93
|
+
<span className={cn("mt-1.5 size-2.5 shrink-0 rounded-full", dotClassName[item.tone ?? "default"])} />
|
|
94
|
+
)}
|
|
95
|
+
<div className="min-w-0 space-y-0.5">
|
|
96
|
+
<div className="truncate text-sm font-medium text-foreground">{item.label}</div>
|
|
97
|
+
{item.description && <div className="text-xs leading-5 text-muted-foreground">{item.description}</div>}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{showCounts && item.count !== undefined && <Badge variant="secondary" className="shrink-0">{item.count}</Badge>}
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</CardContent>
|
|
104
|
+
</Card>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { StatusLegend }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { XIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { Badge } from "@/components/ui/badge"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
export type TagListItem = {
|
|
8
|
+
key: string
|
|
9
|
+
label: React.ReactNode
|
|
10
|
+
variant?: React.ComponentProps<typeof Badge>["variant"]
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TagListProps = React.ComponentProps<"div"> & {
|
|
15
|
+
items: TagListItem[]
|
|
16
|
+
max?: number
|
|
17
|
+
removable?: boolean
|
|
18
|
+
onRemove?: (item: TagListItem) => void
|
|
19
|
+
overflowLabel?: (count: number) => React.ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TagList({ items, max, removable = false, onRemove, overflowLabel = (count) => `+${count}`, className, ...props }: TagListProps) {
|
|
23
|
+
const visibleItems = typeof max === "number" ? items.slice(0, max) : items
|
|
24
|
+
const overflowCount = typeof max === "number" ? Math.max(items.length - max, 0) : 0
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div data-slot="tag-list" className={cn("flex flex-wrap items-center gap-1.5", className)} {...props}>
|
|
28
|
+
{visibleItems.map((item) => (
|
|
29
|
+
<Badge
|
|
30
|
+
key={item.key}
|
|
31
|
+
variant={item.variant}
|
|
32
|
+
className={cn("gap-1", item.disabled && "opacity-55")}
|
|
33
|
+
>
|
|
34
|
+
<span>{item.label}</span>
|
|
35
|
+
{removable && !item.disabled && (
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
aria-label="Remove tag"
|
|
39
|
+
className="rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
40
|
+
onClick={() => onRemove?.(item)}
|
|
41
|
+
>
|
|
42
|
+
<XIcon className="size-3" />
|
|
43
|
+
</button>
|
|
44
|
+
)}
|
|
45
|
+
</Badge>
|
|
46
|
+
))}
|
|
47
|
+
{overflowCount > 0 && <Badge variant="outline">{overflowLabel(overflowCount)}</Badge>}
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { TagList }
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { CircleIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
export type TimelineTone = "default" | "success" | "info" | "warning" | "danger" | "muted"
|
|
7
|
+
export type TimelineOrientation = "vertical" | "horizontal"
|
|
8
|
+
|
|
9
|
+
export type TimelineItem = {
|
|
10
|
+
key: string
|
|
11
|
+
title?: React.ReactNode
|
|
12
|
+
description?: React.ReactNode
|
|
13
|
+
time?: React.ReactNode
|
|
14
|
+
icon?: React.ReactNode
|
|
15
|
+
tone?: TimelineTone
|
|
16
|
+
content?: React.ReactNode
|
|
17
|
+
actions?: React.ReactNode
|
|
18
|
+
hidden?: boolean
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TimelineProps = React.ComponentProps<"div"> & {
|
|
23
|
+
items: TimelineItem[]
|
|
24
|
+
orientation?: TimelineOrientation
|
|
25
|
+
pending?: boolean
|
|
26
|
+
pendingLabel?: React.ReactNode
|
|
27
|
+
compact?: boolean
|
|
28
|
+
itemClassName?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const dotClassName: Record<TimelineTone, string> = {
|
|
32
|
+
default: "border-primary bg-primary text-primary-foreground",
|
|
33
|
+
success: "border-emerald-500 bg-emerald-500 text-white",
|
|
34
|
+
info: "border-blue-500 bg-blue-500 text-white",
|
|
35
|
+
warning: "border-amber-500 bg-amber-500 text-white",
|
|
36
|
+
danger: "border-destructive bg-destructive text-destructive-foreground",
|
|
37
|
+
muted: "border-muted-foreground bg-muted-foreground text-background",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Timeline({
|
|
41
|
+
items,
|
|
42
|
+
orientation = "vertical",
|
|
43
|
+
pending = false,
|
|
44
|
+
pendingLabel = "Pending",
|
|
45
|
+
compact = false,
|
|
46
|
+
itemClassName,
|
|
47
|
+
className,
|
|
48
|
+
...props
|
|
49
|
+
}: TimelineProps) {
|
|
50
|
+
const visibleItems = items.filter((item) => !item.hidden)
|
|
51
|
+
|
|
52
|
+
if (orientation === "horizontal") {
|
|
53
|
+
return (
|
|
54
|
+
<div data-slot="timeline" data-orientation="horizontal" className={cn("overflow-x-auto", className)} {...props}>
|
|
55
|
+
<div className="flex min-w-max gap-3 pb-1">
|
|
56
|
+
{visibleItems.map((item) => (
|
|
57
|
+
<TimelineHorizontalItem key={item.key} item={item} compact={compact} className={itemClassName} />
|
|
58
|
+
))}
|
|
59
|
+
{pending && <TimelineHorizontalItem item={{ key: "pending", title: pendingLabel, tone: "muted" }} compact={compact} className={itemClassName} />}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div data-slot="timeline" data-orientation="vertical" className={cn("grid gap-0", className)} {...props}>
|
|
67
|
+
{visibleItems.map((item, index) => (
|
|
68
|
+
<TimelineVerticalItem
|
|
69
|
+
key={item.key}
|
|
70
|
+
item={item}
|
|
71
|
+
compact={compact}
|
|
72
|
+
className={itemClassName}
|
|
73
|
+
isLast={index === visibleItems.length - 1 && !pending}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
{pending && <TimelineVerticalItem item={{ key: "pending", title: pendingLabel, tone: "muted" }} compact={compact} className={itemClassName} isLast />}
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function TimelineDot({ item }: { item: TimelineItem }) {
|
|
82
|
+
const tone = item.tone ?? "default"
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<span
|
|
86
|
+
data-slot="timeline-dot"
|
|
87
|
+
data-tone={tone}
|
|
88
|
+
className={cn("flex size-7 shrink-0 items-center justify-center rounded-full border text-[10px]", dotClassName[tone])}
|
|
89
|
+
>
|
|
90
|
+
{item.icon ?? <CircleIcon className="size-2 fill-current" />}
|
|
91
|
+
</span>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function TimelineVerticalItem({ item, compact, className, isLast }: { item: TimelineItem; compact: boolean; className?: string; isLast?: boolean }) {
|
|
96
|
+
return (
|
|
97
|
+
<div data-slot="timeline-item" className={cn("grid grid-cols-[auto_1fr] gap-3", className, item.className)}>
|
|
98
|
+
<div className="flex flex-col items-center">
|
|
99
|
+
<TimelineDot item={item} />
|
|
100
|
+
{!isLast && <div data-slot="timeline-line" className="w-px flex-1 bg-border" />}
|
|
101
|
+
</div>
|
|
102
|
+
<div className={cn("min-w-0 pb-5", compact && "pb-3")}>
|
|
103
|
+
<div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
|
|
104
|
+
<div className="min-w-0">
|
|
105
|
+
{item.title && <div className="text-sm font-medium text-foreground">{item.title}</div>}
|
|
106
|
+
{item.description && <div className="text-sm text-muted-foreground">{item.description}</div>}
|
|
107
|
+
</div>
|
|
108
|
+
{item.time && <div className="shrink-0 text-xs text-muted-foreground">{item.time}</div>}
|
|
109
|
+
</div>
|
|
110
|
+
{item.content && <div className="mt-2 text-sm text-muted-foreground">{item.content}</div>}
|
|
111
|
+
{item.actions && <div className="mt-2">{item.actions}</div>}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function TimelineHorizontalItem({ item, compact, className }: { item: TimelineItem; compact: boolean; className?: string }) {
|
|
118
|
+
return (
|
|
119
|
+
<div data-slot="timeline-item" className={cn("min-w-48 rounded-xl border bg-card p-3", compact && "min-w-40 p-2", className, item.className)}>
|
|
120
|
+
<div className="mb-3 flex items-center gap-2">
|
|
121
|
+
<TimelineDot item={item} />
|
|
122
|
+
{item.time && <div className="text-xs text-muted-foreground">{item.time}</div>}
|
|
123
|
+
</div>
|
|
124
|
+
{item.title && <div className="text-sm font-medium">{item.title}</div>}
|
|
125
|
+
{item.description && <div className="mt-1 text-xs text-muted-foreground">{item.description}</div>}
|
|
126
|
+
{item.content && <div className="mt-2 text-sm text-muted-foreground">{item.content}</div>}
|
|
127
|
+
{item.actions && <div className="mt-2">{item.actions}</div>}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { Timeline }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ChevronRightIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
export type TreeViewItem = {
|
|
7
|
+
key: string
|
|
8
|
+
label: React.ReactNode
|
|
9
|
+
icon?: React.ReactNode
|
|
10
|
+
extra?: React.ReactNode
|
|
11
|
+
children?: TreeViewItem[]
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TreeViewProps = React.ComponentProps<"div"> & {
|
|
16
|
+
items: TreeViewItem[]
|
|
17
|
+
defaultExpandedKeys?: string[]
|
|
18
|
+
expandedKeys?: string[]
|
|
19
|
+
onExpandedKeysChange?: (keys: string[]) => void
|
|
20
|
+
selectedKey?: string
|
|
21
|
+
onSelect?: (item: TreeViewItem) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function TreeView({ items, defaultExpandedKeys = [], expandedKeys, onExpandedKeysChange, selectedKey, onSelect, className, ...props }: TreeViewProps) {
|
|
25
|
+
const [internalExpandedKeys, setInternalExpandedKeys] = React.useState(defaultExpandedKeys)
|
|
26
|
+
const currentExpandedKeys = expandedKeys ?? internalExpandedKeys
|
|
27
|
+
|
|
28
|
+
const setExpandedKeys = (nextKeys: string[]) => {
|
|
29
|
+
if (expandedKeys === undefined) setInternalExpandedKeys(nextKeys)
|
|
30
|
+
onExpandedKeysChange?.(nextKeys)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const toggle = (key: string) => {
|
|
34
|
+
setExpandedKeys(currentExpandedKeys.includes(key) ? currentExpandedKeys.filter((item) => item !== key) : [...currentExpandedKeys, key])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div data-slot="tree-view" role="tree" className={cn("grid gap-1 text-sm", className)} {...props}>
|
|
39
|
+
{items.map((item) => (
|
|
40
|
+
<TreeViewNode
|
|
41
|
+
key={item.key}
|
|
42
|
+
item={item}
|
|
43
|
+
level={1}
|
|
44
|
+
expandedKeys={currentExpandedKeys}
|
|
45
|
+
selectedKey={selectedKey}
|
|
46
|
+
onToggle={toggle}
|
|
47
|
+
onSelect={onSelect}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function TreeViewNode({
|
|
55
|
+
item,
|
|
56
|
+
level,
|
|
57
|
+
expandedKeys,
|
|
58
|
+
selectedKey,
|
|
59
|
+
onToggle,
|
|
60
|
+
onSelect,
|
|
61
|
+
}: {
|
|
62
|
+
item: TreeViewItem
|
|
63
|
+
level: number
|
|
64
|
+
expandedKeys: string[]
|
|
65
|
+
selectedKey?: string
|
|
66
|
+
onToggle: (key: string) => void
|
|
67
|
+
onSelect?: (item: TreeViewItem) => void
|
|
68
|
+
}) {
|
|
69
|
+
const hasChildren = Boolean(item.children?.length)
|
|
70
|
+
const expanded = expandedKeys.includes(item.key)
|
|
71
|
+
const selected = selectedKey === item.key
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div data-slot="tree-node" role="treeitem" aria-expanded={hasChildren ? expanded : undefined} aria-selected={selected || undefined}>
|
|
75
|
+
<div
|
|
76
|
+
className={cn(
|
|
77
|
+
"flex items-center gap-1 rounded-md px-2 py-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
78
|
+
selected && "bg-muted text-foreground",
|
|
79
|
+
item.disabled && "pointer-events-none opacity-50"
|
|
80
|
+
)}
|
|
81
|
+
style={{ paddingLeft: `${level * 0.75}rem` }}
|
|
82
|
+
>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
aria-label={expanded ? "Collapse" : "Expand"}
|
|
86
|
+
className={cn("flex size-5 items-center justify-center rounded-sm", !hasChildren && "invisible")}
|
|
87
|
+
onClick={() => onToggle(item.key)}
|
|
88
|
+
>
|
|
89
|
+
<ChevronRightIcon className={cn("size-4 transition-transform", expanded && "rotate-90")} />
|
|
90
|
+
</button>
|
|
91
|
+
{item.icon}
|
|
92
|
+
<button type="button" className="min-w-0 flex-1 truncate text-left" onClick={() => onSelect?.(item)}>
|
|
93
|
+
{item.label}
|
|
94
|
+
</button>
|
|
95
|
+
{item.extra}
|
|
96
|
+
</div>
|
|
97
|
+
{hasChildren && expanded && (
|
|
98
|
+
<div role="group" className="grid gap-1">
|
|
99
|
+
{item.children?.map((child) => (
|
|
100
|
+
<TreeViewNode
|
|
101
|
+
key={child.key}
|
|
102
|
+
item={child}
|
|
103
|
+
level={level + 1}
|
|
104
|
+
expandedKeys={expandedKeys}
|
|
105
|
+
selectedKey={selectedKey}
|
|
106
|
+
onToggle={onToggle}
|
|
107
|
+
onSelect={onSelect}
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { TreeView }
|