@toolr/ui-design 0.1.5 → 0.1.7
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.
- package/agent-rules.json +91 -0
- package/ai-manifest.json +190 -0
- package/components/content/info-panel-primitives.tsx +14 -14
- package/components/hooks/use-click-outside.ts +10 -3
- package/components/hooks/use-modal-behavior.ts +24 -0
- package/components/hooks/use-navigation-history.ts +7 -2
- package/components/hooks/use-resizable-sidebar.ts +38 -0
- package/components/lib/ai-tools.tsx +1 -1
- package/components/lib/form-colors.ts +40 -0
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +7 -7
- package/components/sections/captured-issues/captured-issues-panel.tsx +13 -13
- package/components/sections/captured-issues/use-captured-issues.ts +9 -3
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +13 -13
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +5 -5
- package/components/sections/golden-snapshots/snapshot-manager.tsx +11 -11
- package/components/sections/golden-snapshots/status-overview.tsx +20 -20
- package/components/sections/golden-snapshots/version-manager.tsx +8 -8
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +8 -44
- package/components/sections/prompt-editor/index.ts +0 -7
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +9 -45
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +11 -43
- package/components/sections/report-bug/report-bug-form.tsx +14 -14
- package/components/sections/report-bug/screenshot-uploader.tsx +6 -6
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +3 -3
- package/components/sections/snapshot-browser/snapshot-tree.tsx +8 -8
- package/components/sections/snippets-editor/snippets-editor.tsx +74 -48
- package/components/settings/SettingsHeader.tsx +1 -2
- package/components/settings/SettingsTreeNav.tsx +31 -16
- package/components/ui/action-dialog.tsx +12 -56
- package/components/ui/badge.tsx +8 -24
- package/components/ui/bottom-panel-header.tsx +4 -4
- package/components/ui/breadcrumb.tsx +8 -68
- package/components/ui/checkbox.tsx +2 -16
- package/components/ui/collapsible-section.tsx +4 -42
- package/components/ui/confirm-badge.tsx +3 -20
- package/components/ui/cookie-consent.tsx +21 -5
- package/components/ui/debounce-border-overlay.tsx +31 -0
- package/components/ui/detail-section.tsx +5 -22
- package/components/ui/editor-placeholder-card.tsx +17 -16
- package/components/ui/editor-toolbar.tsx +12 -0
- package/components/ui/execution-details-panel.tsx +8 -13
- package/components/ui/extension-list-card.tsx +3 -3
- package/components/ui/file-structure-section.tsx +20 -35
- package/components/ui/file-tree.tsx +4 -14
- package/components/ui/files-panel.tsx +28 -18
- package/components/ui/filter-dropdown.tsx +5 -5
- package/components/ui/form-actions.tsx +7 -6
- package/components/ui/frontmatter-form-header.tsx +4 -4
- package/components/ui/icon-button.tsx +3 -2
- package/components/ui/input.tsx +15 -31
- package/components/ui/label.tsx +7 -21
- package/components/ui/layout-tab-bar.tsx +4 -4
- package/components/ui/modal.tsx +5 -17
- package/components/ui/nav-card.tsx +5 -20
- package/components/ui/navigation-bar.tsx +13 -74
- package/components/ui/number-input.tsx +4 -4
- package/components/ui/registry-browser.tsx +10 -24
- package/components/ui/registry-card.tsx +16 -20
- package/components/ui/registry-detail.tsx +6 -6
- package/components/ui/resizable-textarea.tsx +13 -35
- package/components/ui/segmented-toggle.tsx +6 -5
- package/components/ui/select.tsx +7 -16
- package/components/ui/selection-grid.tsx +6 -54
- package/components/ui/setting-row.tsx +2 -4
- package/components/ui/settings-card.tsx +3 -3
- package/components/ui/settings-info-box.tsx +6 -23
- package/components/ui/settings-section-title.tsx +1 -1
- package/components/ui/snapshot-card.tsx +7 -7
- package/components/ui/snippets-panel.tsx +10 -10
- package/components/ui/sort-dropdown.tsx +2 -2
- package/components/ui/status-card.tsx +6 -17
- package/components/ui/tab-bar.tsx +5 -31
- package/components/ui/toggle.tsx +3 -19
- package/components/ui/tooltip.tsx +9 -21
- package/dist/content.js +14 -14
- package/dist/index.d.ts +71 -141
- package/dist/index.js +1634 -2450
- package/dist/tokens/primitives.css +9 -2
- package/index.ts +8 -7
- package/package.json +13 -3
- package/tokens/primitives.css +9 -2
- package/components/sections/prompt-editor/use-prompt-editor.ts +0 -131
package/agent-rules.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"description": "Critical rules for AI agents building apps with @toolr/ui-design. These are the things agents commonly get wrong.",
|
|
4
|
+
"lastUpdated": "2026-03-05"
|
|
5
|
+
},
|
|
6
|
+
"categories": [
|
|
7
|
+
{
|
|
8
|
+
"name": "Setup",
|
|
9
|
+
"color": "#2dd4bf",
|
|
10
|
+
"rules": [
|
|
11
|
+
{ "type": "do", "text": "body { background-color: var(--background); } — portals render at document.body with semi-transparent backgrounds; without this, white bleeds through" },
|
|
12
|
+
{ "type": "do", "text": "@import \"@toolr/ui-design/tokens\" in app CSS — without it, all semantic tokens are undefined" },
|
|
13
|
+
{ "type": "do", "text": "Add @utility text-xs { font-size: 11px; } in app CSS — Tailwind v4 @theme blocks in consuming apps don't inherit --font-size-xss from ui-design tokens" },
|
|
14
|
+
{ "type": "do", "text": "Import from @toolr/ui-design barrel only, never deep paths like @toolr/ui-design/components/ui/input" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "Typography",
|
|
19
|
+
"color": "#fbbf24",
|
|
20
|
+
"rules": [
|
|
21
|
+
{ "type": "dont", "text": "Minimum font size is 11px (text-xs) — never go below" },
|
|
22
|
+
{ "type": "do", "text": "text-neutral-300 is primary text, not white — white is for emphasis only" },
|
|
23
|
+
{ "type": "do", "text": "font-medium (500) is the default weight — not semibold, not normal" }
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "Components",
|
|
28
|
+
"color": "#60a5fa",
|
|
29
|
+
"rules": [
|
|
30
|
+
{ "type": "do", "text": "Every IconButton must have a tooltip prop — tooltip={{ description: \"...\" }}, not a plain string" },
|
|
31
|
+
{ "type": "do", "text": "onChange delivers the value directly, never a React event — onChange={setName} not onChange={(e) => setName(e.target.value))" },
|
|
32
|
+
{ "type": "do", "text": "Icons are string names — icon=\"search\", not icon={<Search />} from lucide-react" },
|
|
33
|
+
{ "type": "do", "text": "Use FilterDropdown for filtering (value \"all\" for unfiltered), SortDropdown for sorting — they are separate components" },
|
|
34
|
+
{ "type": "dont", "text": "Never use Select with an \"All\" option for filtering — use FilterDropdown" },
|
|
35
|
+
{ "type": "dont", "text": "Never use raw HTML <select>, <input type=\"checkbox\">, <button>, or inline dropdown implementations" },
|
|
36
|
+
{ "type": "do", "text": "ConfirmModal for actions (confirm/cancel), AlertModal for info (dismiss only) — don't mix them" },
|
|
37
|
+
{ "type": "do", "text": "Use cn() for class composition, never string concatenation — cn() resolves Tailwind conflicts" }
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "Styling",
|
|
42
|
+
"color": "#c084fc",
|
|
43
|
+
"rules": [
|
|
44
|
+
{ "type": "do", "text": "Use semantic tokens (--background, --surface, --border) for structure, Tailwind utilities for accent colors" },
|
|
45
|
+
{ "type": "dont", "text": "Never hardcode hex values for accents — use Tailwind {color}-{shade} classes" },
|
|
46
|
+
{ "type": "dont", "text": "Never use transition-all — specify the property (transition-colors, transition-transform)" },
|
|
47
|
+
{ "type": "dont", "text": "Never use shadow-sm/shadow-md — use shadow-xl for dropdowns/tooltips, shadow-2xl for modals only" },
|
|
48
|
+
{ "type": "dont", "text": "Never use rounded-full except for toggle knobs, checkmarks, and avatars — badges use rounded-lg" },
|
|
49
|
+
{ "type": "dont", "text": "Never use CSS group-hover for tooltips — always use the portal-based Tooltip component" }
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "Portals",
|
|
54
|
+
"color": "#fb923c",
|
|
55
|
+
"rules": [
|
|
56
|
+
{ "type": "do", "text": "Select, Tooltip, Modal all render via createPortal to document.body — parent overflow:hidden and z-index don't affect them" },
|
|
57
|
+
{ "type": "do", "text": "Popover background is rgba(0,0,0,0.8) — semi-transparent by design, which is why body background matters" },
|
|
58
|
+
{ "type": "dont", "text": "Never wrap portal components in overflow:hidden expecting to clip them — they escape to document.body" }
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "State & Data",
|
|
63
|
+
"color": "#4ade80",
|
|
64
|
+
"rules": [
|
|
65
|
+
{ "type": "do", "text": "Zustand empty array selectors: use a module-level constant — const EMPTY: T[] = [], never inline []" },
|
|
66
|
+
{ "type": "do", "text": "Immutable store updates: always spread nested state — { ...state.settings, key: value }" },
|
|
67
|
+
{ "type": "dont", "text": "Never prop drill data available in stores — components should read from stores directly" },
|
|
68
|
+
{ "type": "dont", "text": "Never call invoke() directly — use the central useTauri.ts wrapper (handles snake_case mapping)" }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "Code Hygiene",
|
|
73
|
+
"color": "#fb7185",
|
|
74
|
+
"rules": [
|
|
75
|
+
{ "type": "dont", "text": "Never comment out code — delete it entirely. No // removed, no _unused renames" },
|
|
76
|
+
{ "type": "dont", "text": "Never create abstractions for one-time operations — only when 3+ identical implementations exist" },
|
|
77
|
+
{ "type": "do", "text": "Features only import from core, never from other features — cross-feature data flows through core stores" },
|
|
78
|
+
{ "type": "do", "text": "Delete unused exports, types, and variables — no speculative interfaces or \"just in case\" code" }
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "Sync Validation",
|
|
83
|
+
"color": "#22d3ee",
|
|
84
|
+
"rules": [
|
|
85
|
+
{ "type": "dont", "text": "Never use --no-verify or --no-hooks to bypass the pre-commit sync check" },
|
|
86
|
+
{ "type": "do", "text": "Run npm run sync-check before committing component or playground changes" },
|
|
87
|
+
{ "type": "do", "text": "Keep playground page, App.tsx route, component-graph entry, and ai-manifest.json in sync within the same commit" }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
package/ai-manifest.json
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"package": "@toolr/ui-design",
|
|
4
|
+
"description": "Shared dark-theme design system for toolr apps. React + Tailwind CSS.",
|
|
5
|
+
"imports": {
|
|
6
|
+
"components": "@toolr/ui-design",
|
|
7
|
+
"tokens": "@toolr/ui-design/tokens",
|
|
8
|
+
"content": "@toolr/ui-design/content",
|
|
9
|
+
"diagrams": "@toolr/ui-design/diagrams",
|
|
10
|
+
"preset": "@toolr/ui-design/preset",
|
|
11
|
+
"manifest": "@toolr/ui-design/manifest"
|
|
12
|
+
},
|
|
13
|
+
"docs": {
|
|
14
|
+
"rules": "docs/rules/",
|
|
15
|
+
"files": [
|
|
16
|
+
"docs/rules/foundations.md",
|
|
17
|
+
"docs/rules/color-system.md",
|
|
18
|
+
"docs/rules/token-conventions.md",
|
|
19
|
+
"docs/rules/component-usage.md",
|
|
20
|
+
"docs/rules/icon-conventions.md",
|
|
21
|
+
"docs/rules/layout-patterns.md",
|
|
22
|
+
"docs/rules/content-conventions.md",
|
|
23
|
+
"docs/rules/diagram-conventions.md"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"howToUse": "Import components from '@toolr/ui-design'. Import CSS tokens via @import '@toolr/ui-design/tokens' in your app CSS. Read the relevant docs/rules/ file for conventions. For full component interfaces, read the source file listed in each component entry."
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"conventions": {
|
|
30
|
+
"onChange": "Receives the new value directly, never a React event",
|
|
31
|
+
"className": "Use cn() from @toolr/ui-design for conditional class composition (clsx + tailwind-merge)",
|
|
32
|
+
"icons": "Use IconButton with icon='name' string prop (lucide icon names). Do NOT import lucide-react directly",
|
|
33
|
+
"tooltips": "Always an object { description: string, title?: string, extra?: string }, never a plain string",
|
|
34
|
+
"colors": "Use Tailwind utilities for accents (text-blue-400, bg-green-500/20). Use CSS vars for structural colors (var(--surface)). Never raw hex",
|
|
35
|
+
"spacing": "4px grid: 0, 0.5, 1, 1.5, 2, 3, 4, 6 (= 0-24px). Never arbitrary values like mt-7 or px-9",
|
|
36
|
+
"typography": "text-xs (12px captions), text-sm (14px body), text-base (16px headings). text-xss (11px) for fine print",
|
|
37
|
+
"modals": "Use ConfirmModal/AlertModal/ActionDialog. Never window.alert/confirm/prompt"
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
"types": {
|
|
41
|
+
"FormColor": "blue | green | red | orange | cyan | yellow | purple | indigo | emerald | amber | violet | gray | sky",
|
|
42
|
+
"AccentColor": "blue | purple | orange | green | pink | amber | emerald | teal | sky",
|
|
43
|
+
"IconName": "string — any lucide icon name (e.g. 'save', 'trash', 'search', 'plus')",
|
|
44
|
+
"TooltipContent": "{ title?: string, description: string | ReactNode, extra?: string }",
|
|
45
|
+
"SelectOption<T>": "{ value: T, label: string, icon?: ReactNode }",
|
|
46
|
+
"ModalKind": "info | warning | error | success",
|
|
47
|
+
"ModalSize": "sm | md | lg | xl"
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
"components": {
|
|
51
|
+
"form": [
|
|
52
|
+
{ "name": "Input", "description": "Text input with search mode, action slot, error state", "replaces": "input", "keyProps": "value, onChange, type?: text|search, variant?: filled|ghost, size?: sm|md, error?, mono?, actionSlot?", "file": "components/ui/input.tsx" },
|
|
53
|
+
{ "name": "NumberInput", "description": "Numeric input with increment/decrement buttons", "replaces": "input[type=number]", "keyProps": "value, onChange, min?, max?, step?, size?: sm|md", "file": "components/ui/number-input.tsx" },
|
|
54
|
+
{ "name": "Select", "description": "Portal dropdown with keyboard nav and check indicators", "replaces": "select", "keyProps": "value, onChange, options: SelectOption[], color?: FormColor, size?: sm|md, variant?: filled|ghost", "file": "components/ui/select.tsx" },
|
|
55
|
+
{ "name": "FilterDropdown", "description": "Clearable single-select filter with 'All' default and optional search", "keyProps": "value, onChange, options, allLabel?, searchable?", "file": "components/ui/filter-dropdown.tsx" },
|
|
56
|
+
{ "name": "SortDropdown", "description": "Field picker with direction toggle arrow", "keyProps": "field, ascending, fields: SortField[], onFieldChange, onToggleDirection", "file": "components/ui/sort-dropdown.tsx" },
|
|
57
|
+
{ "name": "Toggle", "description": "iOS-style boolean switch", "replaces": "input[type=checkbox] (for on/off settings)", "keyProps": "checked, onChange, color?: FormColor, size?: sm|md|lg", "file": "components/ui/toggle.tsx" },
|
|
58
|
+
{ "name": "Checkbox", "description": "Check-icon boolean for multi-select lists", "replaces": "input[type=checkbox] (for checklists)", "keyProps": "checked, onChange, color?: FormColor, size?: sm|md|lg", "file": "components/ui/checkbox.tsx" },
|
|
59
|
+
{ "name": "SegmentedToggle", "description": "Horizontal icon-based mutually-exclusive toggle group", "keyProps": "options: SegmentedToggleOption[], value, onChange, color?: FormColor", "file": "components/ui/segmented-toggle.tsx" },
|
|
60
|
+
{ "name": "ResizableTextarea", "description": "Textarea with drag-to-resize handle", "replaces": "textarea", "keyProps": "value, onChange, variant?: filled|ghost, minHeight?, maxHeight?", "file": "components/ui/resizable-textarea.tsx" },
|
|
61
|
+
{ "name": "FormActions", "description": "Flex row for form buttons with optional nav slot", "keyProps": "padding?: compact|normal|modal, children", "file": "components/ui/form-actions.tsx" },
|
|
62
|
+
{ "name": "SelectionGrid", "description": "Grid of selectable cards with icons and descriptions", "keyProps": "items: SelectionCardItem[], selected, onSelect", "file": "components/ui/selection-grid.tsx" }
|
|
63
|
+
],
|
|
64
|
+
|
|
65
|
+
"action": [
|
|
66
|
+
{ "name": "IconButton", "description": "Icon-only button with tooltip, badge, status indicator, href mode", "replaces": "button", "keyProps": "icon: IconName|ReactNode, tooltip: TooltipContent, color?: FormColor, size?: xss|xs|sm|lg, status?, badge?, href?, strikethrough?, disabled?", "file": "components/ui/icon-button.tsx" },
|
|
67
|
+
{ "name": "CollapseButton", "description": "Animated chevron toggle button for expand/collapse", "keyProps": "isOpen, onToggle, tooltip: TooltipContent", "file": "components/ui/icon-button.tsx" },
|
|
68
|
+
{ "name": "AiActionButton", "description": "Button with AI loading/success/error states for async AI operations", "keyProps": "icon, tooltip, status: AiActionStatus, onClick, completionResult?", "file": "components/ui/ai-action-button.tsx" },
|
|
69
|
+
{ "name": "AiExecutionActionButtons", "description": "Run/stop/retry button group for AI execution workflows", "keyProps": "status: ExecutionStatus, onRun, onStop, onRetry", "file": "components/ui/ai-execution-action-buttons.tsx" },
|
|
70
|
+
{ "name": "ConfirmBadge", "description": "Badge that requires click-confirmation before triggering action", "keyProps": "label, onConfirm, color?: ConfirmBadgeColor", "file": "components/ui/confirm-badge.tsx" }
|
|
71
|
+
],
|
|
72
|
+
|
|
73
|
+
"display": [
|
|
74
|
+
{ "name": "Label", "description": "Border-focused colored badge/tag with icon and tooltip", "keyProps": "text, color: LabelColor, icon?, tooltip?: TooltipContent, size?: xs|sm|md", "file": "components/ui/label.tsx" },
|
|
75
|
+
{ "name": "Badge", "description": "Minimal inline badge for counts or short labels", "keyProps": "text, color?: BadgeColor", "file": "components/ui/badge.tsx" },
|
|
76
|
+
{ "name": "ScopeBadge", "description": "Pre-configured Label for user/project/local/read-only scopes", "keyProps": "scope: ScopeType", "file": "components/ui/scope-badge.tsx" },
|
|
77
|
+
{ "name": "Tooltip", "description": "Portal-based tooltip with auto-positioning and arrow", "keyProps": "content: TooltipContent, position?: top|bottom|left|right|auto, children", "file": "components/ui/tooltip.tsx" },
|
|
78
|
+
{ "name": "TooltipButton", "description": "Icon button that toggles a persistent tooltip on click", "keyProps": "content: TooltipContent, icon?: IconName", "file": "components/ui/tooltip.tsx" },
|
|
79
|
+
{ "name": "StatusCard", "description": "Card showing a list of status items with icons and values", "keyProps": "title, items: StatusItem[]", "file": "components/ui/status-card.tsx" },
|
|
80
|
+
{ "name": "SnapshotCard", "description": "Card displaying snapshot metadata with timestamp and actions", "keyProps": "snapshot, onRestore?, onDelete?", "file": "components/ui/snapshot-card.tsx" }
|
|
81
|
+
],
|
|
82
|
+
|
|
83
|
+
"layout": [
|
|
84
|
+
{ "name": "CollapsibleSection", "description": "Section with animated expand/collapse and header actions", "keyProps": "title, defaultOpen?, children, actions?", "file": "components/ui/collapsible-section.tsx" },
|
|
85
|
+
{ "name": "DetailSection", "description": "Key-value detail rows in a bordered section", "keyProps": "title, rows: DetailRow[]", "file": "components/ui/detail-section.tsx" },
|
|
86
|
+
{ "name": "DetailViewWrapper", "description": "Standard wrapper for detail/edit views with header and back navigation", "keyProps": "title, onBack?, children, actions?", "file": "components/ui/detail-view-wrapper.tsx" },
|
|
87
|
+
{ "name": "BottomPanelHeader", "description": "Tab bar header for bottom panels with status banner", "keyProps": "tabs: PanelTab[], activeTab, onTabChange, banner?: StatusBanner", "file": "components/ui/bottom-panel-header.tsx" },
|
|
88
|
+
{ "name": "EditorToolbar", "description": "Toolbar row for editor panels with title, actions, and status", "keyProps": "title?, children, actions?", "file": "components/ui/editor-toolbar.tsx" },
|
|
89
|
+
{ "name": "EditorPlaceholderCard", "description": "Centered placeholder card shown when no editor content is active", "keyProps": "icon, title, description", "file": "components/ui/editor-placeholder-card.tsx" },
|
|
90
|
+
{ "name": "FrontmatterFormHeader", "description": "Form header for YAML frontmatter editing with field labels", "keyProps": "fields, onChange", "file": "components/ui/frontmatter-form-header.tsx" },
|
|
91
|
+
{ "name": "FilesPanel", "description": "File list panel with add/remove actions", "keyProps": "files: FileEntry[], onAdd?, onRemove?", "file": "components/ui/files-panel.tsx" },
|
|
92
|
+
{ "name": "SnippetsPanel", "description": "Panel displaying a list of text snippets with copy action", "keyProps": "snippets: Snippet[], onCopy?", "file": "components/ui/snippets-panel.tsx" }
|
|
93
|
+
],
|
|
94
|
+
|
|
95
|
+
"navigation": [
|
|
96
|
+
{ "name": "Breadcrumb", "description": "Breadcrumb trail with clickable segments", "keyProps": "segments: BreadcrumbSegment[], onNavigate", "file": "components/ui/breadcrumb.tsx" },
|
|
97
|
+
{ "name": "NavigationBar", "description": "Top navigation bar with title, back button, and actions", "keyProps": "title, onBack?, children", "file": "components/ui/navigation-bar.tsx" },
|
|
98
|
+
{ "name": "TabBar", "description": "Horizontal tab strip with optional badge counts", "keyProps": "tabs: Tab[], activeTab, onTabChange", "file": "components/ui/tab-bar.tsx" },
|
|
99
|
+
{ "name": "LayoutTabBar", "description": "Tab bar for switching layout views (e.g. split/full)", "keyProps": "tabs: LayoutTab[], activeTab, onTabChange", "file": "components/ui/layout-tab-bar.tsx" },
|
|
100
|
+
{ "name": "NavCard", "description": "Navigation card with icon, title, and description for app selection", "keyProps": "title, description, icon, onClick, color?", "file": "components/ui/nav-card.tsx" },
|
|
101
|
+
{ "name": "ExtensionListCard", "description": "Card showing extension info with type indicator and actions", "keyProps": "title, type, description, actions?", "file": "components/ui/extension-list-card.tsx" }
|
|
102
|
+
],
|
|
103
|
+
|
|
104
|
+
"modal": [
|
|
105
|
+
{ "name": "ConfirmModal", "description": "Action confirmation dialog with confirm/cancel buttons", "replaces": "window.confirm", "keyProps": "isOpen, onClose, onConfirm, title, kind?: ModalKind, confirmLabel?, loading?, children", "file": "components/ui/modal.tsx" },
|
|
106
|
+
{ "name": "AlertModal", "description": "Dismiss-only alert dialog", "replaces": "window.alert", "keyProps": "isOpen, onClose, title, kind?: ModalKind, children", "file": "components/ui/modal.tsx" },
|
|
107
|
+
{ "name": "ActionDialog", "description": "Full-featured dialog with custom content and action buttons", "keyProps": "isOpen, onClose, title, size?: ModalSize, children, actions?", "file": "components/ui/action-dialog.tsx" }
|
|
108
|
+
],
|
|
109
|
+
|
|
110
|
+
"section": [
|
|
111
|
+
{ "name": "TabbedPromptEditor", "description": "Multi-tab prompt editor with variable insertion and dirty detection. Use with usePromptEditor() hook", "keyProps": "...usePromptEditor(), standalone?, className?", "hook": "usePromptEditor", "file": "components/sections/prompt-editor/index.ts" },
|
|
112
|
+
{ "name": "FileTypeTabbedPromptEditor", "description": "Prompt editor variant with file type tabs", "keyProps": "...usePromptEditor(), fileTypes: FileTypeOption[]", "hook": "usePromptEditor", "file": "components/sections/prompt-editor/index.ts" },
|
|
113
|
+
{ "name": "SimulatorPromptEditor", "description": "Prompt editor with scenario selector for testing prompts", "keyProps": "...usePromptEditor(), scenarios: ScenarioOption[]", "file": "components/sections/prompt-editor/index.ts" },
|
|
114
|
+
{ "name": "RegistryBrowser", "description": "Browsable grid of registry items with search, filter, sort", "keyProps": "items, onSelect, selectedId?", "file": "components/ui/registry-browser.tsx" },
|
|
115
|
+
{ "name": "RegistryDetail", "description": "Detail view for a single registry item with file structure", "keyProps": "item, onBack, onInstall?", "file": "components/ui/registry-detail.tsx" },
|
|
116
|
+
{ "name": "RegistryCard", "description": "Card displaying a registry item with type badge and metadata", "keyProps": "item, onClick?, selected?", "file": "components/ui/registry-card.tsx" },
|
|
117
|
+
{ "name": "GoldenSyncPanel", "description": "Golden snapshot sync panel with diff viewer. Use with useGoldenSync() hook", "keyProps": "...useGoldenSync()", "hook": "useGoldenSync", "file": "components/sections/golden-snapshots/index.ts" },
|
|
118
|
+
{ "name": "SnapshotBrowserPanel", "description": "Browse and manage snapshots. Use with useSnapshotBrowser() hook", "keyProps": "...useSnapshotBrowser()", "hook": "useSnapshotBrowser", "file": "components/sections/snapshot-browser/index.ts" },
|
|
119
|
+
{ "name": "SnippetsEditor", "description": "Create and edit text snippets. Use with useSnippetsEditor() hook", "keyProps": "...useSnippetsEditor()", "hook": "useSnippetsEditor", "file": "components/sections/snippets-editor/index.ts" },
|
|
120
|
+
{ "name": "ReportBugForm", "description": "Bug report form with screenshot capture. Use with useReportBug() hook", "keyProps": "...useReportBug()", "hook": "useReportBug", "file": "components/sections/report-bug/index.ts" },
|
|
121
|
+
{ "name": "ScreenshotUploader", "description": "Drag & drop multi-image upload with base64 encoding and preview thumbnails", "keyProps": "screenshots: Screenshot[], onChange, maxTotalSize?, disabled?", "file": "components/sections/report-bug/index.ts" },
|
|
122
|
+
{ "name": "ToolsPathsPanel", "description": "AI tool path configuration panel. Use with useToolsPaths() hook", "keyProps": "...useToolsPaths()", "hook": "useToolsPaths", "file": "components/sections/ai-tools-paths/index.ts" },
|
|
123
|
+
{ "name": "CapturedIssuesPanel", "description": "View and manage captured error issues. Use with useCapturedIssues() hook", "keyProps": "...useCapturedIssues()", "hook": "useCapturedIssues", "file": "components/sections/captured-issues/index.ts" },
|
|
124
|
+
{ "name": "FileStructureSection", "description": "File tree with code preview, syntax highlighting, and copy", "keyProps": "files: FileTreeNode[], accentColor?: AccentColor", "file": "components/ui/file-structure-section.tsx" },
|
|
125
|
+
{ "name": "FileTree", "description": "Expandable file/folder tree with selection", "keyProps": "nodes: FileTreeNode[], selectedPath?, onSelect", "file": "components/ui/file-tree.tsx" },
|
|
126
|
+
{ "name": "ExecutionDetailsPanel", "description": "Panel showing execution result rows with expandable details", "keyProps": "rows: ExecutionDetailRow[]", "file": "components/ui/execution-details-panel.tsx" }
|
|
127
|
+
],
|
|
128
|
+
|
|
129
|
+
"settings": [
|
|
130
|
+
{ "name": "SettingsPanel", "description": "Full settings page with tree navigation and content area", "keyProps": "tree: SettingsTreeNode[], activePath, onNavigate, children", "file": "components/settings/index.ts" },
|
|
131
|
+
{ "name": "SettingsTreeNav", "description": "Tree navigation sidebar for settings", "keyProps": "tree: SettingsTreeNode[], activePath, onNavigate", "file": "components/settings/index.ts" },
|
|
132
|
+
{ "name": "SettingsHeader", "description": "Header for settings pages with breadcrumb", "keyProps": "path: string[], tree: SettingsTreeNode[]", "file": "components/settings/index.ts" },
|
|
133
|
+
{ "name": "SettingRow", "description": "Single settings row with label, description, and control slot", "keyProps": "label, description?, children (control element)", "file": "components/ui/setting-row.tsx" },
|
|
134
|
+
{ "name": "SettingsCard", "description": "Bordered card container for grouping settings", "keyProps": "title?, children", "file": "components/ui/settings-card.tsx" },
|
|
135
|
+
{ "name": "SettingsInfoBox", "description": "Informational callout box within settings", "keyProps": "title?, children, color?: SettingsInfoBoxColor", "file": "components/ui/settings-info-box.tsx" },
|
|
136
|
+
{ "name": "SettingsSectionTitle", "description": "Section heading for settings groups", "keyProps": "title, description?", "file": "components/ui/settings-section-title.tsx" }
|
|
137
|
+
],
|
|
138
|
+
|
|
139
|
+
"brand": [
|
|
140
|
+
{ "name": "ToolrAppLogo", "description": "App logo with icon and name for toolr suite apps", "keyProps": "appId: ToolrAppId, size?: sm|md|lg", "file": "components/lib/toolr-brand.tsx" },
|
|
141
|
+
{ "name": "AiToolIcon", "description": "Logo icon for AI tools (Claude, GPT, Gemini, etc.)", "keyProps": "tool: AiToolKey, size?: number", "file": "components/lib/ai-tools.tsx" },
|
|
142
|
+
{ "name": "CookieConsent", "description": "Cookie consent banner with accept/reject", "keyProps": "onConsent: (choice: ConsentChoice) => void", "file": "components/ui/cookie-consent.tsx" }
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
"hooks": [
|
|
147
|
+
{ "name": "usePromptEditor", "description": "State manager for TabbedPromptEditor — tabs, dirty detection, save, variables", "file": "components/sections/prompt-editor/index.ts" },
|
|
148
|
+
{ "name": "useGoldenSync", "description": "State manager for GoldenSyncPanel — diff tree, sync status, file comparison", "file": "components/sections/golden-snapshots/index.ts" },
|
|
149
|
+
{ "name": "useSnapshotBrowser", "description": "State manager for SnapshotBrowserPanel — snapshot list, categories, actions", "file": "components/sections/snapshot-browser/index.ts" },
|
|
150
|
+
{ "name": "useSnippetsEditor", "description": "State manager for SnippetsEditor — CRUD operations on snippets", "file": "components/sections/snippets-editor/index.ts" },
|
|
151
|
+
{ "name": "useReportBug", "description": "State manager for ReportBugForm — issue types, screenshots, submission", "file": "components/sections/report-bug/index.ts" },
|
|
152
|
+
{ "name": "useToolsPaths", "description": "State manager for ToolsPathsPanel — AI tool detection, path configuration", "file": "components/sections/ai-tools-paths/index.ts" },
|
|
153
|
+
{ "name": "useCapturedIssues", "description": "State manager for CapturedIssuesPanel — error list, submission, dismissal", "file": "components/sections/captured-issues/index.ts" },
|
|
154
|
+
{ "name": "useClickOutside", "description": "Close menus/dropdowns when clicking outside the ref element", "file": "components/hooks/use-click-outside.ts" },
|
|
155
|
+
{ "name": "useDropdownMaxHeight", "description": "Calculate max height for dropdowns to stay within viewport", "file": "components/hooks/use-dropdown-max-height.ts" },
|
|
156
|
+
{ "name": "useNavigationHistory", "description": "Browser-like back/forward navigation state", "file": "components/hooks/use-navigation-history.ts" }
|
|
157
|
+
],
|
|
158
|
+
|
|
159
|
+
"utilities": [
|
|
160
|
+
{ "name": "cn", "description": "clsx + tailwind-merge for conditional class composition", "usage": "cn('base-class', condition && 'conditional-class', className)", "file": "components/lib/cn.ts" },
|
|
161
|
+
{ "name": "FORM_COLORS", "description": "Color config map — FORM_COLORS[color].border/.hover/.focus/.selectedBg/.accent", "file": "components/lib/form-colors.ts" },
|
|
162
|
+
{ "name": "applyTheme", "description": "Apply theme to document — applyTheme(themeId, accentHue)", "file": "components/lib/theme-engine.ts" },
|
|
163
|
+
{ "name": "ACCENT_DEFS", "description": "Array of { id, label, hue } for all available accent colors", "file": "components/lib/theme-engine.ts" },
|
|
164
|
+
{ "name": "iconMap", "description": "Map of icon name strings to lucide-react components", "file": "components/ui/icon-button.tsx" }
|
|
165
|
+
],
|
|
166
|
+
|
|
167
|
+
"enforcement": {
|
|
168
|
+
"htmlReplacements": {
|
|
169
|
+
"button": "IconButton",
|
|
170
|
+
"input": "Input",
|
|
171
|
+
"select": "Select",
|
|
172
|
+
"textarea": "ResizableTextarea"
|
|
173
|
+
},
|
|
174
|
+
"allowedImportPaths": [
|
|
175
|
+
"@toolr/ui-design",
|
|
176
|
+
"@toolr/ui-design/tokens",
|
|
177
|
+
"@toolr/ui-design/content",
|
|
178
|
+
"@toolr/ui-design/diagrams",
|
|
179
|
+
"@toolr/ui-design/preset",
|
|
180
|
+
"@toolr/ui-design/manifest"
|
|
181
|
+
],
|
|
182
|
+
"bannedImports": ["lucide-react"],
|
|
183
|
+
"bannedGlobals": ["alert", "confirm", "prompt"],
|
|
184
|
+
"bannedGlobalReplacements": {
|
|
185
|
+
"alert": "AlertModal",
|
|
186
|
+
"confirm": "ConfirmModal",
|
|
187
|
+
"prompt": "ConfirmModal with an Input"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -10,7 +10,7 @@ import type { ReactNode } from 'react'
|
|
|
10
10
|
// ─── Paragraph ──────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
export function P({ children }: { children: ReactNode }) {
|
|
13
|
-
return <p className="text-
|
|
13
|
+
return <p className="text-md text-neutral-400 leading-relaxed mb-5">{children}</p>
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// ─── Section Header ─────────────────────────────────────────────────
|
|
@@ -18,7 +18,7 @@ export function P({ children }: { children: ReactNode }) {
|
|
|
18
18
|
export function SectionHeader({ color, children }: { color?: string; children: ReactNode }) {
|
|
19
19
|
const textColor = color ? `text-${color}-500/70` : 'text-neutral-500'
|
|
20
20
|
return (
|
|
21
|
-
<p className={`${textColor} text-
|
|
21
|
+
<p className={`${textColor} text-sm uppercase font-semibold pb-0.5 border-b border-neutral-700/50 mb-2.5`} style={{ letterSpacing: '0.8px' }}>
|
|
22
22
|
{children}
|
|
23
23
|
</p>
|
|
24
24
|
)
|
|
@@ -38,10 +38,10 @@ export function DLRow({ term, children, even }: { term: ReactNode; children: Rea
|
|
|
38
38
|
const bg = even ? 'bg-white/[0.015]' : ''
|
|
39
39
|
return (
|
|
40
40
|
<>
|
|
41
|
-
<div className={`py-2 border-b border-neutral-800/60 ${bg} font-semibold text-
|
|
41
|
+
<div className={`py-2 border-b border-neutral-800/60 ${bg} font-semibold text-md whitespace-nowrap`}>
|
|
42
42
|
{term}
|
|
43
43
|
</div>
|
|
44
|
-
<div className={`py-2 border-b border-neutral-800/60 ${bg} text-
|
|
44
|
+
<div className={`py-2 border-b border-neutral-800/60 ${bg} text-md text-neutral-400`}>{children}</div>
|
|
45
45
|
</>
|
|
46
46
|
)
|
|
47
47
|
}
|
|
@@ -49,7 +49,7 @@ export function DLRow({ term, children, even }: { term: ReactNode; children: Rea
|
|
|
49
49
|
// ─── Unordered List ─────────────────────────────────────────────────
|
|
50
50
|
|
|
51
51
|
export function UL({ children }: { children: ReactNode }) {
|
|
52
|
-
return <ul className="text-
|
|
52
|
+
return <ul className="text-md text-neutral-400 space-y-2 mb-5">{children}</ul>
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function LI({ color, children }: { color: string; children: ReactNode }) {
|
|
@@ -69,7 +69,7 @@ export function OL({ children }: { children: ReactNode }) {
|
|
|
69
69
|
|
|
70
70
|
export function OLI({ n, color, children }: { n: number; color: string; children: ReactNode }) {
|
|
71
71
|
return (
|
|
72
|
-
<div className="flex items-start gap-2.5 text-
|
|
72
|
+
<div className="flex items-start gap-2.5 text-md">
|
|
73
73
|
<span className={`text-${color}-400 shrink-0 w-4 text-right font-bold`}>{n}.</span>
|
|
74
74
|
<span className="text-neutral-400">{children}</span>
|
|
75
75
|
</div>
|
|
@@ -105,7 +105,7 @@ export function getCalloutColors(color: string) {
|
|
|
105
105
|
export function Callout({ color, children }: { color: string; children: ReactNode }) {
|
|
106
106
|
const c = CALLOUT_COLORS[color] ?? CALLOUT_COLORS.blue
|
|
107
107
|
return (
|
|
108
|
-
<div className={`border-l-4 ${c.borderL} border-y ${c.borderY} ${c.bg} px-3 py-2.5 rounded-r mb-5 text-
|
|
108
|
+
<div className={`border-l-4 ${c.borderL} border-y ${c.borderY} ${c.bg} px-3 py-2.5 rounded-r mb-5 text-md text-neutral-400`}>
|
|
109
109
|
{children}
|
|
110
110
|
</div>
|
|
111
111
|
)
|
|
@@ -114,7 +114,7 @@ export function Callout({ color, children }: { color: string; children: ReactNod
|
|
|
114
114
|
export function CalloutCode({ color, children }: { color: string; children: ReactNode }) {
|
|
115
115
|
const c = CALLOUT_COLORS[color] ?? CALLOUT_COLORS.blue
|
|
116
116
|
return (
|
|
117
|
-
<code className={`block bg-neutral-800/80 px-2 py-1 rounded mt-1.5 text-
|
|
117
|
+
<code className={`block bg-neutral-800/80 px-2 py-1 rounded mt-1.5 text-md ${c.codeText}`}>
|
|
118
118
|
{children}
|
|
119
119
|
</code>
|
|
120
120
|
)
|
|
@@ -128,7 +128,7 @@ export function CalloutDim({ children }: { children: ReactNode }) {
|
|
|
128
128
|
|
|
129
129
|
export function CodeBlock({ children }: { children: ReactNode }) {
|
|
130
130
|
return (
|
|
131
|
-
<div className="bg-neutral-900/60 rounded-md p-3 font-mono text-
|
|
131
|
+
<div className="bg-neutral-900/60 rounded-md p-3 font-mono text-sm text-neutral-400 mb-5 whitespace-pre overflow-x-auto leading-normal">{children}</div>
|
|
132
132
|
)
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -179,9 +179,9 @@ export function LocationItem({
|
|
|
179
179
|
<span
|
|
180
180
|
className={`w-2 h-2 rounded-full bg-${color}-500/50 border border-${color}-500 shrink-0`}
|
|
181
181
|
/>
|
|
182
|
-
<span className={`text-${color}-400 text-
|
|
182
|
+
<span className={`text-${color}-400 text-md font-semibold`}>{label}</span>
|
|
183
183
|
</div>
|
|
184
|
-
<div className={`py-2 border-b border-neutral-800/60 ${bg} text-
|
|
184
|
+
<div className={`py-2 border-b border-neutral-800/60 ${bg} text-md text-neutral-400`}>{children}</div>
|
|
185
185
|
</>
|
|
186
186
|
)
|
|
187
187
|
}
|
|
@@ -190,7 +190,7 @@ export function LocationItem({
|
|
|
190
190
|
|
|
191
191
|
export function TitledLI({ color, title, children }: { color: string; title: string; children: ReactNode }) {
|
|
192
192
|
return (
|
|
193
|
-
<li className="flex items-start gap-2 text-
|
|
193
|
+
<li className="flex items-start gap-2 text-md text-neutral-400">
|
|
194
194
|
<span className={`text-${color}-400 shrink-0`} style={{ marginTop: '3px', fontSize: '18px', lineHeight: '14px' }}>•</span>
|
|
195
195
|
<span>
|
|
196
196
|
<span className={`text-${color}-300 font-semibold`}>{title}</span>
|
|
@@ -204,7 +204,7 @@ export function TitledLI({ color, title, children }: { color: string; title: str
|
|
|
204
204
|
|
|
205
205
|
export function CalloutDialog({ color, lines }: { color: string; lines: { speaker: string; text: string }[] }) {
|
|
206
206
|
return (
|
|
207
|
-
<div className="bg-neutral-800/80 rounded px-2 py-1 mt-1.5 flex flex-col gap-0.5 text-
|
|
207
|
+
<div className="bg-neutral-800/80 rounded px-2 py-1 mt-1.5 flex flex-col gap-0.5 text-md">
|
|
208
208
|
{lines.map((line, idx) => (
|
|
209
209
|
<div key={idx}>
|
|
210
210
|
<span className={`text-${color}-300 font-semibold mr-1`}>{line.speaker}:</span>
|
|
@@ -238,7 +238,7 @@ export function StatusBadge({ value, badgeColor, label, children, even }: {
|
|
|
238
238
|
<DLRow
|
|
239
239
|
term={
|
|
240
240
|
<span className="flex items-center gap-1.5">
|
|
241
|
-
<span className={`inline-flex items-center justify-center w-5 h-5 rounded-full bg-${badgeColor}-500/20 text-${badgeColor}-400 text-
|
|
241
|
+
<span className={`inline-flex items-center justify-center w-5 h-5 rounded-full bg-${badgeColor}-500/20 text-${badgeColor}-400 text-xs font-bold shrink-0`}>{value}</span>
|
|
242
242
|
<span className={`text-${badgeColor}-400 font-semibold`}>{label}</span>
|
|
243
243
|
</span>
|
|
244
244
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useEffect, type RefObject } from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Close a menu/dropdown when clicking outside its ref element.
|
|
4
|
+
* Close a menu/dropdown when clicking outside its ref element(s).
|
|
5
|
+
* Accepts a single ref, an array of refs, or null.
|
|
6
|
+
* When given an array, closes only if the click is outside ALL refs.
|
|
5
7
|
* If ref is null, closes on any mousedown event.
|
|
6
8
|
*/
|
|
7
9
|
export function useClickOutside(
|
|
8
|
-
ref: RefObject<HTMLElement | null> | null,
|
|
10
|
+
ref: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[] | null,
|
|
9
11
|
isOpen: boolean,
|
|
10
12
|
onClose: () => void
|
|
11
13
|
): void {
|
|
@@ -17,7 +19,12 @@ export function useClickOutside(
|
|
|
17
19
|
onClose()
|
|
18
20
|
return
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
const refs = Array.isArray(ref) ? ref : [ref]
|
|
24
|
+
const isOutsideAll = refs.every(
|
|
25
|
+
(r) => !r.current || !r.current.contains(event.target as Node)
|
|
26
|
+
)
|
|
27
|
+
if (isOutsideAll) {
|
|
21
28
|
onClose()
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared modal behavior: Escape key to close + body overflow lock.
|
|
5
|
+
*/
|
|
6
|
+
export function useModalBehavior(isOpen: boolean, onClose: () => void): void {
|
|
7
|
+
// Escape key handler
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (!isOpen) return
|
|
10
|
+
const handler = (e: KeyboardEvent) => {
|
|
11
|
+
if (e.key === 'Escape') onClose()
|
|
12
|
+
}
|
|
13
|
+
document.addEventListener('keydown', handler)
|
|
14
|
+
return () => document.removeEventListener('keydown', handler)
|
|
15
|
+
}, [isOpen, onClose])
|
|
16
|
+
|
|
17
|
+
// Body overflow lock
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!isOpen) return
|
|
20
|
+
const prev = document.body.style.overflow
|
|
21
|
+
document.body.style.overflow = 'hidden'
|
|
22
|
+
return () => { document.body.style.overflow = prev }
|
|
23
|
+
}, [isOpen])
|
|
24
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Hook for managing back/forward navigation history with a breadcrumb segment stack. */
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback } from 'react'
|
|
3
|
+
import { useState, useCallback, useMemo } from 'react'
|
|
4
4
|
import type { BreadcrumbSegment } from '../ui/breadcrumb.tsx'
|
|
5
5
|
|
|
6
6
|
interface NavigationState {
|
|
@@ -81,6 +81,11 @@ export function useNavigationHistory(
|
|
|
81
81
|
})
|
|
82
82
|
}, [])
|
|
83
83
|
|
|
84
|
+
const history = useMemo(
|
|
85
|
+
() => [...state.backStack, ...(state.current ? [state.current] : [])],
|
|
86
|
+
[state.backStack, state.current],
|
|
87
|
+
)
|
|
88
|
+
|
|
84
89
|
return {
|
|
85
90
|
current: state.current,
|
|
86
91
|
canGoBack: state.backStack.length > 0,
|
|
@@ -89,6 +94,6 @@ export function useNavigationHistory(
|
|
|
89
94
|
goBack,
|
|
90
95
|
goForward,
|
|
91
96
|
goTo,
|
|
92
|
-
history
|
|
97
|
+
history,
|
|
93
98
|
}
|
|
94
99
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface UseResizableSidebarOptions {
|
|
4
|
+
min: number
|
|
5
|
+
max: number
|
|
6
|
+
defaultWidth: number
|
|
7
|
+
/** 'left' = drag right shrinks (sidebar on right), 'right' = drag right grows (sidebar on left) */
|
|
8
|
+
direction?: 'left' | 'right'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useResizableSidebar({ min, max, defaultWidth, direction = 'right' }: UseResizableSidebarOptions) {
|
|
12
|
+
const [width, setWidth] = useState(defaultWidth)
|
|
13
|
+
const widthRef = useRef(defaultWidth)
|
|
14
|
+
|
|
15
|
+
const onPointerDown = useCallback((e: React.PointerEvent) => {
|
|
16
|
+
e.preventDefault()
|
|
17
|
+
const el = e.currentTarget as HTMLElement
|
|
18
|
+
el.setPointerCapture(e.pointerId)
|
|
19
|
+
const startX = e.clientX
|
|
20
|
+
const startW = widthRef.current
|
|
21
|
+
|
|
22
|
+
const onMove = (ev: PointerEvent) => {
|
|
23
|
+
const delta = direction === 'left' ? startX - ev.clientX : ev.clientX - startX
|
|
24
|
+
const next = Math.max(min, Math.min(max, startW + delta))
|
|
25
|
+
widthRef.current = next
|
|
26
|
+
setWidth(next)
|
|
27
|
+
}
|
|
28
|
+
const onUp = () => {
|
|
29
|
+
el.removeEventListener('pointermove', onMove)
|
|
30
|
+
el.removeEventListener('pointerup', onUp)
|
|
31
|
+
el.releasePointerCapture(e.pointerId)
|
|
32
|
+
}
|
|
33
|
+
el.addEventListener('pointermove', onMove)
|
|
34
|
+
el.addEventListener('pointerup', onUp)
|
|
35
|
+
}, [min, max, direction])
|
|
36
|
+
|
|
37
|
+
return { width, onPointerDown }
|
|
38
|
+
}
|
|
@@ -38,7 +38,7 @@ export function AiToolIcon({ tool, size, showName, className, style }: {
|
|
|
38
38
|
return (
|
|
39
39
|
<span style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
|
|
40
40
|
{img}
|
|
41
|
-
<span className="text-
|
|
41
|
+
<span className="text-xs text-neutral-400">{AI_TOOL_NAMES[tool as AiToolKey] ?? tool}</span>
|
|
42
42
|
</span>
|
|
43
43
|
)
|
|
44
44
|
}
|
|
@@ -16,6 +16,46 @@ interface FormColorConfig {
|
|
|
16
16
|
accent: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type AccentColor = FormColor
|
|
20
|
+
|
|
21
|
+
export const ACCENT_TEXT: Record<AccentColor, string> = {
|
|
22
|
+
blue: 'text-blue-400',
|
|
23
|
+
green: 'text-green-400',
|
|
24
|
+
red: 'text-red-400',
|
|
25
|
+
orange: 'text-orange-400',
|
|
26
|
+
cyan: 'text-cyan-400',
|
|
27
|
+
yellow: 'text-yellow-400',
|
|
28
|
+
purple: 'text-purple-400',
|
|
29
|
+
indigo: 'text-indigo-400',
|
|
30
|
+
emerald: 'text-emerald-400',
|
|
31
|
+
amber: 'text-amber-400',
|
|
32
|
+
violet: 'text-violet-400',
|
|
33
|
+
neutral: 'text-neutral-400',
|
|
34
|
+
sky: 'text-sky-400',
|
|
35
|
+
pink: 'text-pink-400',
|
|
36
|
+
teal: 'text-teal-400',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const ACCENT_ICON = ACCENT_TEXT
|
|
40
|
+
|
|
41
|
+
export const ACCENT_NAV: Record<AccentColor, { bg: string; text: string }> = {
|
|
42
|
+
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
|
43
|
+
green: { bg: 'bg-green-500/10', text: 'text-green-400' },
|
|
44
|
+
red: { bg: 'bg-red-500/10', text: 'text-red-400' },
|
|
45
|
+
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
|
|
46
|
+
cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
|
|
47
|
+
yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
|
|
48
|
+
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
|
|
49
|
+
indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
|
|
50
|
+
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
|
|
51
|
+
amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
|
|
52
|
+
violet: { bg: 'bg-violet-500/10', text: 'text-violet-400' },
|
|
53
|
+
neutral: { bg: 'bg-neutral-500/10', text: 'text-neutral-400' },
|
|
54
|
+
sky: { bg: 'bg-sky-500/10', text: 'text-sky-400' },
|
|
55
|
+
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
|
|
56
|
+
teal: { bg: 'bg-teal-500/10', text: 'text-teal-400' },
|
|
57
|
+
}
|
|
58
|
+
|
|
19
59
|
export const FORM_COLORS: Record<FormColor, FormColorConfig> = {
|
|
20
60
|
blue: { border: 'border-blue-500/30', hover: 'hover:bg-blue-500/20 hover:border-blue-500/40', focus: 'focus:border-blue-500', selectedBg: 'bg-blue-600/20', accent: 'text-blue-400' },
|
|
21
61
|
green: { border: 'border-green-500/30', hover: 'hover:bg-green-500/20 hover:border-green-500/40', focus: 'focus:border-green-500', selectedBg: 'bg-green-600/20', accent: 'text-green-400' },
|