azamat-ui-kit-cli 0.2.2 → 0.3.4
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 +11 -0
- package/dist/index.cjs +452 -0
- package/package.json +2 -2
- package/vendor/src/components/actions/action-menu.tsx +21 -18
- package/vendor/src/components/calendar/calendar.tsx +153 -102
- package/vendor/src/components/calendar/date-picker.tsx +24 -14
- package/vendor/src/components/calendar/date-range-picker.tsx +137 -58
- package/vendor/src/components/charts/charts.tsx +32 -21
- package/vendor/src/components/command/command-palette.tsx +68 -57
- package/vendor/src/components/data-table/data-table-bulk-actions.tsx +23 -20
- package/vendor/src/components/data-table/data-table-column-visibility-menu.tsx +21 -10
- package/vendor/src/components/data-table/data-table-pagination.tsx +6 -6
- package/vendor/src/components/data-table/data-table-toolbar.tsx +72 -44
- package/vendor/src/components/data-table/data-table.tsx +15 -11
- package/vendor/src/components/data-table/table-export-menu.tsx +1 -1
- package/vendor/src/components/data-table/table-import-button.tsx +1 -1
- package/vendor/src/components/display/data-state.tsx +20 -8
- package/vendor/src/components/display/index.ts +19 -15
- package/vendor/src/components/display/metric-card.tsx +35 -0
- package/vendor/src/components/display/progress-circle.tsx +24 -0
- package/vendor/src/components/display/smart-card.tsx +49 -27
- package/vendor/src/components/display/status-dot.tsx +45 -0
- package/vendor/src/components/display/user-card.tsx +30 -0
- package/vendor/src/components/feedback/alert.tsx +21 -11
- package/vendor/src/components/feedback/empty-state.tsx +2 -2
- package/vendor/src/components/feedback/loading-state.tsx +2 -2
- package/vendor/src/components/feedback/page-state.tsx +19 -15
- package/vendor/src/components/feedback/status-badge.tsx +43 -43
- package/vendor/src/components/form/form-app-input.tsx +147 -0
- package/vendor/src/components/form/form-date-input.tsx +16 -19
- package/vendor/src/components/form/form-field-shell.tsx +11 -8
- package/vendor/src/components/form/form-field-utils.ts +76 -0
- package/vendor/src/components/form/form-input.tsx +423 -44
- package/vendor/src/components/form/form-number-input.tsx +16 -15
- package/vendor/src/components/form/form-phone-input.tsx +15 -9
- package/vendor/src/components/form/form-search-input.tsx +16 -19
- package/vendor/src/components/form/form-select.tsx +4 -3
- package/vendor/src/components/form/public.ts +16 -14
- package/vendor/src/components/form/smart-form-shell.tsx +13 -12
- package/vendor/src/components/inputs/app-input.tsx +27 -0
- package/vendor/src/components/inputs/async-select.tsx +113 -84
- package/vendor/src/components/inputs/clearable-input.tsx +81 -61
- package/vendor/src/components/inputs/date-input.tsx +21 -17
- package/vendor/src/components/inputs/date-range-input.tsx +10 -10
- package/vendor/src/components/inputs/index.ts +1 -0
- package/vendor/src/components/inputs/input-decorator.tsx +101 -57
- package/vendor/src/components/inputs/masked-input.tsx +20 -20
- package/vendor/src/components/inputs/money-input.tsx +2 -2
- package/vendor/src/components/inputs/number-input.tsx +29 -19
- package/vendor/src/components/inputs/password-input.tsx +82 -45
- package/vendor/src/components/inputs/phone-input.tsx +24 -2
- package/vendor/src/components/inputs/quantity-input.tsx +2 -2
- package/vendor/src/components/inputs/search-input.tsx +54 -3
- package/vendor/src/components/inputs/simple-select.tsx +110 -22
- package/vendor/src/components/layout/app-shell.tsx +2 -2
- package/vendor/src/components/layout/index.ts +5 -4
- package/vendor/src/components/layout/page-header.tsx +79 -35
- package/vendor/src/components/layout/public.ts +12 -10
- package/vendor/src/components/layout/section-header.tsx +56 -0
- package/vendor/src/components/layout/stack.tsx +106 -0
- package/vendor/src/components/layout/stat-card.tsx +66 -29
- package/vendor/src/components/navigation/index.ts +1 -0
- package/vendor/src/components/navigation/nav-tabs.tsx +60 -0
- package/vendor/src/components/navigation/page-tabs.tsx +41 -26
- package/vendor/src/components/navigation/pagination.tsx +14 -10
- package/vendor/src/components/overlay/alert-dialog.tsx +65 -0
- package/vendor/src/components/overlay/drawer.tsx +71 -0
- package/vendor/src/components/overlay/index.ts +4 -2
- package/vendor/src/components/patterns/data-view.tsx +13 -8
- package/vendor/src/components/ui/badge.tsx +96 -52
- package/vendor/src/components/ui/button.tsx +99 -61
- package/vendor/src/components/ui/card.tsx +84 -25
- package/vendor/src/components/ui/checkbox.tsx +68 -68
- package/vendor/src/components/ui/command.tsx +32 -32
- package/vendor/src/components/ui/dialog.tsx +135 -138
- package/vendor/src/components/ui/dropdown-menu.tsx +21 -21
- package/vendor/src/components/ui/hover-card.tsx +49 -0
- package/vendor/src/components/ui/input-primitive.tsx +24 -0
- package/vendor/src/components/ui/input.tsx +191 -20
- package/vendor/src/components/ui/kbd.tsx +33 -0
- package/vendor/src/components/ui/popover.tsx +11 -11
- package/vendor/src/components/ui/radio-group.tsx +102 -0
- package/vendor/src/components/ui/right-click-menu.tsx +60 -0
- package/vendor/src/components/ui/scroll-box.tsx +27 -0
- package/vendor/src/components/ui/segmented-control.tsx +21 -17
- package/vendor/src/components/ui/select.tsx +187 -189
- package/vendor/src/components/ui/skeleton.tsx +2 -2
- package/vendor/src/components/ui/switch.tsx +60 -60
- package/vendor/src/components/ui/table.tsx +114 -114
- package/vendor/src/components/ui/tabs.tsx +2 -2
- package/vendor/src/components/ui/textarea.tsx +1 -1
- package/vendor/src/components/upload/file-dropzone.tsx +38 -0
- package/vendor/src/components/upload/file-upload.tsx +4 -4
- package/vendor/src/components/upload/image-upload.tsx +22 -19
- package/vendor/src/components/upload/index.ts +2 -0
- package/vendor/src/families/catalog.ts +1 -0
- package/vendor/src/families/docs-groups.ts +10 -1
- package/vendor/src/families/member-metadata.ts +24 -0
- package/vendor/src/families/member-snippets.ts +41 -2
- package/vendor/src/families/migration-map.ts +3 -0
- package/vendor/src/index.ts +23 -18
- package/vendor/templates/styles/globals.css +253 -0
- package/dist/index.js +0 -432
|
@@ -1,21 +1,72 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { SearchIcon } from "lucide-react"
|
|
2
|
+
import { LoaderCircleIcon, SearchIcon } from "lucide-react"
|
|
3
3
|
|
|
4
4
|
import { ClearableInput, type ClearableInputProps } from "@/components/inputs/clearable-input"
|
|
5
5
|
|
|
6
|
-
export type SearchInputProps = Omit<ClearableInputProps, "leadingIcon" | "type"> & {
|
|
6
|
+
export type SearchInputProps = Omit<ClearableInputProps, "leadingIcon" | "type" | "onValueChange"> & {
|
|
7
7
|
searchIcon?: React.ReactNode
|
|
8
|
+
loading?: boolean
|
|
9
|
+
loadingLabel?: string
|
|
10
|
+
resultCount?: number
|
|
11
|
+
shortcut?: React.ReactNode
|
|
12
|
+
debounceMs?: number
|
|
13
|
+
onValueChange?: (value: string) => void
|
|
14
|
+
onDebouncedValueChange?: (value: string) => void
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
|
|
11
|
-
(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
searchIcon,
|
|
21
|
+
loading = false,
|
|
22
|
+
loadingLabel = "Searching",
|
|
23
|
+
resultCount,
|
|
24
|
+
shortcut,
|
|
25
|
+
debounceMs,
|
|
26
|
+
value,
|
|
27
|
+
onValueChange,
|
|
28
|
+
onDebouncedValueChange,
|
|
29
|
+
placeholder = "Search...",
|
|
30
|
+
inputMode = "search",
|
|
31
|
+
trailing,
|
|
32
|
+
disabled,
|
|
33
|
+
...props
|
|
34
|
+
},
|
|
35
|
+
ref
|
|
36
|
+
) => {
|
|
37
|
+
const stringValue = value == null ? "" : String(value)
|
|
38
|
+
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
if (debounceMs == null || !onDebouncedValueChange) return
|
|
41
|
+
const timeoutId = window.setTimeout(() => onDebouncedValueChange(stringValue), debounceMs)
|
|
42
|
+
return () => window.clearTimeout(timeoutId)
|
|
43
|
+
}, [debounceMs, onDebouncedValueChange, stringValue])
|
|
44
|
+
|
|
45
|
+
const meta = (
|
|
46
|
+
<span data-slot="search-input-meta" className="inline-flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
|
|
47
|
+
{loading ? (
|
|
48
|
+
<span aria-live="polite" className="inline-flex items-center gap-1.5">
|
|
49
|
+
<LoaderCircleIcon className="size-3.5 animate-spin" aria-hidden="true" />
|
|
50
|
+
<span className="sr-only">{loadingLabel}</span>
|
|
51
|
+
</span>
|
|
52
|
+
) : null}
|
|
53
|
+
{typeof resultCount === "number" ? <span>{resultCount}</span> : null}
|
|
54
|
+
{shortcut ? <span className="rounded-md border border-border/80 bg-muted/60 px-1.5 py-0.5 font-mono">{shortcut}</span> : null}
|
|
55
|
+
{trailing}
|
|
56
|
+
</span>
|
|
57
|
+
)
|
|
58
|
+
|
|
12
59
|
return (
|
|
13
60
|
<ClearableInput
|
|
14
61
|
ref={ref}
|
|
15
62
|
type="search"
|
|
16
63
|
inputMode={inputMode}
|
|
17
64
|
placeholder={placeholder}
|
|
65
|
+
value={value}
|
|
66
|
+
disabled={disabled || loading}
|
|
18
67
|
leadingIcon={searchIcon ?? <SearchIcon />}
|
|
68
|
+
trailing={meta}
|
|
69
|
+
onValueChange={onValueChange}
|
|
19
70
|
{...props}
|
|
20
71
|
/>
|
|
21
72
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
+
import { CheckIcon, LoaderCircleIcon, SearchIcon, XIcon } from "lucide-react"
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
Select,
|
|
@@ -14,6 +15,7 @@ export type SimpleSelectOption = {
|
|
|
14
15
|
value: string
|
|
15
16
|
disabled?: boolean
|
|
16
17
|
description?: React.ReactNode
|
|
18
|
+
keywords?: string[]
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export type SimpleSelectProps = Omit<
|
|
@@ -21,13 +23,32 @@ export type SimpleSelectProps = Omit<
|
|
|
21
23
|
"value" | "onValueChange"
|
|
22
24
|
> & {
|
|
23
25
|
value?: string
|
|
24
|
-
onValueChange?: (value: string) => void
|
|
26
|
+
onValueChange?: (value: string | undefined) => void
|
|
25
27
|
options: SimpleSelectOption[]
|
|
26
28
|
placeholder?: string
|
|
29
|
+
searchPlaceholder?: string
|
|
30
|
+
emptyLabel?: React.ReactNode
|
|
31
|
+
clearLabel?: string
|
|
27
32
|
size?: "sm" | "default"
|
|
33
|
+
clearable?: boolean
|
|
34
|
+
searchable?: boolean
|
|
35
|
+
loading?: boolean
|
|
36
|
+
loadingLabel?: React.ReactNode
|
|
37
|
+
disabled?: boolean
|
|
28
38
|
triggerClassName?: string
|
|
29
39
|
contentClassName?: string
|
|
30
40
|
itemClassName?: string
|
|
41
|
+
searchClassName?: string
|
|
42
|
+
renderOption?: (option: SimpleSelectOption, state: { selected: boolean }) => React.ReactNode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function optionMatchesSearch(option: SimpleSelectOption, search: string) {
|
|
46
|
+
const normalized = search.trim().toLowerCase()
|
|
47
|
+
if (!normalized) return true
|
|
48
|
+
|
|
49
|
+
const labelText = typeof option.label === "string" || typeof option.label === "number" ? String(option.label) : option.value
|
|
50
|
+
const haystack = [labelText, option.value, ...(option.keywords ?? [])].join(" ").toLowerCase()
|
|
51
|
+
return haystack.includes(normalized)
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
function SimpleSelect({
|
|
@@ -35,35 +56,102 @@ function SimpleSelect({
|
|
|
35
56
|
onValueChange,
|
|
36
57
|
options,
|
|
37
58
|
placeholder = "Select",
|
|
59
|
+
searchPlaceholder = "Search options...",
|
|
60
|
+
emptyLabel = "No options found",
|
|
61
|
+
clearLabel = "Clear selection",
|
|
38
62
|
size = "default",
|
|
63
|
+
clearable = false,
|
|
64
|
+
searchable = false,
|
|
65
|
+
loading = false,
|
|
66
|
+
loadingLabel = "Loading options...",
|
|
67
|
+
disabled = false,
|
|
39
68
|
triggerClassName,
|
|
40
69
|
contentClassName,
|
|
41
70
|
itemClassName,
|
|
71
|
+
searchClassName,
|
|
72
|
+
renderOption,
|
|
42
73
|
...props
|
|
43
74
|
}: SimpleSelectProps) {
|
|
75
|
+
const [search, setSearch] = React.useState("")
|
|
76
|
+
const selectedOption = options.find((option) => option.value === value)
|
|
77
|
+
const filteredOptions = options.filter((option) => optionMatchesSearch(option, search))
|
|
78
|
+
|
|
44
79
|
return (
|
|
45
|
-
<Select value={value} onValueChange={(val) => onValueChange?.(val as string)} {...props}>
|
|
46
|
-
<SelectTrigger
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
80
|
+
<Select value={value} onValueChange={(val) => onValueChange?.(val as string)} disabled={disabled || loading} {...props}>
|
|
81
|
+
<SelectTrigger
|
|
82
|
+
size={size}
|
|
83
|
+
className={cn(
|
|
84
|
+
"w-full border-border/80 bg-background/96 shadow-[0_1px_0_rgba(255,255,255,0.06)]",
|
|
85
|
+
triggerClassName
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
<SelectValue placeholder={placeholder}>{selectedOption?.label}</SelectValue>
|
|
89
|
+
{loading ? <LoaderCircleIcon className="size-4 animate-spin text-muted-foreground" /> : null}
|
|
90
|
+
{clearable && value && !disabled && !loading ? (
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
aria-label={clearLabel}
|
|
94
|
+
className="ml-1 rounded-sm p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
95
|
+
onClick={(event) => {
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
event.stopPropagation()
|
|
98
|
+
onValueChange?.(undefined)
|
|
99
|
+
}}
|
|
56
100
|
>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
<XIcon className="size-3.5" />
|
|
102
|
+
</button>
|
|
103
|
+
) : null}
|
|
104
|
+
</SelectTrigger>
|
|
105
|
+
<SelectContent
|
|
106
|
+
className={cn(
|
|
107
|
+
"border-border/80 bg-popover/98 shadow-[0_20px_60px_rgba(15,23,42,0.18)] backdrop-blur",
|
|
108
|
+
contentClassName
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{searchable ? (
|
|
112
|
+
<div className="sticky top-0 z-10 mb-1 flex items-center gap-2 rounded-[min(var(--radius-lg),14px)] border border-border/70 bg-background/95 px-2.5 py-2 text-sm">
|
|
113
|
+
<SearchIcon className="size-4 text-muted-foreground" />
|
|
114
|
+
<input
|
|
115
|
+
value={search}
|
|
116
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
117
|
+
placeholder={searchPlaceholder}
|
|
118
|
+
className={cn("min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground", searchClassName)}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
) : null}
|
|
122
|
+
|
|
123
|
+
{loading ? (
|
|
124
|
+
<div className="flex items-center gap-2 rounded-[min(var(--radius-lg),14px)] px-3 py-2.5 text-sm text-muted-foreground">
|
|
125
|
+
<LoaderCircleIcon className="size-4 animate-spin" />
|
|
126
|
+
{loadingLabel}
|
|
127
|
+
</div>
|
|
128
|
+
) : filteredOptions.length === 0 ? (
|
|
129
|
+
<div className="rounded-[min(var(--radius-lg),14px)] px-3 py-2.5 text-sm text-muted-foreground">{emptyLabel}</div>
|
|
130
|
+
) : (
|
|
131
|
+
filteredOptions.map((option) => {
|
|
132
|
+
const selected = option.value === value
|
|
133
|
+
return (
|
|
134
|
+
<SelectItem
|
|
135
|
+
key={option.value}
|
|
136
|
+
value={option.value}
|
|
137
|
+
disabled={option.disabled}
|
|
138
|
+
className={cn("rounded-[min(var(--radius-lg),14px)]", itemClassName)}
|
|
139
|
+
>
|
|
140
|
+
{renderOption ? (
|
|
141
|
+
renderOption(option, { selected })
|
|
142
|
+
) : (
|
|
143
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
144
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
145
|
+
<span className="truncate">{option.label}</span>
|
|
146
|
+
{selected ? <CheckIcon className="ml-auto size-3.5 text-primary" /> : null}
|
|
147
|
+
</span>
|
|
148
|
+
{option.description && <span className="truncate text-xs text-muted-foreground">{option.description}</span>}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</SelectItem>
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
)}
|
|
67
155
|
</SelectContent>
|
|
68
156
|
</Select>
|
|
69
157
|
)
|
|
@@ -137,7 +137,7 @@ function AppShell({
|
|
|
137
137
|
<div
|
|
138
138
|
data-slot="app-shell-sidebar"
|
|
139
139
|
className={cn(
|
|
140
|
-
"fixed inset-y-0 left-0 z-40 hidden border-r border-sidebar-border/70 bg-sidebar/96 backdrop-blur transition-[width] duration-200 md:block",
|
|
140
|
+
"fixed inset-y-0 left-0 z-40 hidden border-r border-sidebar-border/70 bg-sidebar/96 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur transition-[width] duration-200 md:block",
|
|
141
141
|
isSidebarCollapsed ? "w-16" : sidebarWidthClassName[sidebarWidth],
|
|
142
142
|
sidebarClassName
|
|
143
143
|
)}
|
|
@@ -224,7 +224,7 @@ function AppShell({
|
|
|
224
224
|
<aside
|
|
225
225
|
data-slot="app-shell-aside"
|
|
226
226
|
className={cn(
|
|
227
|
-
"hidden shrink-0 border-l border-border/70 bg-card/
|
|
227
|
+
"hidden shrink-0 border-l border-border/70 bg-card/55 backdrop-blur xl:block",
|
|
228
228
|
asideWidthClassName[asideWidth],
|
|
229
229
|
asideClassName
|
|
230
230
|
)}
|
|
@@ -5,7 +5,8 @@ export * from "./page-header"
|
|
|
5
5
|
export * from "./stat-card"
|
|
6
6
|
export * from "./sidebar-nav"
|
|
7
7
|
export * from "./breadcrumbs"
|
|
8
|
-
export * from "./page-container"
|
|
9
|
-
export * from "./section"
|
|
10
|
-
export * from "./
|
|
11
|
-
export * from "./
|
|
8
|
+
export * from "./page-container"
|
|
9
|
+
export * from "./section"
|
|
10
|
+
export * from "./stack"
|
|
11
|
+
export * from "./sticky-footer-bar"
|
|
12
|
+
export * from "./workspace-shell"
|
|
@@ -1,60 +1,104 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
2
3
|
|
|
3
4
|
import { cn } from "@/lib/utils"
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
6
|
+
const pageHeaderVariants = cva("flex flex-col border transition-[background-color,border-color,box-shadow]", {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
default: "border-border/75 bg-card/96 shadow-sm ring-1 ring-foreground/5",
|
|
10
|
+
elevated: "border-border/70 bg-card shadow-[0_1px_2px_rgba(15,23,42,0.06),0_18px_45px_rgba(15,23,42,0.08)] ring-1 ring-foreground/5",
|
|
11
|
+
outline: "border-border bg-transparent shadow-none",
|
|
12
|
+
ghost: "border-transparent bg-transparent shadow-none",
|
|
13
|
+
soft: "border-transparent bg-muted/45 shadow-none",
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
sm: "gap-3 rounded-[var(--radius-2xl)] p-4",
|
|
17
|
+
default: "gap-4 rounded-[var(--radius-3xl)] p-5",
|
|
18
|
+
lg: "gap-5 rounded-[calc(var(--radius-3xl)*1.1)] p-6",
|
|
19
|
+
},
|
|
20
|
+
tone: {
|
|
21
|
+
neutral: "",
|
|
22
|
+
info: "border-blue-500/20 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--card),oklch(0.94_0.03_235)_28%),var(--card))]",
|
|
23
|
+
success: "border-emerald-500/20 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--card),oklch(0.94_0.04_155)_30%),var(--card))]",
|
|
24
|
+
warning: "border-amber-500/24 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--card),oklch(0.94_0.05_85)_30%),var(--card))]",
|
|
25
|
+
danger: "border-destructive/24 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--card),var(--destructive)_8%),var(--card))]",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "default",
|
|
31
|
+
tone: "neutral",
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export type PageHeaderProps = React.ComponentProps<"div"> &
|
|
36
|
+
VariantProps<typeof pageHeaderVariants> & {
|
|
37
|
+
title?: React.ReactNode
|
|
38
|
+
description?: React.ReactNode
|
|
39
|
+
eyebrow?: React.ReactNode
|
|
40
|
+
breadcrumbs?: React.ReactNode
|
|
41
|
+
actions?: React.ReactNode
|
|
42
|
+
meta?: React.ReactNode
|
|
43
|
+
leading?: React.ReactNode
|
|
44
|
+
footer?: React.ReactNode
|
|
45
|
+
sticky?: boolean
|
|
46
|
+
titleClassName?: string
|
|
47
|
+
descriptionClassName?: string
|
|
48
|
+
actionsClassName?: string
|
|
49
|
+
}
|
|
14
50
|
|
|
15
51
|
function PageHeader({
|
|
16
52
|
className,
|
|
53
|
+
variant,
|
|
54
|
+
size,
|
|
55
|
+
tone,
|
|
17
56
|
title,
|
|
18
57
|
description,
|
|
19
58
|
eyebrow,
|
|
20
59
|
breadcrumbs,
|
|
21
60
|
actions,
|
|
22
61
|
meta,
|
|
62
|
+
leading,
|
|
63
|
+
footer,
|
|
23
64
|
sticky = false,
|
|
65
|
+
titleClassName,
|
|
66
|
+
descriptionClassName,
|
|
67
|
+
actionsClassName,
|
|
24
68
|
children,
|
|
25
69
|
...props
|
|
26
70
|
}: PageHeaderProps) {
|
|
27
71
|
return (
|
|
28
72
|
<div
|
|
29
|
-
data-slot="page-header"
|
|
30
|
-
data-sticky={sticky || undefined}
|
|
31
|
-
className={cn(
|
|
32
|
-
|
|
33
|
-
sticky && "sticky top-0 z-30 bg-background/92
|
|
34
|
-
className
|
|
35
|
-
)}
|
|
36
|
-
{...props}
|
|
37
|
-
>
|
|
38
|
-
{breadcrumbs && <div className="text-sm text-muted-foreground/95">{breadcrumbs}</div>}
|
|
39
|
-
|
|
40
|
-
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
41
|
-
<div className="min-w-0
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
</
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</div>
|
|
73
|
+
data-slot="page-header"
|
|
74
|
+
data-sticky={sticky || undefined}
|
|
75
|
+
className={cn(
|
|
76
|
+
pageHeaderVariants({ variant, size, tone }),
|
|
77
|
+
sticky && "sticky top-0 z-30 bg-background/92 backdrop-blur supports-[backdrop-filter]:bg-background/78",
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
{breadcrumbs && <div className="text-sm text-muted-foreground/95">{breadcrumbs}</div>}
|
|
83
|
+
|
|
84
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
85
|
+
<div className="flex min-w-0 gap-4">
|
|
86
|
+
{leading ? <div className="shrink-0">{leading}</div> : null}
|
|
87
|
+
<div className="min-w-0 space-y-2">
|
|
88
|
+
{eyebrow && <div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-muted-foreground">{eyebrow}</div>}
|
|
89
|
+
{title && <h1 className={cn("truncate text-3xl font-semibold tracking-[-0.03em] text-foreground", size === "sm" && "text-2xl", size === "lg" && "text-4xl", titleClassName)}>{title}</h1>}
|
|
90
|
+
{description && <p className={cn("max-w-3xl text-sm leading-7 text-muted-foreground", descriptionClassName)}>{description}</p>}
|
|
91
|
+
{meta && <div className="pt-1 text-sm text-muted-foreground">{meta}</div>}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{actions && <div className={cn("flex shrink-0 flex-wrap items-center gap-2.5", actionsClassName)}>{actions}</div>}
|
|
96
|
+
</div>
|
|
54
97
|
|
|
55
98
|
{children}
|
|
99
|
+
{footer && <div data-slot="page-header-footer" className="border-t border-border/70 pt-4">{footer}</div>}
|
|
56
100
|
</div>
|
|
57
101
|
)
|
|
58
102
|
}
|
|
59
103
|
|
|
60
|
-
export { PageHeader }
|
|
104
|
+
export { PageHeader, pageHeaderVariants }
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export * from "./app-shell"
|
|
2
|
-
export * from "./app-header"
|
|
3
|
-
export * from "./app-sidebar"
|
|
4
|
-
export * from "./page-header"
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./
|
|
7
|
-
export * from "./
|
|
8
|
-
export * from "./
|
|
9
|
-
export * from "./
|
|
10
|
-
export * from "./
|
|
1
|
+
export * from "./app-shell"
|
|
2
|
+
export * from "./app-header"
|
|
3
|
+
export * from "./app-sidebar"
|
|
4
|
+
export * from "./page-header"
|
|
5
|
+
export * from "./section-header"
|
|
6
|
+
export * from "./stat-card"
|
|
7
|
+
export * from "./sidebar-nav"
|
|
8
|
+
export * from "./breadcrumbs"
|
|
9
|
+
export * from "./page-container"
|
|
10
|
+
export * from "./section"
|
|
11
|
+
export * from "./stack"
|
|
12
|
+
export * from "./sticky-footer-bar"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
export type SectionHeaderProps = React.ComponentProps<"div"> & {
|
|
6
|
+
eyebrow?: React.ReactNode
|
|
7
|
+
title?: React.ReactNode
|
|
8
|
+
description?: React.ReactNode
|
|
9
|
+
actions?: React.ReactNode
|
|
10
|
+
meta?: React.ReactNode
|
|
11
|
+
align?: "start" | "center"
|
|
12
|
+
size?: "sm" | "default" | "lg"
|
|
13
|
+
titleClassName?: string
|
|
14
|
+
descriptionClassName?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const titleSizeClassName = {
|
|
18
|
+
sm: "text-xl",
|
|
19
|
+
default: "text-2xl",
|
|
20
|
+
lg: "text-3xl",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function SectionHeader({
|
|
24
|
+
className,
|
|
25
|
+
eyebrow,
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
actions,
|
|
29
|
+
meta,
|
|
30
|
+
align = "start",
|
|
31
|
+
size = "default",
|
|
32
|
+
titleClassName,
|
|
33
|
+
descriptionClassName,
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: SectionHeaderProps) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-slot="section-header"
|
|
40
|
+
data-align={align}
|
|
41
|
+
className={cn("flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between", align === "center" && "text-center sm:flex-col sm:items-center", className)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
<div className={cn("min-w-0 space-y-2", align === "center" && "mx-auto max-w-2xl")}>
|
|
45
|
+
{eyebrow ? <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">{eyebrow}</p> : null}
|
|
46
|
+
{title ? <h2 className={cn("font-semibold tracking-[-0.03em] text-foreground", titleSizeClassName[size], titleClassName)}>{title}</h2> : null}
|
|
47
|
+
{description ? <p className={cn("max-w-3xl text-sm leading-7 text-muted-foreground", descriptionClassName)}>{description}</p> : null}
|
|
48
|
+
{meta ? <div className="text-sm text-muted-foreground">{meta}</div> : null}
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
51
|
+
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2.5">{actions}</div> : null}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { SectionHeader }
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
type StackGap = "xs" | "sm" | "md" | "lg" | "xl"
|
|
6
|
+
type InlineAlign = "start" | "center" | "end" | "stretch"
|
|
7
|
+
type InlineJustify = "start" | "center" | "end" | "between"
|
|
8
|
+
type GridColumns = 1 | 2 | 3 | 4 | 5 | 6
|
|
9
|
+
|
|
10
|
+
const stackGapClassName: Record<StackGap, string> = {
|
|
11
|
+
xs: "gap-2",
|
|
12
|
+
sm: "gap-3",
|
|
13
|
+
md: "gap-4",
|
|
14
|
+
lg: "gap-6",
|
|
15
|
+
xl: "gap-8",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const inlineAlignClassName: Record<InlineAlign, string> = {
|
|
19
|
+
start: "items-start",
|
|
20
|
+
center: "items-center",
|
|
21
|
+
end: "items-end",
|
|
22
|
+
stretch: "items-stretch",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const inlineJustifyClassName: Record<InlineJustify, string> = {
|
|
26
|
+
start: "justify-start",
|
|
27
|
+
center: "justify-center",
|
|
28
|
+
end: "justify-end",
|
|
29
|
+
between: "justify-between",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const gridColumnClassName: Record<GridColumns, string> = {
|
|
33
|
+
1: "grid-cols-1",
|
|
34
|
+
2: "grid-cols-2",
|
|
35
|
+
3: "grid-cols-3",
|
|
36
|
+
4: "grid-cols-4",
|
|
37
|
+
5: "grid-cols-5",
|
|
38
|
+
6: "grid-cols-6",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type StackProps = React.ComponentProps<"div"> & {
|
|
42
|
+
gap?: StackGap
|
|
43
|
+
splitAfter?: React.ReactNode
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Stack({ gap = "md", splitAfter, className, children, ...props }: StackProps) {
|
|
47
|
+
return (
|
|
48
|
+
<div data-slot="stack" className={cn("flex min-w-0 flex-col", stackGapClassName[gap], className)} {...props}>
|
|
49
|
+
{children}
|
|
50
|
+
{splitAfter ? <div data-slot="stack-split-after">{splitAfter}</div> : null}
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type InlineProps = React.ComponentProps<"div"> & {
|
|
56
|
+
gap?: StackGap
|
|
57
|
+
align?: InlineAlign
|
|
58
|
+
justify?: InlineJustify
|
|
59
|
+
wrap?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type GridProps = React.ComponentProps<"div"> & {
|
|
63
|
+
gap?: StackGap
|
|
64
|
+
columns?: GridColumns
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Inline({
|
|
68
|
+
gap = "md",
|
|
69
|
+
align = "center",
|
|
70
|
+
justify = "start",
|
|
71
|
+
wrap = true,
|
|
72
|
+
className,
|
|
73
|
+
children,
|
|
74
|
+
...props
|
|
75
|
+
}: InlineProps) {
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
data-slot="inline"
|
|
79
|
+
className={cn(
|
|
80
|
+
"flex min-w-0",
|
|
81
|
+
stackGapClassName[gap],
|
|
82
|
+
inlineAlignClassName[align],
|
|
83
|
+
inlineJustifyClassName[justify],
|
|
84
|
+
wrap ? "flex-wrap" : "flex-nowrap",
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
{...props}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Grid({ gap = "md", columns = 2, className, children, ...props }: GridProps) {
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
data-slot="grid"
|
|
98
|
+
className={cn("grid min-w-0", stackGapClassName[gap], gridColumnClassName[columns], className)}
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { Grid, Inline, Stack }
|