@toolr/ui-design 0.1.3 → 0.1.5

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 (46) 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/theme-engine.ts +10 -0
  5. package/components/sections/captured-issues/captured-issues-panel.tsx +1 -1
  6. package/components/sections/golden-snapshots/file-diff-viewer.tsx +2 -2
  7. package/components/sections/golden-snapshots/snapshot-manager.tsx +3 -3
  8. package/components/sections/golden-snapshots/status-overview.tsx +4 -4
  9. package/components/sections/golden-snapshots/version-manager.tsx +3 -3
  10. package/components/sections/prompt-editor/tabbed-prompt-editor.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 +4 -4
  16. package/components/ui/breadcrumb.tsx +1 -1
  17. package/components/ui/file-structure-section.tsx +4 -4
  18. package/components/ui/files-panel.tsx +5 -5
  19. package/components/ui/filter-dropdown.tsx +2 -2
  20. package/components/ui/frontmatter-form-header.tsx +2 -2
  21. package/components/ui/input.tsx +3 -3
  22. package/components/ui/label.tsx +4 -4
  23. package/components/ui/modal.tsx +1 -1
  24. package/components/ui/nav-card.tsx +17 -10
  25. package/components/ui/navigation-bar.tsx +1 -1
  26. package/components/ui/number-input.tsx +1 -1
  27. package/components/ui/registry-browser.tsx +2 -2
  28. package/components/ui/registry-card.tsx +7 -7
  29. package/components/ui/registry-detail.tsx +2 -2
  30. package/components/ui/segmented-toggle.tsx +2 -2
  31. package/components/ui/select.tsx +2 -2
  32. package/components/ui/selection-grid.tsx +7 -19
  33. package/components/ui/snapshot-card.tsx +2 -2
  34. package/components/ui/snippets-panel.tsx +9 -9
  35. package/components/ui/tab-bar.tsx +1 -1
  36. package/dist/content.js +3 -3
  37. package/dist/index.d.ts +23 -3
  38. package/dist/index.js +92 -92
  39. package/dist/tokens/primitives.css +10 -0
  40. package/dist/tokens/semantic.css +3 -0
  41. package/package.json +1 -7
  42. package/tokens/primitives.css +10 -0
  43. package/tokens/semantic.css +3 -0
  44. package/dist/preset.d.ts +0 -24
  45. package/dist/preset.js +0 -17
  46. package/tailwind-preset.ts +0 -22
package/README.md CHANGED
@@ -14,19 +14,12 @@ Shared UI design system for toolr applications. Provides components, design toke
14
14
  @import "@toolr/ui-design/tokens";
