@toolr/ui-design 0.1.6 → 0.1.8

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 (61) hide show
  1. package/components/hooks/use-click-outside.ts +10 -3
  2. package/components/hooks/use-modal-behavior.ts +53 -0
  3. package/components/hooks/use-navigation-history.ts +7 -2
  4. package/components/hooks/use-resizable-sidebar.ts +38 -0
  5. package/components/lib/form-colors.ts +40 -0
  6. package/components/sections/captured-issues/captured-issues-panel.tsx +3 -3
  7. package/components/sections/captured-issues/use-captured-issues.ts +9 -3
  8. package/components/sections/golden-snapshots/file-diff-viewer.tsx +1 -1
  9. package/components/sections/golden-snapshots/status-overview.tsx +1 -1
  10. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +4 -40
  11. package/components/sections/prompt-editor/index.ts +0 -7
  12. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +4 -40
  13. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +4 -36
  14. package/components/sections/snippets-editor/snippets-editor.tsx +6 -39
  15. package/components/settings/SettingsHeader.tsx +0 -1
  16. package/components/settings/SettingsTreeNav.tsx +9 -12
  17. package/components/ui/action-dialog.tsx +19 -55
  18. package/components/ui/ai-action-button.tsx +2 -4
  19. package/components/ui/badge.tsx +15 -23
  20. package/components/ui/breadcrumb.tsx +11 -71
  21. package/components/ui/checkbox.tsx +19 -27
  22. package/components/ui/collapsible-section.tsx +4 -41
  23. package/components/ui/confirm-badge.tsx +14 -23
  24. package/components/ui/cookie-consent.tsx +18 -2
  25. package/components/ui/debounce-border-overlay.tsx +31 -0
  26. package/components/ui/detail-section.tsx +2 -19
  27. package/components/ui/editor-placeholder-card.tsx +10 -9
  28. package/components/ui/execution-details-panel.tsx +2 -7
  29. package/components/ui/extension-list-card.tsx +1 -1
  30. package/components/ui/file-structure-section.tsx +3 -18
  31. package/components/ui/file-tree.tsx +6 -18
  32. package/components/ui/files-panel.tsx +3 -11
  33. package/components/ui/filter-dropdown.tsx +5 -2
  34. package/components/ui/form-actions.tsx +11 -8
  35. package/components/ui/icon-button.tsx +7 -6
  36. package/components/ui/input.tsx +18 -29
  37. package/components/ui/label.tsx +7 -17
  38. package/components/ui/layout-tab-bar.tsx +5 -5
  39. package/components/ui/modal.tsx +10 -18
  40. package/components/ui/nav-card.tsx +3 -18
  41. package/components/ui/navigation-bar.tsx +12 -73
  42. package/components/ui/number-input.tsx +6 -0
  43. package/components/ui/registry-browser.tsx +6 -20
  44. package/components/ui/registry-card.tsx +3 -7
  45. package/components/ui/resizable-textarea.tsx +13 -35
  46. package/components/ui/segmented-toggle.tsx +4 -1
  47. package/components/ui/select.tsx +8 -14
  48. package/components/ui/selection-grid.tsx +6 -50
  49. package/components/ui/setting-row.tsx +5 -5
  50. package/components/ui/settings-card.tsx +2 -2
  51. package/components/ui/settings-info-box.tsx +6 -24
  52. package/components/ui/sort-dropdown.tsx +8 -5
  53. package/components/ui/status-card.tsx +2 -13
  54. package/components/ui/tab-bar.tsx +17 -33
  55. package/components/ui/toggle.tsx +22 -30
  56. package/components/ui/tooltip.tsx +11 -23
  57. package/dist/index.d.ts +71 -142
  58. package/dist/index.js +1630 -2436
  59. package/index.ts +8 -7
  60. package/package.json +9 -1
  61. package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
