@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.
Files changed (67) hide show
  1. package/README.md +1463 -127
  2. package/dist/{chunk-5OKSXPWK.js → chunk-2DWZVHZS.js} +2 -2
  3. package/dist/chunk-2DWZVHZS.js.map +1 -0
  4. package/dist/form.d.ts +6 -6
  5. package/dist/form.js +2 -3
  6. package/dist/form.js.map +1 -1
  7. package/dist/index.d.ts +510 -52
  8. package/dist/index.js +2996 -15
  9. package/dist/index.js.map +1 -1
  10. package/dist/{select-nnBJUO8U.d.ts → select-B2wXqqSM.d.ts} +2 -2
  11. package/package.json +24 -26
  12. package/src/components/animated-counter.tsx +8 -5
  13. package/src/components/avatar.tsx +2 -1
  14. package/src/components/badge.tsx +3 -2
  15. package/src/components/button.tsx +3 -2
  16. package/src/components/card.tsx +13 -12
  17. package/src/components/checkbox.tsx +3 -2
  18. package/src/components/color-input.tsx +427 -0
  19. package/src/components/command-bar.tsx +435 -0
  20. package/src/components/confidence-bar.tsx +115 -0
  21. package/src/components/confirm-dialog.tsx +2 -1
  22. package/src/components/copy-block.tsx +224 -0
  23. package/src/components/data-table.tsx +9 -13
  24. package/src/components/diff-viewer.tsx +340 -0
  25. package/src/components/dropdown-menu.tsx +2 -1
  26. package/src/components/empty-state.tsx +2 -1
  27. package/src/components/filter-pill.tsx +2 -1
  28. package/src/components/form-input.tsx +5 -4
  29. package/src/components/heatmap-calendar.tsx +218 -0
  30. package/src/components/infinite-scroll.tsx +248 -0
  31. package/src/components/kanban-column.tsx +198 -0
  32. package/src/components/live-feed.tsx +222 -0
  33. package/src/components/log-viewer.tsx +4 -1
  34. package/src/components/metric-card.tsx +2 -1
  35. package/src/components/notification-stack.tsx +233 -0
  36. package/src/components/pipeline-stage.tsx +2 -1
  37. package/src/components/popover.tsx +2 -1
  38. package/src/components/port-status-grid.tsx +2 -1
  39. package/src/components/progress.tsx +2 -1
  40. package/src/components/radio-group.tsx +2 -1
  41. package/src/components/realtime-value.tsx +283 -0
  42. package/src/components/select.tsx +2 -1
  43. package/src/components/severity-timeline.tsx +2 -1
  44. package/src/components/sheet.tsx +2 -1
  45. package/src/components/skeleton.tsx +4 -3
  46. package/src/components/slider.tsx +2 -1
  47. package/src/components/smart-table.tsx +383 -0
  48. package/src/components/sortable-list.tsx +272 -0
  49. package/src/components/sparkline.tsx +2 -1
  50. package/src/components/status-badge.tsx +2 -1
  51. package/src/components/status-pulse.tsx +2 -1
  52. package/src/components/step-wizard.tsx +380 -0
  53. package/src/components/streaming-text.tsx +160 -0
  54. package/src/components/success-checkmark.tsx +2 -1
  55. package/src/components/tabs.tsx +2 -1
  56. package/src/components/threshold-gauge.tsx +2 -1
  57. package/src/components/time-range-selector.tsx +2 -1
  58. package/src/components/toast.tsx +2 -1
  59. package/src/components/toggle-switch.tsx +2 -1
  60. package/src/components/tooltip.tsx +2 -1
  61. package/src/components/truncated-text.tsx +2 -1
  62. package/src/components/typing-indicator.tsx +123 -0
  63. package/src/components/uptime-tracker.tsx +2 -1
  64. package/src/components/utilization-bar.tsx +2 -1
  65. package/src/theme.css +6 -0
  66. package/src/utils.ts +1 -1
  67. 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
+ }