15
15
  ```
16
16
 
17
- ```ts
18
- // tailwind.config.ts
19
- import { toolrPreset } from '@toolr/ui-design/preset'
20
- export default { presets: [toolrPreset] }
21
- ```
22
-
23
17
  ## Exports
24
18
 
25
19
  | Path | Content |
26
20
  |------|---------|
27
21
  | `@toolr/ui-design` | All components, hooks, types, utilities |
28
22
  | `@toolr/ui-design/tokens` | CSS design tokens (semantic + primitives + keyframes) |
29
- | `@toolr/ui-design/preset` | Tailwind preset (Inter font, extended breakpoints) |
30
23
  | `@toolr/ui-design/content` | Info panel primitives |
31
24
  | `@toolr/ui-design/diagrams` | Diagram utilities |
32
25
 
@@ -114,7 +114,7 @@ export function Callout({ color, children }: { color: string; children: ReactNod
114
114
  export function CalloutCode({ color, children }: { color: string; children: ReactNode }) {
115
115
  const c = CALLOUT_COLORS[color] ?? CALLOUT_COLORS.blue
116
116
  return (
117
- <code className={`block bg-neutral-800/80 px-2 py-1 rounded mt-1.5 text-[13px] ${c.codeText}`}>
117
+ <code className={`block bg-neutral-800/80 px-2 py-1 rounded mt-1.5 text-sm ${c.codeText}`}>
118
118
  {children}
119
119
  </code>
120
120
  )
@@ -204,7 +204,7 @@ export function TitledLI({ color, title, children }: { color: string; title: str
204
204
 
205
205
  export function CalloutDialog({ color, lines }: { color: string; lines: { speaker: string; text: string }[] }) {
206
206
  return (
207
- <div className="bg-neutral-800/80 rounded px-2 py-1 mt-1.5 flex flex-col gap-0.5 text-[13px]">
207
+ <div className="bg-neutral-800/80 rounded px-2 py-1 mt-1.5 flex flex-col gap-0.5 text-sm">
208
208
  {lines.map((line, idx) => (
209
209
  <div key={idx}>
210
210
  <span className={`text-${color}-300 font-semibold mr-1`}>{line.speaker}:</span>
@@ -238,7 +238,7 @@ export function StatusBadge({ value, badgeColor, label, children, even }: {
238
238
  <DLRow
239
239
  term={
240
240
  <span className="flex items-center gap-1.5">
241
- <span className={`inline-flex items-center justify-center w-5 h-5 rounded-full bg-${badgeColor}-500/20 text-${badgeColor}-400 text-[11px] font-bold shrink-0`}>{value}</span>
241
+ <span className={`inline-flex items-center justify-center w-5 h-5 rounded-full bg-${badgeColor}-500/20 text-${badgeColor}-400 text-xss font-bold shrink-0`}>{value}</span>
242
242
  <span className={`text-${badgeColor}-400 font-semibold`}>{label}</span>
243
243
  </span>
244
244
  }
@@ -38,7 +38,7 @@ export function AiToolIcon({ tool, size, showName, className, style }: {
38
38
  return (
39
39
  <span style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
40
40
  {img}
41
- <span className="text-[11px] text-neutral-400">{AI_TOOL_NAMES[tool as AiToolKey] ?? tool}</span>
41
+ <span className="text-xss text-neutral-400">{AI_TOOL_NAMES[tool as AiToolKey] ?? tool}</span>
42
42
  </span>
43
43
  )
44
44
  }
@@ -109,6 +109,16 @@ export function isLightTheme(themeId: ThemeId): boolean {
109
109
  return LIGHT_THEMES.includes(themeId)
110
110
  }
111
111
 
112
+ /**
113
+ * Apply a theme to the document by setting CSS custom properties on the root element.
114
+ *
115
+ * IMPORTANT — body background: Portal-based components (Select, Tooltip) render at
116
+ * document.body via createPortal. Their backgrounds use --popover which is semi-transparent
117
+ * (rgba(0,0,0,0.8)). If <body> has no background-color, the browser default (white) bleeds
118
+ * through, making dropdowns/tooltips appear gray instead of dark.
119
+ *
120
+ * Consuming apps MUST set: body { background-color: var(--background); }
121
+ */
112
122
  export function applyTheme(themeId: ThemeId, accentHue: number | null, dims: Record<SurfaceKey, number> = DEFAULT_DIMS, outline: number = DEFAULT_OUTLINE, root: HTMLElement = document.documentElement): void {
113
123
  const scale = generateScale(themeId, accentHue, BASE_THEMES[themeId].maxSat, dims, outline)
114
124
  const light = isLightTheme(themeId)
@@ -163,7 +163,7 @@ export function CapturedIssuesPanel({
163
163
  <Check className="w-5 h-5 text-green-400" />
164
164
  </div>
165
165
  <div>
166
- <h3 className="text-neutral-300 font-medium">No Issues Captured</h3>
166
+ <h3 className="text-sm text-neutral-300 font-medium">No Issues Captured</h3>
167
167
  <p className="text-sm text-neutral-500">Everything is running smoothly.</p>
168
168
  </div>
169
169
  </div>
@@ -93,7 +93,7 @@ function DiffFileItem({ file, isSelected, onSelect, onReset, resettingFile, anyR
93
93
  {icon}
94
94
  <span className="truncate">{filename}</span>
95
95
  {statusLabel && (
96
- <span className={`text-[10px] font-medium ml-auto flex-shrink-0 ${statusColor}`}>
96
+ <span className={`text-xss font-medium ml-auto flex-shrink-0 ${statusColor}`}>
97
97
  {statusLabel}
98
98
  </span>
99
99
  )}
@@ -171,7 +171,7 @@ function DiffFileTreePanel({ sync, componentLabels, renderFileIcon }: DiffFileTr
171
171
  const chevronSize = isRoot ? 'w-4 h-4' : 'w-3 h-3'
172
172
 
173
173
  const countElement = isRoot ? (
174
- <span className={`ml-auto px-1.5 py-0.5 rounded-full text-[10px] font-medium ${color.pillBg} ${color.text}`}>
174
+ <span className={`ml-auto px-1.5 py-0.5 rounded-full text-xss font-medium ${color.pillBg} ${color.text}`}>
175
175
  {fileCount}
176
176
  </span>
177
177
  ) : (
@@ -54,7 +54,7 @@ export function SnapshotManager({ sync }: SnapshotManagerProps) {
54
54
  <div className="bg-neutral-900 rounded-lg p-4 border border-neutral-700">
55
55
  <div className="flex items-center gap-3 mb-3">
56
56
  <Plus className="w-5 h-5 text-green-400" />
57
- <h4 className="text-neutral-300 font-medium">Create Snapshot</h4>
57
+ <h4 className="text-sm text-neutral-300 font-medium">Create Snapshot</h4>
58
58
  </div>
59
59
  <p className="text-xs text-neutral-600 mb-3">
60
60
  Archives the current live state. If components differ from golden, their patch version is auto-bumped.
@@ -85,7 +85,7 @@ export function SnapshotManager({ sync }: SnapshotManagerProps) {
85
85
  <div className="flex items-center justify-between px-4 py-3 border-b border-neutral-700">
86
86
  <div className="flex items-center gap-2">
87
87
  <Archive className="w-4 h-4 text-neutral-500" />
88
- <h4 className="text-neutral-300 font-medium">Snapshots</h4>
88
+ <h4 className="text-sm text-neutral-300 font-medium">Snapshots</h4>
89
89
  <span className="text-xs text-neutral-500">({manifest?.snapshots.length ?? 0})</span>
90
90
  </div>
91
91
  <IconButton
@@ -114,7 +114,7 @@ export function SnapshotManager({ sync }: SnapshotManagerProps) {
114
114
  <div className="flex items-center gap-2">
115
115
  <span className="text-sm font-mono text-neutral-300">v{snap.version}</span>
116
116
  {snap.version === manifest.activeVersion && (
117
- <span className="px-1.5 py-0.5 bg-green-500/20 text-green-400 text-[10px] rounded font-medium">
117
+ <span className="px-1.5 py-0.5 bg-green-500/20 text-green-400 text-xss rounded font-medium">
118
118
  active
119
119
  </span>
120
120
  )}
@@ -84,7 +84,7 @@ export function StatusOverview({
84
84
  <div className="bg-neutral-900 rounded-lg p-4 border border-amber-500/30">
85
85
  <div className="flex items-center gap-3 mb-3">
86
86
  <Archive className="w-5 h-5 text-amber-400" />
87
- <h3 className="text-neutral-300 font-medium">Bundled Seed (App Distribution)</h3>
87
+ <h3 className="text-sm text-neutral-300 font-medium">Bundled Seed (App Distribution)</h3>
88
88
  {status?.seed.meta && renderVersionBadge(status.seed.meta, 'bg-amber-500/20 text-amber-400')}
89
89
  </div>
90
90
  <div className="grid grid-cols-2 gap-4 text-sm">
@@ -121,7 +121,7 @@ export function StatusOverview({
121
121
  <div className="bg-neutral-900 rounded-lg p-4 border border-blue-500/30">
122
122
  <div className="flex items-center gap-2 mb-3">
123
123
  <div className="w-3 h-3 rounded-full bg-blue-400" />
124
- <h4 className="text-neutral-300 font-medium">Golden (Reference)</h4>
124
+ <h4 className="text-sm text-neutral-300 font-medium">Golden (Reference)</h4>
125
125
  {renderVersionBadge(status?.goldenMeta, 'bg-blue-500/20 text-blue-400')}
126
126
  </div>
127
127
  <div className="space-y-2 text-sm">
@@ -153,7 +153,7 @@ export function StatusOverview({
153
153
  <div className="bg-neutral-900 rounded-lg p-4 border border-green-500/30">
154
154
  <div className="flex items-center gap-2 mb-3">
155
155
  <div className="w-3 h-3 rounded-full bg-green-400" />
156
- <h4 className="text-neutral-300 font-medium">Live (Working Copy)</h4>
156
+ <h4 className="text-sm text-neutral-300 font-medium">Live (Working Copy)</h4>
157
157
  {renderVersionBadge(status?.liveMeta, 'bg-green-500/20 text-green-400')}
158
158
  <div className="ml-auto relative" ref={resetMenuRef}>
159
159
  <IconButton
@@ -290,7 +290,7 @@ export function StatusOverview({
290
290
  <div className="bg-neutral-900 rounded-lg p-4 border border-neutral-700">
291
291
  <div className="flex items-center gap-2 mb-3">
292
292
  <Archive className="w-4 h-4 text-neutral-500" />
293
- <h4 className="text-neutral-300 font-medium">Local Snapshots</h4>
293
+ <h4 className="text-sm text-neutral-300 font-medium">Local Snapshots</h4>
294
294
  </div>
295
295
  <div className="flex gap-6 text-sm">
296
296
  <div>
@@ -56,7 +56,7 @@ export function VersionManager({ sync, components, componentLabels }: VersionMan
56
56
  <div className="bg-neutral-900 rounded-lg p-4 border border-teal-500/30">
57
57
  <div className="flex items-center gap-3 mb-3">
58
58
  <Tag className="w-5 h-5 text-teal-400" />
59
- <h4 className="text-neutral-300 font-medium">Golden Version</h4>
59
+ <h4 className="text-sm text-neutral-300 font-medium">Golden Version</h4>
60
60
  {status?.goldenMeta && (
61
61
  <span className="px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded font-mono">
62
62
  {status.goldenMeta.version}
@@ -100,7 +100,7 @@ export function VersionManager({ sync, components, componentLabels }: VersionMan
100
100
  {/* Component Versions */}
101
101
  <div className="bg-neutral-900 rounded-lg border border-neutral-700 overflow-hidden">
102
102
  <div className="px-4 py-3 border-b border-neutral-700">
103
- <h4 className="text-neutral-300 font-medium">Component Versions</h4>
103
+ <h4 className="text-sm text-neutral-300 font-medium">Component Versions</h4>
104
104
  <p className="text-xs text-neutral-600 mt-1">
105
105
  Update individual component versions. Click a component to edit.
106
106
  </p>
@@ -122,7 +122,7 @@ export function VersionManager({ sync, components, componentLabels }: VersionMan
122
122
  <span className="text-xs font-mono text-neutral-500">v{currentVersion}</span>
123
123
  )}
124
124
  {mismatch && (
125
- <span className="text-[10px] text-yellow-400">
125
+ <span className="text-xss text-yellow-400">
126
126
  (live: v{liveVersion})
127
127
  </span>
128
128
  )}
@@ -204,10 +204,10 @@ export function TabbedPromptEditor({
204
204
  style.id = styleId
205
205
  style.textContent = `
