@toolr/ui-design 0.1.8 → 0.1.9

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 (100) hide show
  1. package/ai-manifest.json +35 -20
  2. package/components/composites/dashboard-list-item.tsx +172 -0
  3. package/components/composites/dashboard-panel.tsx +218 -0
  4. package/components/content/info-panel-primitives.tsx +9 -8
  5. package/components/diagrams/diagram-utils.tsx +2 -1
  6. package/components/hooks/use-dropdown-portal.ts +39 -0
  7. package/components/lib/accent-context.ts +10 -0
  8. package/components/lib/{ai-tools.tsx → coding-agents.tsx} +23 -8
  9. package/components/lib/custom-icons.tsx +37 -0
  10. package/components/lib/git-providers.tsx +39 -0
  11. package/components/lib/theme-engine.ts +59 -10
  12. package/components/lib/toolr-brand.tsx +23 -9
  13. package/components/sections/captured-issues/captured-issues-panel.tsx +17 -8
  14. package/components/sections/{ai-tools-paths/tools-paths-panel.tsx → coding-agent-paths/agent-paths-panel.tsx} +70 -62
  15. package/components/sections/coding-agent-paths/index.ts +37 -0
  16. package/components/sections/{ai-tools-paths → coding-agent-paths}/types.ts +28 -28
  17. package/components/sections/coding-agent-paths/use-agent-paths.ts +159 -0
  18. package/components/sections/golden-snapshots/file-diff-viewer.tsx +10 -9
  19. package/components/sections/golden-snapshots/golden-sync-panel.tsx +12 -3
  20. package/components/sections/golden-snapshots/snapshot-manager.tsx +9 -7
  21. package/components/sections/golden-snapshots/status-overview.tsx +8 -8
  22. package/components/sections/golden-snapshots/version-manager.tsx +6 -6
  23. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +3 -3
  24. package/components/sections/prompt-editor/index.ts +1 -1
  25. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +13 -5
  26. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +18 -10
  27. package/components/sections/prompt-editor/types.ts +2 -2
  28. package/components/sections/report-bug/report-bug-form.tsx +12 -4
  29. package/components/sections/report-bug/screenshot-uploader.tsx +11 -3
  30. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +12 -4
  31. package/components/sections/snapshot-browser/snapshot-tree.tsx +5 -4
  32. package/components/sections/snapshot-browser/types.ts +1 -1
  33. package/components/sections/snippets-editor/snippets-editor.tsx +16 -9
  34. package/components/settings/SettingsHeader.tsx +2 -2
  35. package/components/settings/SettingsPanel.tsx +11 -3
  36. package/components/settings/SettingsTreeNav.tsx +15 -9
  37. package/components/ui/action-dialog.tsx +24 -30
  38. package/components/ui/ai-action-button.tsx +10 -7
  39. package/components/ui/ai-execution-action-buttons.tsx +13 -5
  40. package/components/ui/badge.tsx +7 -4
  41. package/components/ui/bottom-panel-header.tsx +9 -5
  42. package/components/ui/breadcrumb.tsx +9 -1
  43. package/components/ui/{extension-list-card.tsx → capability-list-card.tsx} +13 -5
  44. package/components/ui/checkbox.tsx +6 -3
  45. package/components/ui/collapsible-section.tsx +38 -29
  46. package/components/ui/confirm-badge.tsx +7 -4
  47. package/components/ui/cookie-consent.tsx +13 -7
  48. package/components/ui/detail-section.tsx +24 -16
  49. package/components/ui/detail-view-wrapper.tsx +30 -22
  50. package/components/ui/editor-placeholder-card.tsx +28 -24
  51. package/components/ui/editor-toolbar.tsx +7 -4
  52. package/components/ui/execution-details-panel.tsx +10 -5
  53. package/components/ui/file-structure-section.tsx +3 -3
  54. package/components/ui/file-tree.tsx +3 -1
  55. package/components/ui/files-panel.tsx +147 -27
  56. package/components/ui/filter-dropdown.tsx +84 -74
  57. package/components/ui/form-actions.tsx +14 -6
  58. package/components/ui/frontmatter-form-header.tsx +10 -2
  59. package/components/ui/icon-button.tsx +22 -9
  60. package/components/ui/input.tsx +7 -4
  61. package/components/ui/label.tsx +5 -5
  62. package/components/ui/layout-tab-bar.tsx +7 -5
  63. package/components/ui/modal.tsx +18 -4
  64. package/components/ui/nav-card.tsx +6 -3
  65. package/components/ui/navigation-bar.tsx +37 -9
  66. package/components/ui/number-input.tsx +8 -4
  67. package/components/ui/project-explorer.tsx +666 -0
  68. package/components/ui/registry-browser.tsx +12 -1
  69. package/components/ui/registry-card.tsx +49 -42
  70. package/components/ui/registry-detail.tsx +34 -11
  71. package/components/ui/resizable-textarea.tsx +18 -11
  72. package/components/ui/scope-badge.tsx +18 -11
  73. package/components/ui/segmented-toggle.tsx +5 -2
  74. package/components/ui/select.tsx +12 -9
  75. package/components/ui/selection-grid.tsx +36 -37
  76. package/components/ui/setting-row.tsx +2 -2
  77. package/components/ui/settings-card.tsx +10 -3
  78. package/components/ui/settings-info-box.tsx +9 -5
  79. package/components/ui/settings-section-title.tsx +14 -2
  80. package/components/ui/snapshot-card.tsx +10 -2
  81. package/components/ui/snippets-panel.tsx +4 -2
  82. package/components/ui/sort-dropdown.tsx +39 -29
  83. package/components/ui/status-card.tsx +9 -1
  84. package/components/ui/tab-bar.tsx +12 -9
  85. package/components/ui/toggle.tsx +13 -7
  86. package/components/ui/tooltip.tsx +9 -1
  87. package/dist/content.js +8 -8
  88. package/dist/diagrams.d.ts +0 -1
  89. package/dist/index.d.ts +421 -182
  90. package/dist/index.js +2984 -1691
  91. package/dist/tokens/primitives.css +28 -6
  92. package/dist/tokens/semantic.css +15 -15
  93. package/dist/tokens/theme.css +23 -0
  94. package/index.ts +25 -11
  95. package/package.json +1 -1
  96. package/tokens/primitives.css +28 -6
  97. package/tokens/semantic.css +15 -15
  98. package/tokens/theme.css +23 -0
  99. package/components/sections/ai-tools-paths/index.ts +0 -37
  100. package/components/sections/ai-tools-paths/use-tools-paths.ts +0 -159
