@annondeveloper/ui-kit 0.1.0 → 0.2.1
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/README.md +1463 -127
- package/dist/{chunk-5OKSXPWK.js → chunk-2DWZVHZS.js} +2 -2
- package/dist/chunk-2DWZVHZS.js.map +1 -0
- package/dist/form.d.ts +6 -6
- package/dist/form.js +2 -3
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +510 -52
- package/dist/index.js +2996 -15
- package/dist/index.js.map +1 -1
- package/dist/{select-nnBJUO8U.d.ts → select-B2wXqqSM.d.ts} +2 -2
- package/package.json +24 -26
- package/src/components/animated-counter.tsx +8 -5
- package/src/components/avatar.tsx +2 -1
- package/src/components/badge.tsx +3 -2
- package/src/components/button.tsx +3 -2
- package/src/components/card.tsx +13 -12
- package/src/components/checkbox.tsx +3 -2
- package/src/components/color-input.tsx +427 -0
- package/src/components/command-bar.tsx +435 -0
- package/src/components/confidence-bar.tsx +115 -0
- package/src/components/confirm-dialog.tsx +2 -1
- package/src/components/copy-block.tsx +224 -0
- package/src/components/data-table.tsx +9 -13
- package/src/components/diff-viewer.tsx +340 -0
- package/src/components/dropdown-menu.tsx +2 -1
- package/src/components/empty-state.tsx +2 -1
- package/src/components/filter-pill.tsx +2 -1
- package/src/components/form-input.tsx +5 -4
- package/src/components/heatmap-calendar.tsx +218 -0
- package/src/components/infinite-scroll.tsx +248 -0
- package/src/components/kanban-column.tsx +198 -0
- package/src/components/live-feed.tsx +222 -0
- package/src/components/log-viewer.tsx +4 -1
- package/src/components/metric-card.tsx +2 -1
- package/src/components/notification-stack.tsx +233 -0
- package/src/components/pipeline-stage.tsx +2 -1
- package/src/components/popover.tsx +2 -1
- package/src/components/port-status-grid.tsx +2 -1
- package/src/components/progress.tsx +2 -1
- package/src/components/radio-group.tsx +2 -1
- package/src/components/realtime-value.tsx +283 -0
- package/src/components/select.tsx +2 -1
- package/src/components/severity-timeline.tsx +2 -1
- package/src/components/sheet.tsx +2 -1
- package/src/components/skeleton.tsx +4 -3
- package/src/components/slider.tsx +2 -1
- package/src/components/smart-table.tsx +383 -0
- package/src/components/sortable-list.tsx +272 -0
- package/src/components/sparkline.tsx +2 -1
- package/src/components/status-badge.tsx +2 -1
- package/src/components/status-pulse.tsx +2 -1
- package/src/components/step-wizard.tsx +380 -0
- package/src/components/streaming-text.tsx +160 -0
- package/src/components/success-checkmark.tsx +2 -1
- package/src/components/tabs.tsx +2 -1
- package/src/components/threshold-gauge.tsx +2 -1
- package/src/components/time-range-selector.tsx +2 -1
- package/src/components/toast.tsx +2 -1
- package/src/components/toggle-switch.tsx +2 -1
- package/src/components/tooltip.tsx +2 -1
- package/src/components/truncated-text.tsx +2 -1
- package/src/components/typing-indicator.tsx +123 -0
- package/src/components/uptime-tracker.tsx +2 -1
- package/src/components/utilization-bar.tsx +2 -1
- package/src/theme.css +6 -0
- package/src/utils.ts +1 -1
- package/dist/chunk-5OKSXPWK.js.map +0 -1
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type React from 'react'
|
|
4
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
|
5
|
+
import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
|
|
6
|
+
import { Copy, Check, Pipette } from 'lucide-react'
|
|
7
|
+
import { cn } from '../utils'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Props for the ColorInput component. */
|
|
14
|
+
export interface ColorInputProps {
|
|
15
|
+
/** Current color value as hex string (e.g. "#ff0000"). */
|
|
16
|
+
value: string
|
|
17
|
+
/** Called when the color changes. */
|
|
18
|
+
onChange: (color: string) => void
|
|
19
|
+
/** Optional label. */
|
|
20
|
+
label?: string
|
|
21
|
+
/** Preset color swatches. */
|
|
22
|
+
presets?: string[]
|
|
23
|
+
/** Show alpha/opacity slider. */
|
|
24
|
+
showAlpha?: boolean
|
|
25
|
+
/** Display format for the text input. Default "hex". */
|
|
26
|
+
format?: 'hex' | 'rgb' | 'hsl'
|
|
27
|
+
/** Additional class name. */
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Color conversion helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
|
36
|
+
const clean = hex.replace('#', '')
|
|
37
|
+
const full = clean.length === 3
|
|
38
|
+
? clean.split('').map(c => c + c).join('')
|
|
39
|
+
: clean
|
|
40
|
+
const num = parseInt(full, 16)
|
|
41
|
+
return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rgbToHex(r: number, g: number, b: number): string {
|
|
45
|
+
return '#' + [r, g, b].map(c => Math.round(c).toString(16).padStart(2, '0')).join('')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
|
|
49
|
+
const rn = r / 255, gn = g / 255, bn = b / 255
|
|
50
|
+
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn)
|
|
51
|
+
const l = (max + min) / 2
|
|
52
|
+
if (max === min) return { h: 0, s: 0, l }
|
|
53
|
+
const d = max - min
|
|
54
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
55
|
+
let h = 0
|
|
56
|
+
if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6
|
|
57
|
+
else if (max === gn) h = ((bn - rn) / d + 2) / 6
|
|
58
|
+
else h = ((rn - gn) / d + 4) / 6
|
|
59
|
+
return { h, s, l }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
|
|
63
|
+
if (s === 0) {
|
|
64
|
+
const v = Math.round(l * 255)
|
|
65
|
+
return { r: v, g: v, b: v }
|
|
66
|
+
}
|
|
67
|
+
const hue2rgb = (p: number, q: number, t: number) => {
|
|
68
|
+
const tt = t < 0 ? t + 1 : t > 1 ? t - 1 : t
|
|
69
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt
|
|
70
|
+
if (tt < 1 / 2) return q
|
|
71
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6
|
|
72
|
+
return p
|
|
73
|
+
}
|
|
74
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
|
75
|
+
const p = 2 * l - q
|
|
76
|
+
return {
|
|
77
|
+
r: Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
|
|
78
|
+
g: Math.round(hue2rgb(p, q, h) * 255),
|
|
79
|
+
b: Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatColor(hex: string, fmt: 'hex' | 'rgb' | 'hsl'): string {
|
|
84
|
+
if (fmt === 'hex') return hex
|
|
85
|
+
const { r, g, b } = hexToRgb(hex)
|
|
86
|
+
if (fmt === 'rgb') return `rgb(${r}, ${g}, ${b})`
|
|
87
|
+
const { h, s, l } = rgbToHsl(r, g, b)
|
|
88
|
+
return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isSafeColor(c: string): boolean {
|
|
92
|
+
return /^#[0-9a-f]{3,8}$/i.test(c) ||
|
|
93
|
+
/^rgba?\(\s*[\d.]+/.test(c) ||
|
|
94
|
+
/^hsla?\(\s*[\d.]+/.test(c)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const RECENT_COLORS_KEY = 'ui-kit-recent-colors'
|
|
98
|
+
const MAX_RECENT = 8
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// ColorInput
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @description A compact color picker input with swatch preview, expandable picker panel
|
|
106
|
+
* featuring hue/saturation area, lightness slider, optional alpha slider, preset swatches,
|
|
107
|
+
* format switching (hex/rgb/hsl), clipboard copy, and recent color history.
|
|
108
|
+
*/
|
|
109
|
+
export function ColorInput({
|
|
110
|
+
value,
|
|
111
|
+
onChange,
|
|
112
|
+
label,
|
|
113
|
+
presets,
|
|
114
|
+
showAlpha = false,
|
|
115
|
+
format = 'hex',
|
|
116
|
+
className,
|
|
117
|
+
}: ColorInputProps): React.JSX.Element {
|
|
118
|
+
const prefersReducedMotion = useReducedMotion()
|
|
119
|
+
const isValidHex = /^#[0-9a-f]{3,8}$/i.test(value)
|
|
120
|
+
const safeValue = isValidHex ? value : '#000000'
|
|
121
|
+
const [open, setOpen] = useState(false)
|
|
122
|
+
const [copied, setCopied] = useState(false)
|
|
123
|
+
const [textInput, setTextInput] = useState('')
|
|
124
|
+
const [alpha, setAlpha] = useState(1)
|
|
125
|
+
const panelRef = useRef<HTMLDivElement>(null)
|
|
126
|
+
const satAreaRef = useRef<HTMLDivElement>(null)
|
|
127
|
+
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
128
|
+
|
|
129
|
+
useEffect(() => () => { if (copyTimerRef.current) clearTimeout(copyTimerRef.current) }, [])
|
|
130
|
+
|
|
131
|
+
// Recent colors
|
|
132
|
+
const [recentColors, setRecentColors] = useState<string[]>(() => {
|
|
133
|
+
if (typeof window === 'undefined') return []
|
|
134
|
+
try {
|
|
135
|
+
const raw = JSON.parse(localStorage.getItem(RECENT_COLORS_KEY) ?? '[]')
|
|
136
|
+
return Array.isArray(raw) ? raw.filter((x: unknown) => typeof x === 'string' && x.length < 256).slice(0, MAX_RECENT) : []
|
|
137
|
+
} catch { return [] }
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const addRecent = useCallback((color: string) => {
|
|
141
|
+
setRecentColors(prev => {
|
|
142
|
+
const updated = [color, ...prev.filter(c => c !== color)].slice(0, MAX_RECENT)
|
|
143
|
+
try { localStorage.setItem(RECENT_COLORS_KEY, JSON.stringify(updated)) } catch { /* noop */ }
|
|
144
|
+
return updated
|
|
145
|
+
})
|
|
146
|
+
}, [])
|
|
147
|
+
|
|
148
|
+
// HSL from current value
|
|
149
|
+
const { r, g, b } = useMemo(() => hexToRgb(safeValue), [safeValue])
|
|
150
|
+
const hsl = useMemo(() => rgbToHsl(r, g, b), [r, g, b])
|
|
151
|
+
|
|
152
|
+
// Sync text input
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
setTextInput(formatColor(safeValue, format))
|
|
155
|
+
}, [safeValue, format])
|
|
156
|
+
|
|
157
|
+
// Close on click outside
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!open) return
|
|
160
|
+
const handler = (e: MouseEvent) => {
|
|
161
|
+
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
|
|
162
|
+
setOpen(false)
|
|
163
|
+
addRecent(value)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
document.addEventListener('mousedown', handler)
|
|
167
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
168
|
+
}, [open, value, addRecent])
|
|
169
|
+
|
|
170
|
+
// Saturation/brightness area interaction
|
|
171
|
+
const handleSatAreaPointer = useCallback(
|
|
172
|
+
(e: React.PointerEvent | PointerEvent) => {
|
|
173
|
+
if (!satAreaRef.current) return
|
|
174
|
+
const rect = satAreaRef.current.getBoundingClientRect()
|
|
175
|
+
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
|
176
|
+
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height))
|
|
177
|
+
// x = saturation, y = 1-lightness (top=light, bottom=dark)
|
|
178
|
+
const s = x
|
|
179
|
+
const l = 1 - y
|
|
180
|
+
const adjustedL = 0.05 + l * 0.9 // Keep within visible range
|
|
181
|
+
const rgb = hslToRgb(hsl.h, s, adjustedL)
|
|
182
|
+
onChange(rgbToHex(rgb.r, rgb.g, rgb.b))
|
|
183
|
+
},
|
|
184
|
+
[hsl.h, onChange],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const handleSatAreaDown = useCallback(
|
|
188
|
+
(e: React.PointerEvent) => {
|
|
189
|
+
e.preventDefault()
|
|
190
|
+
handleSatAreaPointer(e)
|
|
191
|
+
const move = (ev: PointerEvent) => handleSatAreaPointer(ev)
|
|
192
|
+
const up = () => {
|
|
193
|
+
document.removeEventListener('pointermove', move)
|
|
194
|
+
document.removeEventListener('pointerup', up)
|
|
195
|
+
}
|
|
196
|
+
document.addEventListener('pointermove', move)
|
|
197
|
+
document.addEventListener('pointerup', up)
|
|
198
|
+
},
|
|
199
|
+
[handleSatAreaPointer],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Hue slider
|
|
203
|
+
const handleHueChange = useCallback(
|
|
204
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
205
|
+
const h = Number(e.target.value) / 360
|
|
206
|
+
const rgb = hslToRgb(h, hsl.s || 0.5, hsl.l || 0.5)
|
|
207
|
+
onChange(rgbToHex(rgb.r, rgb.g, rgb.b))
|
|
208
|
+
},
|
|
209
|
+
[hsl.s, hsl.l, onChange],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Text input commit
|
|
213
|
+
const handleTextCommit = useCallback(() => {
|
|
214
|
+
const v = textInput.trim()
|
|
215
|
+
// Try hex
|
|
216
|
+
if (/^#?[0-9a-f]{3,6}$/i.test(v)) {
|
|
217
|
+
const hex = v.startsWith('#') ? v : '#' + v
|
|
218
|
+
onChange(hex)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
// Try rgb()
|
|
222
|
+
const rgbMatch = v.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/)
|
|
223
|
+
if (rgbMatch) {
|
|
224
|
+
onChange(rgbToHex(Number(rgbMatch[1]), Number(rgbMatch[2]), Number(rgbMatch[3])))
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
// Try hsl()
|
|
228
|
+
const hslMatch = v.match(/hsl\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*\)/)
|
|
229
|
+
if (hslMatch) {
|
|
230
|
+
const rgb = hslToRgb(Number(hslMatch[1]) / 360, Number(hslMatch[2]) / 100, Number(hslMatch[3]) / 100)
|
|
231
|
+
onChange(rgbToHex(rgb.r, rgb.g, rgb.b))
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
// Revert
|
|
235
|
+
setTextInput(formatColor(safeValue, format))
|
|
236
|
+
}, [textInput, safeValue, format, onChange])
|
|
237
|
+
|
|
238
|
+
const handleCopy = useCallback(async () => {
|
|
239
|
+
try {
|
|
240
|
+
await navigator.clipboard.writeText(formatColor(safeValue, format))
|
|
241
|
+
setCopied(true)
|
|
242
|
+
if (copyTimerRef.current) clearTimeout(copyTimerRef.current)
|
|
243
|
+
copyTimerRef.current = setTimeout(() => setCopied(false), 1500)
|
|
244
|
+
} catch { /* noop */ }
|
|
245
|
+
}, [safeValue, format])
|
|
246
|
+
|
|
247
|
+
// Position for sat/brightness marker
|
|
248
|
+
const markerX = hsl.s * 100
|
|
249
|
+
const markerY = (1 - (hsl.l - 0.05) / 0.9) * 100
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div ref={panelRef} className={cn('relative inline-block', className)}>
|
|
253
|
+
{/* Label */}
|
|
254
|
+
{label && (
|
|
255
|
+
<label className="block text-xs font-medium text-[hsl(var(--text-secondary))] mb-1.5">
|
|
256
|
+
{label}
|
|
257
|
+
</label>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{/* Compact input */}
|
|
261
|
+
<button
|
|
262
|
+
onClick={() => setOpen(o => !o)}
|
|
263
|
+
className={cn(
|
|
264
|
+
'inline-flex items-center gap-2 rounded-lg border border-[hsl(var(--border-subtle))]',
|
|
265
|
+
'bg-[hsl(var(--bg-surface))] px-3 py-2 text-sm',
|
|
266
|
+
'hover:border-[hsl(var(--border-default))] transition-colors',
|
|
267
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--brand-primary))]',
|
|
268
|
+
)}
|
|
269
|
+
>
|
|
270
|
+
<span
|
|
271
|
+
className="h-5 w-5 rounded-md border border-[hsl(var(--border-subtle))]"
|
|
272
|
+
style={{ backgroundColor: isSafeColor(safeValue) ? safeValue : undefined }}
|
|
273
|
+
/>
|
|
274
|
+
<span className="font-mono text-xs text-[hsl(var(--text-primary))]">
|
|
275
|
+
{formatColor(value, format)}
|
|
276
|
+
</span>
|
|
277
|
+
</button>
|
|
278
|
+
|
|
279
|
+
{/* Expanded picker panel */}
|
|
280
|
+
<AnimatePresence>
|
|
281
|
+
{open && (
|
|
282
|
+
<motion.div
|
|
283
|
+
initial={prefersReducedMotion ? undefined : { opacity: 0, scale: 0.96, y: -4 }}
|
|
284
|
+
animate={prefersReducedMotion ? undefined : { opacity: 1, scale: 1, y: 0 }}
|
|
285
|
+
exit={prefersReducedMotion ? undefined : { opacity: 0, scale: 0.96, y: -4 }}
|
|
286
|
+
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.15 }}
|
|
287
|
+
className={cn(
|
|
288
|
+
'absolute z-50 mt-2 w-64 rounded-xl overflow-hidden',
|
|
289
|
+
'border border-[hsl(var(--border-default))]',
|
|
290
|
+
'bg-[hsl(var(--bg-elevated))] shadow-xl',
|
|
291
|
+
'p-3',
|
|
292
|
+
)}
|
|
293
|
+
>
|
|
294
|
+
{/* Saturation/Brightness area */}
|
|
295
|
+
<div
|
|
296
|
+
ref={satAreaRef}
|
|
297
|
+
onPointerDown={handleSatAreaDown}
|
|
298
|
+
className="relative h-36 w-full rounded-lg cursor-crosshair overflow-hidden mb-3"
|
|
299
|
+
style={{
|
|
300
|
+
background: `linear-gradient(to top, #000, transparent),
|
|
301
|
+
linear-gradient(to right, #fff, hsl(${Math.round(hsl.h * 360)}, 100%, 50%))`,
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{/* Marker */}
|
|
305
|
+
<div
|
|
306
|
+
className="absolute w-4 h-4 rounded-full border-2 border-white shadow-md -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
|
307
|
+
style={{
|
|
308
|
+
left: `${markerX}%`,
|
|
309
|
+
top: `${Math.max(0, Math.min(100, markerY))}%`,
|
|
310
|
+
backgroundColor: isSafeColor(safeValue) ? safeValue : undefined,
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* Hue slider */}
|
|
316
|
+
<div className="mb-3">
|
|
317
|
+
<input
|
|
318
|
+
type="range"
|
|
319
|
+
min={0}
|
|
320
|
+
max={360}
|
|
321
|
+
value={Math.round(hsl.h * 360)}
|
|
322
|
+
onChange={handleHueChange}
|
|
323
|
+
className="w-full h-3 rounded-full appearance-none cursor-pointer"
|
|
324
|
+
style={{
|
|
325
|
+
background: 'linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00)',
|
|
326
|
+
}}
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{/* Alpha slider */}
|
|
331
|
+
{showAlpha && (
|
|
332
|
+
<div className="mb-3">
|
|
333
|
+
<input
|
|
334
|
+
type="range"
|
|
335
|
+
min={0}
|
|
336
|
+
max={100}
|
|
337
|
+
value={Math.round(alpha * 100)}
|
|
338
|
+
onChange={e => setAlpha(Number(e.target.value) / 100)}
|
|
339
|
+
className="w-full h-3 rounded-full appearance-none cursor-pointer"
|
|
340
|
+
style={{
|
|
341
|
+
background: `linear-gradient(to right, transparent, ${isSafeColor(safeValue) ? safeValue : '#000'})`,
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{/* Text input + copy */}
|
|
348
|
+
<div className="flex items-center gap-2 mb-3">
|
|
349
|
+
<input
|
|
350
|
+
type="text"
|
|
351
|
+
value={textInput}
|
|
352
|
+
onChange={e => setTextInput(e.target.value)}
|
|
353
|
+
onBlur={handleTextCommit}
|
|
354
|
+
onKeyDown={e => { if (e.key === 'Enter') handleTextCommit() }}
|
|
355
|
+
className={cn(
|
|
356
|
+
'flex-1 rounded-md border border-[hsl(var(--border-subtle))]',
|
|
357
|
+
'bg-[hsl(var(--bg-surface))] px-2 py-1 text-xs font-mono',
|
|
358
|
+
'text-[hsl(var(--text-primary))] outline-none',
|
|
359
|
+
'focus:border-[hsl(var(--brand-primary))] transition-colors',
|
|
360
|
+
)}
|
|
361
|
+
/>
|
|
362
|
+
<button
|
|
363
|
+
onClick={handleCopy}
|
|
364
|
+
className={cn(
|
|
365
|
+
'p-1.5 rounded-md transition-colors',
|
|
366
|
+
'text-[hsl(var(--text-tertiary))] hover:text-[hsl(var(--text-primary))]',
|
|
367
|
+
'hover:bg-[hsl(var(--bg-surface))]',
|
|
368
|
+
)}
|
|
369
|
+
title="Copy color"
|
|
370
|
+
>
|
|
371
|
+
{copied ? <Check className="h-3.5 w-3.5 text-[hsl(var(--status-ok))]" /> : <Copy className="h-3.5 w-3.5" />}
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
{/* Presets */}
|
|
376
|
+
{presets && presets.length > 0 && (
|
|
377
|
+
<div className="mb-2">
|
|
378
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-[hsl(var(--text-tertiary))] mb-1.5 block">
|
|
379
|
+
Presets
|
|
380
|
+
</span>
|
|
381
|
+
<div className="flex flex-wrap gap-1.5">
|
|
382
|
+
{presets.map(color => (
|
|
383
|
+
<button
|
|
384
|
+
key={color}
|
|
385
|
+
onClick={() => { onChange(color); addRecent(color) }}
|
|
386
|
+
className={cn(
|
|
387
|
+
'h-6 w-6 rounded-md border transition-all',
|
|
388
|
+
value === color
|
|
389
|
+
? 'border-[hsl(var(--brand-primary))] ring-2 ring-[hsl(var(--brand-primary)/0.3)] scale-110'
|
|
390
|
+
: 'border-[hsl(var(--border-subtle))] hover:scale-110',
|
|
391
|
+
)}
|
|
392
|
+
style={{ backgroundColor: isSafeColor(color) ? color : undefined }}
|
|
393
|
+
title={color}
|
|
394
|
+
/>
|
|
395
|
+
))}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{/* Recent colors */}
|
|
401
|
+
{recentColors.length > 0 && (
|
|
402
|
+
<div>
|
|
403
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-[hsl(var(--text-tertiary))] mb-1.5 block">
|
|
404
|
+
Recent
|
|
405
|
+
</span>
|
|
406
|
+
<div className="flex flex-wrap gap-1.5">
|
|
407
|
+
{recentColors.map(color => (
|
|
408
|
+
<button
|
|
409
|
+
key={color}
|
|
410
|
+
onClick={() => onChange(color)}
|
|
411
|
+
className={cn(
|
|
412
|
+
'h-6 w-6 rounded-md border border-[hsl(var(--border-subtle))]',
|
|
413
|
+
'hover:scale-110 transition-transform',
|
|
414
|
+
)}
|
|
415
|
+
style={{ backgroundColor: isSafeColor(color) ? color : undefined }}
|
|
416
|
+
title={color}
|
|
417
|
+
/>
|
|
418
|
+
))}
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</motion.div>
|
|
423
|
+
)}
|
|
424
|
+
</AnimatePresence>
|
|
425
|
+
</div>
|
|
426
|
+
)
|
|
427
|
+
}
|