206
206
  .template-variable-highlight {
207
- background-color: rgba(180, 190, 254, 0.2);
208
- border: 1px solid rgba(180, 190, 254, 0.4);
207
+ background-color: rgba(129, 140, 248, 0.2);
208
+ border: 1px solid rgba(129, 140, 248, 0.4);
209
209
  border-radius: 3px;
210
- color: #b4befe !important;
210
+ color: rgb(129, 140, 248) !important;
211
211
  font-weight: 500;
212
212
  }
213
213
  `
@@ -195,7 +195,7 @@ export function ScreenshotUploader({
195
195
  alt={s.filename}
196
196
  className="w-full h-full object-cover"
197
197
  />
198
- <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-between p-2">
198
+ <div className="absolute inset-0 bg-[var(--background)]/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-between p-2">
199
199
  <button
200
200
  type="button"
201
201
  onClick={(e) => {
@@ -209,7 +209,7 @@ export function ScreenshotUploader({
209
209
  </button>
210
210
  <span className="text-xs text-white truncate">{s.filename}</span>
211
211
  </div>
212
- <div className="absolute bottom-1 right-1 px-1.5 py-0.5 bg-black/70 rounded text-xs text-neutral-400">
212
+ <div className="absolute bottom-1 right-1 px-1.5 py-0.5 bg-[var(--background)]/70 rounded text-xs text-neutral-400">
213
213
  {formatFileSize(s.size)}
214
214
  </div>
215
215
  </div>
@@ -201,7 +201,7 @@ function SnapshotEntryRow({
201
201
  <span className="text-xs flex-1 truncate">
202
202
  {searchQuery ? highlightMatch(displayName, searchQuery) : displayName}
203
203
  </span>
204
- <span className="text-[10px] text-neutral-500 shrink-0" title={formatFullDate(entry.savedAt)}>
204
+ <span className="text-xss text-neutral-500 shrink-0" title={formatFullDate(entry.savedAt)}>
205
205
  {formatRelativeTime(entry.savedAt)}
206
206
  </span>
207
207
  <IconButton
@@ -144,7 +144,7 @@ export function SnippetsEditor({
144
144
  <p className="text-xs text-neutral-500 mb-1">
145
145
  {searchQuery ? 'No matching snippets' : 'No snippets defined'}
146
146
  </p>
147
- <p className="text-[10px] text-neutral-600">
147
+ <p className="text-xss text-neutral-600">
148
148
  {searchQuery ? 'Try a different search term' : 'Click + to add your first snippet'}
149
149
  </p>
150
150
  </div>
@@ -228,11 +228,11 @@ function SnippetListItem({ snippet, selected, onSelect, onDelete }: SnippetListI
228
228
  <p className="text-xs font-mono font-medium text-neutral-300 truncate">
229
229
  {snippet.name}
230
230
  </p>
231
- <p className="text-[10px] text-neutral-500 truncate mt-0.5">
231
+ <p className="text-xss text-neutral-500 truncate mt-0.5">
232
232
  {snippet.description}
233
233
  </p>
234
234
  {snippet.value && (
235
- <p className="text-[10px] text-neutral-600 truncate mt-0.5 font-mono">
235
+ <p className="text-xss text-neutral-600 truncate mt-0.5 font-mono">
236
236
  {snippet.value.slice(0, 80)}{snippet.value.length > 80 ? '...' : ''}
237
237
  </p>
238
238
  )}
@@ -292,7 +292,7 @@ function SnippetForm({
292
292
  error={nameHasError}
293
293
  autoFocus={!isEditing}
294
294
  />
295
- <p className="mt-1 text-[10px] text-neutral-600">
295
+ <p className="mt-1 text-xss text-neutral-600">
296
296
  Use in prompts as <span className="font-mono text-purple-400">{'{{' + (formData.name || 'NAME') + '}}'}</span>
297
297
  </p>
298
298
  </div>
@@ -319,7 +319,7 @@ function SnippetForm({
319
319
  onChange={(val) => setFormField('value', val)}
320
320
  minHeight={160}
321
321
  />
322
- <p className="mt-1 text-[10px] text-neutral-600">
322
+ <p className="mt-1 text-xss text-neutral-600">
323
323
  Can be a single value, multi-line text, or an entire document
324
324
  </p>
325
325
  </div>
@@ -194,7 +194,7 @@ export function ActionDialog({
194
194
 
195
195
  return createPortal(
196
196
  <div className="fixed inset-0 z-50 flex items-center justify-center">
197
- <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onCancel} />
197
+ <div className="absolute inset-0 bg-[var(--dialog-backdrop)] backdrop-blur-sm" onClick={onCancel} />
198
198
  <div
199
199
  className={cn(
200
200
  'relative bg-neutral-950 border border-neutral-700 rounded-xl shadow-2xl w-full max-w-[800px] mx-4 flex flex-col',
@@ -42,10 +42,10 @@ const colorClasses: Record<BadgeColor, string> = {
42
42
  }
43
43
 
44
44
  const sizeClasses = {
45
- xss: 'min-w-[14px] h-[14px] px-0.5 text-[9px]',
46
- xs: 'min-w-[16px] h-[16px] px-1 text-[10px]',
47
- sm: 'min-w-[18px] h-[18px] px-1 text-[10px]',
48
- md: 'min-w-[20px] h-[20px] px-1.5 text-[11px]',
45
+ xss: 'min-w-[14px] h-[14px] px-0.5 text-xss',
46
+ xs: 'min-w-[16px] h-[16px] px-1 text-xss',
47
+ sm: 'min-w-[18px] h-[18px] px-1 text-xss',
48
+ md: 'min-w-[20px] h-[20px] px-1.5 text-xss',
49
49
  lg: 'min-w-[22px] h-[22px] px-1.5 text-xs',
50
50
  }
51
51
 
@@ -66,7 +66,7 @@ export interface BreadcrumbProps {
66
66
  }
67
67
 
68
68
  const sizeConfig = {
69
- xss: { text: 'text-[10px]', icon: 'w-2.5 h-2.5', px: 'px-1', py: 'py-0.5', gap: 'gap-0.5', sep: 'w-2 h-2' },
69
+ xss: { text: 'text-xss', icon: 'w-2.5 h-2.5', px: 'px-1', py: 'py-0.5', gap: 'gap-0.5', sep: 'w-2 h-2' },
70
70
  xs: { text: 'text-xs', icon: 'w-3 h-3', px: 'px-1.5', py: 'py-0.5', gap: 'gap-1', sep: 'w-2.5 h-2.5' },
71
71
  sm: { text: 'text-sm', icon: 'w-3.5 h-3.5', px: 'px-2', py: 'py-1', gap: 'gap-1.5', sep: 'w-3 h-3' },
72
72
  md: { text: 'text-base', icon: 'w-4 h-4', px: 'px-2.5', py: 'py-1', gap: 'gap-1.5', sep: 'w-3.5 h-3.5' },
@@ -89,7 +89,7 @@ function renderMarkdownContent(content: string) {
89
89
  while (i < lines.length && lines[i] !== '---') { fmLines.push(lines[i]); i++ }
90
90
  i++ // skip closing ---
91
91
  nodes.push(
92
- <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">
93
93
  <div className="text-neutral-600">---</div>
94
94
  {fmLines.map((l, j) => <div key={j}>{l}</div>)}
95
95
  <div className="text-neutral-600">---</div>
@@ -104,12 +104,12 @@ function renderMarkdownContent(content: string) {
104
104
  i++
105
105
  while (i < lines.length && !lines[i].startsWith('```')) { codeLines.push(lines[i]); i++ }
