azamat-ui-kit-cli 0.2.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +11 -0
  2. package/dist/index.cjs +452 -0
  3. package/package.json +2 -2
  4. package/vendor/src/components/actions/action-menu.tsx +21 -18
  5. package/vendor/src/components/calendar/calendar.tsx +153 -102
  6. package/vendor/src/components/calendar/date-picker.tsx +24 -14
  7. package/vendor/src/components/calendar/date-range-picker.tsx +137 -58
  8. package/vendor/src/components/charts/charts.tsx +32 -21
  9. package/vendor/src/components/command/command-palette.tsx +68 -57
  10. package/vendor/src/components/data-table/data-table-bulk-actions.tsx +23 -20
  11. package/vendor/src/components/data-table/data-table-column-visibility-menu.tsx +21 -10
  12. package/vendor/src/components/data-table/data-table-pagination.tsx +6 -6
  13. package/vendor/src/components/data-table/data-table-toolbar.tsx +72 -44
  14. package/vendor/src/components/data-table/data-table.tsx +15 -11
  15. package/vendor/src/components/data-table/table-export-menu.tsx +1 -1
  16. package/vendor/src/components/data-table/table-import-button.tsx +1 -1
  17. package/vendor/src/components/display/data-state.tsx +20 -8
  18. package/vendor/src/components/display/index.ts +19 -15
  19. package/vendor/src/components/display/metric-card.tsx +35 -0
  20. package/vendor/src/components/display/progress-circle.tsx +24 -0
  21. package/vendor/src/components/display/smart-card.tsx +49 -27
  22. package/vendor/src/components/display/status-dot.tsx +45 -0
  23. package/vendor/src/components/display/user-card.tsx +30 -0
  24. package/vendor/src/components/feedback/alert.tsx +21 -11
  25. package/vendor/src/components/feedback/empty-state.tsx +2 -2
  26. package/vendor/src/components/feedback/loading-state.tsx +2 -2
  27. package/vendor/src/components/feedback/page-state.tsx +19 -15
  28. package/vendor/src/components/feedback/status-badge.tsx +43 -43
  29. package/vendor/src/components/form/form-app-input.tsx +147 -0
  30. package/vendor/src/components/form/form-date-input.tsx +16 -19
  31. package/vendor/src/components/form/form-field-shell.tsx +11 -8
  32. package/vendor/src/components/form/form-field-utils.ts +76 -0
  33. package/vendor/src/components/form/form-input.tsx +423 -44
  34. package/vendor/src/components/form/form-number-input.tsx +16 -15
  35. package/vendor/src/components/form/form-phone-input.tsx +15 -9
  36. package/vendor/src/components/form/form-search-input.tsx +16 -19
  37. package/vendor/src/components/form/form-select.tsx +4 -3
  38. package/vendor/src/components/form/public.ts +16 -14
  39. package/vendor/src/components/form/smart-form-shell.tsx +13 -12
  40. package/vendor/src/components/inputs/app-input.tsx +27 -0
  41. package/vendor/src/components/inputs/async-select.tsx +113 -84
  42. package/vendor/src/components/inputs/clearable-input.tsx +81 -61
  43. package/vendor/src/components/inputs/date-input.tsx +21 -17
  44. package/vendor/src/components/inputs/date-range-input.tsx +10 -10
  45. package/vendor/src/components/inputs/index.ts +1 -0
  46. package/vendor/src/components/inputs/input-decorator.tsx +101 -57
  47. package/vendor/src/components/inputs/masked-input.tsx +20 -20
  48. package/vendor/src/components/inputs/money-input.tsx +2 -2
  49. package/vendor/src/components/inputs/number-input.tsx +29 -19
  50. package/vendor/src/components/inputs/password-input.tsx +82 -45
  51. package/vendor/src/components/inputs/phone-input.tsx +24 -2
  52. package/vendor/src/components/inputs/quantity-input.tsx +2 -2
  53. package/vendor/src/components/inputs/search-input.tsx +54 -3
  54. package/vendor/src/components/inputs/simple-select.tsx +110 -22
  55. package/vendor/src/components/layout/app-shell.tsx +2 -2
  56. package/vendor/src/components/layout/index.ts +5 -4
  57. package/vendor/src/components/layout/page-header.tsx +79 -35
  58. package/vendor/src/components/layout/public.ts +12 -10
  59. package/vendor/src/components/layout/section-header.tsx +56 -0
  60. package/vendor/src/components/layout/stack.tsx +106 -0
  61. package/vendor/src/components/layout/stat-card.tsx +66 -29
  62. package/vendor/src/components/navigation/index.ts +1 -0
  63. package/vendor/src/components/navigation/nav-tabs.tsx +60 -0
  64. package/vendor/src/components/navigation/page-tabs.tsx +41 -26
  65. package/vendor/src/components/navigation/pagination.tsx +14 -10
  66. package/vendor/src/components/overlay/alert-dialog.tsx +65 -0
  67. package/vendor/src/components/overlay/drawer.tsx +71 -0
  68. package/vendor/src/components/overlay/index.ts +4 -2
  69. package/vendor/src/components/patterns/data-view.tsx +13 -8
  70. package/vendor/src/components/ui/badge.tsx +96 -52
  71. package/vendor/src/components/ui/button.tsx +99 -61
  72. package/vendor/src/components/ui/card.tsx +84 -25
  73. package/vendor/src/components/ui/checkbox.tsx +68 -68
  74. package/vendor/src/components/ui/command.tsx +32 -32
  75. package/vendor/src/components/ui/dialog.tsx +135 -138
  76. package/vendor/src/components/ui/dropdown-menu.tsx +21 -21
  77. package/vendor/src/components/ui/hover-card.tsx +49 -0
  78. package/vendor/src/components/ui/input-primitive.tsx +24 -0
  79. package/vendor/src/components/ui/input.tsx +191 -20
  80. package/vendor/src/components/ui/kbd.tsx +33 -0
  81. package/vendor/src/components/ui/popover.tsx +11 -11
  82. package/vendor/src/components/ui/radio-group.tsx +102 -0
  83. package/vendor/src/components/ui/right-click-menu.tsx +60 -0
  84. package/vendor/src/components/ui/scroll-box.tsx +27 -0
  85. package/vendor/src/components/ui/segmented-control.tsx +21 -17
  86. package/vendor/src/components/ui/select.tsx +187 -189
  87. package/vendor/src/components/ui/skeleton.tsx +2 -2
  88. package/vendor/src/components/ui/switch.tsx +60 -60
  89. package/vendor/src/components/ui/table.tsx +114 -114
  90. package/vendor/src/components/ui/tabs.tsx +2 -2
  91. package/vendor/src/components/ui/textarea.tsx +1 -1
  92. package/vendor/src/components/upload/file-dropzone.tsx +38 -0
  93. package/vendor/src/components/upload/file-upload.tsx +4 -4
  94. package/vendor/src/components/upload/image-upload.tsx +22 -19
  95. package/vendor/src/components/upload/index.ts +2 -0
  96. package/vendor/src/families/catalog.ts +1 -0
  97. package/vendor/src/families/docs-groups.ts +10 -1
  98. package/vendor/src/families/member-metadata.ts +24 -0
  99. package/vendor/src/families/member-snippets.ts +41 -2
  100. package/vendor/src/families/migration-map.ts +3 -0
  101. package/vendor/src/index.ts +23 -18
  102. package/vendor/templates/styles/globals.css +253 -0
  103. 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
