@toolr/ui-design 0.1.0

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 (112) hide show
  1. package/README.md +63 -0
  2. package/components/content/info-panel-primitives.tsx +297 -0
  3. package/components/diagrams/diagram-utils.tsx +908 -0
  4. package/components/hooks/use-click-outside.ts +27 -0
  5. package/components/hooks/use-dropdown-max-height.ts +20 -0
  6. package/components/hooks/use-navigation-history.ts +94 -0
  7. package/components/lib/ai-tools.tsx +44 -0
  8. package/components/lib/cn.ts +6 -0
  9. package/components/lib/form-colors.ts +32 -0
  10. package/components/lib/theme-engine.ts +97 -0
  11. package/components/lib/toolr-brand.tsx +31 -0
  12. package/components/sections/ai-tools-paths/index.ts +37 -0
  13. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
  14. package/components/sections/ai-tools-paths/types.ts +111 -0
  15. package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
  16. package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
  17. package/components/sections/captured-issues/index.ts +38 -0
  18. package/components/sections/captured-issues/types.ts +113 -0
  19. package/components/sections/captured-issues/use-captured-issues.ts +111 -0
  20. package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
  21. package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
  22. package/components/sections/golden-snapshots/index.ts +145 -0
  23. package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
  24. package/components/sections/golden-snapshots/status-overview.tsx +305 -0
  25. package/components/sections/golden-snapshots/types.ts +288 -0
  26. package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
  27. package/components/sections/golden-snapshots/version-manager.tsx +186 -0
  28. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
  29. package/components/sections/prompt-editor/index.ts +121 -0
  30. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
  31. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
  32. package/components/sections/prompt-editor/types.ts +101 -0
  33. package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
  34. package/components/sections/report-bug/error-logger.ts +392 -0
  35. package/components/sections/report-bug/index.ts +59 -0
  36. package/components/sections/report-bug/issue-reporter-api.ts +83 -0
  37. package/components/sections/report-bug/report-bug-form.tsx +282 -0
  38. package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
  39. package/components/sections/report-bug/use-report-bug.ts +170 -0
  40. package/components/sections/snapshot-browser/index.ts +53 -0
  41. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
  42. package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
  43. package/components/sections/snapshot-browser/types.ts +106 -0
  44. package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
  45. package/components/sections/snippets-editor/index.ts +31 -0
  46. package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
  47. package/components/sections/snippets-editor/types.ts +48 -0
  48. package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
  49. package/components/ui/action-dialog.tsx +309 -0
  50. package/components/ui/ai-action-button.tsx +137 -0
  51. package/components/ui/ai-execution-action-buttons.tsx +106 -0
  52. package/components/ui/badge.tsx +67 -0
  53. package/components/ui/bottom-panel-header.tsx +240 -0
  54. package/components/ui/breadcrumb.tsx +168 -0
  55. package/components/ui/checkbox.tsx +102 -0
  56. package/components/ui/collapsible-section.tsx +100 -0
  57. package/components/ui/confirm-badge.tsx +71 -0
  58. package/components/ui/detail-section.tsx +67 -0
  59. package/components/ui/detail-view-wrapper.tsx +55 -0
  60. package/components/ui/editor-placeholder-card.tsx +197 -0
  61. package/components/ui/editor-toolbar.tsx +123 -0
  62. package/components/ui/execution-details-panel.tsx +93 -0
  63. package/components/ui/extension-list-card.tsx +105 -0
  64. package/components/ui/file-structure-section.tsx +373 -0
  65. package/components/ui/file-tree.tsx +171 -0
  66. package/components/ui/files-panel.tsx +251 -0
  67. package/components/ui/filter-dropdown.tsx +173 -0
  68. package/components/ui/form-actions.tsx +127 -0
  69. package/components/ui/frontmatter-form-header.tsx +80 -0
  70. package/components/ui/icon-button.tsx +388 -0
  71. package/components/ui/input.tsx +211 -0
  72. package/components/ui/label.tsx +159 -0
  73. package/components/ui/layout-tab-bar.tsx +289 -0
  74. package/components/ui/modal.tsx +194 -0
  75. package/components/ui/nav-card.tsx +81 -0
  76. package/components/ui/navigation-bar.tsx +285 -0
  77. package/components/ui/number-input.tsx +165 -0
  78. package/components/ui/registry-browser.tsx +261 -0
  79. package/components/ui/registry-card.tsx +710 -0
  80. package/components/ui/registry-detail.tsx +224 -0
  81. package/components/ui/resizable-textarea.tsx +290 -0
  82. package/components/ui/scope-badge.tsx +67 -0
  83. package/components/ui/segmented-toggle.tsx +133 -0
  84. package/components/ui/select.tsx +172 -0
  85. package/components/ui/selection-grid.tsx +313 -0
  86. package/components/ui/setting-row.tsx +97 -0
  87. package/components/ui/snapshot-card.tsx +107 -0
  88. package/components/ui/snippets-panel.tsx +161 -0
  89. package/components/ui/sort-dropdown.tsx +109 -0
  90. package/components/ui/status-card.tsx +96 -0
  91. package/components/ui/tab-bar.tsx +340 -0
  92. package/components/ui/toggle.tsx +142 -0
  93. package/components/ui/tooltip.tsx +326 -0
  94. package/dist/content.d.ts +110 -0
  95. package/dist/content.js +195 -0
  96. package/dist/diagrams.d.ts +371 -0
  97. package/dist/diagrams.js +702 -0
  98. package/dist/index.d.ts +2714 -0
  99. package/dist/index.js +11220 -0
  100. package/dist/preset.d.ts +24 -0
  101. package/dist/preset.js +17 -0
  102. package/dist/tokens/tokens/primitives.css +45 -0
  103. package/dist/tokens/tokens/semantic.css +46 -0
  104. package/dist/tokens/tokens/theme.css +11 -0
  105. package/dist/tokens/tokens/tokens.json +65 -0
  106. package/index.ts +123 -0
  107. package/package.json +63 -0
  108. package/tailwind-preset.ts +22 -0
  109. package/tokens/primitives.css +45 -0
  110. package/tokens/semantic.css +46 -0
  111. package/tokens/theme.css +11 -0
  112. package/tokens/tokens.json +65 -0
