@etherealui/react 0.0.0-stage → 0.0.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/index.d.mts +583 -0
  4. package/dist/index.d.ts +583 -0
  5. package/dist/index.js +5189 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +5189 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +83 -3
  10. package/src/blocks/app-shell.tsx +441 -0
  11. package/src/blocks/billing-card.tsx +2 -0
  12. package/src/blocks/data-table.tsx +479 -0
  13. package/src/blocks/filter-bar.tsx +134 -0
  14. package/src/blocks/header-navbar.tsx +7 -0
  15. package/src/blocks/metric-grid.tsx +5 -0
  16. package/src/blocks/profile-form.tsx +2 -0
  17. package/src/blocks/security-settings.tsx +2 -0
  18. package/src/blocks/settings-blocks.tsx +406 -0
  19. package/src/blocks/sidebar.tsx +6 -0
  20. package/src/blocks/stat-card.tsx +7 -0
  21. package/src/blocks/stat-cards.tsx +212 -0
  22. package/src/blocks/team-members.tsx +5 -0
  23. package/src/index.ts +114 -0
  24. package/src/primitives/accordion.tsx +61 -0
  25. package/src/primitives/announcement.tsx +33 -0
  26. package/src/primitives/avatar.tsx +156 -0
  27. package/src/primitives/badge.tsx +108 -0
  28. package/src/primitives/breadcrumb.tsx +45 -0
  29. package/src/primitives/button-group.tsx +79 -0
  30. package/src/primitives/button.tsx +165 -0
  31. package/src/primitives/carousel.tsx +84 -0
  32. package/src/primitives/chart.tsx +831 -0
  33. package/src/primitives/checkbox.tsx +26 -0
  34. package/src/primitives/chip.tsx +37 -0
  35. package/src/primitives/close-button.tsx +29 -0
  36. package/src/primitives/color-picker.tsx +62 -0
  37. package/src/primitives/combobox.tsx +123 -0
  38. package/src/primitives/command-palette.tsx +216 -0
  39. package/src/primitives/context-menu.tsx +84 -0
  40. package/src/primitives/date-calendar.tsx +76 -0
  41. package/src/primitives/date-picker.tsx +400 -0
  42. package/src/primitives/dialog.tsx +279 -0
  43. package/src/primitives/divider.tsx +31 -0
  44. package/src/primitives/drawer.tsx +95 -0
  45. package/src/primitives/dropdown-menu.tsx +164 -0
  46. package/src/primitives/file-upload.tsx +123 -0
  47. package/src/primitives/icon-button.tsx +23 -0
  48. package/src/primitives/input.tsx +124 -0
  49. package/src/primitives/modal.tsx +26 -0
  50. package/src/primitives/pagination.tsx +58 -0
  51. package/src/primitives/popover.tsx +28 -0
  52. package/src/primitives/progress.tsx +145 -0
  53. package/src/primitives/radio-group.tsx +63 -0
  54. package/src/primitives/select.tsx +207 -0
  55. package/src/primitives/skeleton.tsx +61 -0
  56. package/src/primitives/slider.tsx +44 -0
  57. package/src/primitives/switch.tsx +146 -0
  58. package/src/primitives/table.tsx +47 -0
  59. package/src/primitives/tabs.tsx +86 -0
  60. package/src/primitives/textarea.tsx +40 -0
  61. package/src/primitives/toast.tsx +71 -0
  62. package/src/primitives/tooltip.tsx +58 -0
  63. package/src/styles/globals.css +49 -0
  64. package/src/templates/control-tower.tsx +41 -0
  65. package/src/templates/executive-analytics.tsx +272 -0
  66. package/src/templates/workspace-settings.tsx +179 -0
  67. package/src/utils/cn.ts +7 -0
