@stampui/blocks 1.0.0
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/dist/components/ai-chat-shell.d.ts +1 -0
- package/dist/components/ai-chat-shell.js +23 -0
- package/dist/components/prompt-input.d.ts +5 -0
- package/dist/components/prompt-input.js +47 -0
- package/dist/components/registry-card.d.ts +6 -0
- package/dist/components/registry-card.js +15 -0
- package/dist/components/registry-explorer.d.ts +8 -0
- package/dist/components/registry-explorer.js +38 -0
- package/dist/components/token-stream.d.ts +7 -0
- package/dist/components/token-stream.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/manifests.d.ts +3 -0
- package/dist/manifests.js +1666 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +2 -0
- package/package.json +28 -0
- package/src/components/blocks/ai-chat-shell.tsx +97 -0
- package/src/components/blocks/auth-panel.tsx +203 -0
- package/src/components/blocks/feature-grid.tsx +122 -0
- package/src/components/blocks/hero-section.tsx +73 -0
- package/src/components/blocks/notification-center.tsx +185 -0
- package/src/components/blocks/onboarding-flow.tsx +230 -0
- package/src/components/blocks/pricing-section.tsx +135 -0
- package/src/components/blocks/project-command-center.tsx +188 -0
- package/src/components/blocks/prompt-input.tsx +81 -0
- package/src/components/blocks/registry-card.tsx +104 -0
- package/src/components/blocks/registry-explorer.tsx +78 -0
- package/src/components/blocks/settings-layout.tsx +178 -0
- package/src/components/blocks/stats-strip.tsx +100 -0
- package/src/components/blocks/token-stream.tsx +42 -0
- package/src/components/blocks/usage-card.tsx +116 -0
- package/src/components/core/accordion.tsx +58 -0
- package/src/components/core/alert-dialog.tsx +113 -0
- package/src/components/core/alert.tsx +48 -0
- package/src/components/core/animated-number.tsx +77 -0
- package/src/components/core/aspect-ratio.tsx +20 -0
- package/src/components/core/avatar-stack.tsx +61 -0
- package/src/components/core/avatar.tsx +90 -0
- package/src/components/core/badge.tsx +39 -0
- package/src/components/core/breadcrumb.tsx +63 -0
- package/src/components/core/button-group.tsx +37 -0
- package/src/components/core/button.tsx +110 -0
- package/src/components/core/calendar.tsx +143 -0
- package/src/components/core/card.tsx +60 -0
- package/src/components/core/carousel.tsx +170 -0
- package/src/components/core/chart.tsx +377 -0
- package/src/components/core/checkbox.tsx +64 -0
- package/src/components/core/collapsible.tsx +30 -0
- package/src/components/core/combobox.tsx +114 -0
- package/src/components/core/command-box.tsx +22 -0
- package/src/components/core/command.tsx +165 -0
- package/src/components/core/confirm-action.tsx +94 -0
- package/src/components/core/context-menu.tsx +139 -0
- package/src/components/core/copy-button.tsx +41 -0
- package/src/components/core/data-table.tsx +173 -0
- package/src/components/core/date-picker.tsx +73 -0
- package/src/components/core/dialog.tsx +83 -0
- package/src/components/core/drawer.tsx +87 -0
- package/src/components/core/dropdown-menu.tsx +147 -0
- package/src/components/core/empty.tsx +34 -0
- package/src/components/core/field.tsx +39 -0
- package/src/components/core/file-upload.tsx +143 -0
- package/src/components/core/hover-card.tsx +31 -0
- package/src/components/core/inline-edit.tsx +104 -0
- package/src/components/core/input-group.tsx +47 -0
- package/src/components/core/input-otp.tsx +108 -0
- package/src/components/core/input.tsx +37 -0
- package/src/components/core/kbd.tsx +47 -0
- package/src/components/core/label.tsx +28 -0
- package/src/components/core/marquee.tsx +61 -0
- package/src/components/core/menubar.tsx +120 -0
- package/src/components/core/multi-select.tsx +145 -0
- package/src/components/core/native-select.tsx +27 -0
- package/src/components/core/navigation-menu.tsx +130 -0
- package/src/components/core/number-stepper.tsx +80 -0
- package/src/components/core/pagination.tsx +80 -0
- package/src/components/core/password-input.tsx +90 -0
- package/src/components/core/popover.tsx +34 -0
- package/src/components/core/progress.tsx +63 -0
- package/src/components/core/radio-group.tsx +77 -0
- package/src/components/core/resizable.tsx +250 -0
- package/src/components/core/scroll-area.tsx +38 -0
- package/src/components/core/select.tsx +128 -0
- package/src/components/core/separator.tsx +47 -0
- package/src/components/core/sheet.tsx +118 -0
- package/src/components/core/sidebar.tsx +129 -0
- package/src/components/core/skeleton.tsx +32 -0
- package/src/components/core/slider.tsx +97 -0
- package/src/components/core/sonner.tsx +29 -0
- package/src/components/core/spinner.tsx +60 -0
- package/src/components/core/status-pulse.tsx +67 -0
- package/src/components/core/stepper.tsx +111 -0
- package/src/components/core/switch.tsx +72 -0
- package/src/components/core/table.tsx +104 -0
- package/src/components/core/tabs.tsx +55 -0
- package/src/components/core/tag-input.tsx +93 -0
- package/src/components/core/textarea.tsx +44 -0
- package/src/components/core/timeline.tsx +81 -0
- package/src/components/core/toggle-group.tsx +56 -0
- package/src/components/core/toggle.tsx +66 -0
- package/src/components/core/tooltip.tsx +31 -0
- package/src/components/core/typing-indicator.tsx +51 -0
- package/src/index.ts +8 -0
- package/src/manifests.ts +1682 -0
- package/src/types.ts +58 -0
- package/src/ui.ts +13 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadixPopover from "@radix-ui/react-popover"
|
|
5
|
+
import { Check, ChevronDown, Search } from "lucide-react"
|
|
6
|
+
import { cx } from "@/lib/cx"
|
|
7
|
+
|
|
8
|
+
export interface ComboboxOption {
|
|
9
|
+
value: string
|
|
10
|
+
label: string
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ComboboxProps {
|
|
15
|
+
options: ComboboxOption[]
|
|
16
|
+
value?: string
|
|
17
|
+
onValueChange?: (value: string) => void
|
|
18
|
+
placeholder?: string
|
|
19
|
+
searchPlaceholder?: string
|
|
20
|
+
emptyText?: string
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Combobox({
|
|
26
|
+
options,
|
|
27
|
+
value,
|
|
28
|
+
onValueChange,
|
|
29
|
+
placeholder = "Select an option...",
|
|
30
|
+
searchPlaceholder = "Search...",
|
|
31
|
+
emptyText = "No results found.",
|
|
32
|
+
disabled,
|
|
33
|
+
className,
|
|
34
|
+
}: ComboboxProps) {
|
|
35
|
+
const [open, setOpen] = React.useState(false)
|
|
36
|
+
const [query, setQuery] = React.useState("")
|
|
37
|
+
|
|
38
|
+
const filtered = query
|
|
39
|
+
? options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))
|
|
40
|
+
: options
|
|
41
|
+
|
|
42
|
+
const selected = options.find((o) => o.value === value)
|
|
43
|
+
|
|
44
|
+
function handleSelect(optionValue: string) {
|
|
45
|
+
onValueChange?.(optionValue === value ? "" : optionValue)
|
|
46
|
+
setOpen(false)
|
|
47
|
+
setQuery("")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<RadixPopover.Root open={open} onOpenChange={(o) => { setOpen(o); if (!o) setQuery("") }}>
|
|
52
|
+
<RadixPopover.Trigger asChild>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
aria-expanded={open}
|
|
57
|
+
className={cx(
|
|
58
|
+
"flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm outline-none",
|
|
59
|
+
"hover:border-border-strong transition-colors text-left",
|
|
60
|
+
"focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
61
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
62
|
+
!selected && "text-muted-foreground",
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<span className="truncate">{selected ? selected.label : placeholder}</span>
|
|
67
|
+
<ChevronDown className={cx("h-4 w-4 shrink-0 text-muted-foreground transition-transform", open && "rotate-180")} />
|
|
68
|
+
</button>
|
|
69
|
+
</RadixPopover.Trigger>
|
|
70
|
+
<RadixPopover.Portal>
|
|
71
|
+
<RadixPopover.Content
|
|
72
|
+
align="start"
|
|
73
|
+
sideOffset={4}
|
|
74
|
+
className="z-50 w-[var(--radix-popover-trigger-width)] min-w-[12rem] rounded-xl border border-border bg-card shadow-lg outline-none animate-in fade-in-0 zoom-in-95"
|
|
75
|
+
>
|
|
76
|
+
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
|
|
77
|
+
<Search className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
78
|
+
<input
|
|
79
|
+
autoFocus
|
|
80
|
+
value={query}
|
|
81
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
82
|
+
placeholder={searchPlaceholder}
|
|
83
|
+
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="max-h-60 overflow-auto p-1.5">
|
|
87
|
+
{filtered.length === 0 ? (
|
|
88
|
+
<p className="px-2 py-4 text-center text-sm text-muted-foreground">{emptyText}</p>
|
|
89
|
+
) : (
|
|
90
|
+
filtered.map((option) => (
|
|
91
|
+
<button
|
|
92
|
+
key={option.value}
|
|
93
|
+
type="button"
|
|
94
|
+
disabled={option.disabled}
|
|
95
|
+
onClick={() => handleSelect(option.value)}
|
|
96
|
+
className={cx(
|
|
97
|
+
"relative flex w-full cursor-default select-none items-center rounded-lg py-1.5 pl-8 pr-2.5 text-sm outline-none transition-colors",
|
|
98
|
+
"hover:bg-surface-2 disabled:pointer-events-none disabled:opacity-50",
|
|
99
|
+
option.value === value && "font-medium text-foreground"
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
103
|
+
{option.value === value && <Check className="h-4 w-4" />}
|
|
104
|
+
</span>
|
|
105
|
+
{option.label}
|
|
106
|
+
</button>
|
|
107
|
+
))
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</RadixPopover.Content>
|
|
111
|
+
</RadixPopover.Portal>
|
|
112
|
+
</RadixPopover.Root>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { CopyButton } from "@/components/core/copy-button"
|
|
3
|
+
import { cx } from "@/lib/cx"
|
|
4
|
+
|
|
5
|
+
interface CommandBoxProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
command: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function CommandBox({ command, className, ...props }: CommandBoxProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className={cx(
|
|
13
|
+
"relative flex w-full max-w-sm items-center justify-between rounded-lg border bg-muted/50 py-2 pl-4 pr-2 font-mono text-sm text-foreground",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<span className="truncate pr-4">$ {command}</span>
|
|
19
|
+
<CopyButton value={command} />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadixDialog from "@radix-ui/react-dialog"
|
|
5
|
+
import { Search, X } from "lucide-react"
|
|
6
|
+
import { cx } from "@/lib/cx"
|
|
7
|
+
|
|
8
|
+
// ── Root ──────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface CommandProps {
|
|
11
|
+
open?: boolean
|
|
12
|
+
onOpenChange?: (open: boolean) => void
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Command({ open, onOpenChange, children }: CommandProps) {
|
|
17
|
+
return (
|
|
18
|
+
<RadixDialog.Root open={open} onOpenChange={onOpenChange}>
|
|
19
|
+
{children}
|
|
20
|
+
</RadixDialog.Root>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const CommandTrigger = RadixDialog.Trigger
|
|
25
|
+
|
|
26
|
+
// ── Dialog shell ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export const CommandDialog = React.forwardRef<
|
|
29
|
+
React.ElementRef<typeof RadixDialog.Content>,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof RadixDialog.Content>
|
|
31
|
+
>(({ className, children, ...props }, ref) => (
|
|
32
|
+
<RadixDialog.Portal>
|
|
33
|
+
<RadixDialog.Overlay className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0" />
|
|
34
|
+
<RadixDialog.Content
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cx(
|
|
37
|
+
"fixed left-1/2 top-[20%] z-50 w-full max-w-lg -translate-x-1/2 rounded-2xl border border-border bg-card shadow-2xl outline-none overflow-hidden",
|
|
38
|
+
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-top-4",
|
|
39
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
<RadixDialog.Title className="sr-only">Command menu</RadixDialog.Title>
|
|
45
|
+
{children}
|
|
46
|
+
</RadixDialog.Content>
|
|
47
|
+
</RadixDialog.Portal>
|
|
48
|
+
))
|
|
49
|
+
CommandDialog.displayName = "CommandDialog"
|
|
50
|
+
|
|
51
|
+
// ── Search input ───────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export interface CommandInputProps {
|
|
54
|
+
placeholder?: string
|
|
55
|
+
value?: string
|
|
56
|
+
onValueChange?: (value: string) => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function CommandInput({ placeholder = "Search...", value, onValueChange }: CommandInputProps) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex items-center gap-2 border-b border-border px-4 py-3">
|
|
62
|
+
<Search className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
63
|
+
<input
|
|
64
|
+
autoFocus
|
|
65
|
+
value={value}
|
|
66
|
+
onChange={(e) => onValueChange?.(e.target.value)}
|
|
67
|
+
placeholder={placeholder}
|
|
68
|
+
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
69
|
+
/>
|
|
70
|
+
{value && (
|
|
71
|
+
<button onClick={() => onValueChange?.("")} className="text-muted-foreground hover:text-foreground transition-colors">
|
|
72
|
+
<X className="h-3.5 w-3.5" />
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── List ───────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export function CommandList({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
82
|
+
return (
|
|
83
|
+
<div className={cx("max-h-72 overflow-y-auto py-2", className)}>
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Empty ─────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export function CommandEmpty({ children }: { children?: React.ReactNode }) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
|
94
|
+
{children ?? "No results found."}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Group ─────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export function CommandGroup({ heading, children, className }: { heading?: string; children: React.ReactNode; className?: string }) {
|
|
102
|
+
return (
|
|
103
|
+
<div className={cx("px-2", className)}>
|
|
104
|
+
{heading && (
|
|
105
|
+
<p className="px-2 py-1.5 text-xs font-medium text-muted-foreground">{heading}</p>
|
|
106
|
+
)}
|
|
107
|
+
{children}
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Separator ─────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export function CommandSeparator({ className }: { className?: string }) {
|
|
115
|
+
return <div className={cx("my-1 h-px bg-border mx-2", className)} />
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Item ──────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export interface CommandItemProps {
|
|
121
|
+
children: React.ReactNode
|
|
122
|
+
onSelect?: () => void
|
|
123
|
+
disabled?: boolean
|
|
124
|
+
className?: string
|
|
125
|
+
shortcut?: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function CommandItem({ children, onSelect, disabled, className, shortcut }: CommandItemProps) {
|
|
129
|
+
return (
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
onClick={onSelect}
|
|
134
|
+
className={cx(
|
|
135
|
+
"flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-sm text-foreground outline-none transition-colors",
|
|
136
|
+
"hover:bg-surface-2 focus:bg-surface-2",
|
|
137
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
138
|
+
className
|
|
139
|
+
)}
|
|
140
|
+
>
|
|
141
|
+
<span className="flex-1 text-left">{children}</span>
|
|
142
|
+
{shortcut && (
|
|
143
|
+
<kbd className="pointer-events-none ml-auto flex h-5 items-center gap-0.5 rounded border border-border bg-surface px-1.5 font-mono text-[10px] text-muted-foreground">
|
|
144
|
+
{shortcut}
|
|
145
|
+
</kbd>
|
|
146
|
+
)}
|
|
147
|
+
</button>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Footer hint ───────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export function CommandFooter({ children, className }: { children?: React.ReactNode; className?: string }) {
|
|
154
|
+
return (
|
|
155
|
+
<div className={cx("flex items-center gap-3 border-t border-border px-4 py-2.5 text-xs text-muted-foreground", className)}>
|
|
156
|
+
{children ?? (
|
|
157
|
+
<>
|
|
158
|
+
<span className="flex items-center gap-1"><kbd className="rounded border border-border px-1 font-mono">↑↓</kbd> navigate</span>
|
|
159
|
+
<span className="flex items-center gap-1"><kbd className="rounded border border-border px-1 font-mono">↵</kbd> select</span>
|
|
160
|
+
<span className="flex items-center gap-1"><kbd className="rounded border border-border px-1 font-mono">esc</kbd> close</span>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
import { cx } from "@/lib/cx"
|
|
6
|
+
|
|
7
|
+
const buttonStyles = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
destructive: "bg-red-500 text-white hover:bg-red-600",
|
|
13
|
+
warning: "bg-orange-400 text-white hover:bg-orange-500",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: { variant: "destructive" },
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
export interface ConfirmActionProps extends VariantProps<typeof buttonStyles> {
|
|
21
|
+
label?: string
|
|
22
|
+
confirmLabel?: string
|
|
23
|
+
onConfirm?: () => void
|
|
24
|
+
timeout?: number
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
className?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ConfirmAction({
|
|
30
|
+
label = "Delete",
|
|
31
|
+
confirmLabel = "Are you sure?",
|
|
32
|
+
onConfirm,
|
|
33
|
+
timeout = 3000,
|
|
34
|
+
variant,
|
|
35
|
+
disabled = false,
|
|
36
|
+
className,
|
|
37
|
+
}: ConfirmActionProps) {
|
|
38
|
+
const [pending, setPending] = React.useState(false)
|
|
39
|
+
const [progress, setProgress] = React.useState(100)
|
|
40
|
+
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
41
|
+
const rafRef = React.useRef<number | null>(null)
|
|
42
|
+
const startRef = React.useRef<number | null>(null)
|
|
43
|
+
|
|
44
|
+
const reset = React.useCallback(() => {
|
|
45
|
+
setPending(false)
|
|
46
|
+
setProgress(100)
|
|
47
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
48
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
|
49
|
+
startRef.current = null
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
const tick = React.useCallback((ts: number) => {
|
|
53
|
+
if (!startRef.current) startRef.current = ts
|
|
54
|
+
const elapsed = ts - startRef.current
|
|
55
|
+
const remaining = Math.max(0, 1 - elapsed / timeout)
|
|
56
|
+
setProgress(remaining * 100)
|
|
57
|
+
if (remaining > 0) {
|
|
58
|
+
rafRef.current = requestAnimationFrame(tick)
|
|
59
|
+
}
|
|
60
|
+
}, [timeout])
|
|
61
|
+
|
|
62
|
+
const handleClick = () => {
|
|
63
|
+
if (!pending) {
|
|
64
|
+
setPending(true)
|
|
65
|
+
startRef.current = null
|
|
66
|
+
rafRef.current = requestAnimationFrame(tick)
|
|
67
|
+
timerRef.current = setTimeout(reset, timeout)
|
|
68
|
+
} else {
|
|
69
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
70
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
|
71
|
+
reset()
|
|
72
|
+
onConfirm?.()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
React.useEffect(() => () => { reset() }, [reset])
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={handleClick}
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
className={cx(buttonStyles({ variant }), "relative overflow-hidden", className)}
|
|
84
|
+
>
|
|
85
|
+
{pending && (
|
|
86
|
+
<span
|
|
87
|
+
className="absolute left-0 top-0 h-full bg-white/20 transition-none"
|
|
88
|
+
style={{ width: `${progress}%` }}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
<span className="relative">{pending ? confirmLabel : label}</span>
|
|
92
|
+
</button>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadixContextMenu from "@radix-ui/react-context-menu"
|
|
5
|
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
6
|
+
import { cx } from "@/lib/cx"
|
|
7
|
+
|
|
8
|
+
export const ContextMenu = RadixContextMenu.Root
|
|
9
|
+
export const ContextMenuTrigger = RadixContextMenu.Trigger
|
|
10
|
+
export const ContextMenuGroup = RadixContextMenu.Group
|
|
11
|
+
export const ContextMenuPortal = RadixContextMenu.Portal
|
|
12
|
+
export const ContextMenuSub = RadixContextMenu.Sub
|
|
13
|
+
export const ContextMenuRadioGroup = RadixContextMenu.RadioGroup
|
|
14
|
+
|
|
15
|
+
export const ContextMenuContent = React.forwardRef<
|
|
16
|
+
React.ElementRef<typeof RadixContextMenu.Content>,
|
|
17
|
+
React.ComponentPropsWithoutRef<typeof RadixContextMenu.Content>
|
|
18
|
+
>(({ className, ...props }, ref) => (
|
|
19
|
+
<RadixContextMenu.Portal>
|
|
20
|
+
<RadixContextMenu.Content
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cx(
|
|
23
|
+
"z-50 min-w-[180px] overflow-hidden rounded-xl border border-border bg-card p-1.5 shadow-lg",
|
|
24
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
</RadixContextMenu.Portal>
|
|
30
|
+
))
|
|
31
|
+
ContextMenuContent.displayName = "ContextMenuContent"
|
|
32
|
+
|
|
33
|
+
const itemBase = cx(
|
|
34
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm outline-none transition-colors",
|
|
35
|
+
"text-foreground focus:bg-surface-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
export const ContextMenuItem = React.forwardRef<
|
|
39
|
+
React.ElementRef<typeof RadixContextMenu.Item>,
|
|
40
|
+
React.ComponentPropsWithoutRef<typeof RadixContextMenu.Item> & { inset?: boolean }
|
|
41
|
+
>(({ className, inset, ...props }, ref) => (
|
|
42
|
+
<RadixContextMenu.Item ref={ref} className={cx(itemBase, inset && "pl-8", className)} {...props} />
|
|
43
|
+
))
|
|
44
|
+
ContextMenuItem.displayName = "ContextMenuItem"
|
|
45
|
+
|
|
46
|
+
export const ContextMenuCheckboxItem = React.forwardRef<
|
|
47
|
+
React.ElementRef<typeof RadixContextMenu.CheckboxItem>,
|
|
48
|
+
React.ComponentPropsWithoutRef<typeof RadixContextMenu.CheckboxItem>
|
|
49
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
50
|
+
<RadixContextMenu.CheckboxItem
|
|
51
|
+
ref={ref}
|
|
52
|
+
checked={checked}
|
|
53
|
+
className={cx(itemBase, "pl-8", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
57
|
+
<RadixContextMenu.ItemIndicator>
|
|
58
|
+
<Check className="h-4 w-4" />
|
|
59
|
+
</RadixContextMenu.ItemIndicator>
|
|
60
|
+
</span>
|
|
61
|
+
{children}
|
|
62
|
+
</RadixContextMenu.CheckboxItem>
|
|
63
|
+
))
|
|
64
|
+
ContextMenuCheckboxItem.displayName = "ContextMenuCheckboxItem"
|
|
65
|
+
|
|
66
|
+
export const ContextMenuRadioItem = React.forwardRef<
|
|
67
|
+
React.ElementRef<typeof RadixContextMenu.RadioItem>,
|
|
68
|
+
React.ComponentPropsWithoutRef<typeof RadixContextMenu.RadioItem>
|
|
69
|
+
>(({ className, children, ...props }, ref) => (
|
|
70
|
+
<RadixContextMenu.RadioItem
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={cx(itemBase, "pl-8", className)}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
76
|
+
<RadixContextMenu.ItemIndicator>
|
|
77
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
78
|
+
</RadixContextMenu.ItemIndicator>
|
|
79
|
+
</span>
|
|
80
|
+
{children}
|
|
81
|
+
</RadixContextMenu.RadioItem>
|
|
82
|
+
))
|
|
83
|
+
ContextMenuRadioItem.displayName = "ContextMenuRadioItem"
|
|
84
|
+
|
|
85
|
+
export const ContextMenuLabel = React.forwardRef<
|
|
86
|
+
React.ElementRef<typeof RadixContextMenu.Label>,
|
|
87
|
+
React.ComponentPropsWithoutRef<typeof RadixContextMenu.Label> & { inset?: boolean }
|
|
88
|
+
>(({ className, inset, ...props }, ref) => (
|
|
89
|
+
<RadixContextMenu.Label
|
|
90
|
+
ref={ref}
|
|
91
|
+
className={cx("px-2.5 py-1.5 text-xs font-medium text-muted-foreground", inset && "pl-8", className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
))
|
|
95
|
+
ContextMenuLabel.displayName = "ContextMenuLabel"
|
|
96
|
+
|
|
97
|
+
export const ContextMenuSeparator = React.forwardRef<
|
|
98
|
+
React.ElementRef<typeof RadixContextMenu.Separator>,
|
|
99
|
+
React.ComponentPropsWithoutRef<typeof RadixContextMenu.Separator>
|
|
100
|
+
>(({ className, ...props }, ref) => (
|
|
101
|
+
<RadixContextMenu.Separator ref={ref} className={cx("-mx-1.5 my-1.5 h-px bg-border", className)} {...props} />
|
|
102
|
+
))
|
|
103
|
+
ContextMenuSeparator.displayName = "ContextMenuSeparator"
|
|
104
|
+
|
|
105
|
+
export const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
106
|
+
<span className={cx("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
|
|
107
|
+
)
|
|
108
|
+
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
|
109
|
+
|
|
110
|
+
export const ContextMenuSubTrigger = React.forwardRef<
|
|
111
|
+
React.ElementRef<typeof RadixContextMenu.SubTrigger>,
|
|
112
|
+
React.ComponentPropsWithoutRef<typeof RadixContextMenu.SubTrigger> & { inset?: boolean }
|
|
113
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
114
|
+
<RadixContextMenu.SubTrigger
|
|
115
|
+
ref={ref}
|
|
116
|
+
className={cx(itemBase, "data-[state=open]:bg-surface-2", inset && "pl-8", className)}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
121
|
+
</RadixContextMenu.SubTrigger>
|
|
122
|
+
))
|
|
123
|
+
ContextMenuSubTrigger.displayName = "ContextMenuSubTrigger"
|
|
124
|
+
|
|
125
|
+
export const ContextMenuSubContent = React.forwardRef<
|
|
126
|
+
React.ElementRef<typeof RadixContextMenu.SubContent>,
|
|
127
|
+
React.ComponentPropsWithoutRef<typeof RadixContextMenu.SubContent>
|
|
128
|
+
>(({ className, ...props }, ref) => (
|
|
129
|
+
<RadixContextMenu.SubContent
|
|
130
|
+
ref={ref}
|
|
131
|
+
className={cx(
|
|
132
|
+
"z-50 min-w-[160px] overflow-hidden rounded-xl border border-border bg-card p-1.5 shadow-lg",
|
|
133
|
+
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
134
|
+
className
|
|
135
|
+
)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
))
|
|
139
|
+
ContextMenuSubContent.displayName = "ContextMenuSubContent"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Check, Copy } from "lucide-react"
|
|
5
|
+
import { cx } from "@/lib/cx"
|
|
6
|
+
|
|
7
|
+
interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
8
|
+
value: string
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CopyButton({ value, className, ...props }: CopyButtonProps) {
|
|
13
|
+
const [hasCopied, setHasCopied] = React.useState(false)
|
|
14
|
+
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (!hasCopied) return
|
|
17
|
+
const t = setTimeout(() => setHasCopied(false), 2000)
|
|
18
|
+
return () => clearTimeout(t)
|
|
19
|
+
}, [hasCopied])
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<button
|
|
23
|
+
className={cx(
|
|
24
|
+
"inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
onClick={() => {
|
|
28
|
+
navigator.clipboard.writeText(value)
|
|
29
|
+
setHasCopied(true)
|
|
30
|
+
}}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
<span className="sr-only">Copy</span>
|
|
34
|
+
{hasCopied ? (
|
|
35
|
+
<Check className="h-3.5 w-3.5" />
|
|
36
|
+
) : (
|
|
37
|
+
<Copy className="h-3.5 w-3.5" />
|
|
38
|
+
)}
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
}
|