@@ -0,0 +1,340 @@
1
+ /** Tab bar with underline, pill, and card variants, closable tabs, color-coded icons, and auto-collapse to icon-only. */
2
+
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'
11
+ import { Badge, type BadgeColor } from './badge.tsx'
12
+ import { cn } from '../lib/cn.ts'
13
+ import { Tooltip } from './tooltip.tsx'
14
+
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
+ export interface Tab {
37
+ id: string
38
+ label: string
39
+ icon?: IconName
40
+ color?: string
41
+ closable?: boolean
42
+ badge?: number | string
43
+ badgeColor?: BadgeColor
44
+ }
45
+
46
+ export interface TabBarProps {
47
+ tabs: Tab[]
48
+ activeId: string
49
+ onSelect: (id: string) => void
50
+ onClose?: (id: string) => void
51
+ variant?: 'underline' | 'pill' | 'card'
52
+ size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
53
+ className?: string
54
+ }
55
+
56
+ const sizeConfig = {
57
+ xss: { text: 'text-[10px]', icon: 'w-2.5 h-2.5', px: 'px-1.5', py: 'py-1', close: 'w-2.5 h-2.5', badgeSize: 'xss' as const, gap: 'gap-1' },
58
+ xs: { text: 'text-xs', icon: 'w-3 h-3', px: 'px-2', py: 'py-1', close: 'w-3 h-3', badgeSize: 'xs' as const, gap: 'gap-1' },
59
+ sm: { text: 'text-sm', icon: 'w-3.5 h-3.5', px: 'px-3', py: 'py-1.5', close: 'w-3 h-3', badgeSize: 'sm' as const, gap: 'gap-1.5' },
60
+ md: { text: 'text-base', icon: 'w-4 h-4', px: 'px-4', py: 'py-2', close: 'w-3.5 h-3.5', badgeSize: 'md' as const, gap: 'gap-2' },
61
+ lg: { text: 'text-lg', icon: 'w-5 h-5', px: 'px-5', py: 'py-2.5', close: 'w-4 h-4', badgeSize: 'lg' as const, gap: 'gap-2' },
62
+ }
63
+
64
+ const colorMap: Record<string, { active: string; icon: string; indicator: string }> = {
65
+ blue: { active: 'text-blue-400', icon: 'text-blue-400', indicator: 'bg-blue-400' },
66
+ green: { active: 'text-green-400', icon: 'text-green-400', indicator: 'bg-green-400' },
67
+ purple: { active: 'text-purple-400', icon: 'text-purple-400', indicator: 'bg-purple-400' },
68
+ red: { active: 'text-red-400', icon: 'text-red-400', indicator: 'bg-red-400' },
69
+ orange: { active: 'text-orange-400', icon: 'text-orange-400', indicator: 'bg-orange-400' },
70
+ cyan: { active: 'text-cyan-400', icon: 'text-cyan-400', indicator: 'bg-cyan-400' },
71
+ yellow: { active: 'text-yellow-400', icon: 'text-yellow-400', indicator: 'bg-yellow-400' },
72
+ amber: { active: 'text-amber-400', icon: 'text-amber-400', indicator: 'bg-amber-400' },
73
+ emerald: { active: 'text-emerald-400', icon: 'text-emerald-400', indicator: 'bg-emerald-400' },
74
+ indigo: { active: 'text-indigo-400', icon: 'text-indigo-400', indicator: 'bg-indigo-400' },
75
+ violet: { active: 'text-violet-400', icon: 'text-violet-400', indicator: 'bg-violet-400' },
76
+ sky: { active: 'text-sky-400', icon: 'text-sky-400', indicator: 'bg-sky-400' },
77
+ neutral: { active: 'text-neutral-200', icon: 'text-neutral-400', indicator: 'bg-neutral-200' },
78
+ }
79
+
80
+ function getColors(color?: string) {
81
+ return color && colorMap[color] ? colorMap[color] : colorMap.neutral
82
+ }
83
+
84
+ // Width estimation for auto-collapse
85
+ const tabWidthEstimates = {
86
+ xss: { base: 12, icon: 14, charW: 5.5, badge: 20, close: 14 },
87
+ xs: { base: 16, icon: 16, charW: 6.5, badge: 24, close: 16 },
88
+ sm: { base: 24, icon: 20, charW: 7.5, badge: 28, close: 18 },
89
+ md: { base: 32, icon: 24, charW: 8.5, badge: 32, close: 22 },
90
+ lg: { base: 40, icon: 28, charW: 9.5, badge: 36, close: 24 },
91
+ }
92
+
93
+ function estimateTabsWidth(tabs: Tab[], size: keyof typeof sizeConfig): number {
94
+ const est = tabWidthEstimates[size]
95
+ return tabs.reduce((sum, tab) => {
96
+ let w = est.base + tab.label.length * est.charW
97
+ if (tab.icon) w += est.icon
98
+ if (tab.badge !== undefined) w += est.badge
99
+ if (tab.closable) w += est.close
100
+ return sum + w
101
+ }, 0)
102
+ }
103
+
104
+ function TabIcon({ icon, size, color }: { icon: IconName; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; color?: string }) {
105
+ const Icon = iconSubset[icon]
106
+ if (!Icon) return null
107
+ const s = sizeConfig[size]
108
+ const c = getColors(color)
109
+ return <Icon className={cn(s.icon, c.icon, 'flex-shrink-0')} />
110
+ }
111
+
112
+ function TabBadge({ badge, size, badgeColor }: { badge: number | string; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; badgeColor?: BadgeColor }) {
113
+ const s = sizeConfig[size]
114
+ return <Badge value={badge} color={badgeColor} size={s.badgeSize} className="flex-shrink-0" />
115
+ }
116
+
117
+ function CloseButton({ size, onClick }: { size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; onClick: () => void }) {
118
+ const s = sizeConfig[size]
119
+ return (
120
+ <button
121
+ type="button"
122
+ onClick={(e) => {
123
+ e.stopPropagation()
124
+ onClick()
125
+ }}
126
+ className="p-0.5 rounded hover:bg-neutral-600 transition-colors cursor-pointer flex-shrink-0 opacity-0 group-hover:opacity-100 group-[.is-active]:opacity-100"
127
+ >
128
+ <X className={cn(s.close, 'text-neutral-500 hover:text-neutral-200')} />
129
+ </button>
130
+ )
131
+ }
132
+
133
+ function CompactTab({
134
+ tab, isActive, size, variant, onSelect,
135
+ }: { tab: Tab; isActive: boolean; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; variant: 'underline' | 'pill' | 'card'; onSelect: () => void }) {
136
+ const s = sizeConfig[size]
137
+ const c = getColors(tab.color)
138
+
139
+ const variantClass = {
140
+ underline: isActive ? c.active : 'text-neutral-500 hover:text-neutral-400',
141
+ pill: isActive
142
+ ? cn('bg-neutral-700 font-medium', c.active)
143
+ : 'text-neutral-500 hover:text-neutral-400 hover:bg-neutral-700/50',
144
+ card: isActive
145
+ ? cn('bg-neutral-800 border-neutral-700', c.active)
146
+ : 'bg-transparent border-transparent text-neutral-500 hover:text-neutral-400 hover:bg-neutral-900',
147
+ }[variant]
148
+
149
+ return (
150
+ <button
151
+ type="button"
152
+ onClick={onSelect}
153
+ className={cn(
154
+ 'relative flex items-center justify-center transition-colors cursor-pointer',
155
+ s.py, 'px-3',
156
+ variant === 'pill' && 'rounded-md',
157
+ variant === 'card' && 'rounded-t-lg border border-b-0',
158
+ variantClass,
159
+ )}
160
+ >
161
+ <span className="relative flex items-center justify-center">
162
+ {tab.icon && <TabIcon icon={tab.icon} size={size} color={isActive ? tab.color : undefined} />}
163
+ {tab.badge !== undefined && (
164
+ <span className="absolute -top-1.5 -right-2.5">
165
+ <Badge value={tab.badge} color={tab.badgeColor} size="xss" />
166
+ </span>
167
+ )}
168
+ </span>
169
+ {isActive && variant === 'underline' && (
170
+ <span className={cn('absolute bottom-0 left-0 right-0 h-0.5 rounded-full', c.indicator)} />
171
+ )}
172
+ {isActive && variant === 'card' && (
173
+ <span className="absolute -bottom-px left-0 right-0 h-px bg-neutral-800" />
174
+ )}
175
+ </button>
176
+ )
177
+ }
178
+
179
+ function UnderlineTab({
180
+ tab, isActive, size, onSelect, onClose,
181
+ }: { tab: Tab; isActive: boolean; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; onSelect: () => void; onClose?: () => void }) {
182
+ const s = sizeConfig[size]
183
+ const c = getColors(tab.color)
184
+ const showClose = tab.closable && onClose
185
+
186
+ return (
187
+ <button
188
+ type="button"
189
+ onClick={onSelect}
190
+ className={cn(
191
+ 'group relative flex items-center whitespace-nowrap transition-colors cursor-pointer',
192
+ s.text, s.px, s.py, s.gap,
193
+ isActive && 'is-active',
194
+ isActive ? c.active : 'text-neutral-500 hover:text-neutral-400',
195
+ )}
196
+ >
197
+ {tab.icon && <TabIcon icon={tab.icon} size={size} color={isActive ? tab.color : undefined} />}
198
+ <span>{tab.label}</span>
199
+ {tab.badge !== undefined && <TabBadge badge={tab.badge} size={size} badgeColor={tab.badgeColor} />}
200
+ {showClose && <CloseButton size={size} onClick={onClose!} />}
201
+ {isActive && (
202
+ <span className={cn('absolute bottom-0 left-0 right-0 h-0.5 rounded-full', c.indicator)} />
203
+ )}
204
+ </button>
205
+ )
206
+ }
207
+
208
+ function PillTab({
209
+ tab, isActive, size, onSelect, onClose,
210
+ }: { tab: Tab; isActive: boolean; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; onSelect: () => void; onClose?: () => void }) {
211
+ const s = sizeConfig[size]
212
+ const c = getColors(tab.color)
213
+ const showClose = tab.closable && onClose
214
+
215
+ return (
216
+ <button
217
+ type="button"
218
+ onClick={onSelect}
219
+ className={cn(
220
+ 'group flex items-center whitespace-nowrap rounded-md transition-colors cursor-pointer',
221
+ s.text, s.px, s.py, s.gap,
222
+ isActive && 'is-active',
223
+ isActive
224
+ ? cn('bg-neutral-700 font-medium', c.active)
225
+ : 'text-neutral-500 hover:text-neutral-400 hover:bg-neutral-700/50',
226
+ )}
227
+ >
228
+ {tab.icon && <TabIcon icon={tab.icon} size={size} color={isActive ? tab.color : undefined} />}
229
+ <span>{tab.label}</span>
230
+ {tab.badge !== undefined && <TabBadge badge={tab.badge} size={size} badgeColor={tab.badgeColor} />}
231
+ {showClose && <CloseButton size={size} onClick={onClose!} />}
232
+ </button>
233
+ )
234
+ }
235
+
236
+ function CardTab({
237
+ tab, isActive, size, onSelect, onClose,
238
+ }: { tab: Tab; isActive: boolean; size: 'xss' | 'xs' | 'sm' | 'md' | 'lg'; onSelect: () => void; onClose?: () => void }) {
239
+ const s = sizeConfig[size]
240
+ const c = getColors(tab.color)
241
+ const showClose = tab.closable && onClose
242
+
243
+ return (
244
+ <button
245
+ type="button"
246
+ onClick={onSelect}
247
+ className={cn(
248
+ 'group relative flex items-center whitespace-nowrap transition-colors cursor-pointer rounded-t-lg border border-b-0',
249
+ s.text, s.px, s.py, s.gap,
250
+ isActive && 'is-active',
251
+ isActive
252
+ ? cn('bg-neutral-800 border-neutral-700', c.active)
253
+ : 'bg-transparent border-transparent text-neutral-500 hover:text-neutral-400 hover:bg-neutral-900',
254
+ )}
255
+ >
256
+ {tab.icon && <TabIcon icon={tab.icon} size={size} color={isActive ? tab.color : undefined} />}
257
+ <span>{tab.label}</span>
258
+ {tab.badge !== undefined && <TabBadge badge={tab.badge} size={size} badgeColor={tab.badgeColor} />}
259
+ {showClose && <CloseButton size={size} onClick={onClose!} />}
260
+ {isActive && (
261
+ <span className="absolute -bottom-px left-0 right-0 h-px bg-neutral-800" />
262
+ )}
263
+ </button>
264
+ )
265
+ }
266
+
267
+ const tabComponents = {
268
+ underline: UnderlineTab,
269
+ pill: PillTab,
270
+ card: CardTab,
271
+ } as const
272
+
273
+ export function TabBar({
274
+ tabs,
275
+ activeId,
276
+ onSelect,
277
+ onClose,
278
+ variant = 'underline',
279
+ size = 'sm',
280
+ className,
281
+ }: TabBarProps) {
282
+ const containerRef = useRef<HTMLDivElement>(null)
283
+ const [compact, setCompact] = useState(false)
284
+ const TabComponent = tabComponents[variant]
285
+
286
+ const computeLayout = useCallback(() => {
287
+ const el = containerRef.current
288
+ if (!el) return
289
+ const available = el.clientWidth
290
+ const needed = estimateTabsWidth(tabs, size)
291
+ setCompact(needed > available)
292
+ }, [tabs, size])
293
+
294
+ useEffect(() => {
295
+ const el = containerRef.current
296
+ if (!el) return
297
+ computeLayout()
298
+ const observer = new ResizeObserver(computeLayout)
299
+ observer.observe(el)
300
+ return () => observer.disconnect()
301
+ }, [computeLayout])
302
+
303
+ return (
304
+ <div
305
+ ref={containerRef}
306
+ className={cn(
307
+ 'flex items-end',
308
+ variant === 'underline' && 'border-b border-neutral-700',
309
+ variant === 'card' && 'border-b border-neutral-700',
310
+ className,
311
+ )}
312
+ >
313
+ {tabs.map((tab) => {
314
+ if (compact) {
315
+ return (
316
+ <Tooltip key={tab.id} content={{ description: tab.label }} position="bottom">
317
+ <CompactTab
318
+ tab={tab}
319
+ isActive={tab.id === activeId}
320
+ size={size}
321
+ variant={variant}
322
+ onSelect={() => onSelect(tab.id)}
323
+ />
324
+ </Tooltip>
325
+ )
326
+ }
327
+ return (
328
+ <TabComponent
329
+ key={tab.id}
330
+ tab={tab}
331
+ isActive={tab.id === activeId}
332
+ size={size}
333
+ onSelect={() => onSelect(tab.id)}
334
+ onClose={onClose ? () => onClose(tab.id) : undefined}
335
+ />
336
+ )
337
+ })}
338
+ </div>
339
+ )
340
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Toggle - iOS-style switch toggle
3
+ *
4
+ * Used by:
5
+ * - Settings pages - boolean preferences
6
+ * - Feature flags - enable/disable features
7
+ * - List items - inline toggle controls
8
+ */
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'
26
+
27
+ // Border colors per accent
28
+ const BORDER_COLORS: Record<ToggleColor, { idle: string; active: string }> = {
29
+ blue: { idle: 'rgba(59,130,246,0.3)', active: 'rgba(59,130,246,0.4)' },
30
+ green: { idle: 'rgba(34,197,94,0.3)', active: 'rgba(34,197,94,0.4)' },
31
+ red: { idle: 'rgba(239,68,68,0.3)', active: 'rgba(239,68,68,0.4)' },
32
+ orange: { idle: 'rgba(249,115,22,0.3)', active: 'rgba(249,115,22,0.4)' },
33
+ cyan: { idle: 'rgba(6,182,212,0.3)', active: 'rgba(6,182,212,0.4)' },
34
+ yellow: { idle: 'rgba(234,179,8,0.3)', active: 'rgba(234,179,8,0.4)' },
35
+ purple: { idle: 'rgba(168,85,247,0.3)', active: 'rgba(168,85,247,0.4)' },
36
+ indigo: { idle: 'rgba(99,102,241,0.3)', active: 'rgba(99,102,241,0.4)' },
37
+ emerald: { idle: 'rgba(16,185,129,0.3)', active: 'rgba(16,185,129,0.4)' },
38
+ amber: { idle: 'rgba(245,158,11,0.3)', active: 'rgba(245,158,11,0.4)' },
39
+ violet: { idle: 'rgba(139,92,246,0.3)', active: 'rgba(139,92,246,0.4)' },
40
+ neutral: { idle: 'rgba(156,163,175,0.3)', active: 'rgba(156,163,175,0.4)' },
41
+ sky: { idle: 'rgba(14,165,233,0.3)', active: 'rgba(14,165,233,0.4)' },
42
+ pink: { idle: 'rgba(236,72,153,0.3)', active: 'rgba(236,72,153,0.4)' },
43
+ teal: { idle: 'rgba(20,184,166,0.3)', active: 'rgba(20,184,166,0.4)' },
44
+ }
45
+
46
+ // Knob colors: checked = accent color, unchecked = gray
47
+ const KNOB_COLORS: Record<ToggleColor, { on: string; off: string }> = {
48
+ blue: { on: '#60a5fa', off: 'rgba(96,165,250,0.35)' },
49
+ green: { on: '#4ade80', off: 'rgba(74,222,128,0.35)' },
50
+ red: { on: '#f87171', off: 'rgba(248,113,113,0.35)' },
51
+ orange: { on: '#fb923c', off: 'rgba(251,146,60,0.35)' },
52
+ cyan: { on: '#22d3ee', off: 'rgba(34,211,238,0.35)' },
53
+ yellow: { on: '#facc15', off: 'rgba(250,204,21,0.35)' },
54
+ purple: { on: '#c084fc', off: 'rgba(192,132,252,0.35)' },
55
+ indigo: { on: '#818cf8', off: 'rgba(129,140,248,0.35)' },
56
+ emerald: { on: '#34d399', off: 'rgba(52,211,153,0.35)' },
57
+ amber: { on: '#fbbf24', off: 'rgba(251,191,36,0.35)' },
58
+ violet: { on: '#a78bfa', off: 'rgba(167,139,250,0.35)' },
59
+ neutral: { on: '#9ca3af', off: 'rgba(156,163,175,0.35)' },
60
+ sky: { on: '#38bdf8', off: 'rgba(56,189,248,0.35)' },
61
+ pink: { on: '#f472b6', off: 'rgba(244,114,182,0.35)' },
62
+ teal: { on: '#2dd4bf', off: 'rgba(45,212,191,0.35)' },
63
+ }
64
+
65
+ // Checked = IconButton hovered/active style
66
+ const TOGGLE_CHECKED_TRACK: Record<ToggleColor, string> = {
67
+ blue: 'bg-blue-500/20', green: 'bg-green-500/20', red: 'bg-red-500/20',
68
+ orange: 'bg-orange-500/20', cyan: 'bg-cyan-500/20', yellow: 'bg-yellow-500/20',
69
+ purple: 'bg-purple-500/20', indigo: 'bg-indigo-500/20', emerald: 'bg-emerald-500/20',
70
+ amber: 'bg-amber-500/20', violet: 'bg-violet-500/20', neutral: 'bg-neutral-500/20',
71
+ sky: 'bg-sky-500/20', pink: 'bg-pink-500/20', teal: 'bg-teal-500/20',
72
+ }
73
+
74
+ // Unchecked = IconButton idle style
75
+ const TOGGLE_UNCHECKED_TRACK: Record<ToggleColor, string> = {
76
+ blue: 'hover:bg-blue-500/20', green: 'hover:bg-green-500/20', red: 'hover:bg-red-500/20',
77
+ orange: 'hover:bg-orange-500/20', cyan: 'hover:bg-cyan-500/20', yellow: 'hover:bg-yellow-500/20',
78
+ purple: 'hover:bg-purple-500/20', indigo: 'hover:bg-indigo-500/20', emerald: 'hover:bg-emerald-500/20',
79
+ amber: 'hover:bg-amber-500/20', violet: 'hover:bg-violet-500/20', neutral: 'hover:bg-neutral-500/20',
80
+ sky: 'hover:bg-sky-500/20', pink: 'hover:bg-pink-500/20', teal: 'hover:bg-teal-500/20',
81
+ }
82
+
83
+ export type ToggleSize = 'xss' | 'xs' | 'sm' | 'md' | 'lg'
84
+
85
+ const TOGGLE_SIZES: Record<ToggleSize, { track: string; knob: string; translate: string }> = {
86
+ xss: { track: 'w-6 h-3', knob: 'w-2 h-2', translate: 'translate-x-3' },
87
+ xs: { track: 'w-7 h-3.5', knob: 'w-2.5 h-2.5', translate: 'translate-x-3.5' },
88
+ sm: { track: 'w-8 h-[18px]', knob: 'w-3.5 h-3.5', translate: 'translate-x-3.5' },
89
+ md: { track: 'w-11 h-6', knob: 'w-5 h-5', translate: 'translate-x-5' },
90
+ lg: { track: 'w-14 h-7', knob: 'w-6 h-6', translate: 'translate-x-7' },
91
+ }
92
+
93
+ export type ToggleVariant = 'outline' | 'filled'
94
+
95
+ export interface ToggleProps {
96
+ checked: boolean
97
+ onChange: (checked: boolean) => void
98
+ disabled?: boolean
99
+ size?: ToggleSize
100
+ className?: string
101
+ color?: ToggleColor
102
+ variant?: ToggleVariant
103
+ /** Test ID for E2E testing */
104
+ testId?: string
105
+ }
106
+
107
+ export function Toggle({
108
+ checked,
109
+ onChange,
110
+ disabled = false,
111
+ size = 'sm',
112
+ className = '',
113
+ color = 'blue',
114
+ testId,
115
+ }: ToggleProps) {
116
+ const s = TOGGLE_SIZES[size]
117
+ const bc = BORDER_COLORS[color]
118
+ const kc = KNOB_COLORS[color]
119
+ return (
120
+ <button
121
+ type="button"
122
+ onClick={() => !disabled && onChange(!checked)}
123
+ disabled={disabled}
124
+ data-testid={testId}
125
+ 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
+ `}
132
+ >
133
+ <span
134
+ 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
+ `}
139
+ />
140
+ </button>
141
+ )
142
+ }