@toolr/ui-design 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai-manifest.json +35 -20
- package/components/composites/dashboard-list-item.tsx +172 -0
- package/components/composites/dashboard-panel.tsx +218 -0
- package/components/content/info-panel-primitives.tsx +9 -8
- package/components/diagrams/diagram-utils.tsx +2 -1
- package/components/hooks/use-dropdown-portal.ts +39 -0
- package/components/lib/accent-context.ts +10 -0
- package/components/lib/{ai-tools.tsx → coding-agents.tsx} +23 -8
- package/components/lib/custom-icons.tsx +37 -0
- package/components/lib/git-providers.tsx +39 -0
- package/components/lib/theme-engine.ts +59 -10
- package/components/lib/toolr-brand.tsx +23 -9
- package/components/sections/captured-issues/captured-issues-panel.tsx +17 -8
- package/components/sections/{ai-tools-paths/tools-paths-panel.tsx → coding-agent-paths/agent-paths-panel.tsx} +70 -62
- package/components/sections/coding-agent-paths/index.ts +37 -0
- package/components/sections/{ai-tools-paths → coding-agent-paths}/types.ts +28 -28
- package/components/sections/coding-agent-paths/use-agent-paths.ts +159 -0
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +10 -9
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +12 -3
- package/components/sections/golden-snapshots/snapshot-manager.tsx +9 -7
- package/components/sections/golden-snapshots/status-overview.tsx +8 -8
- package/components/sections/golden-snapshots/version-manager.tsx +6 -6
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +3 -3
- package/components/sections/prompt-editor/index.ts +1 -1
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +13 -5
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +18 -10
- package/components/sections/prompt-editor/types.ts +2 -2
- package/components/sections/report-bug/report-bug-form.tsx +12 -4
- package/components/sections/report-bug/screenshot-uploader.tsx +11 -3
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +12 -4
- package/components/sections/snapshot-browser/snapshot-tree.tsx +5 -4
- package/components/sections/snapshot-browser/types.ts +1 -1
- package/components/sections/snippets-editor/snippets-editor.tsx +16 -9
- package/components/settings/SettingsHeader.tsx +2 -2
- package/components/settings/SettingsPanel.tsx +11 -3
- package/components/settings/SettingsTreeNav.tsx +15 -9
- package/components/ui/action-dialog.tsx +24 -30
- package/components/ui/ai-action-button.tsx +10 -7
- package/components/ui/ai-execution-action-buttons.tsx +13 -5
- package/components/ui/badge.tsx +7 -4
- package/components/ui/bottom-panel-header.tsx +9 -5
- package/components/ui/breadcrumb.tsx +9 -1
- package/components/ui/{extension-list-card.tsx → capability-list-card.tsx} +13 -5
- package/components/ui/checkbox.tsx +6 -3
- package/components/ui/collapsible-section.tsx +38 -29
- package/components/ui/confirm-badge.tsx +7 -4
- package/components/ui/cookie-consent.tsx +13 -7
- package/components/ui/detail-section.tsx +24 -16
- package/components/ui/detail-view-wrapper.tsx +30 -22
- package/components/ui/editor-placeholder-card.tsx +28 -24
- package/components/ui/editor-toolbar.tsx +7 -4
- package/components/ui/execution-details-panel.tsx +10 -5
- package/components/ui/file-structure-section.tsx +3 -3
- package/components/ui/file-tree.tsx +3 -1
- package/components/ui/files-panel.tsx +147 -27
- package/components/ui/filter-dropdown.tsx +84 -74
- package/components/ui/form-actions.tsx +14 -6
- package/components/ui/frontmatter-form-header.tsx +10 -2
- package/components/ui/icon-button.tsx +22 -9
- package/components/ui/input.tsx +7 -4
- package/components/ui/label.tsx +5 -5
- package/components/ui/layout-tab-bar.tsx +7 -5
- package/components/ui/modal.tsx +18 -4
- package/components/ui/nav-card.tsx +6 -3
- package/components/ui/navigation-bar.tsx +37 -9
- package/components/ui/number-input.tsx +8 -4
- package/components/ui/project-explorer.tsx +666 -0
- package/components/ui/registry-browser.tsx +12 -1
- package/components/ui/registry-card.tsx +49 -42
- package/components/ui/registry-detail.tsx +34 -11
- package/components/ui/resizable-textarea.tsx +18 -11
- package/components/ui/scope-badge.tsx +18 -11
- package/components/ui/segmented-toggle.tsx +5 -2
- package/components/ui/select.tsx +12 -9
- package/components/ui/selection-grid.tsx +36 -37
- package/components/ui/setting-row.tsx +2 -2
- package/components/ui/settings-card.tsx +10 -3
- package/components/ui/settings-info-box.tsx +9 -5
- package/components/ui/settings-section-title.tsx +14 -2
- package/components/ui/snapshot-card.tsx +10 -2
- package/components/ui/snippets-panel.tsx +4 -2
- package/components/ui/sort-dropdown.tsx +39 -29
- package/components/ui/status-card.tsx +9 -1
- package/components/ui/tab-bar.tsx +12 -9
- package/components/ui/toggle.tsx +13 -7
- package/components/ui/tooltip.tsx +9 -1
- package/dist/content.js +8 -8
- package/dist/diagrams.d.ts +0 -1
- package/dist/index.d.ts +421 -182
- package/dist/index.js +2984 -1691
- package/dist/tokens/primitives.css +28 -6
- package/dist/tokens/semantic.css +15 -15
- package/dist/tokens/theme.css +23 -0
- package/index.ts +25 -11
- package/package.json +1 -1
- package/tokens/primitives.css +28 -6
- package/tokens/semantic.css +15 -15
- package/tokens/theme.css +23 -0
- package/components/sections/ai-tools-paths/index.ts +0 -37
- package/components/sections/ai-tools-paths/use-tools-paths.ts +0 -159
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { useState, useEffect,
|
|
1
|
+
import { useState, useEffect, type ReactNode } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { ChevronDown, Check, X, Search, Filter } from 'lucide-react'
|
|
3
4
|
import { useClickOutside } from '../hooks/use-click-outside.ts'
|
|
4
|
-
import {
|
|
5
|
+
import { useDropdownPortal } from '../hooks/use-dropdown-portal.ts'
|
|
5
6
|
import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
|
|
7
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
6
8
|
|
|
7
9
|
const SEARCH_THRESHOLD = 20
|
|
8
10
|
|
|
9
11
|
const VARIANT_CLASSES = {
|
|
10
|
-
filled: { bg: 'bg-neutral-
|
|
12
|
+
filled: { bg: 'bg-neutral-960', hoverBg: 'hover:bg-neutral-700' },
|
|
11
13
|
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700' },
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -19,7 +21,7 @@ export interface FilterDropdownProps {
|
|
|
19
21
|
labelExtra?: ReactNode
|
|
20
22
|
clearable?: boolean
|
|
21
23
|
variant?: 'filled' | 'outline'
|
|
22
|
-
|
|
24
|
+
accentColor?: FormColor
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function FilterDropdown({
|
|
@@ -30,19 +32,20 @@ export function FilterDropdown({
|
|
|
30
32
|
labelExtra,
|
|
31
33
|
clearable = true,
|
|
32
34
|
variant = 'outline',
|
|
33
|
-
|
|
35
|
+
accentColor,
|
|
34
36
|
}: FilterDropdownProps) {
|
|
37
|
+
const contextAccent = useAccentColor()
|
|
38
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
35
39
|
const [isOpen, setIsOpen] = useState(false)
|
|
36
40
|
const [search, setSearch] = useState('')
|
|
37
41
|
const [highlightIdx, setHighlightIdx] = useState(-1)
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const searchRef = useRef<HTMLInputElement>(null)
|
|
42
|
+
const { triggerRef, menuRef, position } = useDropdownPortal(isOpen)
|
|
43
|
+
const searchRef = { current: null as HTMLInputElement | null }
|
|
41
44
|
const isActive = value !== 'all'
|
|
42
45
|
const showSearch = options.length > SEARCH_THRESHOLD
|
|
43
46
|
const v = VARIANT_CLASSES[variant]
|
|
44
47
|
|
|
45
|
-
useClickOutside(
|
|
48
|
+
useClickOutside([triggerRef, menuRef], isOpen, () => setIsOpen(false))
|
|
46
49
|
|
|
47
50
|
useEffect(() => {
|
|
48
51
|
if (!isOpen) { setSearch(''); setHighlightIdx(-1) }
|
|
@@ -85,21 +88,85 @@ export function FilterDropdown({
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
const menu = isOpen && createPortal(
|
|
92
|
+
<div
|
|
93
|
+
ref={menuRef}
|
|
94
|
+
role="listbox"
|
|
95
|
+
style={{ position: 'fixed', top: position.top, left: position.left, minWidth: position.minWidth, zIndex: 9999 }}
|
|
96
|
+
className={`whitespace-nowrap bg-[var(--popover)] border ${FORM_COLORS[effectiveColor].border} rounded-lg shadow-lg overflow-hidden`}
|
|
97
|
+
>
|
|
98
|
+
{showSearch && (
|
|
99
|
+
<div className={`sticky top-0 p-1.5 bg-[var(--popover)] border-b ${FORM_COLORS[effectiveColor].border} z-10`}>
|
|
100
|
+
<div className="relative">
|
|
101
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-neutral-500" />
|
|
102
|
+
<input
|
|
103
|
+
ref={(el) => { searchRef.current = el }}
|
|
104
|
+
type="text"
|
|
105
|
+
value={search}
|
|
106
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
107
|
+
onKeyDown={handleKeyDown}
|
|
108
|
+
placeholder="Search..."
|
|
109
|
+
className={`w-full pl-6 pr-2 py-1 text-sm bg-[var(--popover)] border border-neutral-600 rounded text-neutral-200 placeholder-neutral-500 outline-none ${FORM_COLORS[effectiveColor].focus}`}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
{hasAllOption && (
|
|
115
|
+
<button
|
|
116
|
+
data-idx={0}
|
|
117
|
+
onClick={() => handleSelect('all')}
|
|
118
|
+
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
119
|
+
highlightIdx === 0
|
|
120
|
+
? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200`
|
|
121
|
+
: !isActive ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
<Check className={`w-3 h-3 shrink-0 ${!isActive ? FORM_COLORS[effectiveColor].accent : 'invisible'}`} />
|
|
125
|
+
<span>All</span>
|
|
126
|
+
</button>
|
|
127
|
+
)}
|
|
128
|
+
{filtered.map((opt, i) => {
|
|
129
|
+
const idx = i + (hasAllOption ? 1 : 0)
|
|
130
|
+
const isHighlighted = highlightIdx === idx
|
|
131
|
+
const isSelected = value === opt.value
|
|
132
|
+
return (
|
|
133
|
+
<button
|
|
134
|
+
key={opt.value}
|
|
135
|
+
data-idx={idx}
|
|
136
|
+
onClick={() => handleSelect(opt.value)}
|
|
137
|
+
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
138
|
+
isHighlighted
|
|
139
|
+
? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200`
|
|
140
|
+
: isSelected ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
141
|
+
}`}
|
|
142
|
+
>
|
|
143
|
+
<Check className={`w-3 h-3 shrink-0 ${isSelected ? FORM_COLORS[effectiveColor].accent : 'invisible'}`} />
|
|
144
|
+
<span>{opt.label}</span>
|
|
145
|
+
</button>
|
|
146
|
+
)
|
|
147
|
+
})}
|
|
148
|
+
{showSearch && search && filtered.length === 0 && (
|
|
149
|
+
<div className="px-3 py-2 text-sm text-neutral-500">No matches</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>,
|
|
152
|
+
document.body,
|
|
153
|
+
)
|
|
154
|
+
|
|
88
155
|
return (
|
|
89
|
-
<div className="relative flex items-center" ref={
|
|
156
|
+
<div className="relative flex items-center" ref={triggerRef} onKeyDown={handleKeyDown}>
|
|
90
157
|
<button
|
|
91
158
|
aria-expanded={isOpen}
|
|
92
159
|
aria-haspopup="listbox"
|
|
93
160
|
onClick={() => setIsOpen(!isOpen)}
|
|
94
|
-
className={`flex items-center gap-1.5
|
|
161
|
+
className={`flex items-center gap-1.5 py-1 px-2 rounded-md border ${v.bg} text-sm transition-colors cursor-pointer ${
|
|
95
162
|
isActive
|
|
96
|
-
? `${clearable ? 'rounded-r-none border-r-0' : ''} ${FORM_COLORS[
|
|
163
|
+
? `${clearable ? 'rounded-r-none border-r-0' : ''} ${FORM_COLORS[effectiveColor].border} text-neutral-200 ${FORM_COLORS[effectiveColor].hover}`
|
|
97
164
|
: isOpen
|
|
98
|
-
? `${FORM_COLORS[
|
|
99
|
-
: `${FORM_COLORS[
|
|
165
|
+
? `${FORM_COLORS[effectiveColor].border} text-neutral-200`
|
|
166
|
+
: `${FORM_COLORS[effectiveColor].border} text-neutral-400 ${FORM_COLORS[effectiveColor].hover} hover:text-neutral-200`
|
|
100
167
|
}`}
|
|
101
168
|
>
|
|
102
|
-
<Filter className={`w-3 h-3 ${isActive ? FORM_COLORS[
|
|
169
|
+
<Filter className={`w-3 h-3 ${isActive ? FORM_COLORS[effectiveColor].accent : ''}`} />
|
|
103
170
|
{labelExtra}
|
|
104
171
|
<span className="truncate">{selectedLabel}</span>
|
|
105
172
|
<ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
@@ -108,69 +175,12 @@ export function FilterDropdown({
|
|
|
108
175
|
<button
|
|
109
176
|
aria-label="Clear filter"
|
|
110
177
|
onClick={() => onChange('all')}
|
|
111
|
-
className={`flex items-center justify-center
|
|
178
|
+
className={`flex items-center justify-center py-1 px-1.5 rounded-r-md border border-l-0 ${FORM_COLORS[effectiveColor].border} ${v.bg} text-neutral-400 ${FORM_COLORS[effectiveColor].hover} hover:text-neutral-200 transition-colors cursor-pointer`}
|
|
112
179
|
>
|
|
113
180
|
<X className="w-3 h-3" />
|
|
114
181
|
</button>
|
|
115
182
|
)}
|
|
116
|
-
|
|
117
|
-
{isOpen && (
|
|
118
|
-
<div ref={menuRef} role="listbox" className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] whitespace-nowrap bg-[var(--popover)] border ${FORM_COLORS[color].border} rounded-lg shadow-lg overflow-hidden`}>
|
|
119
|
-
{showSearch && (
|
|
120
|
-
<div className={`sticky top-0 p-1.5 bg-[var(--popover)] border-b ${FORM_COLORS[color].border} z-10`}>
|
|
121
|
-
<div className="relative">
|
|
122
|
-
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-neutral-500" />
|
|
123
|
-
<input
|
|
124
|
-
ref={searchRef}
|
|
125
|
-
type="text"
|
|
126
|
-
value={search}
|
|
127
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
128
|
-
onKeyDown={handleKeyDown}
|
|
129
|
-
placeholder="Search..."
|
|
130
|
-
className={`w-full pl-6 pr-2 py-1 text-sm bg-[var(--popover)] border border-neutral-600 rounded text-neutral-200 placeholder-neutral-500 outline-none ${FORM_COLORS[color].focus}`}
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
)}
|
|
135
|
-
{hasAllOption && (
|
|
136
|
-
<button
|
|
137
|
-
data-idx={0}
|
|
138
|
-
onClick={() => handleSelect('all')}
|
|
139
|
-
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
140
|
-
highlightIdx === 0
|
|
141
|
-
? `${FORM_COLORS[color].selectedBg} text-neutral-200`
|
|
142
|
-
: !isActive ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
143
|
-
}`}
|
|
144
|
-
>
|
|
145
|
-
<Check className={`w-3 h-3 shrink-0 ${!isActive ? FORM_COLORS[color].accent : 'invisible'}`} />
|
|
146
|
-
<span>All</span>
|
|
147
|
-
</button>
|
|
148
|
-
)}
|
|
149
|
-
{filtered.map((opt, i) => {
|
|
150
|
-
const idx = i + (hasAllOption ? 1 : 0)
|
|
151
|
-
const isHighlighted = highlightIdx === idx
|
|
152
|
-
const isSelected = value === opt.value
|
|
153
|
-
return (
|
|
154
|
-
<button
|
|
155
|
-
key={opt.value}
|
|
156
|
-
data-idx={idx}
|
|
157
|
-
onClick={() => handleSelect(opt.value)}
|
|
158
|
-
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
159
|
-
isHighlighted
|
|
160
|
-
? `${FORM_COLORS[color].selectedBg} text-neutral-200`
|
|
161
|
-
: isSelected ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
162
|
-
}`}
|
|
163
|
-
>
|
|
164
|
-
<Check className={`w-3 h-3 shrink-0 ${isSelected ? FORM_COLORS[color].accent : 'invisible'}`} />
|
|
165
|
-
<span>{opt.label}</span>
|
|
166
|
-
</button>
|
|
167
|
-
)
|
|
168
|
-
})}
|
|
169
|
-
{showSearch && search && filtered.length === 0 && (
|
|
170
|
-
<div className="px-3 py-2 text-sm text-neutral-500">No matches</div>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
)}
|
|
183
|
+
{menu}
|
|
174
184
|
</div>
|
|
175
185
|
)
|
|
176
186
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { IconButton, type IconName, type IconButtonProps, type IconButtonStatus } from './icon-button.tsx'
|
|
2
|
+
import { AccentColorProvider, useAccentColor } from '../lib/accent-context.ts'
|
|
3
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
2
4
|
import { cn } from '../lib/cn.ts'
|
|
3
5
|
|
|
4
6
|
export interface FormActionsProps {
|
|
@@ -18,7 +20,7 @@ export interface FormActionsProps {
|
|
|
18
20
|
onConfirm?: () => void
|
|
19
21
|
confirmTooltip?: string
|
|
20
22
|
confirmIcon?: IconName
|
|
21
|
-
confirmColor?: IconButtonProps['
|
|
23
|
+
confirmColor?: IconButtonProps['accentColor']
|
|
22
24
|
confirmDisabled?: boolean
|
|
23
25
|
confirmStatus?: IconButtonStatus
|
|
24
26
|
|
|
@@ -31,6 +33,7 @@ export interface FormActionsProps {
|
|
|
31
33
|
|
|
32
34
|
border?: boolean
|
|
33
35
|
padding?: 'compact' | 'normal' | 'modal'
|
|
36
|
+
accentColor?: FormColor
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
const PADDING_CLASSES = {
|
|
@@ -65,11 +68,15 @@ export function FormActions({
|
|
|
65
68
|
statusText,
|
|
66
69
|
border,
|
|
67
70
|
padding = 'normal',
|
|
71
|
+
accentColor,
|
|
68
72
|
}: FormActionsProps) {
|
|
73
|
+
const contextAccent = useAccentColor()
|
|
74
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
69
75
|
const showBorder = border ?? DEFAULT_BORDER[padding]
|
|
70
76
|
const hasLeft = onBack || statusText
|
|
71
77
|
|
|
72
78
|
return (
|
|
79
|
+
<AccentColorProvider value={effectiveColor}>
|
|
73
80
|
<div className={cn(
|
|
74
81
|
'flex items-center gap-2',
|
|
75
82
|
hasLeft ? 'justify-between' : 'justify-end',
|
|
@@ -81,7 +88,7 @@ export function FormActions({
|
|
|
81
88
|
{onBack && (
|
|
82
89
|
<IconButton
|
|
83
90
|
icon="arrow-left"
|
|
84
|
-
|
|
91
|
+
accentColor="neutral"
|
|
85
92
|
onClick={onBack}
|
|
86
93
|
tooltip={{ description: backTooltip }}
|
|
87
94
|
/>
|
|
@@ -93,7 +100,7 @@ export function FormActions({
|
|
|
93
100
|
{onMinimize && (
|
|
94
101
|
<IconButton
|
|
95
102
|
icon="minimize"
|
|
96
|
-
|
|
103
|
+
accentColor="neutral"
|
|
97
104
|
onClick={onMinimize}
|
|
98
105
|
tooltip={{ description: minimizeTooltip }}
|
|
99
106
|
/>
|
|
@@ -101,7 +108,7 @@ export function FormActions({
|
|
|
101
108
|
{onCancel && (
|
|
102
109
|
<IconButton
|
|
103
110
|
icon="x"
|
|
104
|
-
|
|
111
|
+
accentColor="neutral"
|
|
105
112
|
onClick={onCancel}
|
|
106
113
|
tooltip={{ description: cancelTooltip }}
|
|
107
114
|
/>
|
|
@@ -109,7 +116,7 @@ export function FormActions({
|
|
|
109
116
|
{onConfirm && (
|
|
110
117
|
<IconButton
|
|
111
118
|
icon={confirmIcon}
|
|
112
|
-
|
|
119
|
+
accentColor={confirmColor}
|
|
113
120
|
onClick={onConfirm}
|
|
114
121
|
disabled={confirmDisabled}
|
|
115
122
|
status={confirmStatus}
|
|
@@ -119,12 +126,13 @@ export function FormActions({
|
|
|
119
126
|
{onNext && (
|
|
120
127
|
<IconButton
|
|
121
128
|
icon="arrow-right"
|
|
122
|
-
|
|
129
|
+
accentColor="neutral"
|
|
123
130
|
onClick={onNext}
|
|
124
131
|
tooltip={{ description: nextTooltip }}
|
|
125
132
|
/>
|
|
126
133
|
)}
|
|
127
134
|
</div>
|
|
128
135
|
</div>
|
|
136
|
+
</AccentColorProvider>
|
|
129
137
|
)
|
|
130
138
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import { ChevronRight } from 'lucide-react'
|
|
3
3
|
import { Checkbox } from './checkbox.tsx'
|
|
4
|
+
import { AccentColorProvider, useAccentColor } from '../lib/accent-context.ts'
|
|
5
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
4
6
|
|
|
5
7
|
export interface FrontmatterFormHeaderProps {
|
|
6
8
|
collapsed: boolean
|
|
@@ -13,6 +15,7 @@ export interface FrontmatterFormHeaderProps {
|
|
|
13
15
|
/** Toggle frontmatter on/off */
|
|
14
16
|
onFrontmatterToggle?: (enabled: boolean) => void
|
|
15
17
|
readOnly?: boolean
|
|
18
|
+
accentColor?: FormColor
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export function FrontmatterFormHeader({
|
|
@@ -23,16 +26,20 @@ export function FrontmatterFormHeader({
|
|
|
23
26
|
frontmatterEnabled,
|
|
24
27
|
onFrontmatterToggle,
|
|
25
28
|
readOnly,
|
|
29
|
+
accentColor,
|
|
26
30
|
}: FrontmatterFormHeaderProps) {
|
|
31
|
+
const contextAccent = useAccentColor()
|
|
32
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
27
33
|
const hasFm = frontmatterEnabled !== false
|
|
28
34
|
|
|
29
35
|
return (
|
|
30
|
-
<
|
|
36
|
+
<AccentColorProvider value={effectiveColor}>
|
|
37
|
+
<div className="bg-neutral-980 border-b border-neutral-960 select-none">
|
|
31
38
|
{/* Header bar — always visible, always expandable */}
|
|
32
39
|
<button
|
|
33
40
|
type="button"
|
|
34
41
|
onClick={onToggle}
|
|
35
|
-
className="flex items-center gap-2 w-full px-3 py-3 hover:bg-neutral-
|
|
42
|
+
className="flex items-center gap-2 w-full px-3 py-3 hover:bg-neutral-960/50 cursor-pointer transition-colors"
|
|
36
43
|
>
|
|
37
44
|
<ChevronRight
|
|
38
45
|
className={`w-3.5 h-3.5 text-neutral-500 transition-transform duration-150 ${
|
|
@@ -76,5 +83,6 @@ export function FrontmatterFormHeader({
|
|
|
76
83
|
</div>
|
|
77
84
|
)}
|
|
78
85
|
</div>
|
|
86
|
+
</AccentColorProvider>
|
|
79
87
|
)
|
|
80
88
|
}
|
|
@@ -37,9 +37,12 @@ import {
|
|
|
37
37
|
PanelBottomClose,
|
|
38
38
|
Package, Wrench, Store, ScrollText, Cpu, FlaskConical, Layers, Timer, Camera,
|
|
39
39
|
AlertCircle, FileCode, Gauge, Home, PieChart, Settings2,
|
|
40
|
+
FolderSearch, PanelLeftClose, History,
|
|
41
|
+
Github, Gitlab, GitBranch,
|
|
40
42
|
} from 'lucide-react'
|
|
41
43
|
import type { LucideIcon } from 'lucide-react'
|
|
42
44
|
import { type AccentColor } from '../lib/form-colors.ts'
|
|
45
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
43
46
|
import { Tooltip, type TooltipContent } from './tooltip.tsx'
|
|
44
47
|
|
|
45
48
|
export const iconMap = {
|
|
@@ -140,6 +143,14 @@ export const iconMap = {
|
|
|
140
143
|
'home': Home,
|
|
141
144
|
'pie-chart': PieChart,
|
|
142
145
|
'settings-2': Settings2,
|
|
146
|
+
'folder-search': FolderSearch,
|
|
147
|
+
'panel-left-close': PanelLeftClose,
|
|
148
|
+
'trash-2': Trash2,
|
|
149
|
+
'rotate-ccw': RotateCcw,
|
|
150
|
+
'history': History,
|
|
151
|
+
'github': Github,
|
|
152
|
+
'gitlab': Gitlab,
|
|
153
|
+
'git-branch': GitBranch,
|
|
143
154
|
} as const
|
|
144
155
|
|
|
145
156
|
export type IconName = keyof typeof iconMap
|
|
@@ -147,7 +158,7 @@ export type IconName = keyof typeof iconMap
|
|
|
147
158
|
export interface ActionItem {
|
|
148
159
|
icon: IconName
|
|
149
160
|
onClick: () => void
|
|
150
|
-
|
|
161
|
+
accentColor?: IconButtonColor
|
|
151
162
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
152
163
|
variant?: IconButtonVariant
|
|
153
164
|
tooltip?: TooltipContent
|
|
@@ -169,11 +180,11 @@ export interface IconButtonProps {
|
|
|
169
180
|
/** When provided, renders an <a> tag instead of <button>. Opens in a new tab. */
|
|
170
181
|
href?: string
|
|
171
182
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
172
|
-
|
|
183
|
+
accentColor?: IconButtonColor
|
|
173
184
|
variant?: IconButtonVariant
|
|
174
185
|
active?: boolean
|
|
175
186
|
disabled?: boolean
|
|
176
|
-
/** Async action status. Overrides icon,
|
|
187
|
+
/** Async action status. Overrides icon, accentColor, and active state when set. */
|
|
177
188
|
status?: IconButtonStatus
|
|
178
189
|
/** Tooltip shown on hover. Title and description are required. */
|
|
179
190
|
tooltip?: TooltipContent
|
|
@@ -276,7 +287,7 @@ export function IconButton({
|
|
|
276
287
|
onClick,
|
|
277
288
|
href,
|
|
278
289
|
size = 'sm',
|
|
279
|
-
|
|
290
|
+
accentColor,
|
|
280
291
|
variant = 'outline',
|
|
281
292
|
active = false,
|
|
282
293
|
disabled = false,
|
|
@@ -289,7 +300,9 @@ export function IconButton({
|
|
|
289
300
|
className = '',
|
|
290
301
|
testId,
|
|
291
302
|
}: IconButtonProps) {
|
|
292
|
-
const
|
|
303
|
+
const contextAccent = useAccentColor()
|
|
304
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'neutral'
|
|
305
|
+
const resolvedColor = status ? (statusConfig[status].color ?? effectiveColor) : effectiveColor
|
|
293
306
|
const resolvedActive = status ? statusConfig[status].active : active
|
|
294
307
|
|
|
295
308
|
const colorStyle = colorClasses[resolvedColor] ?? colorClasses.neutral
|
|
@@ -299,7 +312,7 @@ export function IconButton({
|
|
|
299
312
|
const borderClass = isOutline ? colorStyle.border : 'border-neutral-600'
|
|
300
313
|
|
|
301
314
|
const sharedClassName = `
|
|
302
|
-
relative flex items-center justify-center border transition-colors ${isOutline ? '' : 'bg-neutral-
|
|
315
|
+
relative flex items-center justify-center border transition-colors ${isOutline ? '' : 'bg-neutral-960'}
|
|
303
316
|
${sizeClasses[size]}
|
|
304
317
|
${colorStyle.text} ${borderClass}
|
|
305
318
|
${resolvedActive ? colorStyle.active : ''}
|
|
@@ -372,16 +385,16 @@ export interface CollapseButtonProps {
|
|
|
372
385
|
collapsed: boolean
|
|
373
386
|
onToggle: () => void
|
|
374
387
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
375
|
-
|
|
388
|
+
accentColor?: IconButtonColor
|
|
376
389
|
tooltipPosition?: 'bottom' | 'bottom-left' | 'left' | 'right' | 'top' | 'top-left' | 'top-right'
|
|
377
390
|
}
|
|
378
391
|
|
|
379
|
-
export function CollapseButton({ collapsed, onToggle, size = 'xss',
|
|
392
|
+
export function CollapseButton({ collapsed, onToggle, size = 'xss', accentColor, tooltipPosition = 'bottom-left' }: CollapseButtonProps) {
|
|
380
393
|
return (
|
|
381
394
|
<IconButton
|
|
382
395
|
icon={collapsed ? 'chevrons-up-down' : 'chevrons-down-up'}
|
|
383
396
|
onClick={onToggle}
|
|
384
|
-
|
|
397
|
+
accentColor={accentColor}
|
|
385
398
|
tooltip={{
|
|
386
399
|
title: collapsed ? 'Expand all' : 'Collapse all',
|
|
387
400
|
description: collapsed ? 'Expand all folders' : 'Collapse all folders',
|
package/components/ui/input.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import { forwardRef, useEffect, useId, useRef, useState, type InputHTMLAttribute
|
|
|
21
21
|
import { Search, X, Eye, EyeOff } from 'lucide-react'
|
|
22
22
|
import { DebounceBorderOverlay } from './debounce-border-overlay.tsx'
|
|
23
23
|
import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
|
|
24
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
24
25
|
|
|
25
26
|
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size' | 'type'> {
|
|
26
27
|
value: string
|
|
@@ -29,7 +30,7 @@ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
|
|
|
29
30
|
debounceMs?: number
|
|
30
31
|
error?: boolean | string
|
|
31
32
|
variant?: 'filled' | 'outline'
|
|
32
|
-
|
|
33
|
+
accentColor?: FormColor
|
|
33
34
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
34
35
|
mono?: boolean
|
|
35
36
|
/** Test ID for E2E testing */
|
|
@@ -47,7 +48,7 @@ const sizeClasses = {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const variantClasses = {
|
|
50
|
-
filled: 'bg-neutral-
|
|
51
|
+
filled: 'bg-neutral-960',
|
|
51
52
|
outline: 'bg-transparent',
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -65,7 +66,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
65
66
|
debounceMs = 0,
|
|
66
67
|
error,
|
|
67
68
|
variant = 'outline',
|
|
68
|
-
|
|
69
|
+
accentColor,
|
|
69
70
|
size = 'sm',
|
|
70
71
|
disabled = false,
|
|
71
72
|
className = '',
|
|
@@ -76,6 +77,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
76
77
|
onBlur: onBlurProp,
|
|
77
78
|
...props
|
|
78
79
|
}, ref) {
|
|
80
|
+
const contextAccent = useAccentColor()
|
|
81
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
79
82
|
const hasError = Boolean(error)
|
|
80
83
|
const hasAction = actionSlot && !disabled
|
|
81
84
|
const [hovered, setHovered] = useState(false)
|
|
@@ -156,7 +159,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
156
159
|
focus:outline-none transition-colors
|
|
157
160
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
158
161
|
${sizeClasses[size]}
|
|
159
|
-
${hasError ? 'border-red-500 focus:border-red-500' : `${variantClasses[variant]} ${FORM_COLORS[
|
|
162
|
+
${hasError ? 'border-red-500 focus:border-red-500' : `${variantClasses[variant]} ${FORM_COLORS[effectiveColor].border} ${FORM_COLORS[effectiveColor].focus}`}
|
|
160
163
|
${mono ? 'font-mono' : ''}
|
|
161
164
|
${isSearch ? 'pl-8 pr-8' : (hasAction || isPassword) ? 'pr-8' : ''}
|
|
162
165
|
${className}
|
package/components/ui/label.tsx
CHANGED
|
@@ -20,7 +20,7 @@ export type LabelColor = AccentColor
|
|
|
20
20
|
|
|
21
21
|
export interface LabelProps {
|
|
22
22
|
text: string
|
|
23
|
-
|
|
23
|
+
accentColor: LabelColor
|
|
24
24
|
/** Leading icon(s). Pass a single name or an array for multiple icons before text. */
|
|
25
25
|
icon?: IconName | IconName[]
|
|
26
26
|
/** Custom icon component. Takes precedence over icon. */
|
|
@@ -91,7 +91,7 @@ function transformText(text: string, textTransform?: 'capitalize' | 'lowercase'
|
|
|
91
91
|
|
|
92
92
|
export function Label({
|
|
93
93
|
text,
|
|
94
|
-
|
|
94
|
+
accentColor,
|
|
95
95
|
icon,
|
|
96
96
|
IconComponent: CustomIcon,
|
|
97
97
|
tooltip,
|
|
@@ -112,7 +112,7 @@ export function Label({
|
|
|
112
112
|
s.text,
|
|
113
113
|
'border rounded font-medium leading-none',
|
|
114
114
|
hasProgress ? 'relative overflow-hidden' : '',
|
|
115
|
-
colorClasses[
|
|
115
|
+
colorClasses[accentColor],
|
|
116
116
|
onClick ? 'cursor-pointer hover:brightness-125 transition-all' : 'cursor-help',
|
|
117
117
|
className,
|
|
118
118
|
]
|
|
@@ -129,7 +129,7 @@ export function Label({
|
|
|
129
129
|
aria-valuenow={Math.min(progress, 100)}
|
|
130
130
|
aria-valuemin={0}
|
|
131
131
|
aria-valuemax={100}
|
|
132
|
-
className={`absolute inset-y-0 left-0 ${progressFillColors[
|
|
132
|
+
className={`absolute inset-y-0 left-0 ${progressFillColors[accentColor]} rounded-[inherit]`}
|
|
133
133
|
style={{ width: `${Math.min(progress, 100)}%` }}
|
|
134
134
|
/>
|
|
135
135
|
)}
|
|
@@ -142,7 +142,7 @@ export function Label({
|
|
|
142
142
|
<span key={i} className={`${s.iconSize} flex-shrink-0 ${hasProgress ? 'relative' : ''}`}><Icon className={s.iconSize} /></span>
|
|
143
143
|
) : null
|
|
144
144
|
})}
|
|
145
|
-
<span className={`min-w-0 truncate ${hasProgress ? 'relative' : ''}`} style={cssTransform ? { textTransform: cssTransform } : undefined}>
|
|
145
|
+
<span className={`min-w-0 truncate -mt-px ${hasProgress ? 'relative' : ''}`} style={cssTransform ? { textTransform: cssTransform } : undefined}>
|
|
146
146
|
{transformText(text, textTransform)}
|
|
147
147
|
</span>
|
|
148
148
|
</>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect, type ReactNode } from 'react'
|
|
4
4
|
import { ChevronLeft, ChevronRight, X } from 'lucide-react'
|
|
5
5
|
import { cn } from '../lib/cn.ts'
|
|
6
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
6
7
|
|
|
7
8
|
export interface LayoutTab {
|
|
8
9
|
id: string
|
|
@@ -26,6 +27,7 @@ export interface LayoutTabBarProps {
|
|
|
26
27
|
onClose?: (id: string) => void
|
|
27
28
|
onReorder?: (fromIndex: number, toIndex: number) => void
|
|
28
29
|
className?: string
|
|
30
|
+
accentColor?: FormColor
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
const COLORS: Record<string, { border: string; text: string }> = {
|
|
@@ -77,7 +79,7 @@ function getGradient(color?: string, isActive?: boolean): string | undefined {
|
|
|
77
79
|
: `linear-gradient(135deg, rgba(${rgb}, 0.36) 0%, rgba(${rgb}, 0.06) 100%)`
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, className }: LayoutTabBarProps) {
|
|
82
|
+
export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, className, accentColor: _accentColor }: LayoutTabBarProps) {
|
|
81
83
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
82
84
|
const [showLeft, setShowLeft] = useState(false)
|
|
83
85
|
const [showRight, setShowRight] = useState(false)
|
|
@@ -167,7 +169,7 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
|
|
|
167
169
|
// ── Render ────────────────────────────────────────────
|
|
168
170
|
|
|
169
171
|
return (
|
|
170
|
-
<div className={cn('relative flex items-center min-h-12 flex-shrink-0 bg-neutral-
|
|
172
|
+
<div className={cn('relative flex items-center min-h-12 flex-shrink-0 bg-neutral-980/80 border-b border-neutral-700/50', className)}>
|
|
171
173
|
{showLeft && (
|
|
172
174
|
<div className="flex items-center px-2 shrink-0">
|
|
173
175
|
<button type="button" onClick={() => scroll('left')} className="text-neutral-500 hover:text-neutral-400 transition-colors cursor-pointer">
|
|
@@ -206,8 +208,8 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
|
|
|
206
208
|
className={cn(
|
|
207
209
|
'group flex flex-col justify-start gap-0.5 px-2.5 rounded-t-lg cursor-pointer transition-all shrink-0 select-none relative border-t-2',
|
|
208
210
|
isActive
|
|
209
|
-
? cn('bg-neutral-
|
|
210
|
-
: 'bg-neutral-
|
|
211
|
+
? cn('bg-neutral-960', borderC)
|
|
212
|
+
: 'bg-neutral-960/50 text-neutral-400 hover:bg-neutral-960/80 hover:text-neutral-300 border-transparent opacity-60 hover:opacity-90',
|
|
211
213
|
isDragged && 'opacity-50',
|
|
212
214
|
)}
|
|
213
215
|
style={{
|
|
@@ -269,7 +271,7 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
|
|
|
269
271
|
const ghostSubIconC = getTextClass(t.subtitleIconColor || t.subtitleColor || t.selectedColor || t.color)
|
|
270
272
|
return (
|
|
271
273
|
<div className="fixed z-50 pointer-events-none opacity-90" style={{ left: drag.x + 12, top: drag.y - 20 }}>
|
|
272
|
-
<div className={cn('flex flex-col gap-0.5 px-2.5 py-1.5 rounded-t-lg border-t-2 shadow-lg bg-neutral-
|
|
274
|
+
<div className={cn('flex flex-col gap-0.5 px-2.5 py-1.5 rounded-t-lg border-t-2 shadow-lg bg-neutral-960', ghostBorder)}>
|
|
273
275
|
<div className="flex items-center gap-1.5">
|
|
274
276
|
{t.icon && <span className={cn('shrink-0 inline-flex', ghostIconC)} style={{ width: 14, height: 14 }}>{t.icon}</span>}
|
|
275
277
|
<span className={cn('text-md font-medium', ghostTitleC)}>{t.title}</span>
|