@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,194 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { Info, AlertTriangle, AlertCircle, Check } from 'lucide-react'
4
+ import { IconButton, type ActionItem } from './icon-button.tsx'
5
+ import { FormActions } from './form-actions.tsx'
6
+ import type { ReactNode } from 'react'
7
+
8
+ export type ModalKind = 'info' | 'warning' | 'error' | 'orange' | 'success'
9
+ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl'
10
+
11
+ const SIZE_CLASSES: Record<ModalSize, string> = {
12
+ sm: 'max-w-sm',
13
+ md: 'max-w-md',
14
+ lg: 'max-w-2xl',
15
+ xl: 'max-w-4xl',
16
+ }
17
+
18
+ const KIND_ICON: Record<ModalKind, ReactNode> = {
19
+ info: <Info className="w-6 h-6 text-blue-400" />,
20
+ warning: <AlertTriangle className="w-6 h-6 text-yellow-400" />,
21
+ error: <AlertCircle className="w-6 h-6 text-red-400" />,
22
+ orange: <AlertTriangle className="w-6 h-6 text-orange-400" />,
23
+ success: <Check className="w-6 h-6 text-green-400" />,
24
+ }
25
+
26
+ interface ModalProps {
27
+ isOpen: boolean
28
+ onClose: () => void
29
+ title: string
30
+ children: ReactNode
31
+ kind?: ModalKind
32
+ /** Modal width size */
33
+ size?: ModalSize
34
+ /** Hide the close button in the header (useful when footer has action buttons) */
35
+ hideCloseButton?: boolean
36
+ /** Optional actions to render in the header (top right) */
37
+ headerActions?: ActionItem[]
38
+ /** Test ID for E2E testing */
39
+ testId?: string
40
+ }
41
+
42
+ function Modal({ isOpen, onClose, title, children, kind = 'info', size = 'md', hideCloseButton = false, headerActions, testId }: ModalProps) {
43
+ const modalRef = useRef<HTMLDivElement>(null)
44
+
45
+ useEffect(() => {
46
+ if (!isOpen) return
47
+ const handleEscape = (e: KeyboardEvent) => {
48
+ if (e.key === 'Escape') onClose()
49
+ }
50
+ document.addEventListener('keydown', handleEscape)
51
+ return () => document.removeEventListener('keydown', handleEscape)
52
+ }, [isOpen, onClose])
53
+
54
+ useEffect(() => {
55
+ if (!isOpen) return
56
+ document.body.style.overflow = 'hidden'
57
+ return () => { document.body.style.overflow = '' }
58
+ }, [isOpen])
59
+
60
+ if (!isOpen) return null
61
+
62
+ return createPortal(
63
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
64
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
65
+ <div
66
+ ref={modalRef}
67
+ data-testid={testId}
68
+ className={`relative bg-neutral-900 border border-neutral-700 rounded-xl shadow-2xl ${SIZE_CLASSES[size]} w-full mx-4 overflow-hidden`}
69
+ >
70
+ <div className="flex items-center gap-3 px-5 py-4 border-b border-neutral-800">
71
+ {KIND_ICON[kind]}
72
+ <h3 className="text-lg font-semibold text-white flex-1">{title}</h3>
73
+ {headerActions?.map((a, i) => <IconButton key={i} {...a} />)}
74
+ {!hideCloseButton && (
75
+ <IconButton
76
+ icon="x"
77
+ onClick={onClose}
78
+ size="sm"
79
+ color="neutral"
80
+ tooltip={{ description: 'Close this modal' }}
81
+ testId="modal-close"
82
+ />
83
+ )}
84
+ </div>
85
+ <div className="px-5 py-4">{children}</div>
86
+ </div>
87
+ </div>,
88
+ document.body,
89
+ )
90
+ }
91
+
92
+ export interface ConfirmModalProps {
93
+ isOpen: boolean
94
+ onClose: () => void
95
+ onConfirm: () => void | Promise<void>
96
+ title: string
97
+ message: string
98
+ warning?: string
99
+ warningItems?: string[]
100
+ kind?: ModalKind
101
+ confirmColor?: 'red' | 'blue' | 'orange' | 'yellow'
102
+ isLoading?: boolean
103
+ confirmDisabled?: boolean
104
+ }
105
+
106
+ export function ConfirmModal({
107
+ isOpen,
108
+ onClose,
109
+ onConfirm,
110
+ title,
111
+ message,
112
+ warning,
113
+ warningItems,
114
+ kind = 'warning',
115
+ confirmColor = 'blue',
116
+ isLoading = false,
117
+ confirmDisabled = false,
118
+ }: ConfirmModalProps) {
119
+ const [isConfirming, setIsConfirming] = useState(false)
120
+
121
+ const isDisabled = isLoading || isConfirming || confirmDisabled
122
+ const isInProgress = isLoading || isConfirming
123
+
124
+ const handleConfirm = async () => {
125
+ setIsConfirming(true)
126
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
127
+ await new Promise(resolve => setTimeout(resolve, 50))
128
+ try {
129
+ await onConfirm()
130
+ } finally {
131
+ setIsConfirming(false)
132
+ onClose()
133
+ }
134
+ }
135
+
136
+ return (
137
+ <Modal isOpen={isOpen} onClose={onClose} title={title} kind={kind} hideCloseButton>
138
+ <div className="text-neutral-300 mb-6">
139
+ {message}
140
+ {warning && (
141
+ <div className="mt-3 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg">
142
+ <p className="text-amber-300 text-sm font-medium">{warning}</p>
143
+ {warningItems && warningItems.length > 0 && (
144
+ <ul className="mt-2 space-y-1">
145
+ {warningItems.map((item, i) => (
146
+ <li key={i} className="text-amber-300/80 text-sm ml-4 list-disc">{item}</li>
147
+ ))}
148
+ </ul>
149
+ )}
150
+ </div>
151
+ )}
152
+ </div>
153
+ <FormActions
154
+ padding="compact"
155
+ border={false}
156
+ onCancel={onClose}
157
+ cancelTooltip="Cancel this action"
158
+ onConfirm={handleConfirm}
159
+ confirmTooltip={`Confirm: ${title}`}
160
+ confirmColor={confirmColor}
161
+ confirmDisabled={isDisabled}
162
+ confirmStatus={isInProgress ? 'loading' : undefined}
163
+ />
164
+ </Modal>
165
+ )
166
+ }
167
+
168
+ export interface AlertModalProps {
169
+ isOpen: boolean
170
+ onClose: () => void
171
+ title: string
172
+ message: string
173
+ kind?: ModalKind
174
+ }
175
+
176
+ export function AlertModal({
177
+ isOpen,
178
+ onClose,
179
+ title,
180
+ message,
181
+ kind = 'info',
182
+ }: AlertModalProps) {
183
+ return (
184
+ <Modal isOpen={isOpen} onClose={onClose} title={title} kind={kind} hideCloseButton>
185
+ <div className="text-neutral-300 mb-6">{message}</div>
186
+ <FormActions
187
+ padding="compact"
188
+ border={false}
189
+ onConfirm={onClose}
190
+ confirmTooltip="Dismiss this alert"
191
+ />
192
+ </Modal>
193
+ )
194
+ }
@@ -0,0 +1,81 @@
1
+ /** Navigation card with icon, description, badge, and hover lift effect. */
2
+
3
+ import {
4
+ Settings, Puzzle, Zap, Shield, Folder, Code, Database, Globe,
5
+ Terminal, Star, Bell, User, Users, File, Tag, Sparkles, Search,
6
+ Lock, Mail, Cloud, Heart, Bookmark, Pin, Bot, Plug,
7
+ } from 'lucide-react'
8
+ import type { LucideIcon } from 'lucide-react'
9
+ import type { IconName } from './icon-button.tsx'
10
+ import { Badge, type BadgeColor } from './badge.tsx'
11
+ import { cn } from '../lib/cn.ts'
12
+
13
+ const iconSubset: Partial<Record<IconName, LucideIcon>> = {
14
+ settings: Settings, puzzle: Puzzle, zap: Zap, shield: Shield,
15
+ folder: Folder, code: Code, database: Database, globe: Globe,
16
+ terminal: Terminal, star: Star, bell: Bell, user: User,
17
+ users: Users, file: File, tag: Tag, sparkles: Sparkles,
18
+ search: Search, lock: Lock, mail: Mail, cloud: Cloud,
19
+ heart: Heart, bookmark: Bookmark, pin: Pin, bot: Bot, plug: Plug,
20
+ }
21
+
22
+ export interface NavCardProps {
23
+ title: string
24
+ description?: string
25
+ icon?: IconName
26
+ iconColor?: string
27
+ badge?: number | string
28
+ badgeColor?: BadgeColor
29
+ onClick?: () => void
30
+ disabled?: boolean
31
+ className?: string
32
+ }
33
+
34
+ export function NavCard({
35
+ title,
36
+ description,
37
+ icon,
38
+ iconColor = '#60a5fa',
39
+ badge,
40
+ badgeColor = 'blue',
41
+ onClick,
42
+ disabled = false,
43
+ className,
44
+ }: NavCardProps) {
45
+ const Icon = icon ? iconSubset[icon] : undefined
46
+
47
+ return (
48
+ <button
49
+ type="button"
50
+ onClick={disabled ? undefined : onClick}
51
+ disabled={disabled}
52
+ className={cn(
53
+ 'relative w-full text-left rounded-lg border border-neutral-700 bg-neutral-800 p-4 transition-all duration-200 cursor-pointer',
54
+ !disabled && 'hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/20 hover:border-neutral-600 hover:bg-neutral-700',
55
+ disabled && 'opacity-50 cursor-not-allowed',
56
+ className,
57
+ )}
58
+ >
59
+ {badge !== undefined && (
60
+ <span className="absolute top-3 right-3">
61
+ <Badge value={badge} color={badgeColor} size="xs" />
62
+ </span>
63
+ )}
64
+
65
+ {Icon && (
66
+ <div
67
+ className="w-9 h-9 rounded-lg flex items-center justify-center mb-3"
68
+ style={{ backgroundColor: `${iconColor}15` }}
69
+ >
70
+ <Icon className="w-4.5 h-4.5" style={{ color: iconColor }} />
71
+ </div>
72
+ )}
73
+
74
+ <h3 className="text-sm font-medium text-neutral-200">{title}</h3>
75
+
76
+ {description && (
77
+ <p className="mt-1 text-xs text-neutral-500 leading-relaxed line-clamp-2">{description}</p>
78
+ )}
79
+ </button>
80
+ )
81
+ }
@@ -0,0 +1,285 @@
1
+ /** Navigation bar with back/forward controls, history dropdown, and inline breadcrumb path. */
2
+
3
+ import { useState, useRef, useCallback } from 'react'
4
+ import {
5
+ ChevronLeft, ChevronRight, History,
6
+ Menu, Home, Layers,
7
+ Settings, Folder, File, Code, Terminal, Database,
8
+ Globe, Star, Users, User, Tag, Search, Heart,
9
+ Zap, Shield, ShieldCheck, Sparkles, Eye, Lock,
10
+ Cloud, Wand2, Bell, Bookmark, Pin, Mail, Send,
11
+ Image, Bot, Puzzle, Plug, Webhook,
12
+ } from 'lucide-react'
13
+ import type { LucideIcon } from 'lucide-react'
14
+ import type { IconName } from './icon-button.tsx'
15
+ import type { BreadcrumbSegment } from './breadcrumb.tsx'
16
+ import { cn } from '../lib/cn.ts'
17
+ import { useClickOutside } from '../hooks/use-click-outside.ts'
18
+
19
+ const iconSubset: Partial<Record<IconName, LucideIcon>> = {
20
+ menu: Menu,
21
+ home: Home,
22
+ layers: Layers,
23
+ folder: Folder,
24
+ file: File,
25
+ settings: Settings,
26
+ code: Code,
27
+ terminal: Terminal,
28
+ database: Database,
29
+ globe: Globe,
30
+ star: Star,
31
+ users: Users,
32
+ user: User,
33
+ tag: Tag,
34
+ zap: Zap,
35
+ shield: Shield,
36
+ 'shield-check': ShieldCheck,
37
+ sparkles: Sparkles,
38
+ eye: Eye,
39
+ lock: Lock,
40
+ search: Search,
41
+ heart: Heart,
42
+ cloud: Cloud,
43
+ wand: Wand2,
44
+ bell: Bell,
45
+ bookmark: Bookmark,
46
+ pin: Pin,
47
+ mail: Mail,
48
+ send: Send,
49
+ image: Image,
50
+ bot: Bot,
51
+ puzzle: Puzzle,
52
+ plug: Plug,
53
+ webhook: Webhook,
54
+ }
55
+
56
+ export interface NavigationBarProps {
57
+ segments: BreadcrumbSegment[]
58
+ canGoBack?: boolean
59
+ canGoForward?: boolean
60
+ onBack?: () => void
61
+ onForward?: () => void
62
+ showHistory?: boolean
63
+ historyEntries?: BreadcrumbSegment[][]
64
+ onHistorySelect?: (index: number) => void
65
+ leadingAction?: { icon: IconName; onClick?: () => void }
66
+ separator?: 'chevron' | 'slash' | 'dot'
67
+ size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
68
+ className?: string
69
+ }
70
+
71
+ const sizeConfig = {
72
+ xss: { text: 'text-[10px]', segIcon: 'w-2.5 h-2.5', navIcon: 'w-2.5 h-2.5', navBtn: 'w-[18px] h-[18px] rounded-[3px]', px: 'px-1', py: 'py-0.5', sep: 'w-2 h-2', divH: 'h-3' },
73
+ xs: { text: 'text-xs', segIcon: 'w-3 h-3', navIcon: 'w-3 h-3', navBtn: 'w-6 h-6 rounded-[5px]', px: 'px-1.5', py: 'py-0.5', sep: 'w-2.5 h-2.5', divH: 'h-3.5' },
74
+ sm: { text: 'text-sm', segIcon: 'w-3.5 h-3.5', navIcon: 'w-3.5 h-3.5', navBtn: 'w-7 h-7 rounded-md', px: 'px-2', py: 'py-1', sep: 'w-3 h-3', divH: 'h-4' },
75
+ md: { text: 'text-base', segIcon: 'w-4 h-4', navIcon: 'w-4 h-4', navBtn: 'w-8 h-8 rounded-md', px: 'px-2.5', py: 'py-1', sep: 'w-3.5 h-3.5', divH: 'h-5' },
76
+ lg: { text: 'text-lg', segIcon: 'w-5 h-5', navIcon: 'w-5 h-5', navBtn: 'w-9 h-9 rounded-md', px: 'px-3', py: 'py-1.5', sep: 'w-4 h-4', divH: 'h-6' },
77
+ }
78
+
79
+ const colorMap: Record<string, { bg: string; text: string }> = {
80
+ blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
81
+ green: { bg: 'bg-green-500/10', text: 'text-green-400' },
82
+ purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
83
+ red: { bg: 'bg-red-500/10', text: 'text-red-400' },
84
+ orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
85
+ cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
86
+ yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
87
+ amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
88
+ emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
89
+ indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
90
+ violet: { bg: 'bg-violet-500/10', text: 'text-violet-400' },
91
+ sky: { bg: 'bg-sky-500/10', text: 'text-sky-400' },
92
+ pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
93
+ teal: { bg: 'bg-teal-500/10', text: 'text-teal-400' },
94
+ neutral: { bg: 'bg-neutral-500/10', text: 'text-neutral-400' },
95
+ }
96
+
97
+ function NavButton({ icon: Icon, onClick, disabled, size, active }: {
98
+ icon: LucideIcon
99
+ onClick?: () => void
100
+ disabled?: boolean
101
+ size: keyof typeof sizeConfig
102
+ active?: boolean
103
+ }) {
104
+ const s = sizeConfig[size]
105
+ return (
106
+ <button
107
+ type="button"
108
+ onClick={onClick}
109
+ disabled={disabled}
110
+ className={cn(
111
+ 'flex items-center justify-center transition-colors',
112
+ s.navBtn,
113
+ disabled
114
+ ? 'text-neutral-600 cursor-not-allowed'
115
+ : active
116
+ ? 'text-white bg-neutral-700/50 border border-neutral-600 cursor-pointer'
117
+ : 'text-neutral-400 border border-neutral-700/50 hover:text-white hover:bg-neutral-700/50 cursor-pointer',
118
+ )}
119
+ >
120
+ <Icon className={s.navIcon} />
121
+ </button>
122
+ )
123
+ }
124
+
125
+ function Divider({ size }: { size: keyof typeof sizeConfig }) {
126
+ return <div className={cn('w-px bg-neutral-700/50 mx-1', sizeConfig[size].divH)} />
127
+ }
128
+
129
+ function SegmentSeparator({ type, size }: { type: 'chevron' | 'slash' | 'dot'; size: keyof typeof sizeConfig }) {
130
+ const s = sizeConfig[size]
131
+ if (type === 'chevron') return <ChevronRight className={cn(s.sep, 'text-neutral-600 flex-shrink-0')} />
132
+ if (type === 'slash') return <span className={cn(s.text, 'text-neutral-600 flex-shrink-0')}>/</span>
133
+ return <span className={cn(s.text, 'text-neutral-600 flex-shrink-0')}>&middot;</span>
134
+ }
135
+
136
+ function SegmentIcon({ icon, color, size }: { icon: IconName; color?: string; size: keyof typeof sizeConfig }) {
137
+ const Icon = iconSubset[icon]
138
+ if (!Icon) return null
139
+ const c = color && colorMap[color] ? colorMap[color] : null
140
+ return (
141
+ <span className={c?.text || ''}>
142
+ <Icon className={sizeConfig[size].segIcon} />
143
+ </span>
144
+ )
145
+ }
146
+
147
+ export function NavigationBar({
148
+ segments,
149
+ canGoBack = false,
150
+ canGoForward = false,
151
+ onBack,
152
+ onForward,
153
+ showHistory = false,
154
+ historyEntries,
155
+ onHistorySelect,
156
+ leadingAction,
157
+ separator = 'chevron',
158
+ size = 'sm',
159
+ className,
160
+ }: NavigationBarProps) {
161
+ const s = sizeConfig[size]
162
+ const hasNav = !!(onBack || onForward)
163
+ const LeadIcon = leadingAction ? iconSubset[leadingAction.icon] : null
164
+
165
+ const [historyOpen, setHistoryOpen] = useState(false)
166
+ const historyRef = useRef<HTMLDivElement>(null)
167
+ const closeHistory = useCallback(() => setHistoryOpen(false), [])
168
+ useClickOutside(historyRef, historyOpen, closeHistory)
169
+
170
+ const hasHistoryEntries = historyEntries && historyEntries.length > 0
171
+
172
+ return (
173
+ <nav className={cn('flex items-center', className)}>
174
+ <div className={cn('flex items-center gap-1', s.px, s.py, 'bg-neutral-800/50 border border-neutral-700/50 rounded-lg')}>
175
+ {leadingAction && LeadIcon && (
176
+ <>
177
+ <NavButton icon={LeadIcon} onClick={leadingAction.onClick} size={size} />
178
+ <Divider size={size} />
179
+ </>
180
+ )}
181
+
182
+ {hasNav && (
183
+ <>
184
+ <div className="flex items-center gap-0.5">
185
+ <NavButton icon={ChevronLeft} onClick={onBack} disabled={!canGoBack} size={size} />
186
+ <NavButton icon={ChevronRight} onClick={onForward} disabled={!canGoForward} size={size} />
187
+ </div>
188
+ <Divider size={size} />
189
+ </>
190
+ )}
191
+
192
+ {showHistory && (
193
+ <>
194
+ <div className="relative" ref={historyRef}>
195
+ <NavButton
196
+ icon={History}
197
+ onClick={() => setHistoryOpen(o => !o)}
198
+ disabled={!hasHistoryEntries}
199
+ size={size}
200
+ active={historyOpen}
201
+ />
202
+ {historyOpen && hasHistoryEntries && (
203
+ <div className="absolute left-0 top-full mt-1 w-max min-w-[200px] max-w-[420px] bg-neutral-800 border border-neutral-700 rounded-lg shadow-xl z-50">
204
+ <div className="px-3 py-1.5 border-b border-neutral-700/50">
205
+ <p className="text-xs font-medium text-neutral-500">History</p>
206
+ </div>
207
+ <div className="max-h-[300px] overflow-y-auto py-1">
208
+ {historyEntries.map((entry, i) => (
209
+ <button
210
+ key={i}
211
+ type="button"
212
+ onClick={() => {
213
+ onHistorySelect?.(i)
214
+ setHistoryOpen(false)
215
+ }}
216
+ className="w-full px-3 py-1.5 flex items-center gap-1 text-left hover:bg-neutral-800 transition-colors cursor-pointer"
217
+ >
218
+ {entry.map((seg, segIdx) => (
219
+ <span key={seg.id} className="flex items-center gap-1 min-w-0">
220
+ {segIdx > 0 && <ChevronRight className="w-2.5 h-2.5 text-neutral-600 flex-shrink-0" />}
221
+ {seg.icon && <SegmentIcon icon={seg.icon} color={seg.color} size="xs" />}
222
+ <span className={cn(
223
+ 'text-xs truncate',
224
+ seg.color && colorMap[seg.color] ? colorMap[seg.color].text : 'text-neutral-300',
225
+ )}>
226
+ {seg.label}
227
+ </span>
228
+ </span>
229
+ ))}
230
+ </button>
231
+ ))}
232
+ </div>
233
+ <div className="px-3 py-1.5 border-t border-neutral-700/50">
234
+ <p className="text-xs text-neutral-600">Click to navigate</p>
235
+ </div>
236
+ </div>
237
+ )}
238
+ </div>
239
+ <Divider size={size} />
240
+ </>
241
+ )}
242
+
243
+ {segments.map((segment, index) => {
244
+ const isLast = index === segments.length - 1
245
+ const isClickable = !isLast && !!segment.onClick
246
+ const colors = segment.color && colorMap[segment.color] ? colorMap[segment.color] : null
247
+
248
+ return (
249
+ <div key={segment.id} className="flex items-center gap-1">
250
+ {index > 0 && <SegmentSeparator type={separator} size={size} />}
251
+ {isClickable ? (
252
+ <button
253
+ type="button"
254
+ onClick={segment.onClick}
255
+ className={cn(
256
+ 'flex items-center gap-1.5 px-2 py-0.5 rounded-md transition-colors cursor-pointer',
257
+ s.text,
258
+ 'font-medium hover:text-white',
259
+ colors ? [colors.text, `hover:${colors.bg}`] : ['text-neutral-300', 'hover:bg-neutral-700/50'],
260
+ )}
261
+ >
262
+ {segment.icon && <SegmentIcon icon={segment.icon} color={segment.color} size={size} />}
263
+ <span className="truncate max-w-[200px]">{segment.label}</span>
264
+ </button>
265
+ ) : (
266
+ <div
267
+ className={cn(
268
+ 'flex items-center gap-1.5 px-2 py-0.5 rounded-md',
269
+ s.text,
270
+ isLast
271
+ ? ['font-medium bg-neutral-700/50', colors ? colors.text : 'text-white']
272
+ : ['font-medium', colors ? colors.text : 'text-neutral-300'],
273
+ )}
274
+ >
275
+ {segment.icon && <SegmentIcon icon={segment.icon} color={segment.color} size={size} />}
276
+ <span className="truncate max-w-[200px]">{segment.label}</span>
277
+ </div>
278
+ )}
279
+ </div>
280
+ )
281
+ })}
282
+ </div>
283
+ </nav>
284
+ )
285
+ }