@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,143 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Upload, X, FileText, Image, Film, Music } from "lucide-react"
|
|
5
|
+
import { cx } from "@/lib/cx"
|
|
6
|
+
|
|
7
|
+
export interface FileUploadFile {
|
|
8
|
+
file: File
|
|
9
|
+
id: string
|
|
10
|
+
preview?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FileUploadProps {
|
|
14
|
+
accept?: string
|
|
15
|
+
multiple?: boolean
|
|
16
|
+
maxSize?: number
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
files?: FileUploadFile[]
|
|
19
|
+
onFilesChange?: (files: FileUploadFile[]) => void
|
|
20
|
+
className?: string
|
|
21
|
+
label?: string
|
|
22
|
+
hint?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getFileIcon(type: string) {
|
|
26
|
+
if (type.startsWith("image/")) return <Image className="h-4 w-4" />
|
|
27
|
+
if (type.startsWith("video/")) return <Film className="h-4 w-4" />
|
|
28
|
+
if (type.startsWith("audio/")) return <Music className="h-4 w-4" />
|
|
29
|
+
return <FileText className="h-4 w-4" />
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatBytes(bytes: number) {
|
|
33
|
+
if (bytes < 1024) return `${bytes} B`
|
|
34
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
35
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function FileUpload({
|
|
39
|
+
accept,
|
|
40
|
+
multiple = false,
|
|
41
|
+
maxSize,
|
|
42
|
+
disabled,
|
|
43
|
+
files = [],
|
|
44
|
+
onFilesChange,
|
|
45
|
+
className,
|
|
46
|
+
label = "Click to upload or drag and drop",
|
|
47
|
+
hint,
|
|
48
|
+
}: FileUploadProps) {
|
|
49
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
50
|
+
const [dragging, setDragging] = React.useState(false)
|
|
51
|
+
|
|
52
|
+
function addFiles(incoming: FileList | File[]) {
|
|
53
|
+
const arr = Array.from(incoming)
|
|
54
|
+
const valid = arr.filter((f) => !maxSize || f.size <= maxSize)
|
|
55
|
+
const newFiles: FileUploadFile[] = valid.map((f) => ({
|
|
56
|
+
file: f,
|
|
57
|
+
id: `${f.name}-${f.size}-${Date.now()}`,
|
|
58
|
+
preview: f.type.startsWith("image/") ? URL.createObjectURL(f) : undefined,
|
|
59
|
+
}))
|
|
60
|
+
onFilesChange?.(multiple ? [...files, ...newFiles] : newFiles)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function remove(id: string) {
|
|
64
|
+
onFilesChange?.(files.filter((f) => f.id !== id))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onDragOver(e: React.DragEvent) {
|
|
68
|
+
e.preventDefault()
|
|
69
|
+
if (!disabled) setDragging(true)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onDrop(e: React.DragEvent) {
|
|
73
|
+
e.preventDefault()
|
|
74
|
+
setDragging(false)
|
|
75
|
+
if (!disabled && e.dataTransfer.files.length) addFiles(e.dataTransfer.files)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={cx("space-y-3", className)}>
|
|
80
|
+
<div
|
|
81
|
+
onClick={() => !disabled && inputRef.current?.click()}
|
|
82
|
+
onDragOver={onDragOver}
|
|
83
|
+
onDragLeave={() => setDragging(false)}
|
|
84
|
+
onDrop={onDrop}
|
|
85
|
+
className={cx(
|
|
86
|
+
"relative flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
|
|
87
|
+
dragging
|
|
88
|
+
? "border-ring bg-surface-2"
|
|
89
|
+
: "border-border hover:border-border-strong hover:bg-surface-2",
|
|
90
|
+
disabled && "cursor-not-allowed opacity-50"
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-xl border border-border bg-surface">
|
|
94
|
+
<Upload className="h-5 w-5 text-muted-foreground" />
|
|
95
|
+
</div>
|
|
96
|
+
<div className="space-y-1">
|
|
97
|
+
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
98
|
+
{hint && <p className="text-xs text-muted-foreground">{hint}</p>}
|
|
99
|
+
{maxSize && (
|
|
100
|
+
<p className="text-xs text-muted-foreground">Max {formatBytes(maxSize)}</p>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
<input
|
|
104
|
+
ref={inputRef}
|
|
105
|
+
type="file"
|
|
106
|
+
accept={accept}
|
|
107
|
+
multiple={multiple}
|
|
108
|
+
disabled={disabled}
|
|
109
|
+
className="sr-only"
|
|
110
|
+
onChange={(e) => e.target.files && addFiles(e.target.files)}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{files.length > 0 && (
|
|
115
|
+
<ul className="space-y-2">
|
|
116
|
+
{files.map(({ file, id, preview }) => (
|
|
117
|
+
<li key={id} className="flex items-center gap-3 rounded-lg border border-border bg-surface-2 px-3 py-2.5">
|
|
118
|
+
{preview ? (
|
|
119
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
120
|
+
<img src={preview} alt={file.name} className="h-8 w-8 rounded-md object-cover shrink-0 border border-border" />
|
|
121
|
+
) : (
|
|
122
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted-foreground">
|
|
123
|
+
{getFileIcon(file.type)}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
<div className="min-w-0 flex-1">
|
|
127
|
+
<p className="truncate text-sm font-medium text-foreground">{file.name}</p>
|
|
128
|
+
<p className="text-xs text-muted-foreground">{formatBytes(file.size)}</p>
|
|
129
|
+
</div>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={() => remove(id)}
|
|
133
|
+
className="shrink-0 rounded-md p-1 text-muted-foreground hover:bg-surface-3 hover:text-foreground transition-colors"
|
|
134
|
+
>
|
|
135
|
+
<X className="h-3.5 w-3.5" />
|
|
136
|
+
</button>
|
|
137
|
+
</li>
|
|
138
|
+
))}
|
|
139
|
+
</ul>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadixHoverCard from "@radix-ui/react-hover-card"
|
|
5
|
+
import { cx } from "@/lib/cx"
|
|
6
|
+
|
|
7
|
+
export const HoverCard = RadixHoverCard.Root
|
|
8
|
+
export const HoverCardTrigger = RadixHoverCard.Trigger
|
|
9
|
+
|
|
10
|
+
export const HoverCardContent = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof RadixHoverCard.Content>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof RadixHoverCard.Content>
|
|
13
|
+
>(({ className, align = "center", sideOffset = 8, ...props }, ref) => (
|
|
14
|
+
<RadixHoverCard.Portal>
|
|
15
|
+
<RadixHoverCard.Content
|
|
16
|
+
ref={ref}
|
|
17
|
+
align={align}
|
|
18
|
+
sideOffset={sideOffset}
|
|
19
|
+
className={cx(
|
|
20
|
+
"z-50 w-72 rounded-xl border border-border bg-card p-4 shadow-lg outline-none",
|
|
21
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
22
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
23
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
24
|
+
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
</RadixHoverCard.Portal>
|
|
30
|
+
))
|
|
31
|
+
HoverCardContent.displayName = "HoverCardContent"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Check, X, Pencil } from "lucide-react"
|
|
5
|
+
import { cx } from "@/lib/cx"
|
|
6
|
+
|
|
7
|
+
export interface InlineEditProps {
|
|
8
|
+
value: string
|
|
9
|
+
onSave?: (value: string) => void
|
|
10
|
+
placeholder?: string
|
|
11
|
+
className?: string
|
|
12
|
+
inputClassName?: string
|
|
13
|
+
as?: "p" | "h1" | "h2" | "h3" | "span"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function InlineEdit({
|
|
17
|
+
value: initialValue,
|
|
18
|
+
onSave,
|
|
19
|
+
placeholder = "Click to edit…",
|
|
20
|
+
className,
|
|
21
|
+
inputClassName,
|
|
22
|
+
as: Tag = "p",
|
|
23
|
+
}: InlineEditProps) {
|
|
24
|
+
const [editing, setEditing] = React.useState(false)
|
|
25
|
+
const [draft, setDraft] = React.useState(initialValue)
|
|
26
|
+
const [value, setValue] = React.useState(initialValue)
|
|
27
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
28
|
+
|
|
29
|
+
const startEdit = () => {
|
|
30
|
+
setDraft(value)
|
|
31
|
+
setEditing(true)
|
|
32
|
+
setTimeout(() => inputRef.current?.select(), 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const save = () => {
|
|
36
|
+
const trimmed = draft.trim()
|
|
37
|
+
if (trimmed) {
|
|
38
|
+
setValue(trimmed)
|
|
39
|
+
onSave?.(trimmed)
|
|
40
|
+
}
|
|
41
|
+
setEditing(false)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cancel = () => {
|
|
45
|
+
setDraft(value)
|
|
46
|
+
setEditing(false)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
50
|
+
if (e.key === "Enter") save()
|
|
51
|
+
if (e.key === "Escape") cancel()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (editing) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex items-center gap-1.5">
|
|
57
|
+
<input
|
|
58
|
+
ref={inputRef}
|
|
59
|
+
value={draft}
|
|
60
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
61
|
+
onKeyDown={onKeyDown}
|
|
62
|
+
onBlur={save}
|
|
63
|
+
className={cx(
|
|
64
|
+
"flex-1 rounded-md border border-border-strong bg-surface-2 px-2 py-1 text-sm text-foreground outline-none focus:border-border-strong",
|
|
65
|
+
inputClassName
|
|
66
|
+
)}
|
|
67
|
+
autoFocus
|
|
68
|
+
/>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onMouseDown={(e) => { e.preventDefault(); save() }}
|
|
72
|
+
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-surface-2 transition-colors"
|
|
73
|
+
aria-label="Save"
|
|
74
|
+
>
|
|
75
|
+
<Check className="h-3.5 w-3.5" />
|
|
76
|
+
</button>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onMouseDown={(e) => { e.preventDefault(); cancel() }}
|
|
80
|
+
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-surface-2 transition-colors"
|
|
81
|
+
aria-label="Cancel"
|
|
82
|
+
>
|
|
83
|
+
<X className="h-3.5 w-3.5" />
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
className={cx("group flex items-center gap-1.5 cursor-pointer", className)}
|
|
92
|
+
onClick={startEdit}
|
|
93
|
+
role="button"
|
|
94
|
+
tabIndex={0}
|
|
95
|
+
onKeyDown={(e) => e.key === "Enter" && startEdit()}
|
|
96
|
+
aria-label="Click to edit"
|
|
97
|
+
>
|
|
98
|
+
<Tag className={cx(!value && "text-muted-foreground/50 italic")}>
|
|
99
|
+
{value || placeholder}
|
|
100
|
+
</Tag>
|
|
101
|
+
<Pencil className="h-3 w-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cx } from "@/lib/cx"
|
|
3
|
+
|
|
4
|
+
interface InputGroupProps {
|
|
5
|
+
left?: React.ReactNode
|
|
6
|
+
right?: React.ReactNode
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function InputGroup({ left, right, children, className }: InputGroupProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className={cx(
|
|
15
|
+
"flex items-center w-full rounded-lg border border-border bg-input",
|
|
16
|
+
"focus-within:border-border-strong focus-within:bg-surface-3",
|
|
17
|
+
"transition-all",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{left && (
|
|
22
|
+
<div className="flex shrink-0 items-center border-r border-border px-3 py-2 text-sm text-muted-foreground select-none">
|
|
23
|
+
{left}
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
<div className="flex-1 min-w-0 [&>input]:border-0 [&>input]:bg-transparent [&>input]:focus:bg-transparent [&>input]:focus:border-0 [&>input]:rounded-none [&>input]:shadow-none">
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
{right && (
|
|
30
|
+
<div className="flex shrink-0 items-center border-l border-border px-3 py-2 text-sm text-muted-foreground select-none">
|
|
31
|
+
{right}
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface InputAddonProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
39
|
+
|
|
40
|
+
export function InputAddon({ className, ...props }: InputAddonProps) {
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
className={cx("text-sm text-muted-foreground font-mono", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cx } from "@/lib/cx"
|
|
5
|
+
|
|
6
|
+
export interface InputOTPProps {
|
|
7
|
+
length?: number
|
|
8
|
+
value?: string
|
|
9
|
+
onChange?: (value: string) => void
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
className?: string
|
|
12
|
+
inputClassName?: string
|
|
13
|
+
type?: "text" | "password" | "number"
|
|
14
|
+
pattern?: "numeric" | "alphanumeric" | "any"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function InputOTP({
|
|
18
|
+
length = 6,
|
|
19
|
+
value = "",
|
|
20
|
+
onChange,
|
|
21
|
+
disabled,
|
|
22
|
+
className,
|
|
23
|
+
inputClassName,
|
|
24
|
+
pattern = "numeric",
|
|
25
|
+
}: InputOTPProps) {
|
|
26
|
+
const inputs = React.useRef<(HTMLInputElement | null)[]>([])
|
|
27
|
+
|
|
28
|
+
const chars = Array.from({ length }, (_, i) => value[i] ?? "")
|
|
29
|
+
|
|
30
|
+
function isAllowed(char: string) {
|
|
31
|
+
if (pattern === "numeric") return /^\d$/.test(char)
|
|
32
|
+
if (pattern === "alphanumeric") return /^[a-zA-Z0-9]$/.test(char)
|
|
33
|
+
return char.length === 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleKeyDown(i: number, e: React.KeyboardEvent<HTMLInputElement>) {
|
|
37
|
+
if (e.key === "Backspace") {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
if (chars[i]) {
|
|
40
|
+
const next = [...chars]
|
|
41
|
+
next[i] = ""
|
|
42
|
+
onChange?.(next.join(""))
|
|
43
|
+
} else if (i > 0) {
|
|
44
|
+
inputs.current[i - 1]?.focus()
|
|
45
|
+
const next = [...chars]
|
|
46
|
+
next[i - 1] = ""
|
|
47
|
+
onChange?.(next.join(""))
|
|
48
|
+
}
|
|
49
|
+
} else if (e.key === "ArrowLeft" && i > 0) {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
inputs.current[i - 1]?.focus()
|
|
52
|
+
} else if (e.key === "ArrowRight" && i < length - 1) {
|
|
53
|
+
e.preventDefault()
|
|
54
|
+
inputs.current[i + 1]?.focus()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleInput(i: number, e: React.FormEvent<HTMLInputElement>) {
|
|
59
|
+
const val = e.currentTarget.value.slice(-1)
|
|
60
|
+
if (!val) return
|
|
61
|
+
if (!isAllowed(val)) return
|
|
62
|
+
const next = [...chars]
|
|
63
|
+
next[i] = val
|
|
64
|
+
onChange?.(next.join(""))
|
|
65
|
+
if (i < length - 1) inputs.current[i + 1]?.focus()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handlePaste(e: React.ClipboardEvent) {
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
const pasted = e.clipboardData.getData("text").slice(0, length)
|
|
71
|
+
const allowed = [...pasted].filter(isAllowed)
|
|
72
|
+
const next = Array.from({ length }, (_, i) => allowed[i] ?? "")
|
|
73
|
+
onChange?.(next.join(""))
|
|
74
|
+
const lastFilled = Math.min(allowed.length, length - 1)
|
|
75
|
+
inputs.current[lastFilled]?.focus()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={cx("flex items-center gap-2", className)} onPaste={handlePaste}>
|
|
80
|
+
{chars.map((char, i) => (
|
|
81
|
+
<React.Fragment key={i}>
|
|
82
|
+
{i === length / 2 && length % 2 === 0 && length > 4 && (
|
|
83
|
+
<span className="text-muted-foreground">–</span>
|
|
84
|
+
)}
|
|
85
|
+
<input
|
|
86
|
+
ref={(el) => { inputs.current[i] = el }}
|
|
87
|
+
type="text"
|
|
88
|
+
inputMode={pattern === "numeric" ? "numeric" : "text"}
|
|
89
|
+
maxLength={1}
|
|
90
|
+
value={char}
|
|
91
|
+
disabled={disabled}
|
|
92
|
+
onKeyDown={(e) => handleKeyDown(i, e)}
|
|
93
|
+
onInput={(e) => handleInput(i, e)}
|
|
94
|
+
onChange={() => {}}
|
|
95
|
+
className={cx(
|
|
96
|
+
"h-10 w-10 rounded-lg border border-border bg-surface-2 text-center text-sm font-medium outline-none transition-colors caret-transparent",
|
|
97
|
+
"hover:border-border-strong",
|
|
98
|
+
"focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
99
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
100
|
+
char && "border-border-strong",
|
|
101
|
+
inputClassName
|
|
102
|
+
)}
|
|
103
|
+
/>
|
|
104
|
+
</React.Fragment>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cx } from "@/lib/cx"
|
|
4
|
+
|
|
5
|
+
const inputStyles = cva(
|
|
6
|
+
"flex w-full rounded-lg text-sm transition-all outline-none focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
surface: "bg-input border border-border px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-border-strong focus:bg-surface-3",
|
|
11
|
+
command: "bg-transparent border-0 px-4 py-3 text-foreground placeholder:text-muted-foreground/60 shadow-none focus:outline-none rounded-none",
|
|
12
|
+
minimal: "bg-transparent border-b border-border px-0 py-2 text-foreground placeholder:text-muted-foreground/50 rounded-none focus:outline-none focus:border-border-strong",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
variant: "surface",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export interface InputProps
|
|
22
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
|
|
23
|
+
VariantProps<typeof inputStyles> {}
|
|
24
|
+
|
|
25
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
26
|
+
({ className, variant, type = "text", ...props }, ref) => {
|
|
27
|
+
return (
|
|
28
|
+
<input
|
|
29
|
+
type={type}
|
|
30
|
+
className={cx(inputStyles({ variant }), className)}
|
|
31
|
+
ref={ref}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
Input.displayName = "Input"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cx } from "@/lib/cx"
|
|
4
|
+
|
|
5
|
+
const kbdStyles = cva(
|
|
6
|
+
[
|
|
7
|
+
"inline-flex items-center justify-center",
|
|
8
|
+
"font-mono font-medium select-none",
|
|
9
|
+
"rounded border border-border bg-surface-2",
|
|
10
|
+
"text-muted-foreground",
|
|
11
|
+
"shadow-[0_1px_0_1px_theme(colors.border)]",
|
|
12
|
+
],
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
size: {
|
|
16
|
+
sm: "h-5 min-w-5 px-1 text-[10px]",
|
|
17
|
+
md: "h-6 min-w-6 px-1.5 text-xs",
|
|
18
|
+
lg: "h-7 min-w-7 px-2 text-sm",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: { size: "md" },
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export interface KbdProps
|
|
26
|
+
extends React.HTMLAttributes<HTMLElement>,
|
|
27
|
+
VariantProps<typeof kbdStyles> {}
|
|
28
|
+
|
|
29
|
+
export function Kbd({ className, size, ...props }: KbdProps) {
|
|
30
|
+
return <kbd className={cx(kbdStyles({ size }), className)} {...props} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface KeyComboProps {
|
|
34
|
+
keys: string[]
|
|
35
|
+
size?: VariantProps<typeof kbdStyles>["size"]
|
|
36
|
+
className?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function KeyCombo({ keys, size, className }: KeyComboProps) {
|
|
40
|
+
return (
|
|
41
|
+
<span className={cx("inline-flex items-center gap-1", className)}>
|
|
42
|
+
{keys.map((key, i) => (
|
|
43
|
+
<Kbd key={i} size={size}>{key}</Kbd>
|
|
44
|
+
))}
|
|
45
|
+
</span>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cx } from "@/lib/cx"
|
|
3
|
+
|
|
4
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
5
|
+
required?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
9
|
+
({ className, required, children, ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<label
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cx(
|
|
14
|
+
"text-sm font-medium leading-none text-foreground",
|
|
15
|
+
"peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
{required && (
|
|
22
|
+
<span className="ml-1 text-red-400 select-none" aria-hidden="true">*</span>
|
|
23
|
+
)}
|
|
24
|
+
</label>
|
|
25
|
+
)
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
Label.displayName = "Label"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cx } from "@/lib/cx"
|
|
3
|
+
|
|
4
|
+
export interface MarqueeProps {
|
|
5
|
+
children: React.ReactNode
|
|
6
|
+
speed?: number
|
|
7
|
+
gap?: number
|
|
8
|
+
direction?: "left" | "right"
|
|
9
|
+
pauseOnHover?: boolean
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Marquee({
|
|
14
|
+
children,
|
|
15
|
+
speed = 40,
|
|
16
|
+
gap = 16,
|
|
17
|
+
direction = "left",
|
|
18
|
+
pauseOnHover = true,
|
|
19
|
+
className,
|
|
20
|
+
}: MarqueeProps) {
|
|
21
|
+
const duration = `${speed}s`
|
|
22
|
+
const animation = direction === "left" ? "marquee-left" : "marquee-right"
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={cx("overflow-hidden [--marquee-gap:var(--gap)] flex w-full", className)}
|
|
27
|
+
style={{ "--gap": `${gap}px` } as React.CSSProperties}
|
|
28
|
+
>
|
|
29
|
+
<style>{`
|
|
30
|
+
@keyframes marquee-left {
|
|
31
|
+
from { transform: translateX(0); }
|
|
32
|
+
to { transform: translateX(calc(-50% - var(--gap) / 2)); }
|
|
33
|
+
}
|
|
34
|
+
@keyframes marquee-right {
|
|
35
|
+
from { transform: translateX(calc(-50% - var(--gap) / 2)); }
|
|
36
|
+
to { transform: translateX(0); }
|
|
37
|
+
}
|
|
38
|
+
`}</style>
|
|
39
|
+
<div
|
|
40
|
+
className={cx(
|
|
41
|
+
"flex min-w-full shrink-0 items-center",
|
|
42
|
+
pauseOnHover && "hover:[animation-play-state:paused]"
|
|
43
|
+
)}
|
|
44
|
+
style={{
|
|
45
|
+
gap: `${gap}px`,
|
|
46
|
+
animationName: animation,
|
|
47
|
+
animationDuration: duration,
|
|
48
|
+
animationTimingFunction: "linear",
|
|
49
|
+
animationIterationCount: "infinite",
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<div className="flex shrink-0 items-center" style={{ gap: `${gap}px` }}>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex shrink-0 items-center" style={{ gap: `${gap}px` }} aria-hidden>
|
|
56
|
+
{children}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|