@toolr/ui-design 0.1.2 → 0.1.4

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 (55) hide show
  1. package/README.md +0 -7
  2. package/components/content/info-panel-primitives.tsx +3 -3
  3. package/components/lib/ai-tools.tsx +1 -1
  4. package/components/lib/form-colors.ts +3 -0
  5. package/components/lib/theme-engine.ts +10 -0
  6. package/components/sections/captured-issues/captured-issues-panel.tsx +1 -1
  7. package/components/sections/golden-snapshots/file-diff-viewer.tsx +2 -2
  8. package/components/sections/golden-snapshots/snapshot-manager.tsx +3 -3
  9. package/components/sections/golden-snapshots/status-overview.tsx +4 -4
  10. package/components/sections/golden-snapshots/version-manager.tsx +3 -3
  11. package/components/sections/report-bug/screenshot-uploader.tsx +2 -2
  12. package/components/sections/snapshot-browser/snapshot-tree.tsx +1 -1
  13. package/components/sections/snippets-editor/snippets-editor.tsx +5 -5
  14. package/components/ui/action-dialog.tsx +1 -1
  15. package/components/ui/badge.tsx +8 -6
  16. package/components/ui/breadcrumb.tsx +1 -1
  17. package/components/ui/confirm-badge.tsx +4 -2
  18. package/components/ui/file-structure-section.tsx +139 -74
  19. package/components/ui/file-tree.tsx +2 -2
  20. package/components/ui/files-panel.tsx +5 -5
  21. package/components/ui/filter-dropdown.tsx +3 -3
  22. package/components/ui/frontmatter-form-header.tsx +2 -2
  23. package/components/ui/icon-button.tsx +6 -2
  24. package/components/ui/input.tsx +1 -1
  25. package/components/ui/label.tsx +8 -5
  26. package/components/ui/modal.tsx +1 -1
  27. package/components/ui/nav-card.tsx +1 -1
  28. package/components/ui/navigation-bar.tsx +1 -1
  29. package/components/ui/number-input.tsx +1 -1
  30. package/components/ui/registry-card.tsx +7 -7
  31. package/components/ui/registry-detail.tsx +5 -2
  32. package/components/ui/resizable-textarea.tsx +2 -2
  33. package/components/ui/segmented-toggle.tsx +36 -18
  34. package/components/ui/select.tsx +3 -3
  35. package/components/ui/selection-grid.tsx +7 -19
  36. package/components/ui/settings-card.tsx +27 -0
  37. package/components/ui/settings-info-box.tsx +80 -0
  38. package/components/ui/settings-section-title.tsx +24 -0
  39. package/components/ui/snapshot-card.tsx +2 -2
  40. package/components/ui/snippets-panel.tsx +9 -9
  41. package/components/ui/sort-dropdown.tsx +1 -1
  42. package/components/ui/tab-bar.tsx +1 -1
  43. package/components/ui/tooltip.tsx +1 -1
  44. package/dist/content.js +3 -3
  45. package/dist/index.d.ts +93 -46
  46. package/dist/index.js +1452 -1236
  47. package/dist/tokens/primitives.css +10 -0
  48. package/dist/tokens/semantic.css +3 -0
  49. package/index.ts +4 -1
  50. package/package.json +1 -7
  51. package/tokens/primitives.css +10 -0
  52. package/tokens/semantic.css +3 -0
  53. package/dist/preset.d.ts +0 -24
  54. package/dist/preset.js +0 -17
  55. package/tailwind-preset.ts +0 -22
@@ -1,21 +1,48 @@
1
1
  import { useState, useEffect, useCallback, useRef, useMemo, type ReactNode } from 'react'
2
2
  import { FileCode, FolderTree, Loader2, AlertCircle, AlignLeft, Code2, Type } from 'lucide-react'
3
- import { CollapseButton } from './icon-button.tsx'
3
+ import { CollapseButton, type IconButtonColor } from './icon-button.tsx'
4
4
  import { SegmentedToggle } from './segmented-toggle.tsx'
5
5
  import { FileTree, collectDirPaths, type FileTreeNode } from './file-tree.tsx'
6
6
 
7
7
  export type PreviewMode = 'format' | 'language' | 'plain'
8
- export type AccentColor = 'blue' | 'purple' | 'orange' | 'green' | 'pink' | 'amber' | 'emerald' | 'teal' | 'sky'
8
+ export type AccentColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
9
9
 
