@toolr/ui-design 0.1.0 → 0.1.2

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 (41) hide show
  1. package/components/lib/theme-engine.ts +48 -15
  2. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +11 -11
  3. package/components/sections/captured-issues/captured-issues-panel.tsx +20 -20
  4. package/components/sections/golden-snapshots/file-diff-viewer.tsx +19 -19
  5. package/components/sections/golden-snapshots/golden-sync-panel.tsx +3 -3
  6. package/components/sections/golden-snapshots/snapshot-manager.tsx +15 -15
  7. package/components/sections/golden-snapshots/status-overview.tsx +40 -40
  8. package/components/sections/golden-snapshots/version-manager.tsx +10 -10
  9. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +11 -11
  10. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +15 -15
  11. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +19 -19
  12. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +10 -10
  13. package/components/sections/snapshot-browser/snapshot-tree.tsx +11 -11
  14. package/components/sections/snippets-editor/snippets-editor.tsx +24 -24
  15. package/components/settings/SettingsHeader.tsx +78 -0
  16. package/components/settings/SettingsPanel.tsx +21 -0
  17. package/components/settings/SettingsTreeNav.tsx +256 -0
  18. package/components/settings/index.ts +7 -0
  19. package/components/settings/settings-tree-utils.ts +120 -0
  20. package/components/ui/breadcrumb.tsx +16 -4
  21. package/components/ui/cookie-consent.tsx +82 -0
  22. package/components/ui/file-tree.tsx +5 -5
  23. package/components/ui/filter-dropdown.tsx +4 -4
  24. package/components/ui/form-actions.tsx +1 -1
  25. package/components/ui/label.tsx +31 -3
  26. package/components/ui/resizable-textarea.tsx +2 -2
  27. package/components/ui/segmented-toggle.tsx +17 -4
  28. package/components/ui/select.tsx +3 -3
  29. package/components/ui/sort-dropdown.tsx +2 -2
  30. package/components/ui/status-card.tsx +1 -1
  31. package/components/ui/tooltip.tsx +2 -2
  32. package/dist/index.d.ts +79 -8
  33. package/dist/index.js +1119 -622
  34. package/dist/tokens/{tokens/primitives.css → primitives.css} +10 -8
  35. package/dist/tokens/{tokens/semantic.css → semantic.css} +5 -0
  36. package/index.ts +13 -0
  37. package/package.json +6 -2
  38. package/tokens/primitives.css +10 -8
  39. package/tokens/semantic.css +5 -0
  40. /package/dist/tokens/{tokens/theme.css → theme.css} +0 -0
  41. /package/dist/tokens/{tokens/tokens.json → tokens.json} +0 -0