- ({ searchIcon, placeholder = "Search...", inputMode = "search", ...props }, ref) => {
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 size={size} className={cn("w-full", triggerClassName)}>
47
- <SelectValue placeholder={placeholder} />
48
- </SelectTrigger>
49
- <SelectContent className={contentClassName}>
50
- {options.map((option) => (
51
- <SelectItem
52
- key={option.value}
53
- value={option.value}
54
- disabled={option.disabled}
55
- className={itemClassName}
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
- <span className="flex min-w-0 flex-col">
58
- <span className="truncate">{option.label}</span>
59
- {option.description && (
60
- <span className="truncate text-xs text-muted-foreground">
61
- {option.description}
62
- </span>
63
- )}
64
- </span>
65
- </SelectItem>
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/45 xl:block",
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 "./sticky-footer-bar"
11
- export * from "./workspace-shell"
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
- export type PageHeaderProps = React.ComponentProps<"div"> & {
6
- title?: React.ReactNode
7
- description?: React.ReactNode
8
- eyebrow?: React.ReactNode
9
- breadcrumbs?: React.ReactNode
10
- actions?: React.ReactNode
11
- meta?: React.ReactNode
12
- sticky?: boolean
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
- "flex flex-col gap-4 border-b border-border/70 pb-5",
33
- sticky && "sticky top-0 z-30 bg-background/92 pt-4 backdrop-blur supports-[backdrop-filter]:bg-background/78",
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 space-y-2">
42
- {eyebrow && (
43
- <div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-muted-foreground">
44
- {eyebrow}
45
- </div>
46
- )}
47
- {title && <h1 className="truncate text-3xl font-semibold tracking-[-0.03em] text-foreground">{title}</h1>}
48
- {description && <p className="max-w-3xl text-sm leading-7 text-muted-foreground">{description}</p>}
49
- {meta && <div className="pt-1 text-sm text-muted-foreground">{meta}</div>}
50
- </div>
51
-
52
- {actions && <div className="flex shrink-0 flex-wrap items-center gap-2.5">{actions}</div>}
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 "./stat-card"
6
- export * from "./sidebar-nav"
7
- export * from "./breadcrumbs"
8
- export * from "./page-container"
9
- export * from "./section"
10
- export * from "./sticky-footer-bar"
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 }