10
10
  const ACCENT_ICON: Record<AccentColor, string> = {
11
- blue: 'text-blue-400', purple: 'text-purple-400', orange: 'text-orange-400',
12
- green: 'text-green-400', pink: 'text-pink-400', amber: 'text-amber-400',
13
- emerald: 'text-emerald-400', teal: 'text-teal-400', sky: 'text-sky-400',
11
+ blue: 'text-blue-400', green: 'text-green-400', red: 'text-red-400',
12
+ orange: 'text-orange-400', cyan: 'text-cyan-400', yellow: 'text-yellow-400',
13
+ purple: 'text-purple-400', indigo: 'text-indigo-400', emerald: 'text-emerald-400',
14
+ amber: 'text-amber-400', violet: 'text-violet-400', neutral: 'text-neutral-400',
15
+ sky: 'text-sky-400', pink: 'text-pink-400', teal: 'text-teal-400',
16
+ }
17
+
18
+ const ACCENT_BORDER: Record<AccentColor, string> = {
19
+ blue: 'border-blue-500/25', green: 'border-green-500/25', red: 'border-red-500/25',
20
+ orange: 'border-orange-500/25', cyan: 'border-cyan-500/25', yellow: 'border-yellow-500/25',
21
+ purple: 'border-purple-500/25', indigo: 'border-indigo-500/25', emerald: 'border-emerald-500/25',
22
+ amber: 'border-amber-500/25', violet: 'border-violet-500/25', neutral: 'border-neutral-500/25',
23
+ sky: 'border-sky-500/25', pink: 'border-pink-500/25', teal: 'border-teal-500/25',
24
+ }
25
+
26
+ const ACCENT_BUTTON: Record<AccentColor, IconButtonColor> = {
27
+ blue: 'blue', green: 'green', red: 'red',
28
+ orange: 'orange', cyan: 'cyan', yellow: 'yellow',
29
+ purple: 'purple', indigo: 'indigo', emerald: 'emerald',
30
+ amber: 'amber', violet: 'violet', neutral: 'neutral',
31
+ sky: 'sky', pink: 'pink', teal: 'teal',
32
+ }
33
+
34
+ const ACCENT_HANDLE: Record<AccentColor, string> = {
35
+ blue: 'bg-blue-500/30 group-hover:bg-blue-400/50', green: 'bg-green-500/30 group-hover:bg-green-400/50', red: 'bg-red-500/30 group-hover:bg-red-400/50',
36
+ orange: 'bg-orange-500/30 group-hover:bg-orange-400/50', cyan: 'bg-cyan-500/30 group-hover:bg-cyan-400/50', yellow: 'bg-yellow-500/30 group-hover:bg-yellow-400/50',
37
+ purple: 'bg-purple-500/30 group-hover:bg-purple-400/50', indigo: 'bg-indigo-500/30 group-hover:bg-indigo-400/50', emerald: 'bg-emerald-500/30 group-hover:bg-emerald-400/50',
38
+ amber: 'bg-amber-500/30 group-hover:bg-amber-400/50', violet: 'bg-violet-500/30 group-hover:bg-violet-400/50', neutral: 'bg-neutral-500/30 group-hover:bg-neutral-400/50',
39
+ sky: 'bg-sky-500/30 group-hover:bg-sky-400/50', pink: 'bg-pink-500/30 group-hover:bg-pink-400/50', teal: 'bg-teal-500/30 group-hover:bg-teal-400/50',
14
40
  }
15
41
 
