@annondeveloper/ui-kit 0.1.0 → 0.2.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 (65) hide show
  1. package/dist/{chunk-5OKSXPWK.js → chunk-2DWZVHZS.js} +2 -2
  2. package/dist/chunk-2DWZVHZS.js.map +1 -0
  3. package/dist/form.d.ts +6 -6
  4. package/dist/form.js +1 -1
  5. package/dist/form.js.map +1 -1
  6. package/dist/index.d.ts +508 -52
  7. package/dist/index.js +2927 -4
  8. package/dist/index.js.map +1 -1
  9. package/dist/{select-nnBJUO8U.d.ts → select-B2wXqqSM.d.ts} +2 -2
  10. package/package.json +1 -1
  11. package/src/components/animated-counter.tsx +2 -1
  12. package/src/components/avatar.tsx +2 -1
  13. package/src/components/badge.tsx +3 -2
  14. package/src/components/button.tsx +3 -2
  15. package/src/components/card.tsx +13 -12
  16. package/src/components/checkbox.tsx +3 -2
  17. package/src/components/color-input.tsx +414 -0
  18. package/src/components/command-bar.tsx +434 -0
  19. package/src/components/confidence-bar.tsx +115 -0
  20. package/src/components/confirm-dialog.tsx +2 -1
  21. package/src/components/copy-block.tsx +229 -0
  22. package/src/components/data-table.tsx +2 -1
  23. package/src/components/diff-viewer.tsx +319 -0
  24. package/src/components/dropdown-menu.tsx +2 -1
  25. package/src/components/empty-state.tsx +2 -1
  26. package/src/components/filter-pill.tsx +2 -1
  27. package/src/components/form-input.tsx +5 -4
  28. package/src/components/heatmap-calendar.tsx +213 -0
  29. package/src/components/infinite-scroll.tsx +243 -0
  30. package/src/components/kanban-column.tsx +198 -0
  31. package/src/components/live-feed.tsx +220 -0
  32. package/src/components/log-viewer.tsx +2 -1
  33. package/src/components/metric-card.tsx +2 -1
  34. package/src/components/notification-stack.tsx +226 -0
  35. package/src/components/pipeline-stage.tsx +2 -1
  36. package/src/components/popover.tsx +2 -1
  37. package/src/components/port-status-grid.tsx +2 -1
  38. package/src/components/progress.tsx +2 -1
  39. package/src/components/radio-group.tsx +2 -1
  40. package/src/components/realtime-value.tsx +283 -0
  41. package/src/components/select.tsx +2 -1
  42. package/src/components/severity-timeline.tsx +2 -1
  43. package/src/components/sheet.tsx +2 -1
  44. package/src/components/skeleton.tsx +4 -3
  45. package/src/components/slider.tsx +2 -1
  46. package/src/components/smart-table.tsx +383 -0
  47. package/src/components/sortable-list.tsx +268 -0
  48. package/src/components/sparkline.tsx +2 -1
  49. package/src/components/status-badge.tsx +2 -1
  50. package/src/components/status-pulse.tsx +2 -1
  51. package/src/components/step-wizard.tsx +372 -0
  52. package/src/components/streaming-text.tsx +163 -0
  53. package/src/components/success-checkmark.tsx +2 -1
  54. package/src/components/tabs.tsx +2 -1
  55. package/src/components/threshold-gauge.tsx +2 -1
  56. package/src/components/time-range-selector.tsx +2 -1
  57. package/src/components/toast.tsx +2 -1
  58. package/src/components/toggle-switch.tsx +2 -1
  59. package/src/components/tooltip.tsx +2 -1
  60. package/src/components/truncated-text.tsx +2 -1
  61. package/src/components/typing-indicator.tsx +123 -0
  62. package/src/components/uptime-tracker.tsx +2 -1
  63. package/src/components/utilization-bar.tsx +2 -1
  64. package/src/utils.ts +1 -1
  65. package/dist/chunk-5OKSXPWK.js.map +0 -1
