@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,224 @@
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
+ const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
52
+
53
+ useEffect(() => () => { if (copyTimerRef.current) clearTimeout(copyTimerRef.current) }, [])
54
+
55
+ // Check if content exceeds maxHeight
56
+ useEffect(() => {
57
+ if (!maxHeight || !contentRef.current) return
58
+ setNeedsCollapse(contentRef.current.scrollHeight > maxHeight)
59
+ }, [content, maxHeight])
60
+
61
+ const lines = useMemo(() => content.split('\n'), [content])
62
+
63
+ const handleCopy = useCallback(async () => {
64
+ try {
65
+ await navigator.clipboard.writeText(content)
66
+ setCopied(true)
67
+ if (copyTimerRef.current) clearTimeout(copyTimerRef.current)
68
+ copyTimerRef.current = setTimeout(() => setCopied(false), 2000)
69
+ } catch {
70
+ // Clipboard API not available
71
+ console.warn('Clipboard API not available')
72
+ }
73
+ }, [content])
74
+
75
+ const shouldCollapse = maxHeight && needsCollapse && isCollapsed
76
+
77
+ return (
78
+ <div
79
+ className={cn(
80
+ 'relative group rounded-xl overflow-hidden',
81
+ 'border border-[hsl(var(--border-subtle))]',
82
+ 'bg-[hsl(var(--bg-base))]',
83
+ className,
84
+ )}
85
+ >
86
+ {/* Header bar */}
87
+ {(label || language) && (
88
+ <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)]">
89
+ <div className="flex items-center gap-2">
90
+ {label && (
91
+ <span className="text-[11px] font-medium text-[hsl(var(--text-secondary))]">
92
+ {label}
93
+ </span>
94
+ )}
95
+ {language && (
96
+ <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))]">
97
+ {language}
98
+ </span>
99
+ )}
100
+ </div>
101
+
102
+ {/* Copy button (always visible in header) */}
103
+ <button
104
+ onClick={handleCopy}
105
+ className={cn(
106
+ 'inline-flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium transition-all',
107
+ copied
108
+ ? 'text-[hsl(var(--status-ok))] bg-[hsl(var(--status-ok)/0.1)]'
109
+ : 'text-[hsl(var(--text-tertiary))] hover:text-[hsl(var(--text-primary))] hover:bg-[hsl(var(--bg-elevated))]',
110
+ )}
111
+ >
112
+ <AnimatePresence mode="wait">
113
+ {copied ? (
114
+ <motion.span
115
+ key="check"
116
+ initial={prefersReducedMotion ? undefined : { scale: 0.5, opacity: 0 }}
117
+ animate={prefersReducedMotion ? undefined : { scale: 1, opacity: 1 }}
118
+ exit={prefersReducedMotion ? undefined : { scale: 0.5, opacity: 0 }}
119
+ transition={{ duration: 0.15 }}
120
+ className="inline-flex items-center gap-1"
121
+ >
122
+ <Check className="h-3.5 w-3.5" />
123
+ Copied!
124
+ </motion.span>
125
+ ) : (
126
+ <motion.span
127
+ key="copy"
128
+ initial={prefersReducedMotion ? undefined : { scale: 0.5, opacity: 0 }}
129
+ animate={prefersReducedMotion ? undefined : { scale: 1, opacity: 1 }}
130
+ exit={prefersReducedMotion ? undefined : { scale: 0.5, opacity: 0 }}
131
+ transition={{ duration: 0.15 }}
132
+ className="inline-flex items-center gap-1"
133
+ >
134
+ <Copy className="h-3.5 w-3.5" />
135
+ Copy
136
+ </motion.span>
137
+ )}
138
+ </AnimatePresence>
139
+ </button>
140
+ </div>
141
+ )}
142
+
143
+ {/* Copy button for headerless blocks */}
144
+ {!label && !language && (
145
+ <button
146
+ onClick={handleCopy}
147
+ className={cn(
148
+ 'absolute top-2 right-2 z-10 rounded-md p-1.5 transition-all',
149
+ 'opacity-0 group-hover:opacity-100',
150
+ copied
151
+ ? 'text-[hsl(var(--status-ok))] bg-[hsl(var(--status-ok)/0.1)]'
152
+ : 'text-[hsl(var(--text-tertiary))] hover:text-[hsl(var(--text-primary))] bg-[hsl(var(--bg-elevated)/0.8)] hover:bg-[hsl(var(--bg-elevated))]',
153
+ )}
154
+ title={copied ? 'Copied!' : 'Copy to clipboard'}
155
+ >
156
+ {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
157
+ </button>
158
+ )}
159
+
160
+ {/* Content area */}
161
+ <div
162
+ className="overflow-x-auto"
163
+ style={shouldCollapse ? { maxHeight, overflow: 'hidden' } : undefined}
164
+ >
165
+ <pre
166
+ ref={contentRef}
167
+ className={cn(
168
+ 'p-4 text-[13px] leading-relaxed font-mono',
169
+ 'text-[hsl(var(--text-primary))]',
170
+ 'whitespace-pre overflow-x-auto',
171
+ )}
172
+ >
173
+ {showLineNumbers ? (
174
+ <table className="border-collapse w-full">
175
+ <tbody>
176
+ {lines.map((line, i) => (
177
+ <tr key={i} className="hover:bg-[hsl(var(--bg-surface)/0.3)]">
178
+ <td className="select-none text-right pr-4 text-[hsl(var(--text-disabled))] text-[11px] tabular-nums w-8 align-top">
179
+ {i + 1}
180
+ </td>
181
+ <td className="whitespace-pre">{line}</td>
182
+ </tr>
183
+ ))}
184
+ </tbody>
185
+ </table>
186
+ ) : (
187
+ content
188
+ )}
189
+ </pre>
190
+ </div>
191
+
192
+ {/* Collapse gradient + toggle */}
193
+ {maxHeight && needsCollapse && (
194
+ <>
195
+ {isCollapsed && (
196
+ <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" />
197
+ )}
198
+ <div className="relative border-t border-[hsl(var(--border-subtle)/0.3)]">
199
+ <button
200
+ onClick={() => setIsCollapsed(c => !c)}
201
+ className={cn(
202
+ 'w-full flex items-center justify-center gap-1.5 py-2 text-[11px] font-medium',
203
+ 'text-[hsl(var(--text-secondary))] hover:text-[hsl(var(--text-primary))]',
204
+ 'hover:bg-[hsl(var(--bg-surface)/0.3)] transition-colors',
205
+ )}
206
+ >
207
+ {isCollapsed ? (
208
+ <>
209
+ Show more ({lines.length} lines)
210
+ <ChevronDown className="h-3.5 w-3.5" />
211
+ </>
212
+ ) : (
213
+ <>
214
+ Show less
215
+ <ChevronUp className="h-3.5 w-3.5" />
216
+ </>
217
+ )}
218
+ </button>
219
+ </div>
220
+ </>
221
+ )}
222
+ </div>
223
+ )
224
+ }
@@ -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,
@@ -18,6 +19,7 @@ import type { LucideIcon } from 'lucide-react'
18
19
  import { TruncatedText } from './truncated-text'