@@ -1,11 +1,14 @@
1
1
  import { Home, Folder, Lock, Eye } from 'lucide-react'
2
2
  import { Label, type LabelColor } from './label.tsx'
3
+ import { AccentColorProvider, useAccentColor } from '../lib/accent-context.ts'
4
+ import type { FormColor } from '../lib/form-colors.ts'
3
5
 
4
6
  export type ScopeType = 'user' | 'project' | 'local' | 'read-only'
5
7
 
6
8
  interface ScopeBadgeProps {
7
9
  scope: ScopeType
8
10
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
11
+ accentColor?: FormColor
9
12
  }
10
13
 
11
14
  const scopeConfig: Record<
@@ -48,20 +51,24 @@ const scopeConfig: Record<
48
51
  },
49
52
  }
50
53
 
51
- export function ScopeBadge({ scope, size = 'xs' }: ScopeBadgeProps) {
54
+ export function ScopeBadge({ scope, size = 'xs', accentColor }: ScopeBadgeProps) {
55
+ const contextAccent = useAccentColor()
56
+ const effectiveColor = accentColor ?? contextAccent ?? 'blue'
52
57
  const config = scopeConfig[scope]
53
58
  const Icon = config.icon
54
59
 
55
60
  return (
56
- <Label
57
- text={config.label}
58
- color={config.color}
59
- IconComponent={Icon}
60
- tooltip={{
61
- title: config.title,
62
- description: config.description,
63
- }}
64
- size={size}
65
- />
61
+ <AccentColorProvider value={effectiveColor}>
62
+ <Label
63
+ text={config.label}
64
+ accentColor={config.color}
65
+ IconComponent={Icon}
66
+ tooltip={{
67
+ title: config.title,
68
+ description: config.description,
69
+ }}
70
+ size={size}
71
+ />
72
+ </AccentColorProvider>
66
73
  )
67
74
  }
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import type { AccentColor } from '../lib/form-colors.ts'
3
+ import { useAccentColor } from '../lib/accent-context.ts'
3
4
  import { Tooltip, type TooltipContent, type TooltipPosition } from './tooltip.tsx'
4
5
 
5
6
  export interface SegmentedToggleOption<T extends string> {
@@ -108,7 +109,7 @@ export function SegmentedToggle<T extends string>({
108
109
  options,
109
110
  value,
110
111
  onChange,
111
- accentColor = 'blue',
112
+ accentColor: accentColorProp,
112
113
  variant = 'outline',
113
114
  size = 'sm',
114
115
  tooltipPosition = 'bottom',
@@ -116,10 +117,12 @@ export function SegmentedToggle<T extends string>({
116
117
  disabled = false,
117
118
  disabledTooltip,
118
119
  }: SegmentedToggleProps<T>) {
120
+ const contextAccent = useAccentColor()
121
+ const accentColor = accentColorProp ?? contextAccent ?? 'blue'
119
122
  const isOutline = variant === 'outline'
120
123
  const containerClasses = isOutline
121
124
  ? OUTLINE_CONTAINER[accentColor] || OUTLINE_CONTAINER.blue
122
- : 'flex items-center bg-neutral-800/50 border border-neutral-700 rounded-md'
125
+ : 'flex items-center bg-neutral-960/50 border border-neutral-700 rounded-md'
123
126
 
124
127
  const toggle = (
125
128
  <div
@@ -4,6 +4,7 @@ import { ChevronDown, Check } from 'lucide-react'
4
4
  import { useClickOutside } from '../hooks/use-click-outside.ts'
5
5
  import { useDropdownMaxHeight } from '../hooks/use-dropdown-max-height.ts'
6
6
  import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
7
+ import { useAccentColor } from '../lib/accent-context.ts'
7
8
 
8
9
  export interface SelectOption<T extends string | number = string> {
9
10
  value: T
@@ -17,7 +18,7 @@ export interface SelectProps<T extends string | number = string> {
17
18
  onChange: (value: T) => void
18
19
  placeholder?: string
19
20
  variant?: 'filled' | 'outline'
20
- color?: FormColor
21
+ accentColor?: FormColor
21
22
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
22
23
  align?: 'left' | 'right'
23
24
  disabled?: boolean
@@ -25,7 +26,7 @@ export interface SelectProps<T extends string | number = string> {
25
26
  }
26
27
 
27
28
  const VARIANT_CLASSES = {
28
- filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
29
+ filled: { bg: 'bg-neutral-960', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
29
30
  outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
30
31
  }
31
32
 
@@ -43,12 +44,14 @@ export function Select<T extends string | number = string>({
43
44
  onChange,
44
45
  placeholder = 'Select...',
45
46
  variant = 'outline',
46
- color = 'blue',
47
+ accentColor,
47
48
  size = 'sm',
48
49
  align = 'left',
49
50
  disabled = false,
50
51
  className = '',
51
52
  }: SelectProps<T>) {
53
+ const contextAccent = useAccentColor()
54
+ const effectiveColor = accentColor ?? contextAccent ?? 'blue'
52
55
  const [isOpen, setIsOpen] = useState(false)
53
56
  const [highlightIdx, setHighlightIdx] = useState(-1)
54
57
  const ref = useRef<HTMLDivElement>(null)
@@ -114,8 +117,8 @@ export function Select<T extends string | number = string>({
114
117
  aria-haspopup="listbox"
115
118
  onClick={() => !disabled && (isOpen ? close() : open())}
116
119
  disabled={disabled}
117
- className={`flex items-center gap-1.5 min-w-0 rounded-lg border ${v.bg} ${FORM_COLORS[color].border} text-neutral-200 focus:outline-none ${FORM_COLORS[color].focus} transition-colors ${
118
- disabled ? 'opacity-50 cursor-not-allowed' : `cursor-pointer ${FORM_COLORS[color].hover}`
120
+ className={`flex items-center gap-1.5 min-w-0 rounded-lg border ${v.bg} ${FORM_COLORS[effectiveColor].border} text-neutral-200 focus:outline-none ${FORM_COLORS[effectiveColor].focus} transition-colors ${
121
+ disabled ? 'opacity-50 cursor-not-allowed' : `cursor-pointer ${FORM_COLORS[effectiveColor].hover}`
119
122
  } ${s}`}
120
123
  >
121
124
  {selectedOption?.icon}
@@ -128,7 +131,7 @@ export function Select<T extends string | number = string>({
128
131
  <div
129
132
  ref={menuRef}
130
133
  role="listbox"
131
- className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} border ${FORM_COLORS[color].border} rounded-lg shadow-lg overflow-hidden`}
134
+ className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} border ${FORM_COLORS[effectiveColor].border} rounded-lg shadow-lg overflow-hidden`}
132
135
  style={{
133
136
  top: menuPos.top,
134
137
  left: align === 'right' ? undefined : menuPos.left,
@@ -148,11 +151,11 @@ export function Select<T extends string | number = string>({
148
151
  onPointerEnter={() => setHighlightIdx(idx)}
149
152
  className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
150
153
  isHighlighted
151
- ? `${FORM_COLORS[color].selectedBg} text-neutral-200`
152
- : isSelected ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
154
+ ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200`
155
+ : isSelected ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
153
156
  }`}
154
157
  >
155
- <Check className={`w-3 h-3 shrink-0 ${isSelected ? FORM_COLORS[color].accent : 'invisible'}`} />
158
+ <Check className={`w-3 h-3 shrink-0 ${isSelected ? FORM_COLORS[effectiveColor].accent : 'invisible'}`} />
156
159
  {opt.icon}
157
160
  <span className="truncate">{opt.label}</span>
158
161
  </button>
@@ -1,17 +1,20 @@
1
1
  import { iconMap, type IconName } from './icon-button.tsx'
2
2
  import type { ConfirmBadgeColor } from './confirm-badge.tsx'
3
+ import { AccentColorProvider, useAccentColor } from '../lib/accent-context.ts'
4
+ import type { FormColor } from '../lib/form-colors.ts'
3
5
  import { cn } from '../lib/cn.ts'
4
- import { AiToolIcon, AI_TOOL_NAMES, type AiToolKey } from '../lib/ai-tools.tsx'
6
+ import { FORM_COLORS } from '../lib/form-colors.ts'
7
+ import { CodingAgentIcon, CODING_AGENT_NAMES, type CodingAgentKey } from '../lib/coding-agents.tsx'
5
8
 
6
- /* ── Preset logos (shared AiToolIcon) ─────────────────────── */
9
+ /* ── Preset logos (shared CodingAgentIcon) ────────────────── */
7
10
 
8
11
  type IconProps = { className?: string; style?: React.CSSProperties }
9
12
 
10
- function makeToolLogo(toolKey: AiToolKey): React.ComponentType<IconProps> {
11
- function ToolLogo({ className, style }: IconProps) {
12
- return <AiToolIcon tool={toolKey} className={className} style={style} />
13
+ function makeAgentLogo(agentKey: CodingAgentKey): React.ComponentType<IconProps> {
14
+ function AgentLogo({ className, style }: IconProps) {
15
+ return <CodingAgentIcon agent={agentKey} className={className} style={style} />
13
16
  }
14
- return ToolLogo
17
+ return AgentLogo
15
18
  }
16
19
 
17
20
  /* ── Preset types & data ──────────────────────────────────── */
@@ -24,12 +27,14 @@ export type CodingToolPresetConfig = CodingToolId | {
24
27
  disabled?: boolean
25
28
  }
26
29
 
30
+ // Brand colors use Tailwind accent hex values (violet-400, orange-500, blue-400, cyan-400, neutral-400)
31
+ // applied via inline style because the color is dynamic per-item
27
32
  const CODING_TOOL_PRESETS: Record<CodingToolId, { name: string; description: string; color: string; badgeColor: ConfirmBadgeColor; Logo: React.ComponentType<IconProps> }> = {
28
- 'claude-code': { name: AI_TOOL_NAMES.claude, description: 'Security & architecture focus', color: '#a78bfa', badgeColor: 'violet', Logo: makeToolLogo('claude') },
29
- 'github-copilot': { name: AI_TOOL_NAMES.copilot, description: 'Code quality & patterns', color: '#f97316', badgeColor: 'orange', Logo: makeToolLogo('copilot') },
30
- 'codex-cli': { name: AI_TOOL_NAMES.codex, description: 'Performance & optimization', color: '#60a5fa', badgeColor: 'blue', Logo: makeToolLogo('codex') },
31
- 'gemini-cli': { name: AI_TOOL_NAMES.gemini, description: 'Best practices & docs', color: '#22d3ee', badgeColor: 'cyan', Logo: makeToolLogo('gemini') },
32
- 'opencode': { name: AI_TOOL_NAMES.opencode, description: 'Provider-agnostic AI coding agent', color: '#a1a1aa', badgeColor: 'neutral', Logo: makeToolLogo('opencode') },
33
+ 'claude-code': { name: CODING_AGENT_NAMES.claude, description: 'Security & architecture focus', color: '#a78bfa', badgeColor: 'violet', Logo: makeAgentLogo('claude') },
34
+ 'github-copilot': { name: CODING_AGENT_NAMES.copilot, description: 'Code quality & patterns', color: '#f97316', badgeColor: 'orange', Logo: makeAgentLogo('copilot') },
35
+ 'codex-cli': { name: CODING_AGENT_NAMES.codex, description: 'Performance & optimization', color: '#60a5fa', badgeColor: 'blue', Logo: makeAgentLogo('codex') },
36
+ 'gemini-cli': { name: CODING_AGENT_NAMES.gemini, description: 'Best practices & docs', color: '#22d3ee', badgeColor: 'cyan', Logo: makeAgentLogo('gemini') },
37
+ 'opencode': { name: CODING_AGENT_NAMES.opencode, description: 'Provider-agnostic AI coding agent', color: '#a1a1aa', badgeColor: 'neutral', Logo: makeAgentLogo('opencode') },
33
38
  }
34
39
 
35
40
  function resolvePresets(presets: CodingToolPresetConfig[]): SelectionCardItem[] {
@@ -85,16 +90,11 @@ export interface SelectionGridProps {
85
90
  /** Number of columns for grid layout (auto if not set) */
86
91
  columns?: number
87
92
  className?: string
93
+ accentColor?: FormColor
88
94
  }
89
95
 
90
96
  /* ── Helpers ───────────────────────────────────────────────── */
91
97
 
92
- const DEFAULT_COLOR = 'blue-400'
93
-
94
- function resolveColor(color?: string): string {
95
- return color || DEFAULT_COLOR
96
- }
97
-
98
98
  function autoColumns(itemCount: number): number {
99
99
  return Math.min(itemCount, 5)
100
100
  }
@@ -110,7 +110,10 @@ export function SelectionGrid({
110
110
  layout = 'grid',
111
111
  columns,
112
112
  className,
113
+ accentColor,
113
114
  }: SelectionGridProps) {
115
+ const contextAccent = useAccentColor()
116
+ const effectiveColor = accentColor ?? contextAccent ?? 'blue'
114
117
  const items = itemsProp || (presets ? resolvePresets(presets) : [])
115
118
 
116
119
  function handleClick(item: SelectionCardItem) {
@@ -132,6 +135,7 @@ export function SelectionGrid({
132
135
 
133
136
  if (layout === 'list') {
134
137
  return (
138
+ <AccentColorProvider value={effectiveColor}>
135
139
  <div
136
140
  className={cn('grid gap-3', className)}
137
141
  style={{ gridTemplateColumns: `repeat(${columns || 1}, 1fr)` }}
@@ -145,10 +149,12 @@ export function SelectionGrid({
145
149
  />
146
150
  ))}
147
151
  </div>
152
+ </AccentColorProvider>
148
153
  )
149
154
  }
150
155
 
151
156
  return (
157
+ <AccentColorProvider value={effectiveColor}>
152
158
  <div
153
159
  className={cn('grid gap-3', className)}
154
160
  style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }}
@@ -162,6 +168,7 @@ export function SelectionGrid({
162
168
  />
163
169
  ))}
164
170
  </div>
171
+ </AccentColorProvider>
165
172
  )
166
173
  }
167
174
 
@@ -174,7 +181,8 @@ interface CardProps {
174
181
  }
175
182
 
176
183
  function GridCard({ item, selected, onClick }: CardProps) {
177
- const color = resolveColor(item.color)
184
+ const accent = useAccentColor() ?? 'blue'
185
+ const accentStyles = FORM_COLORS[accent]
178
186
  const Icon = item.IconComponent || (item.icon ? iconMap[item.icon] : undefined)
179
187
 
180
188
  return (
@@ -187,21 +195,16 @@ function GridCard({ item, selected, onClick }: CardProps) {
187
195
  className={cn(
188
196
  'relative p-3 rounded-lg text-center transition-all border-2 flex flex-col items-center',
189
197
  item.disabled
190
- ? 'opacity-30 cursor-not-allowed border-neutral-800 bg-[var(--background)]'
198
+ ? 'opacity-30 cursor-not-allowed border-neutral-960 bg-[var(--background)]'
191
199
  : selected
192
- ? 'cursor-pointer'
193
- : 'bg-neutral-800/50 border-neutral-700 hover:bg-[var(--surface-hover)] hover:border-neutral-600 cursor-pointer',
200
+ ? `cursor-pointer ${accentStyles.selectedBg} ${accentStyles.border}`
201
+ : 'bg-neutral-960/50 border-neutral-700 hover:bg-[var(--surface-hover)] hover:border-neutral-600 cursor-pointer',
194
202
  )}
195
- style={
196
- selected && !item.disabled
197
- ? { borderColor: color }
198
- : undefined
199
- }
200
203
  >
201
204
  {Icon && (
202
205
  <Icon
203
206
  className="w-7 h-7 mb-1"
204
- style={{ color: item.disabled ? undefined : (item.color || '#9ca3af') }}
207
+ style={{ color: item.disabled ? undefined : item.color }}
205
208
  />
206
209
  )}
207
210
 
@@ -215,7 +218,8 @@ function GridCard({ item, selected, onClick }: CardProps) {
215
218
  }
216
219
 
217
220
  function ListCard({ item, selected, onClick }: CardProps) {
218
- const color = resolveColor(item.color)
221
+ const accent = useAccentColor() ?? 'blue'
222
+ const accentStyles = FORM_COLORS[accent]
219
223
  const Icon = item.IconComponent || (item.icon ? iconMap[item.icon] : undefined)
220
224
 
221
225
  return (
@@ -228,21 +232,16 @@ function ListCard({ item, selected, onClick }: CardProps) {
228
232
  className={cn(
229
233
  'relative p-3 rounded-lg transition-all border-2 flex items-center gap-3 text-left',
230
234
  item.disabled
231
- ? 'opacity-30 cursor-not-allowed border-neutral-800 bg-[var(--background)]'
235
+ ? 'opacity-30 cursor-not-allowed border-neutral-960 bg-[var(--background)]'
232
236
  : selected
233
- ? 'cursor-pointer'
234
- : 'bg-neutral-800/50 border-neutral-700 hover:bg-[var(--surface-hover)] hover:border-neutral-600 cursor-pointer',
237
+ ? `cursor-pointer ${accentStyles.selectedBg} ${accentStyles.border}`
238
+ : 'bg-neutral-960/50 border-neutral-700 hover:bg-[var(--surface-hover)] hover:border-neutral-600 cursor-pointer',
235
239
  )}
236
- style={
237
- selected && !item.disabled
238
- ? { borderColor: color }
239
- : undefined
240
- }
241
240
  >
242
241
  {Icon && (
243
242
  <Icon
244
243
  className="w-5 h-5 flex-shrink-0"
245
- style={{ color: item.disabled ? undefined : (item.color || '#9ca3af') }}
244
+ style={{ color: item.disabled ? undefined : item.color }}
246
245
  />
247
246
  )}
248
247
 
@@ -27,7 +27,7 @@ interface SettingRowToggle extends SettingRowBase {
27
27
  type: 'toggle'
28
28
  checked: boolean
29
29
  onChange: (checked: boolean) => void
30
- color?: ToggleColor
30
+ accentColor?: ToggleColor
31
31
  size?: ToggleSize
32
32
  }
33
33
 
@@ -65,7 +65,7 @@ export function SettingRow(props: SettingRowProps) {
65
65
  checked={props.checked}
66
66
  onChange={props.onChange}
67
67
  disabled={disabled}
68
- color={props.color}
68
+ accentColor={props.accentColor}
69
69
  size={props.size}
70
70
  aria-label={label}
71
71
  />
@@ -1,4 +1,6 @@
1
1
  import { cn } from '../lib/cn.ts'
2
+ import type { FormColor } from '../lib/form-colors.ts'
3
+ import { useAccentColor, AccentColorProvider } from '../lib/accent-context.ts'
2
4
 
3
5
  export interface SettingsCardProps {
4
6
  children: React.ReactNode
@@ -6,12 +8,15 @@ export interface SettingsCardProps {
6
8
  title?: string
7
9
  description?: string
8
10
  testId?: string
11
+ accentColor?: FormColor
9
12
  }
10
13
 
11
- export function SettingsCard({ children, className, title, description, testId }: SettingsCardProps) {
14
+ export function SettingsCard({ children, className, title, description, testId, accentColor }: SettingsCardProps) {
15
+ const contextAccent = useAccentColor()
16
+ const effectiveColor = accentColor ?? contextAccent ?? 'blue'
12
17
  return (
13
18
  <div
14
- className={cn('bg-neutral-900 border border-neutral-800 rounded-lg p-4 space-y-4', className)}
19
+ className={cn('bg-neutral-980 border border-neutral-960 rounded-lg p-4 space-y-4', className)}
15
20
  data-testid={testId}
16
21
  >
17
22
  {title && (
@@ -21,7 +26,9 @@ export function SettingsCard({ children, className, title, description, testId }
21
26
  </div>
22
27
  )}
23
28
  {!title && description && <p className="text-md text-neutral-500">{description}</p>}
24
- {children}
29
+ <AccentColorProvider value={effectiveColor}>
30
+ {children}
31
+ </AccentColorProvider>
25
32
  </div>
26
33
  )
27
34
  }
@@ -1,5 +1,6 @@
1
1
  import { Info, AlertTriangle, CheckCircle, AlertCircle } from 'lucide-react'
2
- import { ACCENT_TEXT, type AccentColor } from '../lib/form-colors.ts'
2
+ import { ACCENT_TEXT, type AccentColor, type FormColor } from '../lib/form-colors.ts'
3
+ import { useAccentColor } from '../lib/accent-context.ts'
3
4
  import { cn } from '../lib/cn.ts'
4
5
 
5
6
  export type SettingsInfoBoxColor = AccentColor
@@ -7,6 +8,7 @@ export type SettingsInfoBoxColor = AccentColor
7
8
  export interface SettingsInfoBoxProps {
8
9
  children: React.ReactNode
9
10
  color?: SettingsInfoBoxColor
11
+ accentColor?: FormColor
10
12
  className?: string
11
13
  testId?: string
12
14
  }
@@ -47,15 +49,17 @@ const borderColorMap: Record<SettingsInfoBoxColor, string> = {
47
49
  teal: 'border-l-teal-500',
48
50
  }
49
51
 
50
- export function SettingsInfoBox({ children, color = 'neutral', className, testId }: SettingsInfoBoxProps) {
51
- const Icon = INFO_BOX_ICONS[color]
52
+ export function SettingsInfoBox({ children, color, accentColor, className, testId }: SettingsInfoBoxProps) {
53
+ const contextAccent = useAccentColor()
54
+ const effectiveColor = (color ?? accentColor ?? contextAccent ?? 'neutral') as SettingsInfoBoxColor
55
+ const Icon = INFO_BOX_ICONS[effectiveColor]
52
56
 
53
57
  return (
54
58
  <div
55
- className={cn('flex items-start gap-3 border-l-2 pl-2.5', borderColorMap[color], className)}
59
+ className={cn('flex items-start gap-3 border-l-2 pl-2.5', borderColorMap[effectiveColor], className)}
56
60
  data-testid={testId}
57
61
  >
58
- <Icon className={cn('w-4 h-4 mt-0.5 shrink-0', ACCENT_TEXT[color])} />
62
+ <Icon className={cn('w-4 h-4 mt-0.5 shrink-0', ACCENT_TEXT[effectiveColor])} />
59
63
  <div className="text-md text-neutral-500">{children}</div>
60
64
  </div>
61
65
  )
@@ -6,16 +6,28 @@
6
6
  * - Configuration panels - section dividers
7
7
  */
8
8
 
9
+ import { ACCENT_TEXT, type AccentColor, type FormColor } from '../lib/form-colors.ts'
10
+ import { useAccentColor } from '../lib/accent-context.ts'
11
+ import { cn } from '../lib/cn.ts'
12
+
9
13
  export interface SettingsSectionTitleProps {
10
14
  children: React.ReactNode
11
15
  className?: string
12
16
  testId?: string
17
+ accentColor?: FormColor
13
18
  }
14
19
 
15
- export function SettingsSectionTitle({ children, className = '', testId }: SettingsSectionTitleProps) {
20
+ export function SettingsSectionTitle({ children, className, testId, accentColor }: SettingsSectionTitleProps) {
21
+ const contextAccent = useAccentColor()
22
+ const effectiveColor = accentColor ?? contextAccent
23
+
16
24
  return (
17
25
  <h3
18
- className={`text-sm font-medium text-neutral-400 uppercase tracking-wider ${className}`}
26
+ className={cn(
27
+ 'text-sm font-medium uppercase tracking-wider',
28
+ effectiveColor ? ACCENT_TEXT[effectiveColor as AccentColor] : 'text-neutral-400',
29
+ className,
30
+ )}
19
31
  data-testid={testId}
20
32
  >
21
33
  {children}
@@ -4,6 +4,8 @@ import { RefreshCw, Eye } from 'lucide-react'
4
4
  import { cn } from '../lib/cn.ts'
5
5
  import { Label, type LabelColor } from './label.tsx'
6
6
  import type { IconName } from './icon-button.tsx'
7
+ import type { FormColor } from '../lib/form-colors.ts'
8
+ import { useAccentColor, AccentColorProvider } from '../lib/accent-context.ts'
7
9
 
8
10
  type SnapshotStatus = 'synced' | 'pending' | 'conflict' | 'outdated'
9
11
 
@@ -16,6 +18,7 @@ export interface SnapshotCardProps {
16
18
  onSync?: () => void
17
19
  onView?: () => void
18
20
  className?: string
21
+ accentColor?: FormColor
19
22
  }
20
23
 
21
24
  const statusStripeColor: Record<SnapshotStatus, string> = {
@@ -41,9 +44,13 @@ export function SnapshotCard({
41
44
  onSync,
42
45
  onView,
43
46
  className,
47
+ accentColor: accentColorProp,
44
48
  }: SnapshotCardProps) {
49
+ const contextAccent = useAccentColor()
50
+ const effectiveColor = accentColorProp ?? contextAccent ?? 'blue'
45
51
  return (
46
- <div className={cn('rounded-lg border border-neutral-700 bg-neutral-800 overflow-hidden', className)}>
52
+ <AccentColorProvider value={effectiveColor}>
53
+ <div className={cn('rounded-lg border border-neutral-700 bg-neutral-960 overflow-hidden', className)}>
47
54
  <div className={cn('h-1', statusStripeColor[status])} />
48
55
 
49
56
  <div className="p-4">
@@ -51,7 +58,7 @@ export function SnapshotCard({
51
58
  <h3 className="text-md font-medium text-neutral-200 truncate">{title}</h3>
52
59
  <Label
53
60
  text={statusLabelConfig[status].text}
54
- color={statusLabelConfig[status].color}
61
+ accentColor={statusLabelConfig[status].color}
55
62
  icon={statusLabelConfig[status].icon}
56
63
  tooltip={{ description: statusLabelConfig[status].tooltip }}
57
64
  size="xs"
@@ -103,5 +110,6 @@ export function SnapshotCard({
103
110
  )}
104
111
  </div>
105
112
  </div>
113
+ </AccentColorProvider>
106
114
  )
107
115
  }
@@ -61,7 +61,7 @@ export function SnippetsPanel({
61
61
  )
62
62
 
63
63
  return (
64
- <div className={cn('flex flex-col bg-neutral-800 rounded-lg overflow-hidden', className)}>
64
+ <div className={cn('flex flex-col bg-neutral-960 rounded-lg overflow-hidden', className)}>
65
65
  <div className="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
66
66
  <span className="text-xs font-semibold uppercase tracking-wider text-neutral-500">Snippets</span>
67
67
  <span className="text-xs text-neutral-500">{snippets.length} snippets</span>
@@ -107,7 +107,7 @@ function SnippetCard({ snippet, onInsert, onCopy }: SnippetCardProps) {
107
107
  const langColor = snippet.language ? LANGUAGE_COLORS[snippet.language.toLowerCase()] ?? '#6b7280' : '#6b7280'
108
108
 
109
109
  return (
110
- <div className="rounded-md border border-neutral-700 bg-neutral-900 hover:border-neutral-600 transition-colors">
110
+ <div className="rounded-md border border-neutral-700 bg-neutral-980 hover:border-neutral-600 transition-colors">
111
111
  <div className="flex items-center justify-between gap-2 px-2.5 py-1.5">
112
112
  <div className="flex items-center gap-2 min-w-0">
113
113
  <span className="text-sm font-medium text-neutral-200 truncate">{snippet.label}</span>
@@ -126,6 +126,7 @@ function SnippetCard({ snippet, onInsert, onCopy }: SnippetCardProps) {
126
126
  icon="arrow-down-to-line"
127
127
  onClick={() => onInsert(snippet.id)}
128
128
  size="xss"
129
+ accentColor="neutral"
129
130
  tooltip={{ title: 'Insert snippet', description: 'Insert this snippet' }}
130
131
  />
131
132
  )}
@@ -134,6 +135,7 @@ function SnippetCard({ snippet, onInsert, onCopy }: SnippetCardProps) {
134
135
  icon="copy"
135
136
  onClick={() => onCopy(snippet.id)}
136
137
  size="xss"
138
+ accentColor="neutral"
137
139
  tooltip={{ title: 'Copy snippet', description: 'Copy to clipboard' }}
138
140
  />
139
141
  )}
@@ -1,11 +1,13 @@
1
- import { useState, useRef, useEffect } from 'react'
1
+ import { useState, useEffect } from 'react'
2
+ import { createPortal } from 'react-dom'
2
3
  import { ArrowUp, ArrowDown, ChevronDown, Check } from 'lucide-react'
3
4
  import { useClickOutside } from '../hooks/use-click-outside.ts'
4
- import { useDropdownMaxHeight } from '../hooks/use-dropdown-max-height.ts'
5
+ import { useDropdownPortal } from '../hooks/use-dropdown-portal.ts'
5
6
  import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
7
+ import { useAccentColor } from '../lib/accent-context.ts'
6
8
 
7
9
  const VARIANT_CLASSES = {
8
- filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700' },
10
+ filled: { bg: 'bg-neutral-960', hoverBg: 'hover:bg-neutral-700' },
9
11
  outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700' },
10
12
  }
11
13
 
@@ -25,7 +27,7 @@ export interface SortDropdownProps {
25
27
  onToggleDirection: () => void
26
28
  fields: SortField[]
27
29
  variant?: 'filled' | 'outline'
28
- color?: FormColor
30
+ accentColor?: FormColor
29
31
  }
30
32
 
31
33
  export function SortDropdown({
@@ -35,15 +37,16 @@ export function SortDropdown({
35
37
  onToggleDirection,
36
38
  fields,
37
39
  variant = 'outline',
38
- color = 'blue',
40
+ accentColor,
39
41
  }: SortDropdownProps) {
42
+ const contextAccent = useAccentColor()
43
+ const effectiveColor = accentColor ?? contextAccent ?? 'blue'
40
44
  const [isOpen, setIsOpen] = useState(false)
41
45
  const [highlightIdx, setHighlightIdx] = useState(-1)
42
- const ref = useRef<HTMLDivElement>(null)
43
- const menuRef = useDropdownMaxHeight<HTMLDivElement>(isOpen)
46
+ const { triggerRef, menuRef, position } = useDropdownPortal(isOpen)
44
47
  const v = VARIANT_CLASSES[variant]
45
48
 
46
- useClickOutside(ref, isOpen, () => setIsOpen(false))
49
+ useClickOutside([triggerRef, menuRef], isOpen, () => setIsOpen(false))
47
50
 
48
51
  useEffect(() => {
49
52
  if (isOpen) setHighlightIdx(fields.findIndex((f) => f.value === field))
@@ -70,18 +73,42 @@ export function SortDropdown({
70
73
  const current = fields.find((f) => f.value === field) ?? fields[0]
71
74
  const DirIcon = ascending ? ArrowUp : ArrowDown
72
75
 
76
+ const menu = isOpen && createPortal(
77
+ <div
78
+ ref={menuRef}
79
+ role="listbox"
80
+ style={{ position: 'fixed', top: position.top, left: position.left, minWidth: position.minWidth, zIndex: 9999 }}
81
+ className={`whitespace-nowrap bg-[var(--popover)] border ${FORM_COLORS[effectiveColor].border} rounded-lg shadow-lg overflow-hidden`}
82
+ >
83
+ {fields.map((f, idx) => (
84
+ <button
85
+ key={f.value}
86
+ onClick={() => { onFieldChange(f.value); setIsOpen(false) }}
87
+ onPointerEnter={() => setHighlightIdx(idx)}
88
+ className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
89
+ idx === highlightIdx ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200` : field === f.value ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
90
+ }`}
91
+ >
92
+ <Check className={`w-3 h-3 shrink-0 ${field === f.value ? FORM_COLORS[effectiveColor].accent : 'invisible'}`} />
93
+ <span>{f.label}</span>
94
+ </button>
95
+ ))}
96
+ </div>,
97
+ document.body,
98
+ )
99
+
73
100
  return (
74
- <div className="relative flex items-center" ref={ref} onKeyDown={handleKeyDown}>
101
+ <div className="relative flex items-center" ref={triggerRef} onKeyDown={handleKeyDown}>
75
102
  <button
76
103
  aria-expanded={isOpen}
77
104
  aria-haspopup="listbox"
78
105
  onClick={() => setIsOpen(!isOpen)}
79
- className={`flex items-center gap-1.5 h-7 px-2 rounded-md border ${v.bg} text-sm transition-colors cursor-pointer ${FORM_COLORS[color].border} text-neutral-200 ${FORM_COLORS[color].hover}`}
106
+ className={`flex items-center gap-1.5 py-1 px-2 rounded-md border ${v.bg} text-sm transition-colors cursor-pointer ${FORM_COLORS[effectiveColor].border} text-neutral-200 ${FORM_COLORS[effectiveColor].hover}`}
80
107
  >
81
108
  <button
82
109
  type="button"
83
110
  aria-label={ascending ? 'Sort descending' : 'Sort ascending'}
84
- className={`${FORM_COLORS[color].accent} hover:brightness-125 transition-colors cursor-pointer`}
111
+ className={`${FORM_COLORS[effectiveColor].accent} hover:brightness-125 transition-colors cursor-pointer`}
85
112
  onClick={(e) => { e.stopPropagation(); onToggleDirection() }}
86
113
  >
87
114
  <DirIcon className="w-3 h-3" />
@@ -89,24 +116,7 @@ export function SortDropdown({
89
116
  <span className="whitespace-nowrap">{current.label}</span>
90
117
  <ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
91
118
  </button>
92
-
93
- {isOpen && (
94
- <div ref={menuRef} role="listbox" className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] bg-[var(--popover)] border ${FORM_COLORS[color].border} rounded-lg shadow-lg overflow-hidden`}>
95
- {fields.map((f, idx) => (
96
- <button
97
- key={f.value}
98
- onClick={() => { onFieldChange(f.value); setIsOpen(false) }}
99
- onPointerEnter={() => setHighlightIdx(idx)}
100
- className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
101
- idx === highlightIdx ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : field === f.value ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
102
- }`}
103
- >
104
- <Check className={`w-3 h-3 shrink-0 ${field === f.value ? FORM_COLORS[color].accent : 'invisible'}`} />
105
- <span>{f.label}</span>
106
- </button>
107
- ))}
108
- </div>
109
- )}
119
+ {menu}
110
120
  </div>
111
121
  )
112
122
  }