@@ -73,22 +73,25 @@ export function SortDropdown({
73
73
  return (
74
74
  <div className="relative flex items-center" ref={ref} onKeyDown={handleKeyDown}>
75
75
  <button
76
+ aria-expanded={isOpen}
77
+ aria-haspopup="listbox"
76
78
  onClick={() => setIsOpen(!isOpen)}
77
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}`}
78
80
  >
79
- <span
80
- className={`${FORM_COLORS[color].accent} hover:brightness-125 transition-colors`}
81
+ <button
82
+ type="button"
83
+ aria-label={ascending ? 'Sort descending' : 'Sort ascending'}
84
+ className={`${FORM_COLORS[color].accent} hover:brightness-125 transition-colors cursor-pointer`}
81
85
  onClick={(e) => { e.stopPropagation(); onToggleDirection() }}
82
- role="button"
83
86
  >
84
87
  <DirIcon className="w-3 h-3" />
85
- </span>
88
+ </button>
86
89
  <span className="whitespace-nowrap">{current.label}</span>
87
90
  <ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
88
91
  </button>
89
92
 
90
93
  {isOpen && (
91
- <div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] bg-[var(--popover)] border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
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`}>
92
95
  {fields.map((f, idx) => (
93
96
  <button
94
97
  key={f.value}
@@ -1,19 +1,8 @@
1
1
  /** Status card displaying a list of labeled items with color-coded status indicators. */
2
2
 
3
- import {
4
- Settings, Shield, Database, Globe, Zap, Code, Terminal,
5
- Star, Cloud, Bell, Heart, Sparkles, Bot, Plug,
6
- } from 'lucide-react'
7
- import type { LucideIcon } from 'lucide-react'
8
- import type { IconName } from './icon-button.tsx'
3
+ import { iconMap, type IconName } from './icon-button.tsx'
9
4
  import { cn } from '../lib/cn.ts'
10
5
 
11
- const iconSubset: Partial<Record<IconName, LucideIcon>> = {
12
- settings: Settings, shield: Shield, database: Database, globe: Globe,
13
- zap: Zap, code: Code, terminal: Terminal, star: Star, cloud: Cloud,
14
- bell: Bell, heart: Heart, sparkles: Sparkles, bot: Bot, plug: Plug,
15
- }
16
-
17
6
  type StatusType = 'success' | 'warning' | 'error' | 'info' | 'neutral'
18
7
 
19
8
  export interface StatusItem {
@@ -55,7 +44,7 @@ export function StatusCard({
55
44
  action,
56
45
  className,
57
46
  }: StatusCardProps) {
58
- const Icon = icon ? iconSubset[icon] : undefined
47
+ const Icon = icon ? iconMap[icon] : undefined
59
48
 
60
49
  return (
61
50
  <div className={cn('rounded-lg border border-neutral-700 bg-neutral-800 overflow-hidden', className)}>
@@ -1,38 +1,12 @@
1
1
  /** Tab bar with underline, pill, and card variants, closable tabs, color-coded icons, and auto-collapse to icon-only. */
2
2
 
3
3
  import { useRef, useState, useEffect, useCallback } from 'react'
4
- import {
5
- Settings, Folder, File, Code, Terminal, Database,
6
- Globe, Star, Users, User, Tag, X,
7
- Zap, Shield, Sparkles, Eye, Lock, Search, Heart,
8
- } from 'lucide-react'
9
- import type { LucideIcon } from 'lucide-react'
10
- import type { IconName } from './icon-button.tsx'
4
+ import { X } from 'lucide-react'
5
+ import { iconMap, type IconName } from './icon-button.tsx'
11
6
  import { Badge, type BadgeColor } from './badge.tsx'
12
7
  import { cn } from '../lib/cn.ts'
13
8
  import { Tooltip } from './tooltip.tsx'
14
9
 
15
- const iconSubset: Partial<Record<IconName, LucideIcon>> = {
16
- folder: Folder,
17
- file: File,
18
- settings: Settings,
19
- code: Code,
20
- terminal: Terminal,
21
- database: Database,
22
- globe: Globe,
23
- star: Star,
24
- users: Users,
25
- user: User,
26
- tag: Tag,
27
- zap: Zap,
28
- shield: Shield,
29
- sparkles: Sparkles,
30
- eye: Eye,
31
- lock: Lock,
32
- search: Search,
33
- heart: Heart,
34
- }
35
-
36
10
  export interface Tab {
37
11
  id: string
38
12
  label: string
@@ -102,7 +76,7 @@ function estimateTabsWidth(tabs: Tab[], size: keyof typeof sizeConfig): number {
102
76
  }
103
77
 
104
78
  function TabIcon({ icon, size, color }: { icon: IconName; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; color?: string }) {
105
- const Icon = iconSubset[icon]
79
+ const Icon = iconMap[icon]
106
80
  if (!Icon) return null
107
81
  const s = sizeConfig[size]
108
82
  const c = getColors(color)
@@ -114,11 +88,12 @@ function TabBadge({ badge, size, badgeColor }: { badge: number | string; size: '
114
88
  return <Badge value={badge} color={badgeColor} size={s.badgeSize} className="flex-shrink-0" />
115
89
  }
116
90
 
117
- function CloseButton({ size, onClick }: { size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; onClick: () => void }) {
91
+ function CloseButton({ size, onClick, tabLabel }: { size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; onClick: () => void; tabLabel: string }) {
118
92
  const s = sizeConfig[size]
119
93
  return (
120
94
  <button
121
95
  type="button"
96
+ aria-label={`Close ${tabLabel}`}
122
97
  onClick={(e) => {
123
98
  e.stopPropagation()
124
99
  onClick()
@@ -149,6 +124,8 @@ function CompactTab({
149
124
  return (
150
125
  <button
151
126
  type="button"
127
+ role="tab"
128
+ aria-selected={isActive}
152
129
  onClick={onSelect}
153
130
  className={cn(
154
131
  'relative flex items-center justify-center transition-colors cursor-pointer',
@@ -186,6 +163,8 @@ function UnderlineTab({
186
163
  return (
187
164
  <button
188
165
  type="button"
166
+ role="tab"
167
+ aria-selected={isActive}
189
168
  onClick={onSelect}
190
169
  className={cn(
191
170
  'group relative flex items-center whitespace-nowrap transition-colors cursor-pointer',
@@ -197,7 +176,7 @@ function UnderlineTab({
197
176
  {tab.icon && <TabIcon icon={tab.icon} size={size} color={isActive ? tab.color : undefined} />}
198
177
  <span>{tab.label}</span>
199
178
  {tab.badge !== undefined && <TabBadge badge={tab.badge} size={size} badgeColor={tab.badgeColor} />}
200
- {showClose && <CloseButton size={size} onClick={onClose!} />}
179
+ {showClose && <CloseButton size={size} onClick={onClose!} tabLabel={tab.label} />}
201
180
  {isActive && (
202
181
  <span className={cn('absolute bottom-0 left-0 right-0 h-0.5 rounded-full', c.indicator)} />
203
182
  )}
@@ -215,6 +194,8 @@ function PillTab({
215
194
  return (
216
195
  <button
217
196
  type="button"
197
+ role="tab"
198
+ aria-selected={isActive}
218
199
  onClick={onSelect}
219
200
  className={cn(
220
201
  'group flex items-center whitespace-nowrap rounded-md transition-colors cursor-pointer',
@@ -228,7 +209,7 @@ function PillTab({
228
209
  {tab.icon && <TabIcon icon={tab.icon} size={size} color={isActive ? tab.color : undefined} />}
229
210
  <span>{tab.label}</span>
230
211
  {tab.badge !== undefined && <TabBadge badge={tab.badge} size={size} badgeColor={tab.badgeColor} />}
231
- {showClose && <CloseButton size={size} onClick={onClose!} />}
212
+ {showClose && <CloseButton size={size} onClick={onClose!} tabLabel={tab.label} />}
232
213
  </button>
233
214
  )
234
215
  }
@@ -243,6 +224,8 @@ function CardTab({
243
224
  return (
244
225
  <button
245
226
  type="button"
227
+ role="tab"
228
+ aria-selected={isActive}
246
229
  onClick={onSelect}
247
230
  className={cn(
248
231
  'group relative flex items-center whitespace-nowrap transition-colors cursor-pointer rounded-t-lg border border-b-0',
@@ -256,7 +239,7 @@ function CardTab({
256
239
  {tab.icon && <TabIcon icon={tab.icon} size={size} color={isActive ? tab.color : undefined} />}
257
240
  <span>{tab.label}</span>
258
241
  {tab.badge !== undefined && <TabBadge badge={tab.badge} size={size} badgeColor={tab.badgeColor} />}
259
- {showClose && <CloseButton size={size} onClick={onClose!} />}
242
+ {showClose && <CloseButton size={size} onClick={onClose!} tabLabel={tab.label} />}
260
243
  {isActive && (
261
244
  <span className="absolute -bottom-px left-0 right-0 h-px bg-neutral-800" />
262
245
  )}
@@ -303,6 +286,7 @@ export function TabBar({
303
286
  return (
304
287
  <div
305
288
  ref={containerRef}
289
+ role="tablist"
306
290
  className={cn(
307
291
  'flex items-end',
308
292
  variant === 'underline' && 'border-b border-neutral-700',
@@ -7,22 +7,10 @@
7
7
  * - List items - inline toggle controls
8
8
  */
9
9
 
10
- export type ToggleColor =
11
- | 'blue'
12
- | 'green'
13
- | 'red'
14
- | 'orange'
15
- | 'cyan'
16
- | 'yellow'
17
- | 'purple'
18
- | 'indigo'
19
- | 'emerald'
20
- | 'amber'
21
- | 'violet'
22
- | 'neutral'
23
- | 'sky'
24
- | 'pink'
25
- | 'teal'
10
+ import { type AccentColor } from '../lib/form-colors.ts'
11
+ import { cn } from '../lib/cn.ts'
12
+
13
+ export type ToggleColor = AccentColor
26
14
 
27
15
  // Border colors per accent
28
16
  const BORDER_COLORS: Record<ToggleColor, { idle: string; active: string }> = {
@@ -90,8 +78,6 @@ const TOGGLE_SIZES: Record<ToggleSize, { track: string; knob: string; translate:
90
78
  lg: { track: 'w-14 h-7', knob: 'w-6 h-6', translate: 'translate-x-7' },
91
79
  }
92
80
 
93
- export type ToggleVariant = 'outline' | 'filled'
94
-
95
81
  export interface ToggleProps {
96
82
  checked: boolean
97
83
  onChange: (checked: boolean) => void
@@ -99,7 +85,8 @@ export interface ToggleProps {
99
85
  size?: ToggleSize
100
86
  className?: string
101
87
  color?: ToggleColor
102
- variant?: ToggleVariant
88
+ /** Accessible label — required for screen readers */
89
+ 'aria-label'?: string
103
90
  /** Test ID for E2E testing */
104
91
  testId?: string
105
92
  }
@@ -109,8 +96,9 @@ export function Toggle({
109
96
  onChange,
110
97
  disabled = false,
111
98
  size = 'sm',
112
- className = '',
99
+ className,
113
100
  color = 'blue',
101
+ 'aria-label': ariaLabel,
114
102
  testId,
115
103
  }: ToggleProps) {
116
104
  const s = TOGGLE_SIZES[size]
@@ -119,23 +107,27 @@ export function Toggle({
119
107
  return (
120
108
  <button
121
109
  type="button"
110
+ role="switch"
111
+ aria-checked={checked}
112
+ aria-label={ariaLabel}
122
113
  onClick={() => !disabled && onChange(!checked)}
123
114
  disabled={disabled}
124
115
  data-testid={testId}
125
116
  style={{ boxShadow: `inset 0 0 0 1px ${checked ? bc.active : bc.idle}` }}
126
- className={`
127
- relative ${s.track} rounded-full transition-all flex-shrink-0
128
- cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed
129
- ${checked ? TOGGLE_CHECKED_TRACK[color] : TOGGLE_UNCHECKED_TRACK[color]}
130
- ${className}
131
- `}
117
+ className={cn(
118
+ 'relative rounded-full transition-all flex-shrink-0 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed',
119
+ s.track,
120
+ checked ? TOGGLE_CHECKED_TRACK[color] : TOGGLE_UNCHECKED_TRACK[color],
121
+ className,
122
+ )}
132
123
  >
133
124
  <span
134
125
  style={{ backgroundColor: checked ? kc.on : kc.off }}
135
- className={`
136
- block absolute top-0.5 left-0.5 ${s.knob} rounded-full transition-transform
137
- ${checked ? s.translate : 'translate-x-0'}
138
- `}
126
+ className={cn(
127
+ 'block absolute top-0.5 left-0.5 rounded-full transition-transform',
128
+ s.knob,
129
+ checked ? s.translate : 'translate-x-0',
130
+ )}
139
131
  />
140
132
  </button>
141
133
  )
@@ -1,6 +1,7 @@
1
- import { useState, useRef, useLayoutEffect, useEffect } from 'react'
1
+ import { useState, useRef, useLayoutEffect, useEffect, useCallback } from 'react'
2
2
  import type { ReactNode } from 'react'
3
3
  import { createPortal } from 'react-dom'
4
+ import { useClickOutside } from '../hooks/use-click-outside.ts'
4
5
 
5
6
  export interface TooltipContent {
6
7
  title?: string
@@ -201,37 +202,25 @@ export function Tooltip({
201
202
  }
202
203
 
203
204
  // Click-outside dismissal for click trigger mode
204
- useEffect(() => {
205
- if (trigger !== 'click' || !isVisible) return
206
- const handleClickOutside = (e: MouseEvent) => {
207
- if (
208
- triggerRef.current && !triggerRef.current.contains(e.target as Node) &&
209
- tooltipRef.current && !tooltipRef.current.contains(e.target as Node)
210
- ) {
211
- setIsVisible(false)
212
- }
213
- }
214
- document.addEventListener('mousedown', handleClickOutside)
215
- return () => document.removeEventListener('mousedown', handleClickOutside)
216
- }, [trigger, isVisible])
205
+ const dismissClickTooltip = useCallback(() => setIsVisible(false), [])
206
+ useClickOutside([triggerRef, tooltipRef], isVisible && trigger === 'click', dismissClickTooltip)
217
207
 
218
208
  const updatePosition = () => {
219
209
  if (!triggerRef.current) return
220
210
  const triggerRect = triggerRef.current.getBoundingClientRect()
221
211
  const tooltipEl = tooltipRef.current
212
+ const tooltipRect = tooltipEl?.getBoundingClientRect()
222
213
 
223
214
  let resolvedPosition: ResolvedPosition = position === 'auto' ? 'top' : position
224
215
 
225
- if (position === 'auto' && tooltipEl) {
226
- const tooltipRect = tooltipEl.getBoundingClientRect()
216
+ if (position === 'auto' && tooltipRect) {
227
217
  resolvedPosition = resolveAutoPosition(triggerRect, tooltipRect)
228
218
  setActualPosition(resolvedPosition)
229
219
  }
230
220
 
231
221
  let newCoords = calculateBasePosition(triggerRect, resolvedPosition, align)
232
222
 
233
- if (tooltipEl) {
234
- const tooltipRect = tooltipEl.getBoundingClientRect()
223
+ if (tooltipRect) {
235
224
  newCoords = adjustForTooltipSize(newCoords, tooltipRect, resolvedPosition, align)
236
225
  newCoords = clampToViewport(newCoords, tooltipRect)
237
226
  }
@@ -258,13 +247,12 @@ export function Tooltip({
258
247
  const tooltipContent = (
259
248
  <div
260
249
  ref={tooltipRef}
261
- className={`fixed px-3 py-1.5 bg-[var(--popover)] border border-neutral-600 rounded-lg shadow-xl z-[9999] ${interactive || trigger === 'click' ? '' : 'pointer-events-none'} ${multiline ? 'whitespace-pre-line' : 'whitespace-nowrap'}`}
250
+ role="tooltip"
251
+ className={`fixed px-3 py-1.5 bg-[var(--popover)] border border-neutral-600 rounded-lg shadow-lg z-[9999] ${interactive || trigger === 'click' ? '' : 'pointer-events-none'} ${multiline ? 'whitespace-pre-line' : 'whitespace-nowrap'}`}
262
252
  style={{
263
253
  top: coords.top,
264
254
  left: coords.left,
265
- opacity: isVisible ? 1 : 0,
266
- visibility: isVisible ? 'visible' : 'hidden',
267
- transition: 'opacity 150ms, visibility 150ms',
255
+ opacity: 1,
268
256
  ...(multiline ? { maxWidth: maxWidth ?? '80vw' } : {}),
269
257
  ...(maxHeight ? { maxHeight, overflowY: 'auto' as const } : {}),
270
258
  }}
@@ -290,7 +278,7 @@ export function Tooltip({
290
278
  >
291
279
  {children}
292
280
  </div>
293
- {createPortal(tooltipContent, document.body)}
281
+ {isVisible && createPortal(tooltipContent, document.body)}
294
282
  </>
295
283
  )
296
284
  }