@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,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
+ }