16
42
  export interface FileStructureSectionProps {
17
43
  files: FileTreeNode[] | null
18
44
  rootName: string
45
+ variant?: 'split' | 'list'
19
46
  isLoading?: boolean
20
47
  error?: string | null
21
48
  onFetchContent: (relativePath: string) => Promise<string>
@@ -33,6 +60,8 @@ export interface FileStructureSectionProps {
33
60
  accentColor?: AccentColor
34
61
  /** Custom renderer called when mode is 'language'. Receives the resolved language as third arg. */
35
62
  renderPreview?: (content: string, filePath: string, language: string) => ReactNode
63
+ /** Initial height in pixels. Defaults to 400 for 'list', 250 for 'split'. */
64
+ initialHeight?: number
36
65
  }
37
66
 
38
67
  export function getLanguageFromPath(filePath: string): string {
@@ -60,7 +89,7 @@ function renderMarkdownContent(content: string) {
60
89
  while (i < lines.length && lines[i] !== '---') { fmLines.push(lines[i]); i++ }
61
90
  i++ // skip closing ---
62
91
  nodes.push(
63
- <div key="fm" className="mb-3 font-mono text-[11px] text-neutral-500 border-l-2 border-neutral-700 pl-2 py-0.5">
92
+ <div key="fm" className="mb-3 font-mono text-xss text-neutral-500 border-l-2 border-neutral-700 pl-2 py-0.5">
64
93
  <div className="text-neutral-600">---</div>
65
94
  {fmLines.map((l, j) => <div key={j}>{l}</div>)}
66
95
  <div className="text-neutral-600">---</div>
@@ -75,12 +104,12 @@ function renderMarkdownContent(content: string) {
75
104
  i++
76
105
  while (i < lines.length && !lines[i].startsWith('```')) { codeLines.push(lines[i]); i++ }
77
106
  nodes.push(
78
- <pre key={i} className="mb-2 p-2 bg-black/30 rounded text-[11px] font-mono text-neutral-300 overflow-x-auto">
107
+ <pre key={i} className="mb-2 p-2 bg-[var(--background)]/30 rounded text-xss font-mono text-neutral-300 overflow-x-auto">
79
108
  {codeLines.join('\n')}
80
109
  </pre>
81
110
  )
82
111
  } else if (line.startsWith('### ')) {
83
- nodes.push(<h3 key={i} className="text-[11px] font-semibold text-neutral-300 mt-2 mb-0.5">{line.slice(4)}</h3>)
112
+ nodes.push(<h3 key={i} className="text-xss font-semibold text-neutral-300 mt-2 mb-0.5">{line.slice(4)}</h3>)
84
113
  } else if (line.startsWith('## ')) {
85
114
  nodes.push(<h2 key={i} className="text-xs font-semibold text-neutral-200 mt-2.5 mb-1">{line.slice(3)}</h2>)
86
115
  } else if (line.startsWith('# ')) {
@@ -88,7 +117,7 @@ function renderMarkdownContent(content: string) {
88
117
  } else if (line === '' || line === '\r') {
89
118
  nodes.push(<div key={i} className="h-1.5" />)
90
119
  } else {
91
- nodes.push(<p key={i} className="text-[11px] text-neutral-400 leading-relaxed">{line}</p>)
120
+ nodes.push(<p key={i} className="text-xss text-neutral-400 leading-relaxed">{line}</p>)
92
121
  }
93
122
  i++
94
123
  }
@@ -103,6 +132,7 @@ const PLAIN_OPTION = { value: 'plain' as const, icon: <Type className="w-3 h-3"
103
132
  export function FileStructureSection({
104
133
  files,
105
134
  rootName,
135
+ variant = 'split',
106
136
  isLoading,
107
137
  error,
108
138
  onFetchContent,
@@ -111,6 +141,7 @@ export function FileStructureSection({
111
141
  default: defaultMode,
112
142
  accentColor = 'blue',
113
143
  renderPreview,
144
+ initialHeight,
114
145
  }: FileStructureSectionProps) {
115
146
  const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null)
116
147
  const [fileContent, setFileContent] = useState<string | null>(null)
@@ -168,7 +199,7 @@ export function FileStructureSection({
168
199
  const startHeight = useRef(0)
169
200
 
170
201
  useEffect(() => {
171
- if (treeHeight !== null || !sectionRef.current) return
202
+ if (variant === 'list' || treeHeight !== null || !sectionRef.current) return
172
203
  const el = sectionRef.current
173
204
  const scrollParent = el.closest('.overflow-y-auto') as HTMLElement | null
174
205
  if (!scrollParent) return
@@ -185,9 +216,9 @@ export function FileStructureSection({
185
216
  })
186
217
  observer.observe(scrollParent)
187
218
  return () => observer.disconnect()
188
- }, [treeHeight, files])
219
+ }, [variant, treeHeight, files])
189
220
 
190
- const effectiveHeight = treeHeight ?? 250
221
+ const effectiveHeight = treeHeight ?? initialHeight ?? (variant === 'list' ? 400 : 250)
191
222
 
192
223
  const handleResizeStart = useCallback((e: React.MouseEvent) => {
193
224
  e.preventDefault()
@@ -224,7 +255,9 @@ export function FileStructureSection({
224
255
  return findFirst(files, rootName)
225
256
  }, [files, rootName])
226
257
 
227
- const effectiveFilePath = selectedFilePath ?? firstFilePath
258
+ const effectiveFilePath = variant === 'list'
259
+ ? selectedFilePath
260
+ : (selectedFilePath ?? firstFilePath)
228
261
  const fileIsLoading = effectiveFilePath != null && effectiveFilePath !== fetchedFilePath
229
262
 
230
263
  useEffect(() => {
@@ -255,10 +288,16 @@ export function FileStructureSection({
255
288
  }, [effectiveFilePath, rootName, onFetchContent])
256
289
 
257
290
  const handleSelectFile = useCallback((filePath: string) => {
291
+ if (variant === 'list' && selectedFilePath === filePath) {
292
+ setSelectedFilePath(null)
293
+ setFileContent(null)
294
+ setFileError(null)
295
+ return
296
+ }
258
297
  setSelectedFilePath(filePath)
259
298
  setFileContent(null)
260
299
  setFileError(null)
261
- }, [])
300
+ }, [variant, selectedFilePath])
262
301
 
263
302
  if (isLoading) {
264
303
  return (
@@ -303,71 +342,97 @@ export function FileStructureSection({
303
342
  )
304
343
  }
305
344
 
306
- return (
307
- <div ref={sectionRef}>
308
- <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
309
- <div className="flex gap-3" style={{ height: `${effectiveHeight}px` }}>
310
- {/* Tree panel */}
311
- <div className={`flex flex-col bg-neutral-900 border border-neutral-700 rounded-lg overflow-hidden ${effectiveFilePath ? 'w-1/3 shrink-0' : 'flex-1'}`}>
312
- <div className="flex items-center px-3 py-2 border-b border-neutral-700 shrink-0 gap-2 min-w-0">
313
- <FolderTree className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
314
- <span className="text-xs text-neutral-200 truncate flex-1">Files</span>
315
- <CollapseButton
316
- collapsed={allCollapsed}
317
- onToggle={() => setExpandedPaths(allCollapsed ? new Set(allDirPaths) : new Set())}
318
- />
319
- </div>
320
- <div className="flex-1 overflow-y-auto p-3">
321
- <FileTree
322
- nodes={files}
323
- rootName={rootName}
324
- selectedPath={effectiveFilePath}
325
- onSelectFile={handleSelectFile}
326
- expandedPaths={expandedPaths}
327
- onTogglePath={togglePath}
328
- accentColor={accentColor}
329
- />
345
+ const treePanel = (
346
+ <div className={`flex flex-col bg-neutral-900 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden ${variant === 'split' && effectiveFilePath ? 'w-1/3 shrink-0' : 'flex-1'}`}>
347
+ <div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
348
+ <FolderTree className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
349
+ <span className="text-xs text-neutral-200 truncate flex-1">Files</span>
350
+ <CollapseButton
351
+ collapsed={allCollapsed}
352
+ onToggle={() => setExpandedPaths(allCollapsed ? new Set(allDirPaths) : new Set())}
353
+ color={ACCENT_BUTTON[accentColor]}
354
+ />
355
+ </div>
356
+ <div className={`${variant === 'split' ? 'flex-1 overflow-y-auto' : ''} p-3`}>
357
+ <FileTree
358
+ nodes={files}
359
+ rootName={rootName}
360
+ selectedPath={effectiveFilePath}
361
+ onSelectFile={handleSelectFile}
362
+ expandedPaths={expandedPaths}
363
+ onTogglePath={togglePath}
364
+ accentColor={accentColor}
365
+ />
366
+ </div>
367
+ </div>
368
+ )
369
+
370
+ const previewPanel = effectiveFilePath ? (
371
+ <div className={`flex-1 flex flex-col bg-neutral-900 border ${ACCENT_BORDER[accentColor]} rounded-lg overflow-hidden`}>
372
+ <div className={`flex items-center px-3 py-2 border-b ${ACCENT_BORDER[accentColor]} shrink-0 gap-2 min-w-0`}>
373
+ <FileCode className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
374
+ <span className="text-xs text-neutral-200 truncate flex-1">{selectedFileName}</span>
375
+ {showToggle && (
376
+ <SegmentedToggle
377
+ options={toggleOptions}
378
+ value={mode}
379
+ onChange={setMode}
380
+ accentColor={accentColor}
381
+ size="xss"
382
+ />
383
+ )}
384
+ </div>
385
+ <div className="flex-1 overflow-auto">
386
+ {fileIsLoading ? (
387
+ <div className="flex items-center gap-2 text-xs text-neutral-500 p-3">
388
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
389
+ Loading...
330
390
  </div>
331
- </div>
391
+ ) : fileError ? (
392
+ <p className="text-xs text-red-400 p-3">{fileError}</p>
393
+ ) : fileContent !== null ? (
394
+ renderContent(fileContent, effectiveFilePath)
395
+ ) : null}
396
+ </div>
397
+ </div>
398
+ ) : null
399
+
400
+ const resizeHandle = (
401
+ <div
402
+ onMouseDown={handleResizeStart}
403
+ className="h-4 -mt-1.5 cursor-grab active:cursor-grabbing flex items-center justify-center group"
404
+ >
405
+ <div className={`w-10 h-1 rounded-full ${ACCENT_HANDLE[accentColor]} group-hover:w-14 group-hover:h-1.5 transition-all`} />
406
+ </div>
407
+ )
332
408
 
333
- {/* Preview panel */}
334
- {effectiveFilePath && (
335
- <div className="flex-1 flex flex-col bg-neutral-900 border border-neutral-700 rounded-lg overflow-hidden">
336
- <div className="flex items-center px-3 py-2 border-b border-neutral-700 shrink-0 gap-2 min-w-0">
337
- <FileCode className={`w-3.5 h-3.5 shrink-0 ${ACCENT_ICON[accentColor]}`} />
338
- <span className="text-xs text-neutral-200 truncate flex-1">{selectedFileName}</span>
339
- {showToggle && (
340
- <SegmentedToggle
341
- options={toggleOptions}
342
- value={mode}
343
- onChange={setMode}
344
- accentColor={accentColor}
345
- size="xss"
346
- />
347
- )}
348
- </div>
349
- <div className="flex-1 overflow-auto">
350
- {fileIsLoading ? (
351
- <div className="flex items-center gap-2 text-xs text-neutral-500 p-3">
352
- <Loader2 className="w-3.5 h-3.5 animate-spin" />
353
- Loading...
354
- </div>
355
- ) : fileError ? (
356
- <p className="text-xs text-red-400 p-3">{fileError}</p>
357
- ) : fileContent !== null ? (
358
- renderContent(fileContent, effectiveFilePath)
359
- ) : null}
409
+ if (variant === 'list') {
410
+ return (
411
+ <div ref={sectionRef}>
412
+ <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
413
+ <div className="space-y-3">
414
+ {treePanel}
415
+ {previewPanel && (
416
+ <div>
417
+ <div className="flex flex-col" style={{ height: `${effectiveHeight}px` }}>
418
+ {previewPanel}
419
+ </div>
420
+ {resizeHandle}
360
421
  </div>
361
- </div>
362
- )}
422
+ )}
423
+ </div>
363
424
  </div>
364
- {/* Resize handle */}
365
- <div
366
- onMouseDown={handleResizeStart}
367
- className="h-4 -mt-1.5 cursor-grab active:cursor-grabbing flex items-center justify-center group"
368
- >
369
- <div className="w-10 h-1 rounded-full bg-neutral-700 group-hover:bg-neutral-500 group-hover:w-14 group-hover:h-1.5 transition-all" />
425
+ )
426
+ }
427
+
428
+ return (
429
+ <div ref={sectionRef}>
430
+ <h3 className="text-xs font-medium text-neutral-500 uppercase tracking-wider mb-2">File Structure</h3>
431
+ <div className="flex gap-3" style={{ height: `${effectiveHeight}px` }}>
432
+ {treePanel}
433
+ {previewPanel}
370
434
  </div>
435
+ {resizeHandle}
371
436
  </div>
372
437
  )
373
438
  }
@@ -126,8 +126,8 @@ function FileTreeNodeItem({ node, path, selectedPath, onSelectFile, expandedPath
126
126
  const rowClass = isSelected
127
127
  ? `${base} ${selectedClass}`
128
128
  : isDir
129
- ? `${base} cursor-pointer hover:text-neutral-200 ${iconColorClass}`
130
- : `${base} cursor-pointer hover:bg-neutral-700/50 hover:text-neutral-200 ${iconColorClass}`
129
+ ? `${base} cursor-pointer text-white hover:text-neutral-200`
130
+ : `${base} cursor-pointer text-white hover:bg-neutral-700/50 hover:text-neutral-200`
131
131
 
132
132
  return (
133
133
  <li>
@@ -144,7 +144,7 @@ function FileNode({ entry, depth, selectedPath, expandedPaths, onToggleExpand, o
144
144
  />
145
145
  <span className="truncate">{entry.name}</span>
146
146
  {entry.badge && (
147
- <span className="ml-auto shrink-0 px-1.5 py-0.5 text-[9px] rounded bg-neutral-700 text-neutral-500">
147
+ <span className="ml-auto shrink-0 px-1.5 py-0.5 text-xss rounded bg-neutral-700 text-neutral-500">
148
148
  {entry.badge}
149
149
  </span>
150
150
  )}
@@ -210,12 +210,12 @@ export function FilesPanel({
210
210
  return (
211
211
  <div className={cn('flex flex-col bg-neutral-800 rounded-lg overflow-hidden', className)}>
212
212
  <div className="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
213
- <span className="text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Files</span>
214
- <span className="text-[10px] text-neutral-500">{fileCount} files</span>
213
+ <span className="text-xss font-semibold uppercase tracking-wider text-neutral-500">Files</span>
214
+ <span className="text-xss text-neutral-500">{fileCount} files</span>
215
215
  </div>
216
216
  {showSearch && (
217
217
  <div className="px-2 py-2 border-b border-neutral-700">
218
- <div className="flex items-center gap-1.5 px-2 py-1 bg-black border border-neutral-700 rounded text-xs">
218
+ <div className="flex items-center gap-1.5 px-2 py-1 bg-[var(--background)] border border-neutral-700 rounded text-xs">
219
219
  <Search className="w-3 h-3 text-neutral-500 shrink-0" />
220
220
  <input
221
221
  type="text"
@@ -243,7 +243,7 @@ export function FilesPanel({
243
243
  ))}
244
244
  </ul>
245
245
  {displayedFiles.length === 0 && (
246
- <p className="text-[11px] text-neutral-500 text-center py-4">No files found</p>
246
+ <p className="text-xss text-neutral-500 text-center py-4">No files found</p>
247
247
  )}
248
248
  </div>
249
249
  </div>
@@ -112,7 +112,7 @@ export function FilterDropdown({
112
112
  )}
113
113
 
114
114
  {isOpen && (
115
- <div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] whitespace-nowrap bg-[var(--popover)] backdrop-blur border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
115
+ <div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] whitespace-nowrap bg-[var(--popover)] border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
116
116
  {showSearch && (
117
117
  <div className={`sticky top-0 p-1.5 bg-[var(--popover)] border-b ${FORM_COLORS[color].border} z-10`}>
118
118
  <div className="relative">
@@ -135,7 +135,7 @@ export function FilterDropdown({
135
135
  onClick={() => handleSelect('all')}
136
136
  className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer ${
137
137
  highlightIdx === 0
138
- ? 'bg-neutral-600 text-neutral-200'
138
+ ? `${FORM_COLORS[color].selectedBg} text-neutral-200`
139
139
  : !isActive ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
140
140
  }`}
141
141
  >
@@ -154,7 +154,7 @@ export function FilterDropdown({
154
154
  onClick={() => handleSelect(opt.value)}
155
155
  className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer ${
156
156
  isHighlighted
157
- ? 'bg-neutral-600 text-neutral-200'
157
+ ? `${FORM_COLORS[color].selectedBg} text-neutral-200`
158
158
  : isSelected ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
159
159
  }`}
160
160
  >
@@ -43,12 +43,12 @@ export function FrontmatterFormHeader({
43
43
  Configuration
44
44
  </span>
45
45
  {collapsed && hasFm && (
46
- <span className="text-[11px] text-neutral-500 font-mono ml-2 truncate">
46
+ <span className="text-xss text-neutral-500 font-mono ml-2 truncate">
47
47
  {renderSummary()}
48
48
  </span>
49
49
  )}
50
50
  {collapsed && !hasFm && (
51
- <span className="text-[11px] text-neutral-600 ml-2">No frontmatter</span>
51
+ <span className="text-xss text-neutral-600 ml-2">No frontmatter</span>
52
52
  )}
53
53
  </button>
54
54
 
@@ -159,7 +159,7 @@ export interface ActionItem {
159
159
 
160
160
  export type IconButtonStatus = 'loading' | 'success' | 'warning' | 'error'
161
161
 
162
- export type IconButtonColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky'
162
+ export type IconButtonColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky' | 'pink' | 'teal'
163
163
  export type IconButtonVariant = 'filled' | 'outline'
164
164
 
165
165
  export interface IconButtonProps {
@@ -216,6 +216,8 @@ const colorClasses = {
216
216
  violet: { text: 'text-violet-400', border: 'border-violet-500/30', hover: 'hover:bg-violet-500/20 hover:border-violet-500/40 hover:text-violet-300', active: 'bg-violet-500/20 text-violet-300 border-violet-500/40' },
217
217
  neutral: { text: 'text-neutral-400', border: 'border-neutral-500/30', hover: 'hover:bg-neutral-500/20 hover:border-neutral-500/40 hover:text-neutral-300', active: 'bg-neutral-500/20 text-neutral-300 border-neutral-500/40' },
218
218
  sky: { text: 'text-sky-400', border: 'border-sky-500/30', hover: 'hover:bg-sky-500/20 hover:border-sky-500/40 hover:text-sky-300', active: 'bg-sky-500/20 text-sky-300 border-sky-500/40' },
219
+ pink: { text: 'text-pink-400', border: 'border-pink-500/30', hover: 'hover:bg-pink-500/20 hover:border-pink-500/40 hover:text-pink-300', active: 'bg-pink-500/20 text-pink-300 border-pink-500/40' },
220
+ teal: { text: 'text-teal-400', border: 'border-teal-500/30', hover: 'hover:bg-teal-500/20 hover:border-teal-500/40 hover:text-teal-300', active: 'bg-teal-500/20 text-teal-300 border-teal-500/40' },
219
221
  }
220
222
 
221
223
  const badgeColorClasses = {
@@ -369,14 +371,16 @@ export interface CollapseButtonProps {
369
371
  collapsed: boolean
370
372
  onToggle: () => void
371
373
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
374
+ color?: IconButtonColor
372
375
  tooltipPosition?: 'bottom' | 'bottom-left' | 'left' | 'right' | 'top' | 'top-left' | 'top-right'
373
376
  }
374
377
 
375
- export function CollapseButton({ collapsed, onToggle, size = 'xss', tooltipPosition = 'bottom-left' }: CollapseButtonProps) {
378
+ export function CollapseButton({ collapsed, onToggle, size = 'xss', color, tooltipPosition = 'bottom-left' }: CollapseButtonProps) {
376
379
  return (
377
380
  <IconButton
378
381
  icon={collapsed ? 'chevrons-up-down' : 'chevrons-down-up'}
379
382
  onClick={onToggle}
383
+ color={color}
380
384
  tooltip={{
381
385
  title: collapsed ? 'Expand all' : 'Collapse all',
382
386
  description: collapsed ? 'Expand all folders' : 'Collapse all folders',
@@ -38,7 +38,7 @@ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
38
38
  }
39
39
 
40
40
  const sizeClasses = {
41
- xss: 'px-1 py-0.5 text-[10px]',
41
+ xss: 'px-1 py-0.5 text-xss',
42
42
  xs: 'px-1.5 py-0.5 text-xs',
43
43
  sm: 'px-2 py-1 text-xs',
44
44
  md: 'px-3 py-1.5 text-sm',
@@ -5,7 +5,7 @@
5
5
  * Matches the SeedrBadges border-focused style: colored border + text, no background.
6
6
  *
7
7
  * Features:
8
- * - 14 color variants
8
+ * - 15 color variants
9
9
  * - Required leading icon and tooltip (always cursor-help)
10
10
  * - Optional trailing icons
11
11
  * - Text transform with smart capitalize (handles after-dash)
@@ -30,6 +30,7 @@ export type LabelColor =
30
30
  | 'teal'
31
31
  | 'violet'
32
32
  | 'pink'
33
+ | 'sky'
33
34
 
34
35
  export interface LabelProps {
35
36
  text: string
@@ -63,6 +64,7 @@ const colorClasses: Record<LabelColor, string> = {
63
64
  teal: 'border-teal-500/50 text-teal-400',
64
65
  violet: 'border-violet-500/50 text-violet-400',
65
66
  pink: 'border-pink-500/50 text-pink-400',
67
+ sky: 'border-sky-500/50 text-sky-400',
66
68
  }
67
69
 
68
70
  const progressFillColors: Record<LabelColor, string> = {
@@ -80,13 +82,14 @@ const progressFillColors: Record<LabelColor, string> = {
80
82
  teal: 'bg-teal-500/20',
81
83
  violet: 'bg-violet-500/20',
82
84
  pink: 'bg-pink-500/20',
85
+ sky: 'bg-sky-500/20',
83
86
  }
84
87
 
85
88
  const sizeConfig = {
86
- xss: { height: 14, padding: 'px-1', text: 'text-[9px]', iconSize: 'w-2 h-2', gap: 'gap-0.5' },
87
- xs: { height: 16, padding: 'px-1.5', text: 'text-[10px]', iconSize: 'w-2.5 h-2.5', gap: 'gap-1' },
88
- sm: { height: 18, padding: 'px-1.5', text: 'text-[10px]', iconSize: 'w-2.5 h-2.5', gap: 'gap-1.5' },
89
- md: { height: 20, padding: 'px-1.5', text: 'text-[11px]', iconSize: 'w-3 h-3', gap: 'gap-1' },
89
+ xss: { height: 14, padding: 'px-1', text: 'text-xss', iconSize: 'w-2 h-2', gap: 'gap-0.5' },
90
+ xs: { height: 16, padding: 'px-1.5', text: 'text-xss', iconSize: 'w-2.5 h-2.5', gap: 'gap-1' },
91
+ sm: { height: 18, padding: 'px-1.5', text: 'text-xss', iconSize: 'w-2.5 h-2.5', gap: 'gap-1.5' },
92
+ md: { height: 20, padding: 'px-1.5', text: 'text-xss', iconSize: 'w-3 h-3', gap: 'gap-1' },
90
93
  lg: { height: 22, padding: 'px-2', text: 'text-xs', iconSize: 'w-3 h-3', gap: 'gap-1' },
91
94
  }
92
95
 
@@ -61,7 +61,7 @@ function Modal({ isOpen, onClose, title, children, kind = 'info', size = 'md', h
61
61
 
62
62
  return createPortal(
63
63
  <div className="fixed inset-0 z-50 flex items-center justify-center">
64
- <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
64
+ <div className="absolute inset-0 bg-[var(--dialog-backdrop)] backdrop-blur-sm" onClick={onClose} />
65
65
  <div
66
66
  ref={modalRef}
67
67
  data-testid={testId}
@@ -51,7 +51,7 @@ export function NavCard({
51
51
  disabled={disabled}
52
52
  className={cn(
53
53
  'relative w-full text-left rounded-lg border border-neutral-700 bg-neutral-800 p-4 transition-all duration-200 cursor-pointer',
54
- !disabled && 'hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/20 hover:border-neutral-600 hover:bg-neutral-700',
54
+ !disabled && 'hover:-translate-y-0.5 hover:border-neutral-600 hover:bg-neutral-700',
55
55
  disabled && 'opacity-50 cursor-not-allowed',
56
56
  className,
57
57
  )}
@@ -69,7 +69,7 @@ export interface NavigationBarProps {
69
69
  }
70
70
 
71
71
  const sizeConfig = {
72
- xss: { text: 'text-[10px]', segIcon: 'w-2.5 h-2.5', navIcon: 'w-2.5 h-2.5', navBtn: 'w-[18px] h-[18px] rounded-[3px]', px: 'px-1', py: 'py-0.5', sep: 'w-2 h-2', divH: 'h-3' },
72
+ xss: { text: 'text-xss', segIcon: 'w-2.5 h-2.5', navIcon: 'w-2.5 h-2.5', navBtn: 'w-[18px] h-[18px] rounded-[3px]', px: 'px-1', py: 'py-0.5', sep: 'w-2 h-2', divH: 'h-3' },
73
73
  xs: { text: 'text-xs', segIcon: 'w-3 h-3', navIcon: 'w-3 h-3', navBtn: 'w-6 h-6 rounded-[5px]', px: 'px-1.5', py: 'py-0.5', sep: 'w-2.5 h-2.5', divH: 'h-3.5' },
74
74
  sm: { text: 'text-sm', segIcon: 'w-3.5 h-3.5', navIcon: 'w-3.5 h-3.5', navBtn: 'w-7 h-7 rounded-md', px: 'px-2', py: 'py-1', sep: 'w-3 h-3', divH: 'h-4' },
75
75
  md: { text: 'text-base', segIcon: 'w-4 h-4', navIcon: 'w-4 h-4', navBtn: 'w-8 h-8 rounded-md', px: 'px-2.5', py: 'py-1', sep: 'w-3.5 h-3.5', divH: 'h-5' },
@@ -16,7 +16,7 @@ export interface NumberInputProps {
16
16
  }
17
17
 
18
18
  const SIZE_CONFIG = {
19
- xss: { wrapper: 'h-[18px]', input: 'px-1 text-[10px]', chevron: 'w-2.5 h-2.5', stepperW: 'w-4' },
19
+ xss: { wrapper: 'h-[18px]', input: 'px-1 text-xss', chevron: 'w-2.5 h-2.5', stepperW: 'w-4' },
20
20
  xs: { wrapper: 'h-6', input: 'px-1.5 text-xs', chevron: 'w-2.5 h-2.5', stepperW: 'w-5' },
21
21
  sm: { wrapper: 'h-7', input: 'px-2 text-xs', chevron: 'w-3 h-3', stepperW: 'w-5' },
22
22
  md: { wrapper: 'h-8', input: 'px-3 text-sm', chevron: 'w-3 h-3', stepperW: 'w-6' },
@@ -412,7 +412,7 @@ export function RegistryCard(props: RegistryCardProps) {
412
412
  <Tooltip key={`pkg-${key}`} content={{ description: `${count} ${count === 1 ? label : labelPlural}` }} position="top">
413
413
  <span className="flex items-center gap-0.5">
414
414
  <Icon className={`w-3 h-3 ${color}`} />
415
- <span className="text-[10px] text-neutral-500">{count}</span>
415
+ <span className="text-xss text-neutral-500">{count}</span>
416
416
  </span>
417
417
  </Tooltip>
418
418
  )]
@@ -469,7 +469,7 @@ export function RegistryCard(props: RegistryCardProps) {
469
469
  ...(props.stars != null && props.stars > 0 ? [(
470
470
  <CardClickable key="stars" onClick={() => props.onSortBy?.('stars')}>
471
471
  <Tooltip content={{ description: `${props.stars.toLocaleString()} stars \u00b7 Click to sort` }} position="top">
472
- <span className="flex items-center gap-1 text-[10px] text-amber-400/80">
472
+ <span className="flex items-center gap-1 text-xss text-amber-400/80">
473
473
  <Star className="w-3 h-3" />
474
474
  {formatCount(props.stars)}
475
475
  </span>
@@ -479,7 +479,7 @@ export function RegistryCard(props: RegistryCardProps) {
479
479
  ...(props.downloads != null && props.downloads > 0 ? [(
480
480
  <CardClickable key="downloads" onClick={() => props.onSortBy?.('downloads')}>
481
481
  <Tooltip content={{ description: `${props.downloads.toLocaleString()} downloads \u00b7 Click to sort` }} position="top">
482
- <span className="flex items-center gap-1 text-[10px] text-emerald-400/80">
482
+ <span className="flex items-center gap-1 text-xss text-emerald-400/80">
483
483
  <Download className="w-3 h-3" />
484
484
  {formatCount(props.downloads)}
485
485
  </span>
@@ -581,7 +581,7 @@ export function RegistryCard(props: RegistryCardProps) {
581
581
  if (props.installs != null && props.installs > 0) {
582
582
  bottomStats = [(
583
583
  <Tooltip key="installs" content={{ description: `${props.installs.toLocaleString()} installs` }} position="top">
584
- <span className="flex items-center gap-1 text-[10px] text-neutral-500">
584
+ <span className="flex items-center gap-1 text-xss text-neutral-500">
585
585
  <Download className="w-3 h-3" />
586
586
  {props.installs.toLocaleString()}
587
587
  </span>
@@ -642,7 +642,7 @@ export function RegistryCard(props: RegistryCardProps) {
642
642
 
643
643
  {/* Error/warning message */}
644
644
  {errorMessage && (
645
- <p className={`text-[11px] mb-2 break-all ${flash === 'warning' ? 'text-amber-400' : 'text-red-400'}`}>{errorMessage}</p>
645
+ <p className={`text-xss mb-2 break-all ${flash === 'warning' ? 'text-amber-400' : 'text-red-400'}`}>{errorMessage}</p>
646
646
  )}
647
647
 
648
648
  {/* Bottom row */}
@@ -650,7 +650,7 @@ export function RegistryCard(props: RegistryCardProps) {
650
650
  const dateNode = updatedAt ? (
651
651
  <Tooltip content={{ description: onDateClick ? `Last updated ${formatFullDate(updatedAt)} \u00b7 Click to sort by date` : `Last updated ${formatFullDate(updatedAt)}` }} position="top">
652
652
  <span
653
- className={`flex items-center gap-1 text-[10px] text-neutral-500 whitespace-nowrap${onDateClick ? ' cursor-pointer hover:brightness-125 transition-all' : ''}`}
653
+ className={`flex items-center gap-1 text-xss text-neutral-500 whitespace-nowrap${onDateClick ? ' cursor-pointer hover:brightness-125 transition-all' : ''}`}
654
654
  onClick={onDateClick ? (e: MouseEvent) => { e.stopPropagation(); onDateClick() } : undefined}
655
655
  >
656
656
  <Clock className="w-3 h-3" />
@@ -690,7 +690,7 @@ export function RegistryCard(props: RegistryCardProps) {
690
690
  // Basic fallback: just install on confirm
691
691
  return (
692
692
  <div onClick={(e) => e.stopPropagation()}>
693
- <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onClick={() => setShowScopeConfirm(false)}>
693
+ <div className="fixed inset-0 bg-[var(--background)]/50 z-50 flex items-center justify-center" onClick={() => setShowScopeConfirm(false)}>
694
694
  <div className="bg-neutral-800 border border-neutral-700 rounded-lg p-4 max-w-sm" onClick={(e) => e.stopPropagation()}>
695
695
  <h3 className="text-sm font-medium text-neutral-200 mb-2">{ALREADY_AT_USER}</h3>
696
696
  <p className="text-xs text-neutral-400 mb-4">