@toolr/ui-design 0.1.7 → 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 (101) 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/hooks/use-modal-behavior.ts +32 -3
  8. package/components/lib/accent-context.ts +10 -0
  9. package/components/lib/{ai-tools.tsx → coding-agents.tsx} +23 -8
  10. package/components/lib/custom-icons.tsx +37 -0
  11. package/components/lib/git-providers.tsx +39 -0
  12. package/components/lib/theme-engine.ts +59 -10
  13. package/components/lib/toolr-brand.tsx +23 -9
  14. package/components/sections/captured-issues/captured-issues-panel.tsx +17 -8
  15. package/components/sections/{ai-tools-paths/tools-paths-panel.tsx → coding-agent-paths/agent-paths-panel.tsx} +70 -62
  16. package/components/sections/coding-agent-paths/index.ts +37 -0
  17. package/components/sections/{ai-tools-paths → coding-agent-paths}/types.ts +28 -28
  18. package/components/sections/coding-agent-paths/use-agent-paths.ts +159 -0
  19. package/components/sections/golden-snapshots/file-diff-viewer.tsx +11 -10
  20. package/components/sections/golden-snapshots/golden-sync-panel.tsx +12 -3
  21. package/components/sections/golden-snapshots/snapshot-manager.tsx +9 -7
  22. package/components/sections/golden-snapshots/status-overview.tsx +8 -8
  23. package/components/sections/golden-snapshots/version-manager.tsx +6 -6
  24. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +3 -3
  25. package/components/sections/prompt-editor/index.ts +1 -1
  26. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +13 -5
  27. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +18 -10
  28. package/components/sections/prompt-editor/types.ts +2 -2
  29. package/components/sections/report-bug/report-bug-form.tsx +12 -4
  30. package/components/sections/report-bug/screenshot-uploader.tsx +11 -3
  31. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +12 -4
  32. package/components/sections/snapshot-browser/snapshot-tree.tsx +5 -4
  33. package/components/sections/snapshot-browser/types.ts +1 -1
  34. package/components/sections/snippets-editor/snippets-editor.tsx +16 -9
  35. package/components/settings/SettingsHeader.tsx +2 -2
  36. package/components/settings/SettingsPanel.tsx +11 -3
  37. package/components/settings/SettingsTreeNav.tsx +15 -9
  38. package/components/ui/action-dialog.tsx +37 -35
  39. package/components/ui/ai-action-button.tsx +12 -11
  40. package/components/ui/ai-execution-action-buttons.tsx +13 -5
  41. package/components/ui/badge.tsx +17 -6
  42. package/components/ui/bottom-panel-header.tsx +9 -5
  43. package/components/ui/breadcrumb.tsx +14 -6
  44. package/components/ui/{extension-list-card.tsx → capability-list-card.tsx} +14 -6
  45. package/components/ui/checkbox.tsx +23 -14
  46. package/components/ui/collapsible-section.tsx +38 -28
  47. package/components/ui/confirm-badge.tsx +17 -6
  48. package/components/ui/cookie-consent.tsx +13 -7
  49. package/components/ui/detail-section.tsx +24 -16
  50. package/components/ui/detail-view-wrapper.tsx +30 -22
  51. package/components/ui/editor-placeholder-card.tsx +28 -24
  52. package/components/ui/editor-toolbar.tsx +7 -4
  53. package/components/ui/execution-details-panel.tsx +10 -5
  54. package/components/ui/file-structure-section.tsx +3 -3
  55. package/components/ui/file-tree.tsx +7 -5
  56. package/components/ui/files-panel.tsx +147 -27
  57. package/components/ui/filter-dropdown.tsx +88 -75
  58. package/components/ui/form-actions.tsx +21 -11
  59. package/components/ui/frontmatter-form-header.tsx +10 -2
  60. package/components/ui/icon-button.tsx +27 -14
  61. package/components/ui/input.tsx +15 -7
  62. package/components/ui/label.tsx +9 -5
  63. package/components/ui/layout-tab-bar.tsx +11 -9
  64. package/components/ui/modal.tsx +26 -8
  65. package/components/ui/nav-card.tsx +7 -4
  66. package/components/ui/navigation-bar.tsx +40 -12
  67. package/components/ui/number-input.tsx +14 -4
  68. package/components/ui/project-explorer.tsx +666 -0
  69. package/components/ui/registry-browser.tsx +12 -1
  70. package/components/ui/registry-card.tsx +49 -42
  71. package/components/ui/registry-detail.tsx +34 -11
  72. package/components/ui/resizable-textarea.tsx +18 -11
  73. package/components/ui/scope-badge.tsx +18 -11
  74. package/components/ui/segmented-toggle.tsx +7 -2
  75. package/components/ui/select.tsx +17 -11
  76. package/components/ui/selection-grid.tsx +40 -37
  77. package/components/ui/setting-row.tsx +6 -4
  78. package/components/ui/settings-card.tsx +12 -5
  79. package/components/ui/settings-info-box.tsx +9 -6
  80. package/components/ui/settings-section-title.tsx +14 -2
  81. package/components/ui/snapshot-card.tsx +10 -2
  82. package/components/ui/snippets-panel.tsx +4 -2
  83. package/components/ui/sort-dropdown.tsx +45 -32
  84. package/components/ui/status-card.tsx +9 -1
  85. package/components/ui/tab-bar.tsx +26 -13
  86. package/components/ui/toggle.tsx +31 -17
  87. package/components/ui/tooltip.tsx +14 -6
  88. package/dist/content.js +8 -8
  89. package/dist/diagrams.d.ts +0 -1
  90. package/dist/index.d.ts +431 -186
  91. package/dist/index.js +3119 -1724
  92. package/dist/tokens/primitives.css +28 -6
  93. package/dist/tokens/semantic.css +15 -15
  94. package/dist/tokens/theme.css +23 -0
  95. package/index.ts +25 -11
  96. package/package.json +9 -1
  97. package/tokens/primitives.css +28 -6
  98. package/tokens/semantic.css +15 -15
  99. package/tokens/theme.css +23 -0
  100. package/components/sections/ai-tools-paths/index.ts +0 -37
  101. package/components/sections/ai-tools-paths/use-tools-paths.ts +0 -159
