@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.
- 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 +1 -1
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +508 -52
- package/dist/index.js +2927 -4
- package/dist/index.js.map +1 -1
- package/dist/{select-nnBJUO8U.d.ts → select-B2wXqqSM.d.ts} +2 -2
- package/package.json +1 -1
- package/src/components/animated-counter.tsx +2 -1
- 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 +414 -0
- package/src/components/command-bar.tsx +434 -0
- package/src/components/confidence-bar.tsx +115 -0
- package/src/components/confirm-dialog.tsx +2 -1
- package/src/components/copy-block.tsx +229 -0
- package/src/components/data-table.tsx +2 -1
- package/src/components/diff-viewer.tsx +319 -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 +213 -0
- package/src/components/infinite-scroll.tsx +243 -0
- package/src/components/kanban-column.tsx +198 -0
- package/src/components/live-feed.tsx +220 -0
- package/src/components/log-viewer.tsx +2 -1
- package/src/components/metric-card.tsx +2 -1
- package/src/components/notification-stack.tsx +226 -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 +268 -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 +372 -0
- package/src/components/streaming-text.tsx +163 -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/utils.ts +1 -1
- 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}>
|