@etherealui/react 0.0.0-stage → 0.0.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.
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/index.d.mts +583 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +5189 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5189 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -3
- package/src/blocks/app-shell.tsx +441 -0
- package/src/blocks/billing-card.tsx +2 -0
- package/src/blocks/data-table.tsx +479 -0
- package/src/blocks/filter-bar.tsx +134 -0
- package/src/blocks/header-navbar.tsx +7 -0
- package/src/blocks/metric-grid.tsx +5 -0
- package/src/blocks/profile-form.tsx +2 -0
- package/src/blocks/security-settings.tsx +2 -0
- package/src/blocks/settings-blocks.tsx +406 -0
- package/src/blocks/sidebar.tsx +6 -0
- package/src/blocks/stat-card.tsx +7 -0
- package/src/blocks/stat-cards.tsx +212 -0
- package/src/blocks/team-members.tsx +5 -0
- package/src/index.ts +114 -0
- package/src/primitives/accordion.tsx +61 -0
- package/src/primitives/announcement.tsx +33 -0
- package/src/primitives/avatar.tsx +156 -0
- package/src/primitives/badge.tsx +108 -0
- package/src/primitives/breadcrumb.tsx +45 -0
- package/src/primitives/button-group.tsx +79 -0
- package/src/primitives/button.tsx +165 -0
- package/src/primitives/carousel.tsx +84 -0
- package/src/primitives/chart.tsx +831 -0
- package/src/primitives/checkbox.tsx +26 -0
- package/src/primitives/chip.tsx +37 -0
- package/src/primitives/close-button.tsx +29 -0
- package/src/primitives/color-picker.tsx +62 -0
- package/src/primitives/combobox.tsx +123 -0
- package/src/primitives/command-palette.tsx +216 -0
- package/src/primitives/context-menu.tsx +84 -0
- package/src/primitives/date-calendar.tsx +76 -0
- package/src/primitives/date-picker.tsx +400 -0
- package/src/primitives/dialog.tsx +279 -0
- package/src/primitives/divider.tsx +31 -0
- package/src/primitives/drawer.tsx +95 -0
- package/src/primitives/dropdown-menu.tsx +164 -0
- package/src/primitives/file-upload.tsx +123 -0
- package/src/primitives/icon-button.tsx +23 -0
- package/src/primitives/input.tsx +124 -0
- package/src/primitives/modal.tsx +26 -0
- package/src/primitives/pagination.tsx +58 -0
- package/src/primitives/popover.tsx +28 -0
- package/src/primitives/progress.tsx +145 -0
- package/src/primitives/radio-group.tsx +63 -0
- package/src/primitives/select.tsx +207 -0
- package/src/primitives/skeleton.tsx +61 -0
- package/src/primitives/slider.tsx +44 -0
- package/src/primitives/switch.tsx +146 -0
- package/src/primitives/table.tsx +47 -0
- package/src/primitives/tabs.tsx +86 -0
- package/src/primitives/textarea.tsx +40 -0
- package/src/primitives/toast.tsx +71 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/styles/globals.css +49 -0
- package/src/templates/control-tower.tsx +41 -0
- package/src/templates/executive-analytics.tsx +272 -0
- package/src/templates/workspace-settings.tsx +179 -0
- 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 “{query}”
|
|
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
|
+
|