@@ -37,9 +37,12 @@ import {
37
37
  PanelBottomClose,
38
38
  Package, Wrench, Store, ScrollText, Cpu, FlaskConical, Layers, Timer, Camera,
39
39
  AlertCircle, FileCode, Gauge, Home, PieChart, Settings2,
40
+ FolderSearch, PanelLeftClose, History,
41
+ Github, Gitlab, GitBranch,
40
42
  } from 'lucide-react'
41
43
  import type { LucideIcon } from 'lucide-react'
42
44
  import { type AccentColor } from '../lib/form-colors.ts'
45
+ import { useAccentColor } from '../lib/accent-context.ts'
43
46
  import { Tooltip, type TooltipContent } from './tooltip.tsx'
44
47
 
45
48
  export const iconMap = {
@@ -140,6 +143,14 @@ export const iconMap = {
140
143
  'home': Home,
141
144
  'pie-chart': PieChart,
142
145
  'settings-2': Settings2,
146
+ 'folder-search': FolderSearch,
147
+ 'panel-left-close': PanelLeftClose,
148
+ 'trash-2': Trash2,
149
+ 'rotate-ccw': RotateCcw,
150
+ 'history': History,
151
+ 'github': Github,
152
+ 'gitlab': Gitlab,
153
+ 'git-branch': GitBranch,
143
154
  } as const
144
155
 
145
156
  export type IconName = keyof typeof iconMap
@@ -147,7 +158,7 @@ export type IconName = keyof typeof iconMap
147
158
  export interface ActionItem {
148
159
  icon: IconName
149
160
  onClick: () => void
150
- color?: IconButtonColor
161
+ accentColor?: IconButtonColor
151
162
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
152
163
  variant?: IconButtonVariant
153
164
  tooltip?: TooltipContent
@@ -169,11 +180,11 @@ export interface IconButtonProps {
169
180
  /** When provided, renders an <a> tag instead of <button>. Opens in a new tab. */
170
181
  href?: string
171
182
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
172
- color?: IconButtonColor
183
+ accentColor?: IconButtonColor
173
184
  variant?: IconButtonVariant
174
185
  active?: boolean
175
186
  disabled?: boolean
176
- /** Async action status. Overrides icon, color, and active state when set. */
187
+ /** Async action status. Overrides icon, accentColor, and active state when set. */
177
188
  status?: IconButtonStatus
178
189
  /** Tooltip shown on hover. Title and description are required. */
179
190
  tooltip?: TooltipContent
@@ -260,9 +271,9 @@ const statusIcons: Record<IconButtonStatus, LucideIcon> = {
260
271
 
261
272
  const statusConfig = {
262
273
  loading: { color: undefined, active: true, animation: 'animate-spin' },
263
- success: { color: 'green' as const, active: true, animation: 'animate-pulse' },
264
- warning: { color: 'amber' as const, active: true, animation: 'animate-pulse' },
265
- error: { color: 'red' as const, active: true, animation: 'animate-pulse' },
274
+ success: { color: 'green' as const, active: true, animation: '' },
275
+ warning: { color: 'amber' as const, active: true, animation: '' },
276
+ error: { color: 'red' as const, active: true, animation: '' },
266
277
  }
267
278
 
268
279
  function resolveIcon(icon: IconName | ReactNode, status: IconButtonStatus | undefined): LucideIcon | null {
@@ -276,7 +287,7 @@ export function IconButton({
276
287
  onClick,
277
288
  href,
278
289
  size = 'sm',
279
- color = 'neutral',
290
+ accentColor,
280
291
  variant = 'outline',
281
292
  active = false,
282
293
  disabled = false,
@@ -289,7 +300,9 @@ export function IconButton({
289
300
  className = '',
290
301
  testId,
291
302
  }: IconButtonProps) {
292
- const resolvedColor = status ? (statusConfig[status].color ?? color) : color
303
+ const contextAccent = useAccentColor()
304
+ const effectiveColor = accentColor ?? contextAccent ?? 'neutral'
305
+ const resolvedColor = status ? (statusConfig[status].color ?? effectiveColor) : effectiveColor
293
306
  const resolvedActive = status ? statusConfig[status].active : active
294
307
 
295
308
  const colorStyle = colorClasses[resolvedColor] ?? colorClasses.neutral
@@ -299,7 +312,7 @@ export function IconButton({
299
312
  const borderClass = isOutline ? colorStyle.border : 'border-neutral-600'
300
313
 
301
314
  const sharedClassName = `
302
- relative flex items-center justify-center border transition-colors ${isOutline ? '' : 'bg-neutral-800'}
315
+ relative flex items-center justify-center border transition-colors ${isOutline ? '' : 'bg-neutral-960'}
303
316
  ${sizeClasses[size]}
304
317
  ${colorStyle.text} ${borderClass}
305
318
  ${resolvedActive ? colorStyle.active : ''}
@@ -337,7 +350,7 @@ export function IconButton({
337
350
  href={href}
338
351
  target="_blank"
339
352
  rel="noopener noreferrer"
340
- aria-label={tooltip?.title}
353
+ aria-label={tooltip?.title || (typeof tooltip?.description === 'string' ? tooltip.description : undefined)}
341
354
  data-testid={testId}
342
355
  className={`${sharedClassName} cursor-pointer no-underline`}
343
356
  >
@@ -348,7 +361,7 @@ export function IconButton({
348
361
  type="button"
349
362
  onClick={onClick}
350
363
  disabled={disabled}
351
- aria-label={tooltip?.title}
364
+ aria-label={tooltip?.title || (typeof tooltip?.description === 'string' ? tooltip.description : undefined)}
352
365
  data-testid={testId}
353
366
  className={`${sharedClassName} cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`}
354
367
  >
@@ -372,16 +385,16 @@ export interface CollapseButtonProps {
372
385
  collapsed: boolean
373
386
  onToggle: () => void
374
387
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
375
- color?: IconButtonColor
388
+ accentColor?: IconButtonColor
376
389
  tooltipPosition?: 'bottom' | 'bottom-left' | 'left' | 'right' | 'top' | 'top-left' | 'top-right'
377
390
  }
378
391
 
379
- export function CollapseButton({ collapsed, onToggle, size = 'xss', color, tooltipPosition = 'bottom-left' }: CollapseButtonProps) {
392
+ export function CollapseButton({ collapsed, onToggle, size = 'xss', accentColor, tooltipPosition = 'bottom-left' }: CollapseButtonProps) {
380
393
  return (
381
394
  <IconButton
382
395
  icon={collapsed ? 'chevrons-up-down' : 'chevrons-down-up'}
383
396
  onClick={onToggle}
384
- color={color}
397
+ accentColor={accentColor}
385
398
  tooltip={{
386
399
  title: collapsed ? 'Expand all' : 'Collapse all',
387
400
  description: collapsed ? 'Expand all folders' : 'Collapse all folders',
@@ -17,10 +17,11 @@
17
17
  * - Extends native input attributes
18
18
  */
19
19
 
20
- import { forwardRef, useEffect, useRef, useState, type InputHTMLAttributes, type ReactNode } from 'react'
20
+ import { forwardRef, useEffect, useId, useRef, useState, type InputHTMLAttributes, type ReactNode } from 'react'
21
21
  import { Search, X, Eye, EyeOff } from 'lucide-react'
22
22
  import { DebounceBorderOverlay } from './debounce-border-overlay.tsx'
23
23
  import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
24
+ import { useAccentColor } from '../lib/accent-context.ts'
24
25
 
25
26
  export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size' | 'type'> {
26
27
  value: string
@@ -29,7 +30,7 @@ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
29
30
  debounceMs?: number
30
31
  error?: boolean | string
31
32
  variant?: 'filled' | 'outline'
32
- color?: FormColor
33
+ accentColor?: FormColor
33
34
  size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
34
35
  mono?: boolean
35
36
  /** Test ID for E2E testing */
@@ -47,7 +48,7 @@ const sizeClasses = {
47
48
  }
48
49
 
49
50
  const variantClasses = {
50
- filled: 'bg-neutral-800',
51
+ filled: 'bg-neutral-960',
51
52
  outline: 'bg-transparent',
52
53
  }
53
54
 
@@ -65,7 +66,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
65
66
  debounceMs = 0,
66
67
  error,
67
68
  variant = 'outline',
68
- color = 'blue',
69
+ accentColor,
69
70
  size = 'sm',
70
71
  disabled = false,
71
72
  className = '',
@@ -76,6 +77,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
76
77
  onBlur: onBlurProp,
77
78
  ...props
78
79
  }, ref) {
80
+ const contextAccent = useAccentColor()
81
+ const effectiveColor = accentColor ?? contextAccent ?? 'blue'
79
82
  const hasError = Boolean(error)
80
83
  const hasAction = actionSlot && !disabled
81
84
  const [hovered, setHovered] = useState(false)
@@ -84,6 +87,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
84
87
  const isSearch = type === 'search'
85
88
  const isPassword = type === 'password'
86
89
  const [isPasswordVisible, setIsPasswordVisible] = useState(false)
90
+ const errorId = useId()
87
91
 
88
92
  // Debounce state
89
93
  const [internalValue, setInternalValue] = useState(value)
@@ -146,13 +150,16 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
146
150
  onBlur={(e) => { setFocused(false); onBlurProp?.(e) }}
147
151
  disabled={disabled}
148
152
  data-testid={testId}
153
+ aria-invalid={hasError || undefined}
154
+ aria-describedby={typeof error === 'string' && error ? errorId : undefined}
155
+ {...(isSearch && !props['aria-label'] ? { 'aria-label': props.placeholder || 'Search' } : {})}
149
156
  {...searchAutoProps}
150
157
  className={`
151
158
  w-full border rounded-lg text-neutral-200 placeholder-neutral-500
152
159
  focus:outline-none transition-colors
153
160
  disabled:opacity-50 disabled:cursor-not-allowed
154
161
  ${sizeClasses[size]}
155
- ${hasError ? 'border-red-500 focus:border-red-500' : `${variantClasses[variant]} ${FORM_COLORS[color].border} ${FORM_COLORS[color].focus}`}
162
+ ${hasError ? 'border-red-500 focus:border-red-500' : `${variantClasses[variant]} ${FORM_COLORS[effectiveColor].border} ${FORM_COLORS[effectiveColor].focus}`}
156
163
  ${mono ? 'font-mono' : ''}
157
164
  ${isSearch ? 'pl-8 pr-8' : (hasAction || isPassword) ? 'pr-8' : ''}
158
165
  ${className}
@@ -162,6 +169,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
162
169
  {showClear && (
163
170
  <button
164
171
  type="button"
172
+ aria-label="Clear search"
165
173
  onClick={handleClear}
166
174
  className="absolute right-2 top-1/2 -translate-y-1/2 w-[18px] h-[18px] flex items-center justify-center rounded-md text-neutral-400 hover:text-neutral-300 hover:bg-neutral-500/20 transition-colors z-10 cursor-pointer"
167
175
  >
@@ -177,7 +185,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
177
185
  <button
178
186
  type="button"
179
187
  onClick={() => setIsPasswordVisible(!isPasswordVisible)}
180
- title={isPasswordVisible ? 'Hide' : 'Reveal'}
188
+ aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
181
189
  className="absolute right-2 top-1/2 -translate-y-1/2 w-[18px] h-[18px] flex items-center justify-center rounded-md text-neutral-400 hover:text-neutral-300 hover:bg-neutral-500/20 transition-colors z-10 cursor-pointer"
182
190
  >
183
191
  {isPasswordVisible ? <EyeOff className="w-2.5 h-2.5" /> : <Eye className="w-2.5 h-2.5" />}
@@ -188,7 +196,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
188
196
  )}
189
197
  </div>
190
198
  {typeof error === 'string' && error && (
191
- <p className="text-sm text-red-400 mt-1 text-right">{error}</p>
199
+ <p id={errorId} className="text-sm text-red-400 mt-1 text-right" role="alert">{error}</p>
192
200
  )}
193
201
  </div>
194
202
  )
@@ -20,7 +20,7 @@ export type LabelColor = AccentColor
20
20
 
21
21
  export interface LabelProps {
22
22
  text: string
23
- color: LabelColor
23
+ accentColor: LabelColor
24
24
  /** Leading icon(s). Pass a single name or an array for multiple icons before text. */
25
25
  icon?: IconName | IconName[]
26
26
  /** Custom icon component. Takes precedence over icon. */
@@ -91,7 +91,7 @@ function transformText(text: string, textTransform?: 'capitalize' | 'lowercase'
91
91
 
92
92
  export function Label({
93
93
  text,
94
- color,
94
+ accentColor,
95
95
  icon,
96
96
  IconComponent: CustomIcon,
97
97
  tooltip,
@@ -112,7 +112,7 @@ export function Label({
112
112
  s.text,
113
113
  'border rounded font-medium leading-none',
114
114
  hasProgress ? 'relative overflow-hidden' : '',
115
- colorClasses[color],
115
+ colorClasses[accentColor],
116
116
  onClick ? 'cursor-pointer hover:brightness-125 transition-all' : 'cursor-help',
117
117
  className,
118
118
  ]
@@ -125,7 +125,11 @@ export function Label({
125
125
  <>
126
126
  {hasProgress && (
127
127
  <span
128
- className={`absolute inset-y-0 left-0 ${progressFillColors[color]} rounded-[inherit]`}
128
+ role="progressbar"
129
+ aria-valuenow={Math.min(progress, 100)}
130
+ aria-valuemin={0}
131
+ aria-valuemax={100}
132
+ className={`absolute inset-y-0 left-0 ${progressFillColors[accentColor]} rounded-[inherit]`}
129
133
  style={{ width: `${Math.min(progress, 100)}%` }}
130
134
  />
131
135
  )}
@@ -138,7 +142,7 @@ export function Label({
138
142
  <span key={i} className={`${s.iconSize} flex-shrink-0 ${hasProgress ? 'relative' : ''}`}><Icon className={s.iconSize} /></span>
139
143
  ) : null
140
144
  })}
141
- <span className={`min-w-0 truncate ${hasProgress ? 'relative' : ''}`} style={cssTransform ? { textTransform: cssTransform } : undefined}>
145
+ <span className={`min-w-0 truncate -mt-px ${hasProgress ? 'relative' : ''}`} style={cssTransform ? { textTransform: cssTransform } : undefined}>
142
146
  {transformText(text, textTransform)}
143
147
  </span>
144
148
  </>
@@ -3,6 +3,7 @@
3
3
  import { useState, useRef, useCallback, useEffect, type ReactNode } from 'react'
4
4
  import { ChevronLeft, ChevronRight, X } from 'lucide-react'
5
5
  import { cn } from '../lib/cn.ts'
6
+ import type { FormColor } from '../lib/form-colors.ts'
6
7
 
7
8
  export interface LayoutTab {
8
9
  id: string
@@ -26,6 +27,7 @@ export interface LayoutTabBarProps {
26
27
  onClose?: (id: string) => void
27
28
  onReorder?: (fromIndex: number, toIndex: number) => void
28
29
  className?: string
30
+ accentColor?: FormColor
29
31
  }
30
32
 
31
33
  const COLORS: Record<string, { border: string; text: string }> = {
@@ -77,7 +79,7 @@ function getGradient(color?: string, isActive?: boolean): string | undefined {
77
79
  : `linear-gradient(135deg, rgba(${rgb}, 0.36) 0%, rgba(${rgb}, 0.06) 100%)`
78
80
  }
79
81
 
80
- export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, className }: LayoutTabBarProps) {
82
+ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, className, accentColor: _accentColor }: LayoutTabBarProps) {
81
83
  const scrollRef = useRef<HTMLDivElement>(null)
82
84
  const [showLeft, setShowLeft] = useState(false)
83
85
  const [showRight, setShowRight] = useState(false)
@@ -167,7 +169,7 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
167
169
  // ── Render ────────────────────────────────────────────
168
170
 
169
171
  return (
170
- <div className={cn('relative flex items-center min-h-12 flex-shrink-0 bg-neutral-900/80 border-b border-neutral-700/50', className)}>
172
+ <div className={cn('relative flex items-center min-h-12 flex-shrink-0 bg-neutral-980/80 border-b border-neutral-700/50', className)}>
171
173
  {showLeft && (
172
174
  <div className="flex items-center px-2 shrink-0">
173
175
  <button type="button" onClick={() => scroll('left')} className="text-neutral-500 hover:text-neutral-400 transition-colors cursor-pointer">
@@ -206,8 +208,8 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
206
208
  className={cn(
207
209
  'group flex flex-col justify-start gap-0.5 px-2.5 rounded-t-lg cursor-pointer transition-all shrink-0 select-none relative border-t-2',
208
210
  isActive
209
- ? cn('bg-neutral-800', borderC)
210
- : 'bg-neutral-800/50 text-neutral-400 hover:bg-neutral-800/80 hover:text-neutral-300 border-transparent opacity-60 hover:opacity-90',
211
+ ? cn('bg-neutral-960', borderC)
212
+ : 'bg-neutral-960/50 text-neutral-400 hover:bg-neutral-960/80 hover:text-neutral-300 border-transparent opacity-60 hover:opacity-90',
211
213
  isDragged && 'opacity-50',
212
214
  )}
213
215
  style={{
@@ -233,15 +235,15 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
233
235
 
234
236
  {/* Close button */}
235
237
  {tab.closable && onClose && (
236
- <span
237
- role="button"
238
+ <button
239
+ type="button"
240
+ aria-label={`Close ${tab.title}`}
238
241
  tabIndex={-1}
239
242
  onClick={(e) => { e.stopPropagation(); onClose(tab.id) }}
240
- onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); onClose(tab.id) } }}
241
243
  className="absolute right-1.5 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 hover:bg-neutral-700 rounded p-0.5 transition-opacity cursor-pointer"
242
244
  >
243
245
  <X className="w-3 h-3" />
244
- </span>
246
+ </button>
245
247
  )}
246
248
  </button>
247
249
 
@@ -269,7 +271,7 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
269
271
  const ghostSubIconC = getTextClass(t.subtitleIconColor || t.subtitleColor || t.selectedColor || t.color)
270
272
  return (
271
273
  <div className="fixed z-50 pointer-events-none opacity-90" style={{ left: drag.x + 12, top: drag.y - 20 }}>
272
- <div className={cn('flex flex-col gap-0.5 px-2.5 py-1.5 rounded-t-lg border-t-2 shadow-xl bg-neutral-800', ghostBorder)}>
274
+ <div className={cn('flex flex-col gap-0.5 px-2.5 py-1.5 rounded-t-lg border-t-2 shadow-lg bg-neutral-960', ghostBorder)}>
273
275
  <div className="flex items-center gap-1.5">
274
276
  {t.icon && <span className={cn('shrink-0 inline-flex', ghostIconC)} style={{ width: 14, height: 14 }}>{t.icon}</span>}
275
277
  <span className={cn('text-md font-medium', ghostTitleC)}>{t.title}</span>
@@ -1,10 +1,12 @@
1
- import { useRef, useState } from 'react'
1
+ import { useId, useRef, useState } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { Info, AlertTriangle, AlertCircle, Check } from 'lucide-react'
4
4
  import { useModalBehavior } from '../hooks/use-modal-behavior.ts'
5
5
  import { IconButton, type ActionItem } from './icon-button.tsx'
6
6
  import { FormActions } from './form-actions.tsx'
7
7
  import type { ReactNode } from 'react'
8
+ import type { FormColor } from '../lib/form-colors.ts'
9
+ import { useAccentColor, AccentColorProvider } from '../lib/accent-context.ts'
8
10
 
9
11
  export type ModalKind = 'info' | 'warning' | 'error' | 'orange' | 'success'
10
12
  export type ModalSize = 'sm' | 'md' | 'lg' | 'xl'
@@ -42,35 +44,39 @@ interface ModalProps {
42
44
 
43
45
  function Modal({ isOpen, onClose, title, children, kind = 'info', size = 'md', hideCloseButton = false, headerActions, testId }: ModalProps) {
44
46
  const modalRef = useRef<HTMLDivElement>(null)
47
+ const titleId = useId()
45
48
 
46
- useModalBehavior(isOpen, onClose)
49
+ useModalBehavior(isOpen, onClose, modalRef)
47
50
 
48
51
  if (!isOpen) return null
49
52
 
50
53
  return createPortal(
51
54
  <div className="fixed inset-0 z-50 flex items-center justify-center">
52
- <div className="absolute inset-0 bg-[var(--dialog-backdrop)] backdrop-blur-sm" onClick={onClose} />
55
+ <div className="absolute inset-0 bg-[var(--dialog-backdrop)]" onClick={onClose} aria-hidden="true" />
53
56
  <div
54
57
  ref={modalRef}
58
+ role="dialog"
59
+ aria-modal="true"
60
+ aria-labelledby={titleId}
55
61
  data-testid={testId}
56
- className={`relative bg-neutral-900 border border-neutral-700 rounded-xl shadow-2xl ${SIZE_CLASSES[size]} w-full mx-4 overflow-hidden`}
62
+ className={`relative bg-neutral-980 border border-neutral-700 rounded-lg shadow-lg ${SIZE_CLASSES[size]} w-full mx-4 overflow-hidden`}
57
63
  >
58
- <div className="flex items-center gap-3 px-5 py-4 border-b border-neutral-800">
64
+ <div className="flex items-center gap-3 px-4 py-4 border-b border-neutral-960">
59
65
  {KIND_ICON[kind]}
60
- <h3 className="text-lg font-semibold text-white flex-1">{title}</h3>
66
+ <h3 id={titleId} className="text-lg font-semibold text-white flex-1 min-w-0 truncate">{title}</h3>
61
67
  {headerActions?.map((a, i) => <IconButton key={i} {...a} />)}
62
68
  {!hideCloseButton && (
63
69
  <IconButton
64
70
  icon="x"
65
71
  onClick={onClose}
66
72
  size="sm"
67
- color="neutral"
73
+ accentColor="neutral"
68
74
  tooltip={{ description: 'Close this modal' }}
69
75
  testId="modal-close"
70
76
  />
71
77
  )}
72
78
  </div>
73
- <div className="px-5 py-4">{children}</div>
79
+ <div className="px-4 py-4">{children}</div>
74
80
  </div>
75
81
  </div>,
76
82
  document.body,
@@ -89,6 +95,7 @@ export interface ConfirmModalProps {
89
95
  confirmColor?: 'red' | 'blue' | 'orange' | 'yellow'
90
96
  isLoading?: boolean
91
97
  confirmDisabled?: boolean
98
+ accentColor?: FormColor
92
99
  }
93
100
 
94
101
  export function ConfirmModal({
@@ -103,7 +110,10 @@ export function ConfirmModal({
103
110
  confirmColor = 'blue',
104
111
  isLoading = false,
105
112
  confirmDisabled = false,
113
+ accentColor: accentColorProp,
106
114
  }: ConfirmModalProps) {
115
+ const contextAccent = useAccentColor()
116
+ const effectiveColor = accentColorProp ?? contextAccent ?? 'blue'
107
117
  const [isConfirming, setIsConfirming] = useState(false)
108
118
 
109
119
  const isDisabled = isLoading || isConfirming || confirmDisabled
@@ -122,6 +132,7 @@ export function ConfirmModal({
122
132
  }
123
133
 
124
134
  return (
135
+ <AccentColorProvider value={effectiveColor}>
125
136
  <Modal isOpen={isOpen} onClose={onClose} title={title} kind={kind} hideCloseButton>
126
137
  <div className="text-neutral-300 mb-6">
127
138
  {message}
@@ -150,6 +161,7 @@ export function ConfirmModal({
150
161
  confirmStatus={isInProgress ? 'loading' : undefined}
151
162
  />
152
163
  </Modal>
164
+ </AccentColorProvider>
153
165
  )
154
166
  }
155
167
 
@@ -159,6 +171,7 @@ export interface AlertModalProps {
159
171
  title: string
160
172
  message: string
161
173
  kind?: ModalKind
174
+ accentColor?: FormColor
162
175
  }
163
176
 
164
177
  export function AlertModal({
@@ -167,8 +180,12 @@ export function AlertModal({
167
180
  title,
168
181
  message,
169
182
  kind = 'info',
183
+ accentColor: accentColorProp,
170
184
  }: AlertModalProps) {
185
+ const contextAccent = useAccentColor()
186
+ const effectiveColor = accentColorProp ?? contextAccent ?? 'blue'
171
187
  return (
188
+ <AccentColorProvider value={effectiveColor}>
172
189
  <Modal isOpen={isOpen} onClose={onClose} title={title} kind={kind} hideCloseButton>
173
190
  <div className="text-neutral-300 mb-6">{message}</div>
174
191
  <FormActions
@@ -178,5 +195,6 @@ export function AlertModal({
178
195
  confirmTooltip="Dismiss this alert"
179
196
  />
180
197
  </Modal>
198
+ </AccentColorProvider>
181
199
  )
182
200
  }
@@ -3,6 +3,7 @@
3
3
  import { iconMap, type IconName } from './icon-button.tsx'
4
4
  import { Label, type LabelColor } from './label.tsx'
5
5
  import { cn } from '../lib/cn.ts'
6
+ import type { FormColor } from '../lib/form-colors.ts'
6
7
 
7
8
  export interface NavCardProps {
8
9
  title: string
@@ -11,11 +12,12 @@ export interface NavCardProps {
11
12
  /** Custom icon component. Takes precedence over icon name. */
12
13
  IconComponent?: React.ComponentType<{ className?: string }>
13
14
  iconColor?: string
14
- label?: { text: string; color: LabelColor; tooltip: { description: string } }
15
+ label?: { text: string; color: LabelColor; icon?: IconName; tooltip: { description: string } }
15
16
  stats?: string
16
17
  onClick?: () => void
17
18
  disabled?: boolean
18
19
  className?: string
20
+ accentColor?: FormColor
19
21
  }
20
22
 
21
23
  export function NavCard({
@@ -29,6 +31,7 @@ export function NavCard({
29
31
  onClick,
30
32
  disabled = false,
31
33
  className,
34
+ accentColor: _accentColor,
32
35
  }: NavCardProps) {
33
36
  const Icon = IconComponent ?? (icon ? iconMap[icon] : undefined)
34
37
 
@@ -38,7 +41,7 @@ export function NavCard({
38
41
  onClick={disabled ? undefined : onClick}
39
42
  disabled={disabled}
40
43
  className={cn(
41
- 'relative w-full text-left rounded-lg border border-neutral-700 bg-neutral-800 p-4 transition-all duration-200 cursor-pointer',
44
+ 'relative w-full text-left rounded-lg border border-neutral-700 bg-neutral-960 p-4 transition-all duration-200 cursor-pointer',
42
45
  !disabled && 'hover:-translate-y-0.5 hover:border-neutral-600 hover:bg-neutral-700',
43
46
  disabled && 'opacity-50 cursor-not-allowed',
44
47
  className,
@@ -46,7 +49,7 @@ export function NavCard({
46
49
  >
47
50
  {label && (
48
51
  <span className="absolute top-3 right-3">
49
- <Label text={label.text} color={label.color} size="xs" tooltip={label.tooltip} />
52
+ <Label text={label.text} accentColor={label.color} icon={label.icon} size="xs" tooltip={label.tooltip} />
50
53
  </span>
51
54
  )}
52
55
 
@@ -59,7 +62,7 @@ export function NavCard({
59
62
  </div>
60
63
  )}
61
64
 
62
- <h3 className="text-md font-medium text-neutral-200">{title}</h3>
65
+ <h3 className="text-md font-medium text-neutral-200 truncate">{title}</h3>
63
66
 
64
67
  {description && (
65
68
  <p className="mt-1 text-sm text-neutral-500 leading-relaxed line-clamp-2">{description}</p>