@@ -0,0 +1,120 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export type SettingsTreeNode = {
4
+ id: string
5
+ label: string
6
+ icon: ReactNode
7
+ children?: SettingsTreeNode[]
8
+ }
9
+
10
+ export function findNodeByPath(tree: SettingsTreeNode[], path: string): SettingsTreeNode | null {
11
+ const parts = path.split('.')
12
+ let current: SettingsTreeNode | undefined
13
+
14
+ for (const part of parts) {
15
+ const found = (current?.children ?? tree).find((n) => n.id === part)
16
+ if (!found) return null
17
+ current = found
18
+ }
19
+
20
+ return current ?? null
21
+ }
22
+
23
+ export function getBreadcrumbFromPath(
24
+ tree: SettingsTreeNode[],
25
+ path: string,
26
+ ): Array<{ label: string; icon: ReactNode; path: string }> {
27
+ const parts = path.split('.')
28
+ const breadcrumb: Array<{ label: string; icon: ReactNode; path: string }> = []
29
+ let currentPath = ''
30
+ let currentNodes: SettingsTreeNode[] = tree
31
+
32
+ for (const part of parts) {
33
+ const node = currentNodes.find((n) => n.id === part)
34
+ if (!node) break
35
+
36
+ currentPath = currentPath ? `${currentPath}.${part}` : part
37
+ breadcrumb.push({
38
+ label: node.label,
39
+ icon: node.icon,
40
+ path: currentPath,
41
+ })
42
+ currentNodes = node.children ?? []
43
+ }
44
+
45
+ return breadcrumb
46
+ }
47
+
48
+ export function isLeafNode(node: SettingsTreeNode): boolean {
49
+ return !node.children || node.children.length === 0
50
+ }
51
+
52
+ export function getAllLeafPaths(tree: SettingsTreeNode[], parentPath = ''): string[] {
53
+ const paths: string[] = []
54
+
55
+ for (const node of tree) {
56
+ const nodePath = parentPath ? `${parentPath}.${node.id}` : node.id
57
+
58
+ if (isLeafNode(node)) {
59
+ paths.push(nodePath)
60
+ } else if (node.children) {
61
+ paths.push(...getAllLeafPaths(node.children, nodePath))
62
+ }
63
+ }
64
+
65
+ return paths
66
+ }
67
+
68
+ export function collectExpandablePaths(nodes: SettingsTreeNode[], parentPath = ''): string[] {
69
+ const paths: string[] = []
70
+ for (const node of nodes) {
71
+ const nodePath = parentPath ? `${parentPath}.${node.id}` : node.id
72
+ if (!isLeafNode(node) && node.children) {
73
+ paths.push(nodePath)
74
+ paths.push(...collectExpandablePaths(node.children, nodePath))
75
+ }
76
+ }
77
+ return paths
78
+ }
79
+
80
+ export function getParentPaths(path: string): string[] {
81
+ const parts = path.split('.')
82
+ const parents: string[] = []
83
+ let current = ''
84
+ for (let i = 0; i < parts.length - 1; i++) {
85
+ current = current ? `${current}.${parts[i]}` : parts[i]
86
+ parents.push(current)
87
+ }
88
+ return parents
89
+ }
90
+
91
+ export function filterTree(
92
+ nodes: SettingsTreeNode[],
93
+ query: string,
94
+ parentPath = '',
95
+ ): SettingsTreeNode[] {
96
+ if (!query.trim()) return nodes
97
+
98
+ const lowerQuery = query.toLowerCase()
99
+
100
+ return nodes
101
+ .map((node) => {
102
+ const nodePath = parentPath ? `${parentPath}.${node.id}` : node.id
103
+ const labelMatches = node.label.toLowerCase().includes(lowerQuery)
104
+
105
+ if (isLeafNode(node)) {
106
+ return labelMatches ? node : null
107
+ }
108
+
109
+ const filteredChildren = filterTree(node.children || [], query, nodePath)
110
+
111
+ if (labelMatches) {
112
+ return node
113
+ } else if (filteredChildren.length > 0) {
114
+ return { ...node, children: filteredChildren }
115
+ }
116
+
117
+ return null
118
+ })
119
+ .filter((n): n is SettingsTreeNode => n !== null)
120
+ }
@@ -6,7 +6,7 @@ import {
6
6
  Globe, Star, Users, User, Tag, Search, Heart,
7
7
  Zap, Shield, ShieldCheck, Sparkles, Eye, Lock,
8
8
  Cloud, Wand2, Bell, Bookmark, Pin, Mail, Send,
9
- Image, Bot, Puzzle, Plug, Webhook,
9
+ Image, Bot, Puzzle, Plug, Webhook, House, Package,
10
10
  } from 'lucide-react'
11
11
  import type { LucideIcon } from 'lucide-react'
12
12
  import type { IconName } from './icon-button.tsx'
@@ -44,6 +44,8 @@ const iconSubset: Partial<Record<IconName, LucideIcon>> = {
44
44
  puzzle: Puzzle,
45
45
  plug: Plug,
46
46
  webhook: Webhook,
47
+ home: House,
48
+ 'package': Package,
47
49
  }
48
50
 
