@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.
Files changed (107) hide show
  1. package/dist/components/ai-chat-shell.d.ts +1 -0
  2. package/dist/components/ai-chat-shell.js +23 -0
  3. package/dist/components/prompt-input.d.ts +5 -0
  4. package/dist/components/prompt-input.js +47 -0
  5. package/dist/components/registry-card.d.ts +6 -0
  6. package/dist/components/registry-card.js +15 -0
  7. package/dist/components/registry-explorer.d.ts +8 -0
  8. package/dist/components/registry-explorer.js +38 -0
  9. package/dist/components/token-stream.d.ts +7 -0
  10. package/dist/components/token-stream.js +21 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +23 -0
  13. package/dist/manifests.d.ts +3 -0
  14. package/dist/manifests.js +1666 -0
  15. package/dist/types.d.ts +44 -0
  16. package/dist/types.js +2 -0
  17. package/package.json +28 -0
  18. package/src/components/blocks/ai-chat-shell.tsx +97 -0
  19. package/src/components/blocks/auth-panel.tsx +203 -0
  20. package/src/components/blocks/feature-grid.tsx +122 -0
  21. package/src/components/blocks/hero-section.tsx +73 -0
  22. package/src/components/blocks/notification-center.tsx +185 -0
  23. package/src/components/blocks/onboarding-flow.tsx +230 -0
  24. package/src/components/blocks/pricing-section.tsx +135 -0
  25. package/src/components/blocks/project-command-center.tsx +188 -0
  26. package/src/components/blocks/prompt-input.tsx +81 -0
  27. package/src/components/blocks/registry-card.tsx +104 -0
  28. package/src/components/blocks/registry-explorer.tsx +78 -0
  29. package/src/components/blocks/settings-layout.tsx +178 -0
  30. package/src/components/blocks/stats-strip.tsx +100 -0
  31. package/src/components/blocks/token-stream.tsx +42 -0
  32. package/src/components/blocks/usage-card.tsx +116 -0
  33. package/src/components/core/accordion.tsx +58 -0
  34. package/src/components/core/alert-dialog.tsx +113 -0
  35. package/src/components/core/alert.tsx +48 -0
  36. package/src/components/core/animated-number.tsx +77 -0
  37. package/src/components/core/aspect-ratio.tsx +20 -0
  38. package/src/components/core/avatar-stack.tsx +61 -0
  39. package/src/components/core/avatar.tsx +90 -0
  40. package/src/components/core/badge.tsx +39 -0
  41. package/src/components/core/breadcrumb.tsx +63 -0
  42. package/src/components/core/button-group.tsx +37 -0
  43. package/src/components/core/button.tsx +110 -0
  44. package/src/components/core/calendar.tsx +143 -0
  45. package/src/components/core/card.tsx +60 -0
  46. package/src/components/core/carousel.tsx +170 -0
  47. package/src/components/core/chart.tsx +377 -0
  48. package/src/components/core/checkbox.tsx +64 -0
  49. package/src/components/core/collapsible.tsx +30 -0
  50. package/src/components/core/combobox.tsx +114 -0
  51. package/src/components/core/command-box.tsx +22 -0
  52. package/src/components/core/command.tsx +165 -0
  53. package/src/components/core/confirm-action.tsx +94 -0
  54. package/src/components/core/context-menu.tsx +139 -0
  55. package/src/components/core/copy-button.tsx +41 -0
  56. package/src/components/core/data-table.tsx +173 -0
  57. package/src/components/core/date-picker.tsx +73 -0
  58. package/src/components/core/dialog.tsx +83 -0
  59. package/src/components/core/drawer.tsx +87 -0
  60. package/src/components/core/dropdown-menu.tsx +147 -0
  61. package/src/components/core/empty.tsx +34 -0
  62. package/src/components/core/field.tsx +39 -0
  63. package/src/components/core/file-upload.tsx +143 -0
  64. package/src/components/core/hover-card.tsx +31 -0
  65. package/src/components/core/inline-edit.tsx +104 -0
  66. package/src/components/core/input-group.tsx +47 -0
  67. package/src/components/core/input-otp.tsx +108 -0
  68. package/src/components/core/input.tsx +37 -0
  69. package/src/components/core/kbd.tsx +47 -0
  70. package/src/components/core/label.tsx +28 -0
  71. package/src/components/core/marquee.tsx +61 -0
  72. package/src/components/core/menubar.tsx +120 -0
  73. package/src/components/core/multi-select.tsx +145 -0
  74. package/src/components/core/native-select.tsx +27 -0
  75. package/src/components/core/navigation-menu.tsx +130 -0
  76. package/src/components/core/number-stepper.tsx +80 -0
  77. package/src/components/core/pagination.tsx +80 -0
  78. package/src/components/core/password-input.tsx +90 -0
  79. package/src/components/core/popover.tsx +34 -0
  80. package/src/components/core/progress.tsx +63 -0
  81. package/src/components/core/radio-group.tsx +77 -0
  82. package/src/components/core/resizable.tsx +250 -0
  83. package/src/components/core/scroll-area.tsx +38 -0
  84. package/src/components/core/select.tsx +128 -0
  85. package/src/components/core/separator.tsx +47 -0
  86. package/src/components/core/sheet.tsx +118 -0
  87. package/src/components/core/sidebar.tsx +129 -0
  88. package/src/components/core/skeleton.tsx +32 -0
  89. package/src/components/core/slider.tsx +97 -0
  90. package/src/components/core/sonner.tsx +29 -0
  91. package/src/components/core/spinner.tsx +60 -0
  92. package/src/components/core/status-pulse.tsx +67 -0
  93. package/src/components/core/stepper.tsx +111 -0
  94. package/src/components/core/switch.tsx +72 -0
  95. package/src/components/core/table.tsx +104 -0
  96. package/src/components/core/tabs.tsx +55 -0
  97. package/src/components/core/tag-input.tsx +93 -0
  98. package/src/components/core/textarea.tsx +44 -0
  99. package/src/components/core/timeline.tsx +81 -0
  100. package/src/components/core/toggle-group.tsx +56 -0
  101. package/src/components/core/toggle.tsx +66 -0
  102. package/src/components/core/tooltip.tsx +31 -0
  103. package/src/components/core/typing-indicator.tsx +51 -0
  104. package/src/index.ts +8 -0
  105. package/src/manifests.ts +1682 -0
  106. package/src/types.ts +58 -0
  107. 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
+ }