@@ -0,0 +1,229 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+ import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
5
+ import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
6
+ import { Copy, Check, ChevronDown, ChevronUp } from 'lucide-react'
7
+ import { cn } from '../utils'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /** Props for the CopyBlock component. */
14
+ export interface CopyBlockProps {
15
+ /** The text content to display and copy. */
16
+ content: string
17
+ /** Language hint for styling/accessibility (e.g. "json", "bash", "sql"). */
18
+ language?: string
19
+ /** Show line numbers in the gutter. Default false. */
20
+ showLineNumbers?: boolean
21
+ /** Max height in pixels before collapsing with a "Show more" toggle. */
22
+ maxHeight?: number
23
+ /** Optional label displayed above the code block. */
24
+ label?: string
25
+ /** Additional class name for the root container. */
26
+ className?: string
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // CopyBlock
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * @description A monospace code/text display block with one-click copy, animated feedback,
35
+ * optional line numbers, collapsible overflow (show more/less), and dark-mode optimized styling.
36
+ * Designed for displaying code snippets, CLI commands, config blocks, and JSON payloads.
37
+ */
38
+ export function CopyBlock({
39
+ content,
40
+ language,
41
+ showLineNumbers = false,
42
+ maxHeight,
43
+ label,
44
+ className,
45
+ }: CopyBlockProps): React.JSX.Element {
46
+ const prefersReducedMotion = useReducedMotion()
47
+ const [copied, setCopied] = useState(false)
48
+ const [isCollapsed, setIsCollapsed] = useState(true)
49
+ const [needsCollapse, setNeedsCollapse] = useState(false)
50
+ const contentRef = useRef<HTMLPreElement>(null)
51
+
52
+ // Check if content exceeds maxHeight
53
+ useEffect(() => {
54
+ if (!maxHeight || !contentRef.current) return
55
+ setNeedsCollapse(contentRef.current.scrollHeight > maxHeight)
56
+ }, [content, maxHeight])
57
+
58
+ const lines = useMemo(() => content.split('\n'), [content])
59
+
60
+ const handleCopy = useCallback(async () => {
61
+ try {
62
+ await navigator.clipboard.writeText(content)
63
+ setCopied(true)
64
+ setTimeout(() => setCopied(false), 2000)
65
+ } catch {
66
+ // Fallback for older browsers
67
+ const textarea = document.createElement('textarea')
68
+ textarea.value = content
69
+ textarea.style.position = 'fixed'
70
+ textarea.style.opacity = '0'
71
+ document.body.appendChild(textarea)
72
+ textarea.select()
73
+ document.execCommand('copy')
74
+ document.body.removeChild(textarea)
75
+ setCopied(true)
76
+ setTimeout(() => setCopied(false), 2000)
77
+ }
78
+ }, [content])
79
+
80
+ const shouldCollapse = maxHeight && needsCollapse && isCollapsed
81
+
82
+ return (
83
+ <div
84
+ className={cn(
85
+ 'relative group rounded-xl overflow-hidden',
86
+ 'border border-[hsl(var(--border-subtle))]',
87
+ 'bg-[hsl(var(--bg-base))]',
88
+ className,
89
+ )}
90
+ >
91
+ {/* Header bar */}
92
+ {(label || language) && (
93
+ <div className="flex items-center justify-between px-4 py-2 border-b border-[hsl(var(--border-subtle)/0.5)] bg-[hsl(var(--bg-surface)/0.3)]">
94
+ <div className="flex items-center gap-2">
95
+ {label && (
96
+ <span className="text-[11px] font-medium text-[hsl(var(--text-secondary))]">
97
+ {label}
98
+ </span>
99
+ )}
100
+ {language && (
101
+ <span className="inline-flex items-center rounded-md bg-[hsl(var(--bg-overlay)/0.5)] px-1.5 py-0.5 text-[10px] font-mono text-[hsl(var(--text-tertiary))]">
102
+ {language}
103
+ </span>
104
+ )}
105
+ </div>
106
+
107
+ {/* Copy button (always visible in header) */}
108
+ <button
109
+ onClick={handleCopy}
110
+ className={cn(
111
+ 'inline-flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium transition-all',
112
+ copied
113
+ ? 'text-[hsl(var(--status-ok))] bg-[hsl(var(--status-ok)/0.1)]'
114
+ : 'text-[hsl(var(--text-tertiary))] hover:text-[hsl(var(--text-primary))] hover:bg-[hsl(var(--bg-elevated))]',
115
+ )}
116
+ >
117
+ <AnimatePresence mode="wait">
118
+ {copied ? (
119
+ <motion.span
120
+ key="check"
121
+ initial={prefersReducedMotion ? undefined : { scale: 0.5, opacity: 0 }}
122
+ animate={prefersReducedMotion ? undefined : { scale: 1, opacity: 1 }}
123
+ exit={prefersReducedMotion ? undefined : { scale: 0.5, opacity: 0 }}
124
+ transition={{ duration: 0.15 }}
125
+ className="inline-flex items-center gap-1"
126
+ >
127
+ <Check className="h-3.5 w-3.5" />
128
+ Copied!
129
+ </motion.span>
130
+ ) : (
131
+ <motion.span
132
+ key="copy"
133
+ initial={prefersReducedMotion ? undefined : { scale: 0.5, opacity: 0 }}
134
+ animate={prefersReducedMotion ? undefined : { scale: 1, opacity: 1 }}
135
+ exit={prefersReducedMotion ? undefined : { scale: 0.5, opacity: 0 }}
136
+ transition={{ duration: 0.15 }}
137
+ className="inline-flex items-center gap-1"
138
+ >
139
+ <Copy className="h-3.5 w-3.5" />
140
+ Copy
141
+ </motion.span>
142
+ )}
143
+ </AnimatePresence>
144
+ </button>
145
+ </div>
146
+ )}
147
+
148
+ {/* Copy button for headerless blocks */}
149
+ {!label && !language && (
150
+ <button
151
+ onClick={handleCopy}
152
+ className={cn(
153
+ 'absolute top-2 right-2 z-10 rounded-md p-1.5 transition-all',
154
+ 'opacity-0 group-hover:opacity-100',
155
+ copied
156
+ ? 'text-[hsl(var(--status-ok))] bg-[hsl(var(--status-ok)/0.1)]'
157
+ : 'text-[hsl(var(--text-tertiary))] hover:text-[hsl(var(--text-primary))] bg-[hsl(var(--bg-elevated)/0.8)] hover:bg-[hsl(var(--bg-elevated))]',
158
+ )}
159
+ title={copied ? 'Copied!' : 'Copy to clipboard'}
160
+ >
161
+ {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
162
+ </button>
163
+ )}
164
+
165
+ {/* Content area */}
166
+ <div
167
+ className="overflow-x-auto"
168
+ style={shouldCollapse ? { maxHeight, overflow: 'hidden' } : undefined}
169
+ >
170
+ <pre
171
+ ref={contentRef}
172
+ className={cn(
173
+ 'p-4 text-[13px] leading-relaxed font-mono',
174
+ 'text-[hsl(var(--text-primary))]',
175
+ 'whitespace-pre overflow-x-auto',
176
+ )}
177
+ >
178
+ {showLineNumbers ? (
179
+ <table className="border-collapse w-full">
180
+ <tbody>
181
+ {lines.map((line, i) => (
182
+ <tr key={i} className="hover:bg-[hsl(var(--bg-surface)/0.3)]">
183
+ <td className="select-none text-right pr-4 text-[hsl(var(--text-disabled))] text-[11px] tabular-nums w-8 align-top">
184
+ {i + 1}
185
+ </td>
186
+ <td className="whitespace-pre">{line}</td>
187
+ </tr>
188
+ ))}
189
+ </tbody>
190
+ </table>
191
+ ) : (
192
+ content
193
+ )}
194
+ </pre>
195
+ </div>
196
+
197
+ {/* Collapse gradient + toggle */}
198
+ {maxHeight && needsCollapse && (
199
+ <>
200
+ {isCollapsed && (
201
+ <div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-[hsl(var(--bg-base))] to-transparent pointer-events-none" />
202
+ )}
203
+ <div className="relative border-t border-[hsl(var(--border-subtle)/0.3)]">
204
+ <button
205
+ onClick={() => setIsCollapsed(c => !c)}
206
+ className={cn(
207
+ 'w-full flex items-center justify-center gap-1.5 py-2 text-[11px] font-medium',
208
+ 'text-[hsl(var(--text-secondary))] hover:text-[hsl(var(--text-primary))]',
209
+ 'hover:bg-[hsl(var(--bg-surface)/0.3)] transition-colors',
210
+ )}
211
+ >
212
+ {isCollapsed ? (
213
+ <>
214
+ Show more ({lines.length} lines)
215
+ <ChevronDown className="h-3.5 w-3.5" />
216
+ </>
217
+ ) : (
218
+ <>
219
+ Show less
220
+ <ChevronUp className="h-3.5 w-3.5" />
221
+ </>
222
+ )}
223
+ </button>
224
+ </div>
225
+ </>
226
+ )}
227
+ </div>
228
+ )
229
+ }
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { useState, useMemo, useCallback, useRef, useEffect } from 'react'
4
5
  import {
5
6
  useReactTable, getCoreRowModel, getSortedRowModel,
@@ -365,7 +366,7 @@ export function DataTable<T>({
365
366
  exportFilename,
366
367
  stickyFirstColumn = false,
367
368
  density: densityProp,
368
- }: DataTableProps<T>) {
369
+ }: DataTableProps<T>): React.JSX.Element {
369
370
  const prefersReducedMotion = useReducedMotion()
370
371
 
371
372
  const [density, setDensity] = useState<Density>(() => {
@@ -0,0 +1,319 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+ import { useMemo, useState } from 'react'
5
+ import { cn } from '../utils'
6
+
7
+ export interface DiffViewerProps {
8
+ /** The original text. */
9
+ oldValue: string
10
+ /** The modified text. */
11
+ newValue: string
12
+ /** Display mode. */
13
+ mode?: 'inline' | 'side-by-side'
14
+ /** Language hint for potential styling. */
15
+ language?: string
16
+ /** Show line numbers in the gutter. */
17
+ showLineNumbers?: boolean
18
+ className?: string
19
+ }
20
+
21
+ type LineType = 'added' | 'removed' | 'unchanged'
22
+
23
+ interface DiffLine {
24
+ type: LineType
25
+ content: string
26
+ oldLineNo?: number
27
+ newLineNo?: number
28
+ }
29
+
30
+ /** Simple line-by-line diff using longest common subsequence. */
31
+ function computeDiff(oldLines: string[], newLines: string[]): DiffLine[] {
32
+ // Build LCS table
33
+ const m = oldLines.length
34
+ const n = newLines.length
35
+ const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))
36
+
37
+ for (let i = 1; i <= m; i++) {
38
+ for (let j = 1; j <= n; j++) {
39
+ if (oldLines[i - 1] === newLines[j - 1]) {
40
+ dp[i][j] = dp[i - 1][j - 1] + 1
41
+ } else {
42
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
43
+ }
44
+ }
45
+ }
46
+
47
+ // Backtrack to produce diff
48
+ const result: DiffLine[] = []
49
+ let i = m
50
+ let j = n
51
+
52
+ while (i > 0 || j > 0) {
53
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
54
+ result.push({ type: 'unchanged', content: oldLines[i - 1], oldLineNo: i, newLineNo: j })
55
+ i--
56
+ j--
57
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
58
+ result.push({ type: 'added', content: newLines[j - 1], newLineNo: j })
59
+ j--
60
+ } else {
61
+ result.push({ type: 'removed', content: oldLines[i - 1], oldLineNo: i })
62
+ i--
63
+ }
64
+ }
65
+
66
+ return result.reverse()
67
+ }
68
+
69
+ const TYPE_BG: Record<LineType, string> = {
70
+ added: 'bg-[hsl(var(--status-ok))]/10',
71
+ removed: 'bg-[hsl(var(--status-critical))]/10',
72
+ unchanged: '',
73
+ }
74
+
75
+ const TYPE_PREFIX: Record<LineType, string> = {
76
+ added: '+',
77
+ removed: '-',
78
+ unchanged: ' ',
79
+ }
80
+
81
+ const TYPE_PREFIX_COLOR: Record<LineType, string> = {
82
+ added: 'text-[hsl(var(--status-ok))]',
83
+ removed: 'text-[hsl(var(--status-critical))]',
84
+ unchanged: 'text-[hsl(var(--text-tertiary))]',
85
+ }
86
+
87
+ /**
88
+ * @description A diff viewer showing line-by-line differences between two text values.
89
+ * Supports inline and side-by-side modes, collapsible unchanged sections, and line numbers.
90
+ */
91
+ export function DiffViewer({
92
+ oldValue,
93
+ newValue,
94
+ mode = 'inline',
95
+ language,
96
+ showLineNumbers = true,
97
+ className,
98
+ }: DiffViewerProps): React.JSX.Element {
99
+ const [expandedSections, setExpandedSections] = useState<Set<number>>(new Set())
100
+
101
+ const diffLines = useMemo(() => {
102
+ const oldLines = oldValue.split('\n')
103
+ const newLines = newValue.split('\n')
104
+ return computeDiff(oldLines, newLines)
105
+ }, [oldValue, newValue])
106
+
107
+ // Group unchanged sections for collapsing
108
+ const sections = useMemo(() => {
109
+ const groups: { type: 'changes' | 'unchanged'; lines: DiffLine[]; startIdx: number }[] = []
110
+ let current: DiffLine[] = []
111
+ let currentType: 'changes' | 'unchanged' | null = null
112
+ let startIdx = 0
113
+
114
+ for (let i = 0; i < diffLines.length; i++) {
115
+ const lineType = diffLines[i].type === 'unchanged' ? 'unchanged' : 'changes'
116
+ if (lineType !== currentType) {
117
+ if (current.length > 0 && currentType !== null) {
118
+ groups.push({ type: currentType, lines: current, startIdx })
119
+ }
120
+ current = [diffLines[i]]
121
+ currentType = lineType
122
+ startIdx = i
123
+ } else {
124
+ current.push(diffLines[i])
125
+ }
126
+ }
127
+ if (current.length > 0 && currentType !== null) {
128
+ groups.push({ type: currentType, lines: current, startIdx })
129
+ }
130
+ return groups
131
+ }, [diffLines])
132
+
133
+ const toggleSection = (idx: number) => {
134
+ setExpandedSections((prev) => {
135
+ const next = new Set(prev)
136
+ if (next.has(idx)) {
137
+ next.delete(idx)
138
+ } else {
139
+ next.add(idx)
140
+ }
141
+ return next
142
+ })
143
+ }
144
+
145
+ if (mode === 'side-by-side') {
146
+ return <SideBySide diffLines={diffLines} showLineNumbers={showLineNumbers} language={language} className={className} />
147
+ }
148
+
149
+ return (
150
+ <div
151
+ className={cn(
152
+ 'w-full overflow-x-auto rounded-xl border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))]',
153
+ 'font-mono text-xs leading-5',
154
+ className,
155
+ )}
156
+ data-language={language}
157
+ >
158
+ {sections.map((section, sIdx) => {
159
+ // Collapse unchanged sections longer than 6 lines
160
+ if (section.type === 'unchanged' && section.lines.length > 6 && !expandedSections.has(sIdx)) {
161
+ const topContext = section.lines.slice(0, 3)
162
+ const bottomContext = section.lines.slice(-3)
163
+ const hiddenCount = section.lines.length - 6
164
+
165
+ return (
166
+ <div key={sIdx}>
167
+ {topContext.map((line, i) => (
168
+ <InlineLine key={`${sIdx}-t-${i}`} line={line} showLineNumbers={showLineNumbers} />
169
+ ))}
170
+ <button
171
+ type="button"
172
+ onClick={() => toggleSection(sIdx)}
173
+ className={cn(
174
+ 'w-full px-3 py-1 text-center text-[10px]',
175
+ 'text-[hsl(var(--text-tertiary))] bg-[hsl(var(--bg-overlay))]/50',
176
+ 'hover:bg-[hsl(var(--bg-overlay))] cursor-pointer transition-colors duration-100',
177
+ )}
178
+ >
179
+ ... {hiddenCount} unchanged {hiddenCount === 1 ? 'line' : 'lines'} ...
180
+ </button>
181
+ {bottomContext.map((line, i) => (
182
+ <InlineLine key={`${sIdx}-b-${i}`} line={line} showLineNumbers={showLineNumbers} />
183
+ ))}
184
+ </div>
185
+ )
186
+ }
187
+
188
+ return (
189
+ <div key={sIdx}>
190
+ {section.type === 'unchanged' && expandedSections.has(sIdx) && (
191
+ <button
192
+ type="button"
193
+ onClick={() => toggleSection(sIdx)}
194
+ className={cn(
195
+ 'w-full px-3 py-0.5 text-center text-[10px]',
196
+ 'text-[hsl(var(--text-tertiary))] bg-[hsl(var(--bg-overlay))]/30',
197
+ 'hover:bg-[hsl(var(--bg-overlay))] cursor-pointer transition-colors duration-100',
198
+ )}
199
+ >
200
+ collapse {section.lines.length} unchanged lines
201
+ </button>
202
+ )}
203
+ {section.lines.map((line, i) => (
204
+ <InlineLine key={`${sIdx}-${i}`} line={line} showLineNumbers={showLineNumbers} />
205
+ ))}
206
+ </div>
207
+ )
208
+ })}
209
+ </div>
210
+ )
211
+ }
212
+
213
+ function InlineLine({ line, showLineNumbers }: { line: DiffLine; showLineNumbers: boolean }): React.JSX.Element {
214
+ return (
215
+ <div className={cn('flex', TYPE_BG[line.type])}>
216
+ {showLineNumbers && (
217
+ <span className="shrink-0 w-9 px-1.5 text-right select-none text-[hsl(var(--text-tertiary))]/50 tabular-nums">
218
+ {line.oldLineNo ?? ''}
219
+ </span>
220
+ )}
221
+ {showLineNumbers && (
222
+ <span className="shrink-0 w-9 px-1.5 text-right select-none text-[hsl(var(--text-tertiary))]/50 tabular-nums">
223
+ {line.newLineNo ?? ''}
224
+ </span>
225
+ )}
226
+ <span className={cn('shrink-0 w-4 text-center select-none font-bold', TYPE_PREFIX_COLOR[line.type])}>
227
+ {TYPE_PREFIX[line.type]}
228
+ </span>
229
+ <span className="flex-1 px-2 whitespace-pre text-[hsl(var(--text-primary))]">
230
+ {line.content}
231
+ </span>
232
+ </div>
233
+ )
234
+ }
235
+
236
+ function SideBySide({
237
+ diffLines,
238
+ showLineNumbers,
239
+ language,
240
+ className,
241
+ }: {
242
+ diffLines: DiffLine[]
243
+ showLineNumbers: boolean
244
+ language?: string
245
+ className?: string
246
+ }): React.JSX.Element {
247
+ // Build paired rows for side-by-side
248
+ const pairs: { left?: DiffLine; right?: DiffLine }[] = []
249
+ let i = 0
250
+ while (i < diffLines.length) {
251
+ const line = diffLines[i]
252
+ if (line.type === 'unchanged') {
253
+ pairs.push({ left: line, right: line })
254
+ i++
255
+ } else if (line.type === 'removed') {
256
+ // Look for a matching added line
257
+ if (i + 1 < diffLines.length && diffLines[i + 1].type === 'added') {
258
+ pairs.push({ left: line, right: diffLines[i + 1] })
259
+ i += 2
260
+ } else {
261
+ pairs.push({ left: line })
262
+ i++
263
+ }
264
+ } else {
265
+ pairs.push({ right: line })
266
+ i++
267
+ }
268
+ }
269
+
270
+ return (
271
+ <div
272
+ className={cn(
273
+ 'w-full overflow-x-auto rounded-xl border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))]',
274
+ 'font-mono text-xs leading-5',
275
+ className,
276
+ )}
277
+ data-language={language}
278
+ >
279
+ <div className="grid grid-cols-2 divide-x divide-[hsl(var(--border-subtle))]">
280
+ {/* Left: old */}
281
+ <div>
282
+ {pairs.map((pair, idx) => (
283
+ <div
284
+ key={`l-${idx}`}
285
+ className={cn('flex', pair.left ? TYPE_BG[pair.left.type] : '')}
286
+ >
287
+ {showLineNumbers && (
288
+ <span className="shrink-0 w-9 px-1.5 text-right select-none text-[hsl(var(--text-tertiary))]/50 tabular-nums">
289
+ {pair.left?.oldLineNo ?? ''}
290
+ </span>
291
+ )}
292
+ <span className="flex-1 px-2 whitespace-pre text-[hsl(var(--text-primary))]">
293
+ {pair.left?.content ?? ''}
294
+ </span>
295
+ </div>
296
+ ))}
297
+ </div>
298
+ {/* Right: new */}
299
+ <div>
300
+ {pairs.map((pair, idx) => (
301
+ <div
302
+ key={`r-${idx}`}
303
+ className={cn('flex', pair.right ? TYPE_BG[pair.right.type] : '')}
304
+ >
305
+ {showLineNumbers && (
306
+ <span className="shrink-0 w-9 px-1.5 text-right select-none text-[hsl(var(--text-tertiary))]/50 tabular-nums">
307
+ {pair.right?.newLineNo ?? ''}
308
+ </span>
309
+ )}
310
+ <span className="flex-1 px-2 whitespace-pre text-[hsl(var(--text-primary))]">
311
+ {pair.right?.content ?? ''}
312
+ </span>
313
+ </div>
314
+ ))}
315
+ </div>
316
+ </div>
317
+ </div>
318
+ )
319
+ }
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { type ReactNode } from 'react'
4
5
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
5
6
  import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
@@ -42,7 +43,7 @@ const contentVariants = {
42
43
  * Features Framer Motion entry/exit animations, keyboard accessibility,
43
44
  * and a danger variant for destructive actions.
44
45
  */
45
- export function DropdownMenu({ trigger, items, align = 'end', className }: DropdownMenuProps) {
46
+ export function DropdownMenu({ trigger, items, align = 'end', className }: DropdownMenuProps): React.JSX.Element {
46
47
  const prefersReducedMotion = useReducedMotion()
47
48
 
48
49
  return (
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { cn } from '../utils'
4
5
  import type { LucideIcon } from 'lucide-react'
5
6
 
@@ -19,7 +20,7 @@ export interface EmptyStateProps {
19
20
  * @description A decorative empty state placeholder with icon, title, description, and optional actions.
20
21
  * Features a subtle gradient background and glass-morphism styling.
21
22
  */
22
- export function EmptyState({ icon: Icon, title, description, actions, className }: EmptyStateProps) {
23
+ export function EmptyState({ icon: Icon, title, description, actions, className }: EmptyStateProps): React.JSX.Element {
23
24
  return (
24
25
  <div
25
26
  className={cn(
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { cn } from '../utils'
4
5
 
5
6
  export interface FilterPillProps {
@@ -18,7 +19,7 @@ export interface FilterPillProps {
18
19
  * @description A rounded pill-style filter toggle with active state and optional count.
19
20
  * Uses CSS custom property tokens for dark/light mode compatibility.
20
21
  */
21
- export function FilterPill({ label, count, active, onClick, className }: FilterPillProps) {
22
+ export function FilterPill({ label, count, active, onClick, className }: FilterPillProps): React.JSX.Element {
22
23
  return (
23
24
  <button
24
25
  type="button"
@@ -1,11 +1,12 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { cn } from '../utils'
4
5
 
5
6
  // ── Shared class constants ──────────────────────────────────────────────────
6
7
  // Import these in any page that needs raw class strings (e.g. for <textarea>)
7
8
 
8
- export const INPUT_CLS = cn(
9
+ export const INPUT_CLS: string = cn(
9
10
  'w-full rounded-lg border border-[hsl(var(--border-default))]',
10
11
  'bg-[hsl(var(--bg-base))] px-3 py-2 text-sm',
11
12
  'text-[hsl(var(--text-primary))] placeholder:text-[hsl(var(--text-tertiary))]',
@@ -13,12 +14,12 @@ export const INPUT_CLS = cn(
13
14
  'disabled:opacity-50 disabled:cursor-not-allowed',
14
15
  )
15
16
 
16
- export const LABEL_CLS = cn(
17
+ export const LABEL_CLS: string = cn(
17
18
  'mb-1.5 block text-xs font-medium uppercase tracking-wider',
18
19
  'text-[hsl(var(--text-secondary))]',
19
20
  )
20
21
 
21
- export const TEXTAREA_CLS = cn(
22
+ export const TEXTAREA_CLS: string = cn(
22
23
  INPUT_CLS,
23
24
  'resize-none font-mono text-xs leading-relaxed',
24
25
  )
@@ -52,7 +53,7 @@ export interface FormInputProps {
52
53
  export function FormInput({
53
54
  label, value, onChange, type = 'text',
54
55
  placeholder, required, disabled, hint, className, autoComplete,
55
- }: FormInputProps) {
56
+ }: FormInputProps): React.JSX.Element {
56
57
  return (
57
58
  <div className={cn('space-y-1.5', className)}>
58
59
  <label className={LABEL_CLS}>