49
51
  export interface BreadcrumbSegment {
@@ -58,6 +60,8 @@ export interface BreadcrumbProps {
58
60
  segments: BreadcrumbSegment[]
59
61
  separator?: 'chevron' | 'slash' | 'dot'
60
62
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
63
+ /** 'box' (default) wraps in a bordered container; 'plain' renders inline with no background */
64
+ variant?: 'box' | 'plain'
61
65
  className?: string
62
66
  }
63
67
 
@@ -116,17 +120,23 @@ export function Breadcrumb({
116
120
  segments,
117
121
  separator = 'chevron',
118
122
  size = 'sm',
123
+ variant = 'box',
119
124
  className,
120
125
  }: BreadcrumbProps) {
121
126
  const s = sizeConfig[size]
127
+ const isBox = variant === 'box'
122
128
 
123
129
  return (
124
130
  <nav className={cn('flex items-center', className)}>
125
- <div className={cn('flex items-center gap-1', s.px, s.py, 'bg-neutral-800/50 border border-neutral-700/50 rounded-lg')}>
131
+ <div className={cn(
132
+ 'flex items-center gap-1',
133
+ isBox && [s.px, s.py, 'bg-neutral-800/50 border border-neutral-700/50 rounded-lg'],
134
+ )}>
126
135
  {segments.map((segment, index) => {
127
136
  const isLast = index === segments.length - 1
128
137
  const isClickable = !isLast && !!segment.onClick
129
138
  const colors = segment.color && colorMap[segment.color] ? colorMap[segment.color] : null
139
+ const isFirstPlain = !isBox && index === 0
130
140
 
131
141
  return (
132
142
  <div key={segment.id} className="flex items-center gap-1">
@@ -136,7 +146,8 @@ export function Breadcrumb({
136
146
  type="button"
137
147
  onClick={segment.onClick}
138
148
  className={cn(
139
- 'flex items-center gap-1.5 px-2 py-0.5 rounded-md transition-colors cursor-pointer',
149
+ 'flex items-center gap-1.5 pr-2 py-0.5 rounded-md transition-colors cursor-pointer',
150
+ isFirstPlain ? 'pl-0' : 'pl-2',
140
151
  s.text,
141
152
  'font-medium hover:text-white',
142
153
  colors ? [colors.text, `hover:${colors.bg}`] : ['text-neutral-300', 'hover:bg-neutral-700/50'],
@@ -148,7 +159,8 @@ export function Breadcrumb({
148
159
  ) : (
149
160
  <div
150
161
  className={cn(
151
- 'flex items-center gap-1.5 px-2 py-0.5 rounded-md',
162
+ 'flex items-center gap-1.5 pr-2 py-0.5 rounded-md',
163
+ isFirstPlain ? 'pl-0' : 'pl-2',
152
164
  s.text,
153
165
  isLast
154
166
  ? ['font-medium bg-neutral-700/50', colors ? colors.text : 'text-white']
@@ -0,0 +1,82 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { IconButton } from './icon-button.tsx'
3
+
4
+ export type ConsentChoice = 'accepted' | 'declined' | 'essential'
5
+
6
+ export interface CookieConsentProps {
7
+ /** localStorage key used to persist the choice */
8
+ storageKey: string
9
+ /** Accent color for the "Accept All" button (default: 'cyan') */
10
+ accentColor?: string
11
+ /** Heading text (default: 'We value your privacy') */
12
+ heading?: string
13
+ /** Description text */
14
+ description?: string
15
+ /** Called after the user makes a choice */
16
+ onConsent?: (choice: ConsentChoice) => void
17
+ }
18
+
19
+ export function CookieConsent({
20
+ storageKey,
21
+ accentColor = 'cyan',
22
+ heading = 'We value your privacy',
23
+ description = 'This site uses only essential cookies for functionality. No tracking or analytics.',
24
+ onConsent,
25
+ }: CookieConsentProps) {
26
+ const [isVisible, setIsVisible] = useState(false)
27
+
28
+ useEffect(() => {
29
+ const consent = localStorage.getItem(storageKey)
30
+ if (!consent) setIsVisible(true)
31
+ }, [storageKey])
32
+
33
+ const handleConsent = (choice: ConsentChoice) => {
34
+ localStorage.setItem(storageKey, choice)
35
+ setIsVisible(false)
36
+ onConsent?.(choice)
37
+ }
38
+
39
+ if (!isVisible) return null
40
+
41
+ return (
42
+ <div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-neutral-900/95 backdrop-blur-sm border-t border-neutral-700/50">
43
+ <div className="max-w-6xl mx-auto flex flex-col sm:flex-row items-start sm:items-center gap-4">
44
+ <div className="flex-grow">
45
+ <p className="text-sm text-neutral-200 mb-1">{heading}</p>
46
+ <p className="text-xs text-neutral-400">{description}</p>
47
+ </div>
48
+
49
+ <div className="flex items-center gap-2 flex-shrink-0">
50
+ <button
51
+ onClick={() => handleConsent('declined')}
52
+ className="px-3 py-1.5 text-sm h-[26px] inline-flex items-center justify-center font-medium rounded-md cursor-pointer text-neutral-400 border border-transparent hover:text-neutral-200 hover:border-neutral-600 hover:bg-neutral-800 transition-colors"
53
+ >
54
+ Decline
55
+ </button>
56
+ <button
57
+ onClick={() => handleConsent('essential')}
58
+ className="px-3 py-1.5 text-sm h-[26px] inline-flex items-center justify-center font-medium rounded-md cursor-pointer text-neutral-400 border border-transparent hover:text-neutral-200 hover:border-neutral-600 hover:bg-neutral-800 transition-colors"
59
+ >
60
+ Essential Only
61
+ </button>
62
+ <button
63
+ onClick={() => handleConsent('accepted')}
64
+ className={`px-3 py-1.5 text-sm h-[26px] inline-flex items-center justify-center font-medium rounded-md cursor-pointer text-white border transition-colors bg-${accentColor}-600 border-${accentColor}-500 hover:bg-${accentColor}-500`}
65
+ >
66
+ Accept All
67
+ </button>
68
+ </div>
69
+
70
+ <IconButton
71
+ icon="x"
72
+ color="neutral"
73
+ size="xs"
74
+ tooltip={{ description: 'Dismiss' }}
75
+ tooltipPosition="top"
76
+ onClick={() => handleConsent('declined')}
77
+ className="absolute top-2 right-2 sm:relative sm:top-auto sm:right-auto"
78
+ />
79
+ </div>
80
+ </div>
81
+ )
82
+ }
@@ -1,4 +1,4 @@
1
- import { FileCode, Folder, ChevronRight, ChevronDown } from 'lucide-react'
1
+ import { FileCode, Folder, FolderOpen, ChevronRight, ChevronDown } from 'lucide-react'
2
2
 
3
3
  export interface FileTreeNode {
4
4
  name: string
@@ -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 text-neutral-400`
130
- : `${base} cursor-pointer hover:bg-neutral-700/50 hover:text-neutral-200 text-neutral-400`
129
+ ? `${base} cursor-pointer hover:text-neutral-200 ${iconColorClass}`
130
+ : `${base} cursor-pointer hover:bg-neutral-700/50 hover:text-neutral-200 ${iconColorClass}`
131
131
 
132
132
  return (
133
133
  <li>
@@ -141,9 +141,9 @@ function FileTreeNodeItem({ node, path, selectedPath, onSelectFile, expandedPath
141
141
  <span className="w-3" />
142
142
  )}
143
143
  {isDir ? (
144
- <Folder className={`w-3.5 h-3.5 shrink-0 ${iconColorClass}`} />
144
+ expanded ? <FolderOpen className={`w-3.5 h-3.5 shrink-0 ${iconColorClass}`} /> : <Folder className={`w-3.5 h-3.5 shrink-0 ${iconColorClass}`} />
145
145
  ) : (
146
- <FileCode className={`w-3.5 h-3.5 shrink-0 ${isSelected ? iconColorClass : 'text-neutral-500'}`} />
146
+ <FileCode className={`w-3.5 h-3.5 shrink-0 ${iconColorClass}`} />
147
147
  )}
148
148
  <span className="truncate">{node.name}</span>
149
149
  </button>
@@ -8,7 +8,7 @@ const SEARCH_THRESHOLD = 20
8
8
 
9
9
  const VARIANT_CLASSES = {
10
10
  filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700' },
11
- outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-800' },
11
+ outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700' },
12
12
  }
13
13
 
14
14
  export interface FilterDropdownProps {
@@ -112,9 +112,9 @@ 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 ${v.bg} 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)] backdrop-blur border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
116
116
  {showSearch && (
117
- <div className={`sticky top-0 p-1.5 ${v.bg} border-b ${FORM_COLORS[color].border} z-10`}>
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">
119
119
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-neutral-500" />
120
120
  <input
@@ -124,7 +124,7 @@ export function FilterDropdown({
124
124
  onChange={(e) => setSearch(e.target.value)}
125
125
  onKeyDown={handleKeyDown}
126
126
  placeholder="Search..."
127
- className={`w-full pl-6 pr-2 py-1 text-xs bg-neutral-800 border border-neutral-600 rounded text-neutral-200 placeholder-neutral-500 outline-none ${FORM_COLORS[color].focus}`}
127
+ className={`w-full pl-6 pr-2 py-1 text-xs bg-[var(--popover)] border border-neutral-600 rounded text-neutral-200 placeholder-neutral-500 outline-none ${FORM_COLORS[color].focus}`}
128
128
  />
129
129
  </div>
130
130
  </div>
@@ -67,7 +67,7 @@ export function FormActions({
67
67
  const base = PADDING_CLASSES[padding]
68
68
  const paddingClass = showBorder
69
69
  ? base
70
- : base.replace(/\s*border-t\s+border-\[#374151\]/g, '')
70
+ : base.replace(/\s*border-t\s+border-neutral-700/g, '')
71
71
 
72
72
  const hasLeft = onBack || statusText
73
73
 
@@ -41,6 +41,8 @@ export interface LabelProps {
41
41
  tooltip: { title?: string; description: string }
42
42
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
43
43
  textTransform?: 'capitalize' | 'lowercase' | 'uppercase'
44
+ /** Progress fill (0–100). Shows a colored background bar behind the label content. */
45
+ progress?: number
44
46
  onClick?: () => void
45
47
  className?: string
46
48
  testId?: string
@@ -63,6 +65,23 @@ const colorClasses: Record<LabelColor, string> = {
63
65
  pink: 'border-pink-500/50 text-pink-400',
64
66
  }
65
67
 
68
+ const progressFillColors: Record<LabelColor, string> = {
69
+ neutral: 'bg-neutral-500/20',
70
+ green: 'bg-green-500/20',
71
+ red: 'bg-red-500/20',
72
+ blue: 'bg-blue-500/20',
73
+ yellow: 'bg-yellow-500/20',
74
+ orange: 'bg-orange-500/20',
75
+ purple: 'bg-purple-500/20',
76
+ amber: 'bg-amber-500/20',
77
+ emerald: 'bg-emerald-500/20',
78
+ cyan: 'bg-cyan-500/20',
79
+ indigo: 'bg-indigo-500/20',
80
+ teal: 'bg-teal-500/20',
81
+ violet: 'bg-violet-500/20',
82
+ pink: 'bg-pink-500/20',
83
+ }
84
+
66
85
  const sizeConfig = {
67
86
  xss: { height: 14, padding: 'px-1', text: 'text-[9px]', iconSize: 'w-2 h-2', gap: 'gap-0.5' },
68
87
  xs: { height: 16, padding: 'px-1.5', text: 'text-[10px]', iconSize: 'w-2.5 h-2.5', gap: 'gap-1' },
@@ -89,18 +108,21 @@ export function Label({
89
108
  tooltip,
90
109
  size = 'sm',
91
110
  textTransform,
111
+ progress,
92
112
  onClick,
93
113
  className = '',
94
114
  testId,
95
115
  }: LabelProps) {
96
116
  const s = sizeConfig[size]
97
117
  const cssTransform = textTransform === 'lowercase' || textTransform === 'uppercase' ? textTransform : undefined
118
+ const hasProgress = progress !== undefined && progress >= 0
98
119
 
99
120
  const baseClasses = [
100
121
  `inline-flex items-center ${s.gap}`,
101
122
  s.padding,
102
123
  s.text,
103
124
  'border rounded font-medium leading-none',
125
+ hasProgress ? 'relative overflow-hidden' : '',
104
126
  colorClasses[color],
105
127
  onClick ? 'cursor-pointer hover:brightness-125 transition-all' : 'cursor-help',
106
128
  className,
@@ -112,16 +134,22 @@ export function Label({
112
134
 
113
135
  const content = (
114
136
  <>
137
+ {hasProgress && (
138
+ <span
139
+ className={`absolute inset-y-0 left-0 ${progressFillColors[color]} rounded-[inherit]`}
140
+ style={{ width: `${Math.min(progress, 100)}%` }}
141
+ />
142
+ )}
115
143
  {CustomIcon && (
116
- <span className={`${s.iconSize} flex-shrink-0`}><CustomIcon className={s.iconSize} /></span>
144
+ <span className={`${s.iconSize} flex-shrink-0 ${hasProgress ? 'relative' : ''}`}><CustomIcon className={s.iconSize} /></span>
117
145
  )}
118
146
  {icons.map((iconName, i) => {
119
147
  const Icon = iconMap[iconName]
120
148
  return Icon ? (
121
- <span key={i} className={`${s.iconSize} flex-shrink-0`}><Icon className={s.iconSize} /></span>
149
+ <span key={i} className={`${s.iconSize} flex-shrink-0 ${hasProgress ? 'relative' : ''}`}><Icon className={s.iconSize} /></span>
122
150
  ) : null
123
151
  })}
124
- <span className="min-w-0 truncate" style={cssTransform ? { textTransform: cssTransform } : undefined}>
152
+ <span className={`min-w-0 truncate ${hasProgress ? 'relative' : ''}`} style={cssTransform ? { textTransform: cssTransform } : undefined}>
125
153
  {transformText(text, textTransform)}
126
154
  </span>
127
155
  </>
@@ -176,8 +176,8 @@ function ResizableCode({
176
176
  inherit: true,
177
177
  rules: [],
178
178
  colors: {
179
- 'editor.background': variant === 'filled' ? '#1f2937' : '#00000000',
180
- 'editorGutter.background': variant === 'filled' ? '#1f2937' : '#00000000',
179
+ 'editor.background': variant === 'filled' ? '#262626' : '#00000000',
180
+ 'editorGutter.background': variant === 'filled' ? '#262626' : '#00000000',
181
181
  'editor.lineHighlightBackground': '#00000000',
182
182
  'editor.lineHighlightBorder': '#00000000',
183
183
  },
@@ -3,7 +3,8 @@ import { Tooltip, type TooltipContent, type TooltipPosition } from './tooltip.ts
3
3
 
4
4
  export interface SegmentedToggleOption<T extends string> {
5
5
  value: T
6
- icon: ReactNode
6
+ icon?: ReactNode
7
+ label?: string
7
8
  tooltip: TooltipContent
8
9
  }
9
10
 
@@ -22,8 +23,8 @@ export interface SegmentedToggleProps<T extends string> {
22
23
  disabledTooltip?: TooltipContent
23
24
  }
24
25
 
25
- /** Button sizes match IconButton dimensions */
26
- const BUTTON_SIZE_CLASSES = {
26
+ /** Icon-only button sizes match IconButton dimensions */
27
+ const ICON_SIZE_CLASSES = {
27
28
  xss: 'w-[18px] h-[18px]',
28
29
  xs: 'w-6 h-6',
29
30
  sm: 'w-7 h-7',
@@ -31,6 +32,15 @@ const BUTTON_SIZE_CLASSES = {
31
32
  lg: 'w-9 h-9',
32
33
  }
33
34
 
35
+ /** Text label button sizes — horizontal padding instead of fixed width */
36
+ const TEXT_SIZE_CLASSES = {
37
+ xss: 'h-[18px] px-1.5 text-[9px]',
38
+ xs: 'h-6 px-2 text-[10px]',
39
+ sm: 'h-7 px-2.5 text-xs',
40
+ md: 'h-8 px-3 text-xs',
41
+ lg: 'h-9 px-3.5 text-sm',
42
+ }
43
+
34
44
  const ROUNDING_CLASSES = {
35
45
  xss: 'rounded-[3px]',
36
46
  xs: 'rounded-[5px]',
@@ -102,18 +112,21 @@ export function SegmentedToggle<T extends string>({
102
112
  const isFirst = i === 0
103
113
  const isLast = i === options.length - 1
104
114
  const rounding = isFirst && isLast ? ROUNDING_CLASSES[size] : isFirst ? `rounded-l-[5px]` : isLast ? `rounded-r-[5px]` : ''
115
+ const hasLabel = !!option.label
116
+ const sizeClass = hasLabel ? TEXT_SIZE_CLASSES[size] : ICON_SIZE_CLASSES[size]
105
117
  return (
106
118
  <Tooltip key={option.value} content={option.tooltip} position={tooltipPosition}>
107
119
  <button
108
120
  onClick={() => onChange(option.value)}
109
121
  disabled={disabled}
110
- className={`flex items-center justify-center ${BUTTON_SIZE_CLASSES[size]} ${rounding} text-xs font-medium transition-all cursor-pointer ${
122
+ className={`flex items-center justify-center ${sizeClass} ${rounding} font-medium transition-all cursor-pointer ${
111
123
  isActive
112
124
  ? ACTIVE_COLORS[accentColor] || ACTIVE_COLORS.blue
113
125
  : `text-neutral-400 ${HOVER_COLORS[accentColor] || HOVER_COLORS.blue}`
114
126
  }`}
115
127
  >
116
128
  {option.icon}
129
+ {option.label}
117
130
  </button>
118
131
  </Tooltip>
119
132
  )
@@ -24,8 +24,8 @@ export interface SelectProps<T extends string | number = string> {
24
24
  }
25
25
 
26
26
  const VARIANT_CLASSES = {
27
- filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-neutral-800' },
28
- outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-800', menuBg: 'bg-neutral-800/90 backdrop-blur-sm' },
27
+ filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
28
+ outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
29
29
  }
30
30
 
31
31
  const SIZE_CLASSES = {
@@ -134,7 +134,7 @@ export function Select<T extends string | number = string>({
134
134
  {isOpen && menuPos && createPortal(
135
135
  <div
136
136
  ref={menuRef}
137
- className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}
137
+ className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} backdrop-blur border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}
138
138
  style={{
139
139
  top: menuPos.top,
140
140
  left: align === 'right' ? undefined : menuPos.left,
@@ -6,7 +6,7 @@ import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
6
6
 
7
7
  const VARIANT_CLASSES = {
8
8
  filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700' },
9
- outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-800' },
9
+ outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700' },
10
10
  }
11
11
 
12
12
  export interface SortField {
@@ -88,7 +88,7 @@ export function SortDropdown({
88
88
  </button>
89
89
 
90
90
  {isOpen && (
91
- <div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] ${v.bg} border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
91
+ <div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] bg-[var(--popover)] backdrop-blur border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
92
92
  {fields.map((f, idx) => (
93
93
  <button
94
94
  key={f.value}
@@ -66,7 +66,7 @@ export function StatusCard({
66
66
  <h3 className="text-sm font-medium text-neutral-200">{title}</h3>
67
67
  </div>
68
68
 
69
- <div className="divide-y divide-[#374151]/60">
69
+ <div className="divide-y divide-neutral-700/60">
70
70
  {items.map((item) => (
71
71
  <div key={item.label} className="flex items-center justify-between px-4 py-2.5">
72
72
  <span className="text-xs text-neutral-400">{item.label}</span>
@@ -135,7 +135,7 @@ function clampToViewport(
135
135
  return { top, left }
136
136
  }
137
137
 
138
- const ARROW_BASE_CLASSES = 'absolute w-3 h-3 bg-neutral-800 border-neutral-600 rotate-45'
138
+ const ARROW_BASE_CLASSES = 'absolute w-3 h-3 bg-[var(--popover)] border-neutral-600 rotate-45'
139
139
 
140
140
  function getAlignmentClass(align: TooltipAlign): string {
141
141
  if (align === 'start') return 'left-2'
@@ -258,7 +258,7 @@ export function Tooltip({
258
258
  const tooltipContent = (
259
259
  <div
260
260
  ref={tooltipRef}
261
- className={`fixed px-3 py-1.5 bg-neutral-800 border border-neutral-600 rounded-lg shadow-xl z-[9999] ${interactive || trigger === 'click' ? '' : 'pointer-events-none'} ${multiline ? 'whitespace-pre-line' : 'whitespace-nowrap'}`}
261
+ className={`fixed px-3 py-1.5 bg-[var(--popover)] backdrop-blur border border-neutral-600 rounded-lg shadow-xl z-[9999] ${interactive || trigger === 'click' ? '' : 'pointer-events-none'} ${multiline ? 'whitespace-pre-line' : 'whitespace-nowrap'}`}
262
262
  style={{
263
263
  top: coords.top,
264
264
  left: coords.left,