19
20
  import { EmptyState } from './empty-state'
20
21
  import { Skeleton } from './skeleton'
22
+ import { Select } from './select'
21
23
  import { cn } from '../utils'
22
24
 
23
25
  // ---------------------------------------------------------------------------
@@ -365,7 +367,7 @@ export function DataTable<T>({
365
367
  exportFilename,
366
368
  stickyFirstColumn = false,
367
369
  density: densityProp,
368
- }: DataTableProps<T>) {
370
+ }: DataTableProps<T>): React.JSX.Element {
369
371
  const prefersReducedMotion = useReducedMotion()
370
372
 
371
373
  const [density, setDensity] = useState<Density>(() => {
@@ -678,18 +680,12 @@ export function DataTable<T>({
678
680
  </span>
679
681
 
680
682
  <div className="flex items-center gap-2">
681
- <select
682
- value={pageSize}
683
- onChange={e => table.setPageSize(Number(e.target.value))}
684
- className="rounded-md border border-[hsl(var(--border-subtle))]
685
- bg-[hsl(var(--bg-surface))] px-2 py-1 text-[12px]
686
- text-[hsl(var(--text-secondary))] outline-none
687
- focus:border-[hsl(var(--brand-primary))] transition-colors"
688
- >
689
- {PAGE_SIZES.map(size => (
690
- <option key={size} value={size}>{size} / page</option>
691
- ))}
692
- </select>
683
+ <Select
684
+ value={String(pageSize)}
685
+ onValueChange={v => table.setPageSize(Number(v))}
686
+ options={PAGE_SIZES.map(size => ({ value: String(size), label: `${size} / page` }))}
687
+ className="w-[110px] text-[12px]"
688
+ />
693
689
 
694
690
  <div className="flex items-center gap-1">
695
691
  <PaginationButton
@@ -0,0 +1,340 @@
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
+ const MAX_LINES = 2000
31
+
32
+ /** Simple line-by-line diff using longest common subsequence. */
33
+ function computeDiff(oldLines: string[], newLines: string[]): DiffLine[] {
34
+ // Fall back to simple line-by-line comparison for large inputs
35
+ if (oldLines.length > MAX_LINES || newLines.length > MAX_LINES) {
36
+ const result: DiffLine[] = oldLines.map((l, i) => ({
37
+ type: (l === (newLines[i] ?? '') ? 'unchanged' : 'removed') as LineType,
38
+ content: l,
39
+ oldLineNo: i + 1,
40
+ newLineNo: i + 1,
41
+ }))
42
+ for (let i = oldLines.length; i < newLines.length; i++) {
43
+ result.push({
44
+ type: 'added' as LineType,
45
+ content: newLines[i],
46
+ oldLineNo: undefined,
47
+ newLineNo: i + 1,
48
+ })
49
+ }
50
+ return result
51
+ }
52
+
53
+ // Build LCS table
54
+ const m = oldLines.length
55
+ const n = newLines.length
56
+ const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))
57
+
58
+ for (let i = 1; i <= m; i++) {
59
+ for (let j = 1; j <= n; j++) {
60
+ if (oldLines[i - 1] === newLines[j - 1]) {
61
+ dp[i][j] = dp[i - 1][j - 1] + 1
62
+ } else {
63
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
64
+ }
65
+ }
66
+ }
67
+
68
+ // Backtrack to produce diff
69
+ const result: DiffLine[] = []
70
+ let i = m
71
+ let j = n
72
+
73
+ while (i > 0 || j > 0) {
74
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
75
+ result.push({ type: 'unchanged', content: oldLines[i - 1], oldLineNo: i, newLineNo: j })
76
+ i--
77
+ j--
78
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
79
+ result.push({ type: 'added', content: newLines[j - 1], newLineNo: j })
80
+ j--
81
+ } else {
82
+ result.push({ type: 'removed', content: oldLines[i - 1], oldLineNo: i })
83
+ i--
84
+ }
85
+ }
86
+
87
+ return result.reverse()
88
+ }
89
+
90
+ const TYPE_BG: Record<LineType, string> = {
91
+ added: 'bg-[hsl(var(--status-ok))]/10',
92
+ removed: 'bg-[hsl(var(--status-critical))]/10',
93
+ unchanged: '',
94
+ }
95
+
96
+ const TYPE_PREFIX: Record<LineType, string> = {
97
+ added: '+',
98
+ removed: '-',
99
+ unchanged: ' ',
100
+ }
101
+
102
+ const TYPE_PREFIX_COLOR: Record<LineType, string> = {
103
+ added: 'text-[hsl(var(--status-ok))]',
104
+ removed: 'text-[hsl(var(--status-critical))]',
105
+ unchanged: 'text-[hsl(var(--text-tertiary))]',
106
+ }
107
+
108
+ /**
109
+ * @description A diff viewer showing line-by-line differences between two text values.
110
+ * Supports inline and side-by-side modes, collapsible unchanged sections, and line numbers.
111
+ */
112
+ export function DiffViewer({
113
+ oldValue,
114
+ newValue,
115
+ mode = 'inline',
116
+ language,
117
+ showLineNumbers = true,
118
+ className,
119
+ }: DiffViewerProps): React.JSX.Element {
120
+ const [expandedSections, setExpandedSections] = useState<Set<number>>(new Set())
121
+
122
+ const diffLines = useMemo(() => {
123
+ const oldLines = oldValue.split('\n')
124
+ const newLines = newValue.split('\n')
125
+ return computeDiff(oldLines, newLines)
126
+ }, [oldValue, newValue])
127
+
128
+ // Group unchanged sections for collapsing
129
+ const sections = useMemo(() => {
130
+ const groups: { type: 'changes' | 'unchanged'; lines: DiffLine[]; startIdx: number }[] = []
131
+ let current: DiffLine[] = []
132
+ let currentType: 'changes' | 'unchanged' | null = null
133
+ let startIdx = 0
134
+
135
+ for (let i = 0; i < diffLines.length; i++) {
136
+ const lineType = diffLines[i].type === 'unchanged' ? 'unchanged' : 'changes'
137
+ if (lineType !== currentType) {
138
+ if (current.length > 0 && currentType !== null) {
139
+ groups.push({ type: currentType, lines: current, startIdx })
140
+ }
141
+ current = [diffLines[i]]
142
+ currentType = lineType
143
+ startIdx = i
144
+ } else {
145
+ current.push(diffLines[i])
146
+ }
147
+ }
148
+ if (current.length > 0 && currentType !== null) {
149
+ groups.push({ type: currentType, lines: current, startIdx })
150
+ }
151
+ return groups
152
+ }, [diffLines])
153
+
154
+ const toggleSection = (idx: number) => {
155
+ setExpandedSections((prev) => {
156
+ const next = new Set(prev)
157
+ if (next.has(idx)) {
158
+ next.delete(idx)
159
+ } else {
160
+ next.add(idx)
161
+ }
162
+ return next
163
+ })
164
+ }
165
+
166
+ if (mode === 'side-by-side') {
167
+ return <SideBySide diffLines={diffLines} showLineNumbers={showLineNumbers} language={language} className={className} />
168
+ }
169
+
170
+ return (
171
+ <div
172
+ className={cn(
173
+ 'w-full overflow-x-auto rounded-xl border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))]',
174
+ 'font-mono text-xs leading-5',
175
+ className,
176
+ )}
177
+ data-language={language}
178
+ >
179
+ {sections.map((section, sIdx) => {
180
+ // Collapse unchanged sections longer than 6 lines
181
+ if (section.type === 'unchanged' && section.lines.length > 6 && !expandedSections.has(sIdx)) {
182
+ const topContext = section.lines.slice(0, 3)
183
+ const bottomContext = section.lines.slice(-3)
184
+ const hiddenCount = section.lines.length - 6
185
+
186
+ return (
187
+ <div key={sIdx}>
188
+ {topContext.map((line, i) => (
189
+ <InlineLine key={`${sIdx}-t-${i}`} line={line} showLineNumbers={showLineNumbers} />
190
+ ))}
191
+ <button
192
+ type="button"
193
+ onClick={() => toggleSection(sIdx)}
194
+ className={cn(
195
+ 'w-full px-3 py-1 text-center text-[10px]',
196
+ 'text-[hsl(var(--text-tertiary))] bg-[hsl(var(--bg-overlay))]/50',
197
+ 'hover:bg-[hsl(var(--bg-overlay))] cursor-pointer transition-colors duration-100',
198
+ )}
199
+ >
200
+ ... {hiddenCount} unchanged {hiddenCount === 1 ? 'line' : 'lines'} ...
201
+ </button>
202
+ {bottomContext.map((line, i) => (
203
+ <InlineLine key={`${sIdx}-b-${i}`} line={line} showLineNumbers={showLineNumbers} />
204
+ ))}
205
+ </div>
206
+ )
207
+ }
208
+
209
+ return (
210
+ <div key={sIdx}>
211
+ {section.type === 'unchanged' && expandedSections.has(sIdx) && (
212
+ <button
213
+ type="button"
214
+ onClick={() => toggleSection(sIdx)}
215
+ className={cn(
216
+ 'w-full px-3 py-0.5 text-center text-[10px]',
217
+ 'text-[hsl(var(--text-tertiary))] bg-[hsl(var(--bg-overlay))]/30',
218
+ 'hover:bg-[hsl(var(--bg-overlay))] cursor-pointer transition-colors duration-100',
219
+ )}
220
+ >
221
+ collapse {section.lines.length} unchanged lines
222
+ </button>
223
+ )}
224
+ {section.lines.map((line, i) => (
225
+ <InlineLine key={`${sIdx}-${i}`} line={line} showLineNumbers={showLineNumbers} />
226
+ ))}
227
+ </div>
228
+ )
229
+ })}
230
+ </div>
231
+ )
232
+ }
233
+
234
+ function InlineLine({ line, showLineNumbers }: { line: DiffLine; showLineNumbers: boolean }): React.JSX.Element {
235
+ return (
236
+ <div className={cn('flex', TYPE_BG[line.type])}>
237
+ {showLineNumbers && (
238
+ <span className="shrink-0 w-9 px-1.5 text-right select-none text-[hsl(var(--text-tertiary))]/50 tabular-nums">
239
+ {line.oldLineNo ?? ''}
240
+ </span>
241
+ )}
242
+ {showLineNumbers && (
243
+ <span className="shrink-0 w-9 px-1.5 text-right select-none text-[hsl(var(--text-tertiary))]/50 tabular-nums">
244
+ {line.newLineNo ?? ''}
245
+ </span>
246
+ )}
247
+ <span className={cn('shrink-0 w-4 text-center select-none font-bold', TYPE_PREFIX_COLOR[line.type])}>
248
+ {TYPE_PREFIX[line.type]}
249
+ </span>
250
+ <span className="flex-1 px-2 whitespace-pre text-[hsl(var(--text-primary))]">
251
+ {line.content}
252
+ </span>
253
+ </div>
254
+ )
255
+ }
256
+
257
+ function SideBySide({
258
+ diffLines,
259
+ showLineNumbers,
260
+ language,
261
+ className,
262
+ }: {
263
+ diffLines: DiffLine[]
264
+ showLineNumbers: boolean
265
+ language?: string
266
+ className?: string
267
+ }): React.JSX.Element {
268
+ // Build paired rows for side-by-side
269
+ const pairs: { left?: DiffLine; right?: DiffLine }[] = []
270
+ let i = 0
271
+ while (i < diffLines.length) {
272
+ const line = diffLines[i]
273
+ if (line.type === 'unchanged') {
274
+ pairs.push({ left: line, right: line })
275
+ i++
276
+ } else if (line.type === 'removed') {
277
+ // Look for a matching added line
278
+ if (i + 1 < diffLines.length && diffLines[i + 1].type === 'added') {
279
+ pairs.push({ left: line, right: diffLines[i + 1] })
280
+ i += 2
281
+ } else {
282
+ pairs.push({ left: line })
283
+ i++
284
+ }
285
+ } else {
286
+ pairs.push({ right: line })
287
+ i++
288
+ }
289
+ }
290
+
291
+ return (
292
+ <div
293
+ className={cn(
294
+ 'w-full overflow-x-auto rounded-xl border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))]',
295
+ 'font-mono text-xs leading-5',
296
+ className,
297
+ )}
298
+ data-language={language}
299
+ >
300
+ <div className="grid grid-cols-2 divide-x divide-[hsl(var(--border-subtle))]">
301
+ {/* Left: old */}
302
+ <div>
303
+ {pairs.map((pair, idx) => (
304
+ <div
305
+ key={`l-${idx}`}
306
+ className={cn('flex', pair.left ? TYPE_BG[pair.left.type] : '')}
307
+ >
308
+ {showLineNumbers && (
309
+ <span className="shrink-0 w-9 px-1.5 text-right select-none text-[hsl(var(--text-tertiary))]/50 tabular-nums">
310
+ {pair.left?.oldLineNo ?? ''}
311
+ </span>
312
+ )}
313
+ <span className="flex-1 px-2 whitespace-pre text-[hsl(var(--text-primary))]">
314
+ {pair.left?.content ?? ''}
315
+ </span>
316
+ </div>
317
+ ))}
318
+ </div>
319
+ {/* Right: new */}
320
+ <div>
321
+ {pairs.map((pair, idx) => (
322
+ <div
323
+ key={`r-${idx}`}
324
+ className={cn('flex', pair.right ? TYPE_BG[pair.right.type] : '')}
325
+ >
326
+ {showLineNumbers && (
327
+ <span className="shrink-0 w-9 px-1.5 text-right select-none text-[hsl(var(--text-tertiary))]/50 tabular-nums">
328
+ {pair.right?.newLineNo ?? ''}
329
+ </span>
330
+ )}
331
+ <span className="flex-1 px-2 whitespace-pre text-[hsl(var(--text-primary))]">
332
+ {pair.right?.content ?? ''}
333
+ </span>
334
+ </div>
335
+ ))}
336
+ </div>
337
+ </div>
338
+ </div>
339
+ )
340
+ }
@@ -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"