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
@@ -8,11 +8,13 @@ import {
8
8
  CardHeader,
9
9
  CardTitle,
10
10
  } from "@/components/ui/card"
11
+ import { Badge } from "@/components/ui/badge"
11
12
  import { cn } from "@/lib/utils"
12
13
 
13
14
  export type StatCardTrend = {
14
15
  value: React.ReactNode
15
- tone?: "success" | "warning" | "danger" | "muted" | "default"
16
+ tone?: "success" | "warning" | "danger" | "info" | "muted" | "default"
17
+ label?: React.ReactNode
16
18
  }
17
19
 
18
20
  export type StatCardProps = React.ComponentProps<typeof Card> & {
@@ -23,7 +25,13 @@ export type StatCardProps = React.ComponentProps<typeof Card> & {
23
25
  action?: React.ReactNode
24
26
  trend?: StatCardTrend
25
27
  footer?: React.ReactNode
28
+ helperText?: React.ReactNode
29
+ loading?: boolean
30
+ valuePrefix?: React.ReactNode
31
+ valueSuffix?: React.ReactNode
26
32
  contentClassName?: string
33
+ iconClassName?: string
34
+ valueClassName?: string
27
35
  }
28
36
 
29
37
  const trendClassName: Record<NonNullable<StatCardTrend["tone"]>, string> = {
@@ -31,9 +39,20 @@ const trendClassName: Record<NonNullable<StatCardTrend["tone"]>, string> = {
31
39
  success: "text-emerald-600 dark:text-emerald-400",
32
40
  warning: "text-amber-600 dark:text-amber-400",
33
41
  danger: "text-destructive",
42
+ info: "text-blue-600 dark:text-blue-400",
34
43
  muted: "text-muted-foreground",
35
44
  }
36
45
 
46
+ function StatSkeleton() {
47
+ return (
48
+ <div className="space-y-3">
49
+ <div className="h-4 w-28 animate-pulse rounded-md bg-muted" />
50
+ <div className="h-8 w-36 animate-pulse rounded-md bg-muted" />
51
+ <div className="h-4 w-44 animate-pulse rounded-md bg-muted" />
52
+ </div>
53
+ )
54
+ }
55
+
37
56
  function StatCard({
38
57
  className,
39
58
  title,
@@ -43,43 +62,61 @@ function StatCard({
43
62
  action,
44
63
  trend,
45
64
  footer,
65
+ helperText,
66
+ loading = false,
67
+ valuePrefix,
68
+ valueSuffix,
46
69
  contentClassName,
70
+ iconClassName,
71
+ valueClassName,
47
72
  ...props
48
73
  }: StatCardProps) {
49
74
  return (
50
75
  <Card data-slot="stat-card" className={cn("min-w-0", className)} {...props}>
51
- <CardHeader>
52
- <div className="flex min-w-0 items-start justify-between gap-3">
53
- <div className="min-w-0 space-y-1">
54
- {title && <CardDescription className="truncate">{title}</CardDescription>}
55
- {value && <CardTitle className="truncate text-2xl">{value}</CardTitle>}
56
- </div>
57
- {(icon || action) && (
58
- <CardAction>
59
- {action ?? (
60
- <div className="flex size-9 items-center justify-center rounded-lg bg-muted text-muted-foreground">
61
- {icon}
62
- </div>
76
+ {loading ? (
77
+ <CardContent>
78
+ <StatSkeleton />
79
+ </CardContent>
80
+ ) : (
81
+ <>
82
+ <CardHeader>
83
+ <div className="flex min-w-0 items-start justify-between gap-3">
84
+ <div className="min-w-0 space-y-1">
85
+ {title && <CardDescription className="truncate">{title}</CardDescription>}
86
+ {value !== undefined && value !== null ? (
87
+ <CardTitle className={cn("flex items-baseline gap-1 truncate text-2xl", valueClassName)}>
88
+ {valuePrefix ? <span className="text-base text-muted-foreground">{valuePrefix}</span> : null}
89
+ <span className="truncate">{value}</span>
90
+ {valueSuffix ? <span className="text-base text-muted-foreground">{valueSuffix}</span> : null}
91
+ </CardTitle>
92
+ ) : null}
93
+ </div>
94
+ {(icon || action) && (
95
+ <CardAction>
96
+ {action ?? <div className={cn("flex size-9 items-center justify-center rounded-lg bg-muted text-muted-foreground", iconClassName)}>{icon}</div>}
97
+ </CardAction>
63
98
  )}
64
- </CardAction>
65
- )}
66
- </div>
67
- </CardHeader>
99
+ </div>
100
+ </CardHeader>
68
101
 
69
- {(description || trend || footer) && (
70
- <CardContent className={cn("space-y-2", contentClassName)}>
71
- {(description || trend) && (
72
- <div className="flex flex-wrap items-center gap-2 text-sm">
73
- {trend && (
74
- <span className={cn("font-medium", trendClassName[trend.tone ?? "default"])}>
75
- {trend.value}
76
- </span>
102
+ {(description || trend || helperText || footer) && (
103
+ <CardContent className={cn("space-y-2", contentClassName)}>
104
+ {(description || trend) && (
105
+ <div className="flex flex-wrap items-center gap-2 text-sm">
106
+ {trend && (
107
+ <Badge variant="ghost" tone={trend.tone === "muted" || trend.tone === "default" ? "neutral" : trend.tone} size="sm" className={cn("font-medium", trendClassName[trend.tone ?? "default"])}>
108
+ {trend.value}
109
+ </Badge>
110
+ )}
111
+ {trend?.label ? <span className="text-muted-foreground">{trend.label}</span> : null}
112
+ {description && <span className="text-muted-foreground">{description}</span>}
113
+ </div>
77
114
  )}
78
- {description && <span className="text-muted-foreground">{description}</span>}
79
- </div>
115
+ {helperText ? <p className="text-xs leading-5 text-muted-foreground">{helperText}</p> : null}
116
+ {footer}
117
+ </CardContent>
80
118
  )}
81
- {footer}
82
- </CardContent>
119
+ </>
83
120
  )}
84
121
  </Card>
85
122
  )
@@ -2,3 +2,4 @@ export * from './pagination'
2
2
  export * from './page-tabs'
3
3
  export * from './stepper-tabs'
4
4
  export * from './anchor-nav'
5
+ export * from './nav-tabs'
@@ -0,0 +1,60 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export type NavTabItem = {
6
+ label: React.ReactNode
7
+ value: string
8
+ href?: string
9
+ icon?: React.ReactNode
10
+ disabled?: boolean
11
+ }
12
+
13
+ export type NavTabsProps = React.ComponentProps<"div"> & {
14
+ items: NavTabItem[]
15
+ value?: string
16
+ onValueChange?: (value: string) => void
17
+ size?: "sm" | "default" | "lg"
18
+ fullWidth?: boolean
19
+ }
20
+
21
+ const sizeClassName = {
22
+ sm: "h-8 px-3 text-xs",
23
+ default: "h-10 px-4 text-sm",
24
+ lg: "h-11 px-5 text-base",
25
+ }
26
+
27
+ function NavTabs({ items, value, onValueChange, size = "default", fullWidth = false, className, ...props }: NavTabsProps) {
28
+ return (
29
+ <div data-slot="nav-tabs" className={cn("inline-flex rounded-[var(--radius-xl)] border border-border/80 bg-muted/45 p-1", fullWidth && "w-full", className)} {...props}>
30
+ {items.map((item) => {
31
+ const active = item.value === value
32
+ const content = (
33
+ <>
34
+ {item.icon ? <span className="[&_svg]:size-4">{item.icon}</span> : null}
35
+ <span className="truncate">{item.label}</span>
36
+ </>
37
+ )
38
+
39
+ const itemClassName = cn(
40
+ "inline-flex items-center justify-center gap-2 rounded-[calc(var(--radius-xl)-4px)] font-medium text-muted-foreground transition hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
41
+ sizeClassName[size],
42
+ fullWidth && "flex-1",
43
+ active && "bg-background text-foreground shadow-sm"
44
+ )
45
+
46
+ return item.href ? (
47
+ <a key={item.value} href={item.href} aria-current={active ? "page" : undefined} className={itemClassName}>
48
+ {content}
49
+ </a>
50
+ ) : (
51
+ <button key={item.value} type="button" disabled={item.disabled} className={itemClassName} onClick={() => onValueChange?.(item.value)}>
52
+ {content}
53
+ </button>
54
+ )
55
+ })}
56
+ </div>
57
+ )
58
+ }
59
+
60
+ export { NavTabs }
@@ -28,38 +28,53 @@ function PageTabs<TValue extends string = string>({
28
28
  size = "default",
29
29
  className,
30
30
  ...props
31
- }: PageTabsProps<TValue>) {
32
- const visibleItems = items.filter((item) => !item.hidden)
33
-
34
- return (
35
- <div data-slot="page-tabs" data-variant={variant} className={cn("flex min-w-0 flex-wrap gap-1 border-b", variant !== "underline" && "border-b-0", className)} {...props}>
36
- {visibleItems.map((item) => {
37
- const active = item.value === value
38
- return (
31
+ }: PageTabsProps<TValue>) {
32
+ const visibleItems = items.filter((item) => !item.hidden)
33
+
34
+ return (
35
+ <div
36
+ data-slot="page-tabs"
37
+ data-variant={variant}
38
+ className={cn(
39
+ "flex min-w-0 flex-wrap gap-1.5 border-b border-border/70",
40
+ variant === "pills" && "rounded-full border border-border/75 bg-muted/22 p-1 shadow-[0_1px_0_rgba(255,255,255,0.04)]",
41
+ variant === "cards" && "gap-2 border-b-0",
42
+ variant !== "underline" && "border-b-0",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ {visibleItems.map((item) => {
48
+ const active = item.value === value
49
+ return (
39
50
  <button
40
51
  key={item.value}
41
52
  type="button"
42
53
  disabled={item.disabled}
43
54
  data-slot="page-tab"
44
55
  data-active={active || undefined}
45
- className={cn(
46
- "inline-flex min-w-0 items-center gap-2 rounded-md px-3 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4",
47
- size === "sm" ? "h-8" : "h-10",
48
- variant === "underline" && "rounded-none border-b-2 border-transparent text-muted-foreground hover:text-foreground",
49
- variant === "underline" && active && "border-primary text-foreground",
50
- variant === "pills" && "bg-muted/35 text-muted-foreground hover:bg-muted hover:text-foreground",
51
- variant === "pills" && active && "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground",
52
- variant === "cards" && "border bg-card text-muted-foreground shadow-sm hover:border-primary/40 hover:text-foreground",
53
- variant === "cards" && active && "border-primary/60 text-foreground"
54
- )}
55
- onClick={() => onValueChange?.(item.value, item)}
56
- >
57
- {item.icon}
58
- <span className="truncate">{item.label}</span>
59
- {item.badge !== undefined && <Badge variant={active && variant === "pills" ? "secondary" : "outline"}>{item.badge}</Badge>}
60
- </button>
61
- )
62
- })}
56
+ className={cn(
57
+ "inline-flex min-w-0 items-center gap-2 rounded-full px-3 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4",
58
+ size === "sm" ? "h-8" : "h-10",
59
+ variant === "underline" && "rounded-none border-b-2 border-transparent px-1 text-muted-foreground hover:text-foreground",
60
+ variant === "underline" && active && "border-primary text-foreground",
61
+ variant === "pills" && "text-muted-foreground hover:bg-background/72 hover:text-foreground",
62
+ variant === "pills" && active && "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground",
63
+ variant === "cards" && "rounded-[var(--radius-2xl)] border border-border/75 bg-card/96 text-muted-foreground shadow-sm ring-1 ring-foreground/4 hover:border-primary/28 hover:text-foreground",
64
+ variant === "cards" && active && "border-primary/50 bg-primary/7 text-foreground"
65
+ )}
66
+ onClick={() => onValueChange?.(item.value, item)}
67
+ >
68
+ {item.icon}
69
+ <span className="truncate">{item.label}</span>
70
+ {item.badge !== undefined && (
71
+ <Badge variant={active && variant === "pills" ? "secondary" : "outline"} className="rounded-full">
72
+ {item.badge}
73
+ </Badge>
74
+ )}
75
+ </button>
76
+ )
77
+ })}
63
78
  </div>
64
79
  )
65
80
  }
@@ -92,7 +92,10 @@ function Pagination({
92
92
  <nav
93
93
  data-slot="pagination"
94
94
  aria-label="Pagination"
95
- className={cn("flex items-center justify-center gap-1.5 rounded-full border border-border/70 bg-background/88 p-1 shadow-sm backdrop-blur", className)}
95
+ className={cn(
96
+ "flex items-center justify-center gap-1.5 rounded-full border border-border/75 bg-background/92 p-1 shadow-[0_1px_0_rgba(255,255,255,0.08)] backdrop-blur",
97
+ className
98
+ )}
96
99
  {...props}
97
100
  >
98
101
  {showEdges && (
@@ -135,17 +138,18 @@ function Pagination({
135
138
 
136
139
  return (
137
140
  <Button
138
- key={item}
139
- type="button"
140
- variant={item === currentPage ? "default" : "outline"}
141
- size="icon-sm"
142
- disabled={disabled}
141
+ key={item}
142
+ type="button"
143
+ variant={item === currentPage ? "default" : "outline"}
144
+ size="icon-sm"
145
+ disabled={disabled}
143
146
  aria-current={item === currentPage ? "page" : undefined}
144
147
  aria-label={labels?.page?.(item) ?? `Page ${item}`}
145
- onClick={() => goToPage(item)}
146
- >
147
- {item}
148
- </Button>
148
+ onClick={() => goToPage(item)}
149
+ className={cn(item !== currentPage && "border-border/80 bg-background/94")}
150
+ >
151
+ {item}
152
+ </Button>
149
153
  )
150
154
  })}
151
155
 
@@ -0,0 +1,65 @@
1
+ import * as React from "react"
2
+ import { AlertTriangleIcon } from "lucide-react"
3
+
4
+ import { Button } from "@/components/ui/button"
5
+ import {
6
+ Dialog,
7
+ DialogClose,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "@/components/ui/dialog"
14
+
15
+ export type AlertDialogProps = Omit<React.ComponentProps<typeof Dialog>, "children"> & {
16
+ title?: React.ReactNode
17
+ description?: React.ReactNode
18
+ icon?: React.ReactNode
19
+ cancelLabel?: React.ReactNode
20
+ actionLabel?: React.ReactNode
21
+ actionTone?: "default" | "destructive"
22
+ loading?: boolean
23
+ onAction?: () => void | Promise<void>
24
+ children?: React.ReactNode
25
+ }
26
+
27
+ function AlertDialog({
28
+ title = "Are you sure?",
29
+ description,
30
+ icon = <AlertTriangleIcon />,
31
+ cancelLabel = "Cancel",
32
+ actionLabel = "Continue",
33
+ actionTone = "destructive",
34
+ loading = false,
35
+ onAction,
36
+ children,
37
+ ...props
38
+ }: AlertDialogProps) {
39
+ return (
40
+ <Dialog {...props}>
41
+ {children}
42
+ <DialogContent>
43
+ <DialogHeader>
44
+ <div className="mb-3 inline-flex size-11 items-center justify-center rounded-2xl border border-destructive/20 bg-destructive/10 text-destructive [&_svg]:size-5">
45
+ {icon}
46
+ </div>
47
+ <DialogTitle>{title}</DialogTitle>
48
+ {description ? <DialogDescription>{description}</DialogDescription> : null}
49
+ </DialogHeader>
50
+ <DialogFooter>
51
+ <DialogClose render={() => (
52
+ <Button type="button" variant="outline" disabled={loading}>
53
+ {cancelLabel}
54
+ </Button>
55
+ )} />
56
+ <Button type="button" variant={actionTone === "destructive" ? "destructive" : "default"} loading={loading} onClick={() => void onAction?.()}>
57
+ {actionLabel}
58
+ </Button>
59
+ </DialogFooter>
60
+ </DialogContent>
61
+ </Dialog>
62
+ )
63
+ }
64
+
65
+ export { AlertDialog }
@@ -0,0 +1,71 @@
1
+ import * as React from "react"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import {
5
+ Dialog,
6
+ DialogClose,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ DialogTrigger,
12
+ } from "@/components/ui/dialog"
13
+ import { cn } from "@/lib/utils"
14
+
15
+ export type DrawerSide = "left" | "right" | "top" | "bottom"
16
+
17
+ export type DrawerProps = Omit<React.ComponentProps<typeof Dialog>, "children"> & {
18
+ trigger?: React.ReactNode
19
+ title?: React.ReactNode
20
+ description?: React.ReactNode
21
+ side?: DrawerSide
22
+ footer?: React.ReactNode
23
+ contentClassName?: string
24
+ showCloseButton?: boolean
25
+ children?: React.ReactNode
26
+ }
27
+
28
+ const sideClassName: Record<DrawerSide, string> = {
29
+ right: "left-auto right-0 top-0 h-dvh max-h-dvh w-full max-w-md translate-x-0 translate-y-0 rounded-none rounded-l-[var(--radius-3xl)] sm:max-w-lg",
30
+ left: "left-0 top-0 h-dvh max-h-dvh w-full max-w-md translate-x-0 translate-y-0 rounded-none rounded-r-[var(--radius-3xl)] sm:max-w-lg",
31
+ top: "left-0 top-0 h-auto max-h-[85dvh] w-full max-w-none translate-x-0 translate-y-0 rounded-none rounded-b-[var(--radius-3xl)]",
32
+ bottom: "bottom-0 left-0 top-auto h-auto max-h-[85dvh] w-full max-w-none translate-x-0 translate-y-0 rounded-none rounded-t-[var(--radius-3xl)]",
33
+ }
34
+
35
+ function Drawer({
36
+ trigger,
37
+ title,
38
+ description,
39
+ side = "right",
40
+ footer,
41
+ contentClassName,
42
+ showCloseButton = true,
43
+ children,
44
+ ...props
45
+ }: DrawerProps) {
46
+ return (
47
+ <Dialog {...props}>
48
+ {trigger ? <DialogTrigger render={trigger as React.ReactElement} /> : null}
49
+ <DialogContent showCloseButton={showCloseButton} className={cn("fixed p-0", sideClassName[side], contentClassName)}>
50
+ {(title || description) && (
51
+ <DialogHeader className="border-b border-border/70 p-6">
52
+ {title ? <DialogTitle>{title}</DialogTitle> : null}
53
+ {description ? <DialogDescription>{description}</DialogDescription> : null}
54
+ </DialogHeader>
55
+ )}
56
+ <div data-slot="drawer-body" className="min-h-0 flex-1 overflow-y-auto p-6">
57
+ {children}
58
+ </div>
59
+ {footer ? <div data-slot="drawer-footer" className="border-t border-border/70 p-5">{footer}</div> : null}
60
+ </DialogContent>
61
+ </Dialog>
62
+ )
63
+ }
64
+
65
+ function DrawerCloseButton({ children = "Close", ...props }: React.ComponentProps<typeof Button>) {
66
+ return (
67
+ <DialogClose render={() => <Button type="button" variant="outline" {...props}>{children}</Button>} />
68
+ )
69
+ }
70
+
71
+ export { Drawer, DrawerCloseButton }
@@ -1,4 +1,6 @@
1
- export * from "./dialog-actions"
2
- export * from "./modal-shell"
1
+ export * from "./dialog-actions"
2
+ export * from "./alert-dialog"
3
+ export * from "./modal-shell"
3
4
  export * from "./confirm-dialog"
4
5
  export * from "./sheet-shell"
6
+ export * from "./drawer"
@@ -5,13 +5,14 @@ import { cn } from "@/lib/utils"
5
5
 
6
6
  export type DataViewState = "idle" | "loading" | "error" | "empty"
7
7
 
8
- export type DataViewLabels = {
9
- loadingTitle?: React.ReactNode
10
- loadingDescription?: React.ReactNode
11
- errorTitle?: React.ReactNode
12
- emptyTitle?: React.ReactNode
13
- emptyDescription?: React.ReactNode
14
- }
8
+ export type DataViewLabels = {
9
+ loadingTitle?: React.ReactNode
10
+ loadingDescription?: React.ReactNode
11
+ errorTitle?: React.ReactNode
12
+ errorDescription?: React.ReactNode
13
+ emptyTitle?: React.ReactNode
14
+ emptyDescription?: React.ReactNode
15
+ }
15
16
 
16
17
  export type DataViewProps<TItem = unknown> = React.ComponentProps<"div"> & {
17
18
  data: TItem[]
@@ -72,7 +73,11 @@ function DataView<TItem = unknown>({
72
73
  {selectionBar}
73
74
  <div data-slot="data-view-content" className={contentClassName}>
74
75
  {state === "loading" ? loadingState ?? <InlineState tone="loading" title={labels?.loadingTitle ?? "Loading"} description={labels?.loadingDescription} /> : null}
75
- {state === "error" ? errorState ?? <InlineState tone="error" title={labels?.errorTitle ?? "Something went wrong"} /> : null}
76
+ {state === "error"
77
+ ? errorState ?? (
78
+ <InlineState tone="error" title={labels?.errorTitle ?? "Something went wrong"} description={labels?.errorDescription} />
79
+ )
80
+ : null}
76
81
  {state === "empty" ? empty ?? <InlineState tone="empty" title={labels?.emptyTitle ?? "No data"} description={labels?.emptyDescription} /> : null}
77
82
  {state === "idle" ? renderContent?.(data, { view, state }) ?? <div className="grid gap-3">{data.map((item, index) => renderItem?.(item, index) ?? <pre key={index} className="rounded-lg border bg-card p-3 text-xs">{JSON.stringify(item, null, 2)}</pre>)}</div> : null}
78
83
  </div>
@@ -1,52 +1,96 @@
1
- import { mergeProps } from "@base-ui/react/merge-props"
2
- import { useRender } from "@base-ui/react/use-render"
3
- import { cva, type VariantProps } from "class-variance-authority"
4
-
5
- import { cn } from "@/lib/utils"
6
-
7
- const badgeVariants = cva(
8
- "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9
- {
10
- variants: {
11
- variant: {
12
- default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
- secondary:
14
- "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
- destructive:
16
- "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
- outline:
18
- "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
- ghost:
20
- "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
- link: "text-primary underline-offset-4 hover:underline",
22
- },
23
- },
24
- defaultVariants: {
25
- variant: "default",
26
- },
27
- }
28
- )
29
-
30
- function Badge({
31
- className,
32
- variant = "default",
33
- render,
34
- ...props
35
- }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
36
- return useRender({
37
- defaultTagName: "span",
38
- props: mergeProps<"span">(
39
- {
40
- className: cn(badgeVariants({ variant }), className),
41
- },
42
- props
43
- ),
44
- render,
45
- state: {
46
- slot: "badge",
47
- variant,
48
- },
49
- })
50
- }
51
-
52
- export { Badge, badgeVariants }
1
+ import * as React from "react"
2
+ import { mergeProps } from "@base-ui/react/merge-props"
3
+ import { useRender } from "@base-ui/react/use-render"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const badgeVariants = cva(
9
+ "group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent font-semibold tracking-[0.01em] whitespace-nowrap transition-[background-color,border-color,color,box-shadow,transform] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default:
14
+ "border-primary/20 bg-primary text-primary-foreground shadow-[0_8px_18px_color-mix(in_oklch,var(--primary),transparent_82%)] [a]:hover:bg-primary/88",
15
+ secondary:
16
+ "border-border/70 bg-secondary text-secondary-foreground shadow-[0_1px_0_rgba(255,255,255,0.08)] [a]:hover:bg-secondary/84",
17
+ destructive:
18
+ "border-destructive/18 bg-destructive/12 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/18",
19
+ outline:
20
+ "border-border/85 bg-background/88 text-foreground shadow-[0_1px_0_rgba(255,255,255,0.08)] [a]:hover:bg-muted [a]:hover:text-foreground",
21
+ ghost:
22
+ "bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:hover:bg-muted/50",
23
+ link: "text-primary underline-offset-4 hover:underline",
24
+ },
25
+ tone: {
26
+ neutral: "",
27
+ info: "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300",
28
+ success: "border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
29
+ warning: "border-amber-500/24 bg-amber-500/12 text-amber-700 dark:text-amber-300",
30
+ danger: "border-destructive/20 bg-destructive/12 text-destructive dark:bg-destructive/20",
31
+ },
32
+ size: {
33
+ sm: "min-h-5 px-2 py-0.5 text-[0.65rem]",
34
+ default: "min-h-6 px-2.5 py-1 text-[0.7rem]",
35
+ lg: "min-h-7 px-3 py-1 text-xs",
36
+ },
37
+ dot: {
38
+ true: "pl-2",
39
+ false: "",
40
+ },
41
+ },
42
+ defaultVariants: {
43
+ variant: "default",
44
+ tone: "neutral",
45
+ size: "default",
46
+ dot: false,
47
+ },
48
+ }
49
+ )
50
+
51
+ type BadgeProps = useRender.ComponentProps<"span"> &
52
+ VariantProps<typeof badgeVariants> & {
53
+ leftIcon?: React.ReactNode
54
+ rightIcon?: React.ReactNode
55
+ }
56
+
57
+ function Badge({
58
+ className,
59
+ variant = "default",
60
+ tone = "neutral",
61
+ size = "default",
62
+ dot = false,
63
+ leftIcon,
64
+ rightIcon,
65
+ children,
66
+ render,
67
+ ...props
68
+ }: BadgeProps) {
69
+ return useRender({
70
+ defaultTagName: "span",
71
+ props: mergeProps<"span">(
72
+ {
73
+ className: cn(badgeVariants({ variant, tone, size, dot }), className),
74
+ children: (
75
+ <>
76
+ {dot ? <span data-slot="badge-dot" className="size-1.5 rounded-full bg-current opacity-75" /> : null}
77
+ {leftIcon ? <span data-icon="inline-start" data-slot="badge-icon" className="inline-flex shrink-0 items-center">{leftIcon}</span> : null}
78
+ {children ? <span data-slot="badge-label">{children}</span> : null}
79
+ {rightIcon ? <span data-icon="inline-end" data-slot="badge-icon" className="inline-flex shrink-0 items-center">{rightIcon}</span> : null}
80
+ </>
81
+ ),
82
+ },
83
+ props
84
+ ),
85
+ render,
86
+ state: {
87
+ slot: "badge",
88
+ variant,
89
+ tone,
90
+ size,
91
+ dot,
92
+ },
93
+ })
94
+ }
95
+
96
+ export { Badge, badgeVariants, type BadgeProps }