@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,26 @@
1
+ import * as React from "react"
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3
+ import { Check } from "lucide-react"
4
+ import { cn } from "../utils/cn"
5
+
6
+ export const Checkbox = React.forwardRef<
7
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <CheckboxPrimitive.Root
11
+ ref={ref}
12
+ className={cn(
13
+ "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",
14
+ className
15
+ )}
16
+ {...props}
17
+ >
18
+ <CheckboxPrimitive.Indicator
19
+ className={cn("flex items-center justify-center text-current")}
20
+ >
21
+ <Check className="h-3 w-3 stroke-[2.5]" />
22
+ </CheckboxPrimitive.Indicator>
23
+ </CheckboxPrimitive.Root>
24
+ ))
25
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
26
+
@@ -0,0 +1,37 @@
1
+ import * as React from "react"
2
+ import { X } from "lucide-react"
3
+ import { cn } from "../utils/cn"
4
+
5
+ export interface ChipProps extends React.HTMLAttributes<HTMLSpanElement> {
6
+ onRemove?: () => void
7
+ active?: boolean
8
+ }
9
+
10
+ export const Chip = React.forwardRef<HTMLSpanElement, ChipProps>(
11
+ ({ className, children, onRemove, active = false, ...props }, ref) => (
12
+ <span
13
+ ref={ref}
14
+ className={cn(
15
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors",
16
+ active
17
+ ? "border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-500/30 dark:bg-blue-500/15 dark:text-blue-300"
18
+ : "border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800",
19
+ className
20
+ )}
21
+ {...props}
22
+ >
23
+ {children}
24
+ {onRemove && (
25
+ <button
26
+ type="button"
27
+ onClick={onRemove}
28
+ className="rounded-full p-0.5 text-zinc-400 hover:bg-zinc-200/60 hover:text-zinc-700 dark:hover:bg-zinc-700 dark:hover:text-zinc-200 cursor-pointer"
29
+ >
30
+ <X className="h-3 w-3" />
31
+ </button>
32
+ )}
33
+ </span>
34
+ )
35
+ )
36
+ Chip.displayName = "Chip"
37
+
@@ -0,0 +1,29 @@
1
+ import * as React from "react"
2
+ import { X } from "lucide-react"
3
+ import { cn } from "../utils/cn"
4
+
5
+ export interface CloseButtonProps
6
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7
+ size?: "sm" | "default"
8
+ }
9
+
10
+ export const CloseButton = React.forwardRef<
11
+ HTMLButtonElement,
12
+ CloseButtonProps
13
+ >(({ className, size = "default", ...props }, ref) => (
14
+ <button
15
+ ref={ref}
16
+ type="button"
17
+ aria-label="Close"
18
+ className={cn(
19
+ "inline-flex items-center justify-center rounded-lg text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer",
20
+ size === "sm" ? "h-6 w-6" : "h-8 w-8",
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ <X className={size === "sm" ? "h-3.5 w-3.5" : "h-4 w-4"} />
26
+ </button>
27
+ ))
28
+ CloseButton.displayName = "CloseButton"
29
+
@@ -0,0 +1,62 @@
1
+ import * as React from "react"
2
+ import { cn } from "../utils/cn"
3
+
4
+ export interface ColorPickerProps {
5
+ className?: string
6
+ defaultColor?: string
7
+ }
8
+
9
+ export const ColorPicker: React.FC<ColorPickerProps> = ({
10
+ className,
11
+ defaultColor = "#2563eb",
12
+ }) => {
13
+ const swatches = [
14
+ "#2563eb",
15
+ "#0ea5e9",
16
+ "#10b981",
17
+ "#f59e0b",
18
+ "#f43f5e",
19
+ "#8b5cf6",
20
+ "#18181b",
21
+ ]
22
+ const [color, setColor] = React.useState(defaultColor)
23
+
24
+ return (
25
+ <div
26
+ className={cn(
27
+ "inline-flex flex-col gap-3 rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900",
28
+ className
29
+ )}
30
+ >
31
+ <div className="flex items-center gap-2.5">
32
+ <span
33
+ className="h-6 w-6 rounded-lg border border-black/10 shrink-0"
34
+ style={{ backgroundColor: color }}
35
+ />
36
+ <input
37
+ type="text"
38
+ value={color}
39
+ onChange={(e) => setColor(e.target.value)}
40
+ className="w-28 rounded-lg border border-zinc-200 bg-zinc-50 px-2.5 py-1 font-mono text-xs text-zinc-800 focus:outline-none dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
41
+ />
42
+ </div>
43
+ <div className="flex items-center gap-1.5">
44
+ {swatches.map((sw) => (
45
+ <button
46
+ key={sw}
47
+ type="button"
48
+ onClick={() => setColor(sw)}
49
+ style={{ backgroundColor: sw }}
50
+ className={cn(
51
+ "h-6 w-6 rounded-full transition-transform cursor-pointer",
52
+ color === sw &&
53
+ "scale-110 ring-2 ring-offset-2 ring-blue-500 dark:ring-offset-zinc-900"
54
+ )}
55
+ />
56
+ ))}
57
+ </div>
58
+ </div>
59
+ )
60
+ }
61
+ ColorPicker.displayName = "ColorPicker"
62
+
@@ -0,0 +1,123 @@
1
+ import * as React from "react"
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
3
+ import { Check, Search, ChevronsUpDown } from "lucide-react"
4
+ import { cn } from "../utils/cn"
5
+
6
+ export interface ComboboxOption {
7
+ value: string
8
+ label: string
9
+ badge?: string
10
+ }
11
+
12
+ export interface ComboboxProps {
13
+ options: ComboboxOption[]
14
+ value: string
15
+ onValueChange: (value: string) => void
16
+ placeholder?: string
17
+ searchPlaceholder?: string
18
+ className?: string
19
+ }
20
+
21
+ export const Combobox: React.FC<ComboboxProps> = ({
22
+ options,
23
+ value,
24
+ onValueChange,
25
+ placeholder = "Select option...",
26
+ searchPlaceholder = "Search...",
27
+ className,
28
+ }) => {
29
+ const [open, setOpen] = React.useState(false)
30
+ const [query, setQuery] = React.useState("")
31
+
32
+ const selected = options.find((o) => o.value === value)
33
+ const filtered = React.useMemo(() => {
34
+ if (!query.trim()) return options
35
+ return options.filter((o) =>
36
+ o.label.toLowerCase().includes(query.toLowerCase())
37
+ )
38
+ }, [options, query])
39
+
40
+ return (
41
+ <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
42
+ <PopoverPrimitive.Trigger asChild>
43
+ <button
44
+ type="button"
45
+ className={cn(
46
+ "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",
47
+ className
48
+ )}
49
+ >
50
+ <span className={cn("truncate", !selected && "text-zinc-400")}>
51
+ {selected ? selected.label : placeholder}
52
+ </span>
53
+ <ChevronsUpDown className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
54
+ </button>
55
+ </PopoverPrimitive.Trigger>
56
+ <PopoverPrimitive.Portal>
57
+ <PopoverPrimitive.Content
58
+ align="start"
59
+ sideOffset={6}
60
+ 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"
61
+ >
62
+ <div className="flex items-center gap-2 border-b border-zinc-200 px-2.5 py-1.5 dark:border-zinc-800">
63
+ <Search className="h-3.5 w-3.5 text-zinc-400" />
64
+ <input
65
+ type="text"
66
+ value={query}
67
+ onChange={(e) => setQuery(e.target.value)}
68
+ placeholder={searchPlaceholder}
69
+ className="w-full bg-transparent text-xs text-zinc-900 placeholder:text-zinc-400 focus:outline-none dark:text-zinc-100"
70
+ />
71
+ </div>
72
+ <div className="mt-1 max-h-52 overflow-y-auto space-y-0.5">
73
+ {filtered.length === 0 ? (
74
+ <div className="py-4 text-center text-xs text-zinc-400">
75
+ No matching items found.
76
+ </div>
77
+ ) : (
78
+ filtered.map((opt) => {
79
+ const isSelected = opt.value === value
80
+ return (
81
+ <button
82
+ key={opt.value}
83
+ type="button"
84
+ onClick={() => {
85
+ onValueChange(opt.value)
86
+ setOpen(false)
87
+ setQuery("")
88
+ }}
89
+ className={cn(
90
+ "flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-xs transition-colors cursor-pointer",
91
+ isSelected
92
+ ? "bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300"
93
+ : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
94
+ )}
95
+ >
96
+ <span className="flex items-center gap-2">
97
+ <Check
98
+ className={cn(
99
+ "h-3.5 w-3.5",
100
+ isSelected
101
+ ? "opacity-100 text-blue-600 dark:text-blue-400"
102
+ : "opacity-0"
103
+ )}
104
+ />
105
+ {opt.label}
106
+ </span>
107
+ {opt.badge && (
108
+ <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">
109
+ {opt.badge}
110
+ </span>
111
+ )}
112
+ </button>
113
+ )
114
+ })
115
+ )}
116
+ </div>
117
+ </PopoverPrimitive.Content>
118
+ </PopoverPrimitive.Portal>
119
+ </PopoverPrimitive.Root>
120
+ )
121
+ }
122
+ Combobox.displayName = "Combobox"
123
+
@@ -0,0 +1,216 @@
1
+ import * as React from "react"
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
3
+ import { Search, CornerDownLeft, FileText, Settings, Users, Terminal, BarChart3 } from "lucide-react"
4
+ import { cn } from "../utils/cn"
5
+
6
+ export interface CommandItem {
7
+ id: string
8
+ label: string
9
+ category: string
10
+ shortcut?: string
11
+ icon?: React.ReactNode
12
+ onSelect?: () => void
13
+ }
14
+
15
+ export interface CommandPaletteProps {
16
+ open?: boolean
17
+ onOpenChange?: (open: boolean) => void
18
+ items?: CommandItem[]
19
+ placeholder?: string
20
+ enableGlobalShortcut?: boolean
21
+ }
22
+
23
+ const defaultCommandItems: CommandItem[] = [
24
+ {
25
+ id: "overview",
26
+ label: "Go to Analytics Overview",
27
+ category: "Navigation",
28
+ shortcut: "G A",
29
+ icon: <BarChart3 className="h-4 w-4 text-blue-600 dark:text-blue-400" />,
30
+ },
31
+ {
32
+ id: "customers",
33
+ label: "Search Customer Directory",
34
+ category: "Navigation",
35
+ shortcut: "G C",
36
+ icon: <Users className="h-4 w-4 text-zinc-500" />,
37
+ },
38
+ {
39
+ id: "docs-cli",
40
+ label: "Copy CLI Installation Command",
41
+ category: "Developer",
42
+ shortcut: "⌘ C",
43
+ icon: <Terminal className="h-4 w-4 text-zinc-500" />,
44
+ },
45
+ {
46
+ id: "invoices",
47
+ label: "Export Monthly Invoice PDF",
48
+ category: "Billing",
49
+ shortcut: "⌘ E",
50
+ icon: <FileText className="h-4 w-4 text-zinc-500" />,
51
+ },
52
+ {
53
+ id: "settings",
54
+ label: "Open Workspace Preferences",
55
+ category: "Settings",
56
+ shortcut: "⌘ ,",
57
+ icon: <Settings className="h-4 w-4 text-zinc-500" />,
58
+ },
59
+ ]
60
+
61
+ export const CommandPalette: React.FC<CommandPaletteProps> = ({
62
+ open: controlledOpen,
63
+ onOpenChange,
64
+ items = defaultCommandItems,
65
+ placeholder = "Type a command or search...",
66
+ enableGlobalShortcut = true,
67
+ }) => {
68
+ const [internalOpen, setInternalOpen] = React.useState(false)
69
+ const [query, setQuery] = React.useState("")
70
+ const [activeIndex, setActiveIndex] = React.useState(0)
71
+
72
+ const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen
73
+ const setOpen = onOpenChange || setInternalOpen
74
+
75
+ React.useEffect(() => {
76
+ if (!enableGlobalShortcut) return
77
+ const onKeyDown = (e: KeyboardEvent) => {
78
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
79
+ e.preventDefault()
80
+ setOpen(!isOpen)
81
+ }
82
+ }
83
+ window.addEventListener("keydown", onKeyDown)
84
+ return () => window.removeEventListener("keydown", onKeyDown)
85
+ }, [enableGlobalShortcut, isOpen, setOpen])
86
+
87
+ const filtered = React.useMemo(() => {
88
+ if (!query.trim()) return items
89
+ const q = query.toLowerCase()
90
+ return items.filter(
91
+ (i) =>
92
+ i.label.toLowerCase().includes(q) ||
93
+ i.category.toLowerCase().includes(q)
94
+ )
95
+ }, [items, query])
96
+
97
+ const grouped = React.useMemo(() => {
98
+ const map = new Map<string, CommandItem[]>()
99
+ for (const item of filtered) {
100
+ const list = map.get(item.category) || []
101
+ list.push(item)
102
+ map.set(item.category, list)
103
+ }
104
+ return Array.from(map.entries())
105
+ }, [filtered])
106
+
107
+ React.useEffect(() => {
108
+ setActiveIndex(0)
109
+ }, [query])
110
+
111
+ const handleKeyDown = (e: React.KeyboardEvent) => {
112
+ if (e.key === "ArrowDown") {
113
+ e.preventDefault()
114
+ setActiveIndex((prev) => (filtered.length ? (prev + 1) % filtered.length : 0))
115
+ } else if (e.key === "ArrowUp") {
116
+ e.preventDefault()
117
+ setActiveIndex((prev) =>
118
+ filtered.length ? (prev - 1 + filtered.length) % filtered.length : 0
119
+ )
120
+ } else if (e.key === "Enter" && filtered[activeIndex]) {
121
+ e.preventDefault()
122
+ filtered[activeIndex].onSelect?.()
123
+ setOpen(false)
124
+ }
125
+ }
126
+
127
+ let flatCursor = -1
128
+
129
+ return (
130
+ <DialogPrimitive.Root open={isOpen} onOpenChange={setOpen}>
131
+ <DialogPrimitive.Portal>
132
+ <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-xs" />
133
+ <DialogPrimitive.Content
134
+ onKeyDown={handleKeyDown}
135
+ className="fixed left-1/2 top-[18%] z-50 w-full max-w-xl -translate-x-1/2 overflow-hidden rounded-2xl border border-zinc-200 bg-white text-zinc-900 shadow-2xl focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
136
+ >
137
+ <DialogPrimitive.Title className="sr-only">
138
+ Command Palette
139
+ </DialogPrimitive.Title>
140
+ <div className="flex items-center gap-3 border-b border-zinc-200 px-4 py-3 dark:border-zinc-800">
141
+ <Search className="h-4 w-4 text-zinc-400 shrink-0" />
142
+ <input
143
+ type="text"
144
+ autoFocus
145
+ value={query}
146
+ onChange={(e) => setQuery(e.target.value)}
147
+ placeholder={placeholder}
148
+ className="w-full bg-transparent text-sm text-zinc-900 placeholder:text-zinc-400 focus:outline-none dark:text-zinc-100"
149
+ />
150
+ <kbd className="rounded border border-zinc-200 bg-zinc-100 px-1.5 py-0.5 font-mono text-[10px] text-zinc-500 dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-400">
151
+ ESC
152
+ </kbd>
153
+ </div>
154
+
155
+ <div className="max-h-80 overflow-y-auto p-2 space-y-3">
156
+ {grouped.length === 0 ? (
157
+ <div className="py-10 text-center text-xs text-zinc-400">
158
+ No matching results for &ldquo;{query}&rdquo;
159
+ </div>
160
+ ) : (
161
+ grouped.map(([category, groupItems]) => (
162
+ <div key={category} className="space-y-1">
163
+ <div className="px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">
164
+ {category}
165
+ </div>
166
+ {groupItems.map((item) => {
167
+ flatCursor += 1
168
+ const idx = flatCursor
169
+ const isActive = idx === activeIndex
170
+ return (
171
+ <button
172
+ key={item.id}
173
+ type="button"
174
+ onMouseEnter={() => setActiveIndex(idx)}
175
+ onClick={() => {
176
+ item.onSelect?.()
177
+ setOpen(false)
178
+ }}
179
+ className={cn(
180
+ "flex w-full items-center justify-between rounded-xl px-3 py-2 text-xs transition-colors cursor-pointer",
181
+ isActive
182
+ ? "bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-white"
183
+ : "text-zinc-600 hover:bg-zinc-50 dark:text-zinc-300 dark:hover:bg-zinc-800/50"
184
+ )}
185
+ >
186
+ <span className="flex items-center gap-2.5">
187
+ {item.icon}
188
+ <span className="font-medium">{item.label}</span>
189
+ </span>
190
+ {item.shortcut && (
191
+ <kbd className="rounded border border-zinc-200 bg-white px-1.5 py-0.5 font-mono text-[10px] text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-400">
192
+ {item.shortcut}
193
+ </kbd>
194
+ )}
195
+ </button>
196
+ )
197
+ })}
198
+ </div>
199
+ ))
200
+ )}
201
+ </div>
202
+
203
+ <div className="flex items-center justify-between border-t border-zinc-200 bg-zinc-50 px-4 py-2 text-[11px] text-zinc-500 dark:border-zinc-800 dark:bg-zinc-950/50 dark:text-zinc-400">
204
+ <div className="flex items-center gap-3">
205
+ <span className="flex items-center gap-1">
206
+ <CornerDownLeft className="h-3 w-3" /> Select
207
+ </span>
208
+ <span>↑↓ Navigate</span>
209
+ </div>
210
+ <span className="font-mono text-[10px]">Quick Search</span>
211
+ </div>
212
+ </DialogPrimitive.Content>
213
+ </DialogPrimitive.Portal>
214
+ </DialogPrimitive.Root>
215
+ )
216
+ }
@@ -0,0 +1,84 @@
1
+ import * as React from "react"
2
+ import { cn } from "../utils/cn"
3
+
4
+ export interface ContextMenuAction {
5
+ label: string
6
+ shortcut?: string
7
+ icon?: React.ReactNode
8
+ destructive?: boolean
9
+ onClick?: () => void
10
+ }
11
+
12
+ export interface ContextMenuBoxProps
13
+ extends React.HTMLAttributes<HTMLDivElement> {
14
+ actions: ContextMenuAction[]
15
+ }
16
+
17
+ export const ContextMenuBox: React.FC<ContextMenuBoxProps> = ({
18
+ actions,
19
+ children,
20
+ className,
21
+ ...props
22
+ }) => {
23
+ const [coords, setCoords] = React.useState<{ x: number; y: number } | null>(
24
+ null
25
+ )
26
+
27
+ React.useEffect(() => {
28
+ const close = () => setCoords(null)
29
+ window.addEventListener("click", close)
30
+ return () => window.removeEventListener("click", close)
31
+ }, [])
32
+
33
+ return (
34
+ <div
35
+ onContextMenu={(e) => {
36
+ e.preventDefault()
37
+ const rect = e.currentTarget.getBoundingClientRect()
38
+ setCoords({
39
+ x: Math.min(e.clientX - rect.left, rect.width - 180),
40
+ y: Math.min(e.clientY - rect.top, rect.height - 140),
41
+ })
42
+ }}
43
+ className={cn("relative select-none", className)}
44
+ {...props}
45
+ >
46
+ {children}
47
+ {coords && (
48
+ <div
49
+ style={{ left: coords.x, top: coords.y }}
50
+ className="absolute z-40 min-w-[180px] rounded-xl border border-zinc-200 bg-white p-1 text-zinc-900 shadow-xl dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
51
+ >
52
+ {actions.map((act, idx) => (
53
+ <button
54
+ key={idx}
55
+ type="button"
56
+ onClick={() => {
57
+ act.onClick?.()
58
+ setCoords(null)
59
+ }}
60
+ className={cn(
61
+ "flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-xs transition-colors cursor-pointer",
62
+ act.destructive
63
+ ? "text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-500/15"
64
+ : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800"
65
+ )}
66
+ >
67
+ <span className="flex items-center gap-2">
68
+ {act.icon}
69
+ {act.label}
70
+ </span>
71
+ {act.shortcut && (
72
+ <span className="font-mono text-[10px] text-zinc-400">
73
+ {act.shortcut}
74
+ </span>
75
+ )}
76
+ </button>
77
+ ))}
78
+ </div>
79
+ )}
80
+ </div>
81
+ )
82
+ }
83
+ ContextMenuBox.displayName = "ContextMenuBox"
84
+
@@ -0,0 +1,76 @@
1
+ import * as React from "react"
2
+ import { ChevronLeft, ChevronRight } from "lucide-react"
3
+ import { cn } from "../utils/cn"
4
+
5
+ export interface DateCalendarProps {
6
+ className?: string
7
+ defaultSelectedDay?: number
8
+ }
9
+
10
+ export const DateCalendar: React.FC<DateCalendarProps> = ({
11
+ className,
12
+ defaultSelectedDay = 14,
13
+ }) => {
14
+ const [selectedDay, setSelectedDay] = React.useState(defaultSelectedDay)
15
+ const days = Array.from({ length: 30 }, (_, i) => i + 1)
16
+
17
+ return (
18
+ <div
19
+ className={cn(
20
+ "w-64 rounded-2xl border border-zinc-200 bg-white p-4 shadow-2xs dark:border-zinc-800 dark:bg-zinc-900",
21
+ className
22
+ )}
23
+ >
24
+ <div className="mb-3 flex items-center justify-between">
25
+ <span className="text-xs font-semibold text-zinc-900 dark:text-zinc-100">
26
+ November 2026
27
+ </span>
28
+ <div className="flex items-center gap-1">
29
+ <button
30
+ type="button"
31
+ className="rounded-md p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer"
32
+ >
33
+ <ChevronLeft className="h-3.5 w-3.5" />
34
+ </button>
35
+ <button
36
+ type="button"
37
+ className="rounded-md p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer"
38
+ >
39
+ <ChevronRight className="h-3.5 w-3.5" />
40
+ </button>
41
+ </div>
42
+ </div>
43
+ <div className="grid grid-cols-7 gap-1 text-center text-[10px] font-medium text-zinc-400 mb-1.5">
44
+ <span>Su</span>
45
+ <span>Mo</span>
46
+ <span>Tu</span>
47
+ <span>We</span>
48
+ <span>Th</span>
49
+ <span>Fr</span>
50
+ <span>Sa</span>
51
+ </div>
52
+ <div className="grid grid-cols-7 gap-1">
53
+ {days.map((d) => {
54
+ const active = d === selectedDay
55
+ return (
56
+ <button
57
+ key={d}
58
+ type="button"
59
+ onClick={() => setSelectedDay(d)}
60
+ className={cn(
61
+ "flex h-7 w-7 items-center justify-center rounded-lg text-xs transition-colors cursor-pointer",
62
+ active
63
+ ? "bg-blue-600 font-semibold text-white"
64
+ : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
65
+ )}
66
+ >
67
+ {d}
68
+ </button>
69
+ )
70
+ })}
71
+ </div>
72
+ </div>
73
+ )
74
+ }
75
+ DateCalendar.displayName = "DateCalendar"
76
+