@@ -0,0 +1,145 @@
1
+ import * as React from "react"
2
+ import { cn } from "../utils/cn"
3
+
4
+ export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ value: number
6
+ max?: number
7
+ variant?: "blue" | "indigo" | "emerald" | "amber" | "rose" | "white"
8
+ size?: "sm" | "default" | "lg"
9
+ showValue?: boolean
10
+ label?: string
11
+ }
12
+
13
+ const barColorMap: Record<NonNullable<ProgressProps["variant"]>, string> = {
14
+ blue: "bg-blue-600 dark:bg-blue-500",
15
+ indigo: "bg-blue-600 dark:bg-blue-500",
16
+ emerald: "bg-emerald-600 dark:bg-emerald-500",
17
+ amber: "bg-amber-500",
18
+ rose: "bg-rose-600 dark:bg-rose-500",
19
+ white: "bg-zinc-900 dark:bg-zinc-100",
20
+ }
21
+
22
+ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
23
+ (
24
+ {
25
+ className,
26
+ value,
27
+ max = 100,
28
+ variant = "blue",
29
+ size = "default",
30
+ showValue = false,
31
+ label,
32
+ ...props
33
+ },
34
+ ref
35
+ ) => {
36
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100))
37
+
38
+ return (
39
+ <div className={cn("w-full space-y-1.5", className)} ref={ref} {...props}>
40
+ {(label || showValue) && (
41
+ <div className="flex items-center justify-between text-xs">
42
+ {label && (
43
+ <span className="text-zinc-700 dark:text-zinc-300 font-medium">
44
+ {label}
45
+ </span>
46
+ )}
47
+ {showValue && (
48
+ <span className="font-mono text-zinc-500 dark:text-zinc-400">
49
+ {Math.round(percentage)}%
50
+ </span>
51
+ )}
52
+ </div>
53
+ )}
54
+ <div
55
+ role="progressbar"
56
+ aria-valuenow={value}
57
+ aria-valuemin={0}
58
+ aria-valuemax={max}
59
+ className={cn(
60
+ "w-full overflow-hidden rounded-full bg-zinc-200/80 dark:bg-zinc-800",
61
+ size === "sm" && "h-1.5",
62
+ size === "default" && "h-2",
63
+ size === "lg" && "h-3"
64
+ )}
65
+ >
66
+ <div
67
+ className={cn(
68
+ "h-full rounded-full transition-all duration-300",
69
+ barColorMap[variant]
70
+ )}
71
+ style={{ width: `${percentage}%` }}
72
+ />
73
+ </div>
74
+ </div>
75
+ )
76
+ }
77
+ )
78
+ Progress.displayName = "Progress"
79
+
80
+ export interface CircularProgressProps extends React.HTMLAttributes<HTMLDivElement> {
81
+ value: number
82
+ size?: number
83
+ strokeWidth?: number
84
+ variant?: "blue" | "indigo" | "emerald" | "amber" | "rose"
85
+ }
86
+
87
+ const strokeColorMap: Record<NonNullable<CircularProgressProps["variant"]>, string> = {
88
+ blue: "text-blue-600 dark:text-blue-500",
89
+ indigo: "text-blue-600 dark:text-blue-500",
90
+ emerald: "text-emerald-600 dark:text-emerald-500",
91
+ amber: "text-amber-500",
92
+ rose: "text-rose-600 dark:text-rose-500",
93
+ }
94
+
95
+ const CircularProgress: React.FC<CircularProgressProps> = ({
96
+ value,
97
+ size = 48,
98
+ strokeWidth = 4,
99
+ variant = "blue",
100
+ className,
101
+ ...props
102
+ }) => {
103
+ const clamped = Math.min(100, Math.max(0, value))
104
+ const radius = (size - strokeWidth) / 2
105
+ const circumference = 2 * Math.PI * radius
106
+ const offset = circumference - (clamped / 100) * circumference
107
+
108
+ return (
109
+ <div
110
+ className={cn("relative inline-flex items-center justify-center", className)}
111
+ style={{ width: size, height: size }}
112
+ {...props}
113
+ >
114
+ <svg width={size} height={size} className="-rotate-90 transform">
115
+ <circle
116
+ cx={size / 2}
117
+ cy={size / 2}
118
+ r={radius}
119
+ fill="transparent"
120
+ stroke="currentColor"
121
+ strokeWidth={strokeWidth}
122
+ className="text-zinc-200 dark:text-zinc-800"
123
+ />
124
+ <circle
125
+ cx={size / 2}
126
+ cy={size / 2}
127
+ r={radius}
128
+ fill="transparent"
129
+ stroke="currentColor"
130
+ strokeWidth={strokeWidth}
131
+ strokeDasharray={circumference}
132
+ strokeDashoffset={offset}
133
+ strokeLinecap="round"
134
+ className={cn("transition-all duration-300", strokeColorMap[variant])}
135
+ />
136
+ </svg>
137
+ <span className="absolute font-mono text-[11px] font-medium text-zinc-800 dark:text-zinc-200">
138
+ {Math.round(clamped)}%
139
+ </span>
140
+ </div>
141
+ )
142
+ }
143
+ CircularProgress.displayName = "CircularProgress"
144
+
145
+ export { Progress, CircularProgress }
@@ -0,0 +1,63 @@
1
+ import * as React from "react"
2
+ import { cn } from "../utils/cn"
3
+
4
+ export interface RadioGroupOption {
5
+ value: string
6
+ label: string
7
+ description?: string
8
+ }
9
+
10
+ export interface RadioGroupProps {
11
+ options: RadioGroupOption[]
12
+ value: string
13
+ onValueChange: (val: string) => void
14
+ className?: string
15
+ }
16
+
17
+ export const RadioGroup: React.FC<RadioGroupProps> = ({
18
+ options,
19
+ value,
20
+ onValueChange,
21
+ className,
22
+ }) => (
23
+ <div role="radiogroup" className={cn("space-y-2", className)}>
24
+ {options.map((opt) => {
25
+ const checked = opt.value === value
26
+ return (
27
+ <label
28
+ key={opt.value}
29
+ onClick={() => onValueChange(opt.value)}
30
+ className={cn(
31
+ "flex items-start gap-3 rounded-xl border p-3.5 transition-colors cursor-pointer",
32
+ checked
33
+ ? "border-blue-500/50 bg-blue-50/40 dark:border-blue-500/40 dark:bg-blue-500/10"
34
+ : "border-zinc-200 bg-white hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900/60 dark:hover:bg-zinc-800/50"
35
+ )}
36
+ >
37
+ <span
38
+ className={cn(
39
+ "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border",
40
+ checked
41
+ ? "border-blue-600 bg-blue-600 text-white"
42
+ : "border-zinc-300 dark:border-zinc-700"
43
+ )}
44
+ >
45
+ {checked && <span className="h-1.5 w-1.5 rounded-full bg-white" />}
46
+ </span>
47
+ <div className="space-y-0.5 text-xs">
48
+ <div className="font-medium text-zinc-900 dark:text-zinc-100">
49
+ {opt.label}
50
+ </div>
51
+ {opt.description && (
52
+ <div className="text-zinc-500 dark:text-zinc-400">
53
+ {opt.description}
54
+ </div>
55
+ )}
56
+ </div>
57
+ </label>
58
+ )
59
+ })}
60
+ </div>
61
+ )
62
+ RadioGroup.displayName = "RadioGroup"
63
+
@@ -0,0 +1,207 @@
1
+ import * as React from "react"
2
+ import * as SelectPrimitive from "@radix-ui/react-select"
3
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
4
+ import { Check, ChevronDown, Search, ChevronsUpDown } from "lucide-react"
5
+ import { cn } from "../utils/cn"
6
+
7
+ const Select = SelectPrimitive.Root
8
+ const SelectGroup = SelectPrimitive.Group
9
+ const SelectValue = SelectPrimitive.Value
10
+
11
+ const SelectTrigger = React.forwardRef<
12
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
13
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
14
+ >(({ className, children, ...props }, ref) => (
15
+ <SelectPrimitive.Trigger
16
+ ref={ref}
17
+ className={cn(
18
+ "flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs text-zinc-900 placeholder:text-zinc-400 transition-colors hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500/20 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800/60 [&>span]:line-clamp-1 cursor-pointer",
19
+ className
20
+ )}
21
+ {...props}
22
+ >
23
+ {children}
24
+ <SelectPrimitive.Icon asChild>
25
+ <ChevronDown className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
26
+ </SelectPrimitive.Icon>
27
+ </SelectPrimitive.Trigger>
28
+ ))
29
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
30
+
31
+ const SelectContent = React.forwardRef<
32
+ React.ElementRef<typeof SelectPrimitive.Content>,
33
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
34
+ >(({ className, children, position = "popper", ...props }, ref) => (
35
+ <SelectPrimitive.Portal>
36
+ <SelectPrimitive.Content
37
+ ref={ref}
38
+ className={cn(
39
+ "relative z-50 max-h-72 min-w-[8rem] overflow-hidden rounded-xl border border-zinc-200 bg-white text-zinc-900 shadow-lg dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100",
40
+ position === "popper" &&
41
+ "data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
42
+ className
43
+ )}
44
+ position={position}
45
+ {...props}
46
+ >
47
+ <SelectPrimitive.Viewport
48
+ className={cn(
49
+ "p-1",
50
+ position === "popper" &&
51
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
52
+ )}
53
+ >
54
+ {children}
55
+ </SelectPrimitive.Viewport>
56
+ </SelectPrimitive.Content>
57
+ </SelectPrimitive.Portal>
58
+ ))
59
+ SelectContent.displayName = SelectPrimitive.Content.displayName
60
+
61
+ const SelectItem = React.forwardRef<
62
+ React.ElementRef<typeof SelectPrimitive.Item>,
63
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
64
+ >(({ className, children, ...props }, ref) => (
65
+ <SelectPrimitive.Item
66
+ ref={ref}
67
+ className={cn(
68
+ "relative flex w-full cursor-pointer select-none items-center rounded-lg py-1.5 pl-8 pr-2.5 text-xs text-zinc-700 outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:text-zinc-200 dark:focus:bg-zinc-800 dark:focus:text-white",
69
+ className
70
+ )}
71
+ {...props}
72
+ >
73
+ <span className="absolute left-2.5 flex h-3.5 w-3.5 items-center justify-center">
74
+ <SelectPrimitive.ItemIndicator>
75
+ <Check className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400" />
76
+ </SelectPrimitive.ItemIndicator>
77
+ </span>
78
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
79
+ </SelectPrimitive.Item>
80
+ ))
81
+ SelectItem.displayName = SelectPrimitive.Item.displayName
82
+
83
+ export interface ComboboxOption {
84
+ value: string
85
+ label: string
86
+ badge?: string
87
+ }
88
+
89
+ export interface ComboboxProps {
90
+ options: ComboboxOption[]
91
+ value: string
92
+ onValueChange: (value: string) => void
93
+ placeholder?: string
94
+ searchPlaceholder?: string
95
+ className?: string
96
+ }
97
+
98
+ const Combobox: React.FC<ComboboxProps> = ({
99
+ options,
100
+ value,
101
+ onValueChange,
102
+ placeholder = "Select option...",
103
+ searchPlaceholder = "Search...",
104
+ className,
105
+ }) => {
106
+ const [open, setOpen] = React.useState(false)
107
+ const [query, setQuery] = React.useState("")
108
+
109
+ const selected = options.find((o) => o.value === value)
110
+ const filtered = React.useMemo(() => {
111
+ if (!query.trim()) return options
112
+ return options.filter((o) =>
113
+ o.label.toLowerCase().includes(query.toLowerCase())
114
+ )
115
+ }, [options, query])
116
+
117
+ return (
118
+ <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
119
+ <PopoverPrimitive.Trigger asChild>
120
+ <button
121
+ type="button"
122
+ className={cn(
123
+ "flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs text-zinc-900 transition-colors hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800/60 cursor-pointer",
124
+ className
125
+ )}
126
+ >
127
+ <span className={cn("truncate", !selected && "text-zinc-400")}>
128
+ {selected ? selected.label : placeholder}
129
+ </span>
130
+ <ChevronsUpDown className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
131
+ </button>
132
+ </PopoverPrimitive.Trigger>
133
+ <PopoverPrimitive.Portal>
134
+ <PopoverPrimitive.Content
135
+ align="start"
136
+ sideOffset={6}
137
+ className="z-50 w-64 rounded-xl border border-zinc-200 bg-white p-1.5 text-zinc-900 shadow-lg focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
138
+ >
139
+ <div className="flex items-center gap-2 border-b border-zinc-200 px-2.5 py-1.5 dark:border-zinc-800">
140
+ <Search className="h-3.5 w-3.5 text-zinc-400" />
141
+ <input
142
+ type="text"
143
+ value={query}
144
+ onChange={(e) => setQuery(e.target.value)}
145
+ placeholder={searchPlaceholder}
146
+ className="w-full bg-transparent text-xs text-zinc-900 placeholder:text-zinc-400 focus:outline-none dark:text-zinc-100"
147
+ />
148
+ </div>
149
+ <div className="mt-1 max-h-52 overflow-y-auto space-y-0.5">
150
+ {filtered.length === 0 ? (
151
+ <div className="py-4 text-center text-xs text-zinc-400">
152
+ No matching items found.
153
+ </div>
154
+ ) : (
155
+ filtered.map((opt) => {
156
+ const isSelected = opt.value === value
157
+ return (
158
+ <button
159
+ key={opt.value}
160
+ type="button"
161
+ onClick={() => {
162
+ onValueChange(opt.value)
163
+ setOpen(false)
164
+ setQuery("")
165
+ }}
166
+ className={cn(
167
+ "flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-xs transition-colors cursor-pointer",
168
+ isSelected
169
+ ? "bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300"
170
+ : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
171
+ )}
172
+ >
173
+ <span className="flex items-center gap-2">
174
+ <Check
175
+ className={cn(
176
+ "h-3.5 w-3.5",
177
+ isSelected ? "opacity-100 text-blue-600 dark:text-blue-400" : "opacity-0"
178
+ )}
179
+ />
180
+ {opt.label}
181
+ </span>
182
+ {opt.badge && (
183
+ <span className="rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-[10px] text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
184
+ {opt.badge}
185
+ </span>
186
+ )}
187
+ </button>
188
+ )
189
+ })
190
+ )}
191
+ </div>
192
+ </PopoverPrimitive.Content>
193
+ </PopoverPrimitive.Portal>
194
+ </PopoverPrimitive.Root>
195
+ )
196
+ }
197
+ Combobox.displayName = "Combobox"
198
+
199
+ export {
200
+ Select,
201
+ SelectGroup,
202
+ SelectValue,
203
+ SelectTrigger,
204
+ SelectContent,
205
+ SelectItem,
206
+ Combobox,
207
+ }
@@ -0,0 +1,61 @@
1
+ import * as React from "react"
2
+ import { cn } from "../utils/cn"
3
+
4
+ function Skeleton({
5
+ className,
6
+ ...props
7
+ }: React.HTMLAttributes<HTMLDivElement>) {
8
+ return (
9
+ <div
10
+ className={cn(
11
+ "animate-pulse rounded-md bg-zinc-200/80 dark:bg-zinc-800",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ function CardSkeleton({ className }: { className?: string }) {
20
+ return (
21
+ <div
22
+ className={cn(
23
+ "rounded-xl border border-zinc-200 bg-white p-5 space-y-4 dark:border-zinc-800 dark:bg-zinc-900",
24
+ className
25
+ )}
26
+ >
27
+ <div className="flex items-center justify-between">
28
+ <Skeleton className="h-4 w-28" />
29
+ <Skeleton className="h-5 w-14 rounded-full" />
30
+ </div>
31
+ <Skeleton className="h-8 w-36" />
32
+ <div className="space-y-2 pt-2">
33
+ <Skeleton className="h-2.5 w-full" />
34
+ <Skeleton className="h-2.5 w-4/5" />
35
+ </div>
36
+ </div>
37
+ )
38
+ }
39
+
40
+ function TableRowSkeleton({ rows = 4 }: { rows?: number }) {
41
+ return (
42
+ <div className="w-full rounded-xl border border-zinc-200 bg-white divide-y divide-zinc-200 dark:border-zinc-800 dark:bg-zinc-900 dark:divide-zinc-800">
43
+ {Array.from({ length: rows }).map((_, idx) => (
44
+ <div key={idx} className="flex items-center justify-between px-4 py-3 gap-4">
45
+ <div className="flex items-center gap-3">
46
+ <Skeleton className="h-8 w-8 rounded-full" />
47
+ <div className="space-y-1.5">
48
+ <Skeleton className="h-3.5 w-32" />
49
+ <Skeleton className="h-2.5 w-24" />
50
+ </div>
51
+ </div>
52
+ <Skeleton className="h-5 w-16 rounded-md" />
53
+ <Skeleton className="h-3.5 w-20" />
54
+ <Skeleton className="h-7 w-7 rounded-md" />
55
+ </div>
56
+ ))}
57
+ </div>
58
+ )
59
+ }
60
+
61
+ export { Skeleton, CardSkeleton, TableRowSkeleton }
@@ -0,0 +1,44 @@
1
+ import * as React from "react"
2
+ import { cn } from "../utils/cn"
3
+
4
+ export interface SliderProps {
5
+ value: number
6
+ min?: number
7
+ max?: number
8
+ step?: number
9
+ onValueChange?: (val: number) => void
10
+ label?: string
11
+ className?: string
12
+ }
13
+
14
+ export const Slider: React.FC<SliderProps> = ({
15
+ value,
16
+ min = 0,
17
+ max = 100,
18
+ step = 1,
19
+ onValueChange,
20
+ label,
21
+ className,
22
+ }) => (
23
+ <div className={cn("space-y-2", className)}>
24
+ {label && (
25
+ <div className="flex items-center justify-between text-xs">
26
+ <span className="font-medium text-zinc-700 dark:text-zinc-300">
27
+ {label}
28
+ </span>
29
+ <span className="font-mono text-zinc-500">{value}%</span>
30
+ </div>
31
+ )}
32
+ <input
33
+ type="range"
34
+ min={min}
35
+ max={max}
36
+ step={step}
37
+ value={value}
38
+ onChange={(e) => onValueChange?.(Number(e.target.value))}
39
+ className="h-1.5 w-full cursor-pointer appearance-none rounded-lg bg-zinc-200 accent-blue-600 dark:bg-zinc-800"
40
+ />
41
+ </div>
42
+ )
43
+ Slider.displayName = "Slider"
44
+
@@ -0,0 +1,146 @@
1
+ import * as React from "react"
2
+ import * as SwitchPrimitives from "@radix-ui/react-switch"
3
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
4
+ import { Check } from "lucide-react"
5
+ import { cn } from "../utils/cn"
6
+
7
+ const Switch = React.forwardRef<
8
+ React.ElementRef<typeof SwitchPrimitives.Root>,
9
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <SwitchPrimitives.Root
12
+ className={cn(
13
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-zinc-200 dark:data-[state=unchecked]:bg-zinc-800",
14
+ className
15
+ )}
16
+ {...props}
17
+ ref={ref}
18
+ >
19
+ <SwitchPrimitives.Thumb
20
+ className={cn(
21
+ "pointer-events-none block h-4 w-4 rounded-full bg-white shadow-xs ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5"
22
+ )}
23
+ />
24
+ </SwitchPrimitives.Root>
25
+ ))
26
+ Switch.displayName = SwitchPrimitives.Root.displayName
27
+
28
+ const Checkbox = React.forwardRef<
29
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
30
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
31
+ >(({ className, ...props }, ref) => (
32
+ <CheckboxPrimitive.Root
33
+ ref={ref}
34
+ className={cn(
35
+ "peer h-4 w-4 shrink-0 rounded border border-zinc-300 bg-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600 data-[state=checked]:text-white dark:border-zinc-700 dark:bg-zinc-900 dark:data-[state=checked]:bg-blue-600 cursor-pointer",
36
+ className
37
+ )}
38
+ {...props}
39
+ >
40
+ <CheckboxPrimitive.Indicator
41
+ className={cn("flex items-center justify-center text-current")}
42
+ >
43
+ <Check className="h-3 w-3 stroke-[2.5]" />
44
+ </CheckboxPrimitive.Indicator>
45
+ </CheckboxPrimitive.Root>
46
+ ))
47
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
48
+
49
+ export interface RadioGroupOption {
50
+ value: string
51
+ label: string
52
+ description?: string
53
+ }
54
+
55
+ export interface RadioGroupProps {
56
+ options: RadioGroupOption[]
57
+ value: string
58
+ onValueChange: (val: string) => void
59
+ className?: string
60
+ }
61
+
62
+ const RadioGroup: React.FC<RadioGroupProps> = ({
63
+ options,
64
+ value,
65
+ onValueChange,
66
+ className,
67
+ }) => (
68
+ <div role="radiogroup" className={cn("space-y-2", className)}>
69
+ {options.map((opt) => {
70
+ const checked = opt.value === value
71
+ return (
72
+ <label
73
+ key={opt.value}
74
+ onClick={() => onValueChange(opt.value)}
75
+ className={cn(
76
+ "flex items-start gap-3 rounded-xl border p-3.5 transition-colors cursor-pointer",
77
+ checked
78
+ ? "border-blue-500/50 bg-blue-50/40 dark:border-blue-500/40 dark:bg-blue-500/10"
79
+ : "border-zinc-200 bg-white hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900/60 dark:hover:bg-zinc-800/50"
80
+ )}
81
+ >
82
+ <span
83
+ className={cn(
84
+ "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border",
85
+ checked
86
+ ? "border-blue-600 bg-blue-600 text-white"
87
+ : "border-zinc-300 dark:border-zinc-700"
88
+ )}
89
+ >
90
+ {checked && <span className="h-1.5 w-1.5 rounded-full bg-white" />}
91
+ </span>
92
+ <div className="space-y-0.5 text-xs">
93
+ <div className="font-medium text-zinc-900 dark:text-zinc-100">
94
+ {opt.label}
95
+ </div>
96
+ {opt.description && (
97
+ <div className="text-zinc-500 dark:text-zinc-400">
98
+ {opt.description}
99
+ </div>
100
+ )}
101
+ </div>
102
+ </label>
103
+ )
104
+ })}
105
+ </div>
106
+ )
107
+
108
+ export interface SliderProps {
109
+ value: number
110
+ min?: number
111
+ max?: number
112
+ step?: number
113
+ onValueChange?: (val: number) => void
114
+ label?: string
115
+ className?: string
116
+ }
117
+
118
+ const Slider: React.FC<SliderProps> = ({
119
+ value,
120
+ min = 0,
121
+ max = 100,
122
+ step = 1,
123
+ onValueChange,
124
+ label,
125
+ className,
126
+ }) => (
127
+ <div className={cn("space-y-2", className)}>
128
+ {label && (
129
+ <div className="flex items-center justify-between text-xs">
130
+ <span className="font-medium text-zinc-700 dark:text-zinc-300">{label}</span>
131
+ <span className="font-mono text-zinc-500">{value}%</span>
132
+ </div>
133
+ )}
134
+ <input
135
+ type="range"
136
+ min={min}
137
+ max={max}
138
+ step={step}
139
+ value={value}
140
+ onChange={(e) => onValueChange?.(Number(e.target.value))}
141
+ className="h-1.5 w-full cursor-pointer appearance-none rounded-lg bg-zinc-200 accent-blue-600 dark:bg-zinc-800"
142
+ />
143
+ </div>
144
+ )
145
+
146
+ export { Switch, Checkbox, RadioGroup, Slider }