106
106
  nodes.push(
107
- <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">
108
108
  {codeLines.join('\n')}
109
109
  </pre>
110
110
  )
111
111
  } else if (line.startsWith('### ')) {
112
- 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>)
113
113
  } else if (line.startsWith('## ')) {
114
114
  nodes.push(<h2 key={i} className="text-xs font-semibold text-neutral-200 mt-2.5 mb-1">{line.slice(3)}</h2>)
115
115
  } else if (line.startsWith('# ')) {
@@ -117,7 +117,7 @@ function renderMarkdownContent(content: string) {
117
117
  } else if (line === '' || line === '\r') {
118
118
  nodes.push(<div key={i} className="h-1.5" />)
119
119
  } else {
120
- 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>)
121
121
  }
122
122
  i++
123
123
  }
@@ -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>
@@ -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
 
@@ -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',
@@ -183,13 +183,13 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
183
183
  {debounceMs > 0 && debounceKey > 0 && (
184
184
  <svg
185
185
  key={debounceKey}
186
- className="absolute inset-0 pointer-events-none"
186
+ className="absolute inset-0 pointer-events-none text-emerald-400/70"
187
187
  style={{ width: '100%', height: '100%' }}
188
188
  >
189
189
  <rect
190
190
  x="1" y="1" rx="5" ry="5"
191
191
  fill="none"
192
- stroke="rgb(52 211 153 / 0.7)"
192
+ stroke="currentColor"
193
193
  strokeWidth="1.5"
194
194
  pathLength="100"
195
195
  strokeDasharray="100"
@@ -86,10 +86,10 @@ const progressFillColors: Record<LabelColor, string> = {
86
86
  }
87
87
 
88
88
  const sizeConfig = {
89
- xss: { height: 14, padding: 'px-1', text: 'text-[9px]', iconSize: 'w-2 h-2', gap: 'gap-0.5' },
90
- xs: { height: 16, padding: 'px-1.5', text: 'text-[10px]', iconSize: 'w-2.5 h-2.5', gap: 'gap-1' },
91
- sm: { height: 18, padding: 'px-1.5', text: 'text-[10px]', iconSize: 'w-2.5 h-2.5', gap: 'gap-1.5' },
92
- 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' },
93
93
  lg: { height: 22, padding: 'px-2', text: 'text-xs', iconSize: 'w-3 h-3', gap: 'gap-1' },
94
94
  }
95
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}
@@ -1,4 +1,4 @@
1
- /** Navigation card with icon, description, badge, and hover lift effect. */
1
+ /** Navigation card with icon, description, label, and hover lift effect. */
2
2
 
3
3
  import {
4
4
  Settings, Puzzle, Zap, Shield, Folder, Code, Database, Globe,
@@ -7,7 +7,7 @@ import {
7
7
  } from 'lucide-react'
8
8
  import type { LucideIcon } from 'lucide-react'
9
9
  import type { IconName } from './icon-button.tsx'
10
- import { Badge, type BadgeColor } from './badge.tsx'
10
+ import { Label, type LabelColor } from './label.tsx'
11
11
  import { cn } from '../lib/cn.ts'
12
12
 
13
13
  const iconSubset: Partial<Record<IconName, LucideIcon>> = {
@@ -23,9 +23,11 @@ export interface NavCardProps {
23
23
  title: string
24
24
  description?: string
25
25
  icon?: IconName
26
+ /** Custom icon component. Takes precedence over icon name. */
27
+ IconComponent?: React.ComponentType<{ className?: string }>
26
28
  iconColor?: string
27
- badge?: number | string
28
- badgeColor?: BadgeColor
29
+ label?: { text: string; color: LabelColor; tooltip: { description: string } }
30
+ stats?: string
29
31
  onClick?: () => void
30
32
  disabled?: boolean
31
33
  className?: string
@@ -35,14 +37,15 @@ export function NavCard({
35
37
  title,
36
38
  description,
37
39
  icon,
40
+ IconComponent,
38
41
  iconColor = '#60a5fa',
39
- badge,
40
- badgeColor = 'blue',
42
+ label,
43
+ stats,
41
44
  onClick,
42
45
  disabled = false,
43
46
  className,
44
47
  }: NavCardProps) {
45
- const Icon = icon ? iconSubset[icon] : undefined
48
+ const Icon = IconComponent ?? (icon ? iconSubset[icon] : undefined)
46
49
 
47
50
  return (
48
51
  <button
@@ -51,14 +54,14 @@ export function NavCard({
51
54
  disabled={disabled}
52
55
  className={cn(
53
56
  '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',
57
+ !disabled && 'hover:-translate-y-0.5 hover:border-neutral-600 hover:bg-neutral-700',
55
58
  disabled && 'opacity-50 cursor-not-allowed',
56
59
  className,
57
60
  )}
58
61
  >
59
- {badge !== undefined && (
62
+ {label && (
60
63
  <span className="absolute top-3 right-3">
61
- <Badge value={badge} color={badgeColor} size="xs" />
64
+ <Label text={label.text} color={label.color} size="xs" tooltip={label.tooltip} />
62
65
  </span>
63
66
  )}
64
67
 
@@ -76,6 +79,10 @@ export function NavCard({
76
79
  {description && (
77
80
  <p className="mt-1 text-xs text-neutral-500 leading-relaxed line-clamp-2">{description}</p>
78
81
  )}
82
+
83
+ {stats && (
84
+ <p className="mt-2 text-xs text-neutral-600">{stats}</p>
85
+ )}
79
86
  </button>
80
87
  )
81
88
  }
@@ -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' },
@@ -172,13 +172,13 @@ export function RegistryBrowser({
172
172
  {debounceKey != null && debounceKey > 0 && (
173
173
  <svg
174
174
  key={debounceKey}
175
- className="absolute inset-0 pointer-events-none"
175
+ className="absolute inset-0 pointer-events-none text-emerald-400/70"
176
176
  style={{ width: '100%', height: '100%' }}
177
177
  >
178
178
  <rect
179
179
  x="1" y="1" rx="5" ry="5"
180
180
  fill="none"
181
- stroke="rgb(52 211 153 / 0.7)"
181
+ stroke="currentColor"
182
182
  strokeWidth="1.5"
183
183
  pathLength="100"
184
184
  strokeDasharray="100"