@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,11 +1,14 @@
|
|
|
1
1
|
import { Home, Folder, Lock, Eye } from 'lucide-react'
|
|
2
2
|
import { Label, type LabelColor } from './label.tsx'
|
|
3
|
+
import { AccentColorProvider, useAccentColor } from '../lib/accent-context.ts'
|
|
4
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
3
5
|
|
|
4
6
|
export type ScopeType = 'user' | 'project' | 'local' | 'read-only'
|
|
5
7
|
|
|
6
8
|
interface ScopeBadgeProps {
|
|
7
9
|
scope: ScopeType
|
|
8
10
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
11
|
+
accentColor?: FormColor
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
const scopeConfig: Record<
|
|
@@ -48,20 +51,24 @@ const scopeConfig: Record<
|
|
|
48
51
|
},
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
export function ScopeBadge({ scope, size = 'xs' }: ScopeBadgeProps) {
|
|
54
|
+
export function ScopeBadge({ scope, size = 'xs', accentColor }: ScopeBadgeProps) {
|
|
55
|
+
const contextAccent = useAccentColor()
|
|
56
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
52
57
|
const config = scopeConfig[scope]
|
|
53
58
|
const Icon = config.icon
|
|
54
59
|
|
|
55
60
|
return (
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
<AccentColorProvider value={effectiveColor}>
|
|
62
|
+
<Label
|
|
63
|
+
text={config.label}
|
|
64
|
+
accentColor={config.color}
|
|
65
|
+
IconComponent={Icon}
|
|
66
|
+
tooltip={{
|
|
67
|
+
title: config.title,
|
|
68
|
+
description: config.description,
|
|
69
|
+
}}
|
|
70
|
+
size={size}
|
|
71
|
+
/>
|
|
72
|
+
</AccentColorProvider>
|
|
66
73
|
)
|
|
67
74
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import type { AccentColor } from '../lib/form-colors.ts'
|
|
3
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
3
4
|
import { Tooltip, type TooltipContent, type TooltipPosition } from './tooltip.tsx'
|
|
4
5
|
|
|
5
6
|
export interface SegmentedToggleOption<T extends string> {
|
|
@@ -108,7 +109,7 @@ export function SegmentedToggle<T extends string>({
|
|
|
108
109
|
options,
|
|
109
110
|
value,
|
|
110
111
|
onChange,
|
|
111
|
-
accentColor
|
|
112
|
+
accentColor: accentColorProp,
|
|
112
113
|
variant = 'outline',
|
|
113
114
|
size = 'sm',
|
|
114
115
|
tooltipPosition = 'bottom',
|
|
@@ -116,10 +117,12 @@ export function SegmentedToggle<T extends string>({
|
|
|
116
117
|
disabled = false,
|
|
117
118
|
disabledTooltip,
|
|
118
119
|
}: SegmentedToggleProps<T>) {
|
|
120
|
+
const contextAccent = useAccentColor()
|
|
121
|
+
const accentColor = accentColorProp ?? contextAccent ?? 'blue'
|
|
119
122
|
const isOutline = variant === 'outline'
|
|
120
123
|
const containerClasses = isOutline
|
|
121
124
|
? OUTLINE_CONTAINER[accentColor] || OUTLINE_CONTAINER.blue
|
|
122
|
-
: 'flex items-center bg-neutral-
|
|
125
|
+
: 'flex items-center bg-neutral-960/50 border border-neutral-700 rounded-md'
|
|
123
126
|
|
|
124
127
|
const toggle = (
|
|
125
128
|
<div
|
package/components/ui/select.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { ChevronDown, Check } from 'lucide-react'
|
|
|
4
4
|
import { useClickOutside } from '../hooks/use-click-outside.ts'
|
|
5
5
|
import { useDropdownMaxHeight } from '../hooks/use-dropdown-max-height.ts'
|
|
6
6
|
import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
|
|
7
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
7
8
|
|
|
8
9
|
export interface SelectOption<T extends string | number = string> {
|
|
9
10
|
value: T
|
|
@@ -17,7 +18,7 @@ export interface SelectProps<T extends string | number = string> {
|
|
|
17
18
|
onChange: (value: T) => void
|
|
18
19
|
placeholder?: string
|
|
19
20
|
variant?: 'filled' | 'outline'
|
|
20
|
-
|
|
21
|
+
accentColor?: FormColor
|
|
21
22
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
22
23
|
align?: 'left' | 'right'
|
|
23
24
|
disabled?: boolean
|
|
@@ -25,7 +26,7 @@ export interface SelectProps<T extends string | number = string> {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const VARIANT_CLASSES = {
|
|
28
|
-
filled: { bg: 'bg-neutral-
|
|
29
|
+
filled: { bg: 'bg-neutral-960', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
|
|
29
30
|
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -43,12 +44,14 @@ export function Select<T extends string | number = string>({
|
|
|
43
44
|
onChange,
|
|
44
45
|
placeholder = 'Select...',
|
|
45
46
|
variant = 'outline',
|
|
46
|
-
|
|
47
|
+
accentColor,
|
|
47
48
|
size = 'sm',
|
|
48
49
|
align = 'left',
|
|
49
50
|
disabled = false,
|
|
50
51
|
className = '',
|
|
51
52
|
}: SelectProps<T>) {
|
|
53
|
+
const contextAccent = useAccentColor()
|
|
54
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
52
55
|
const [isOpen, setIsOpen] = useState(false)
|
|
53
56
|
const [highlightIdx, setHighlightIdx] = useState(-1)
|
|
54
57
|
const ref = useRef<HTMLDivElement>(null)
|
|
@@ -114,8 +117,8 @@ export function Select<T extends string | number = string>({
|
|
|
114
117
|
aria-haspopup="listbox"
|
|
115
118
|
onClick={() => !disabled && (isOpen ? close() : open())}
|
|
116
119
|
disabled={disabled}
|
|
117
|
-
className={`flex items-center gap-1.5 min-w-0 rounded-lg border ${v.bg} ${FORM_COLORS[
|
|
118
|
-
disabled ? 'opacity-50 cursor-not-allowed' : `cursor-pointer ${FORM_COLORS[
|
|
120
|
+
className={`flex items-center gap-1.5 min-w-0 rounded-lg border ${v.bg} ${FORM_COLORS[effectiveColor].border} text-neutral-200 focus:outline-none ${FORM_COLORS[effectiveColor].focus} transition-colors ${
|
|
121
|
+
disabled ? 'opacity-50 cursor-not-allowed' : `cursor-pointer ${FORM_COLORS[effectiveColor].hover}`
|
|
119
122
|
} ${s}`}
|
|
120
123
|
>
|
|
121
124
|
{selectedOption?.icon}
|
|
@@ -128,7 +131,7 @@ export function Select<T extends string | number = string>({
|
|
|
128
131
|
<div
|
|
129
132
|
ref={menuRef}
|
|
130
133
|
role="listbox"
|
|
131
|
-
className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} border ${FORM_COLORS[
|
|
134
|
+
className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} border ${FORM_COLORS[effectiveColor].border} rounded-lg shadow-lg overflow-hidden`}
|
|
132
135
|
style={{
|
|
133
136
|
top: menuPos.top,
|
|
134
137
|
left: align === 'right' ? undefined : menuPos.left,
|
|
@@ -148,11 +151,11 @@ export function Select<T extends string | number = string>({
|
|
|
148
151
|
onPointerEnter={() => setHighlightIdx(idx)}
|
|
149
152
|
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
150
153
|
isHighlighted
|
|
151
|
-
? `${FORM_COLORS[
|
|
152
|
-
: isSelected ? `${FORM_COLORS[
|
|
154
|
+
? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200`
|
|
155
|
+
: isSelected ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
153
156
|
}`}
|
|
154
157
|
>
|
|
155
|
-
<Check className={`w-3 h-3 shrink-0 ${isSelected ? FORM_COLORS[
|
|
158
|
+
<Check className={`w-3 h-3 shrink-0 ${isSelected ? FORM_COLORS[effectiveColor].accent : 'invisible'}`} />
|
|
156
159
|
{opt.icon}
|
|
157
160
|
<span className="truncate">{opt.label}</span>
|
|
158
161
|
</button>
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { iconMap, type IconName } from './icon-button.tsx'
|
|
2
2
|
import type { ConfirmBadgeColor } from './confirm-badge.tsx'
|
|
3
|
+
import { AccentColorProvider, useAccentColor } from '../lib/accent-context.ts'
|
|
4
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
3
5
|
import { cn } from '../lib/cn.ts'
|
|
4
|
-
import {
|
|
6
|
+
import { FORM_COLORS } from '../lib/form-colors.ts'
|
|
7
|
+
import { CodingAgentIcon, CODING_AGENT_NAMES, type CodingAgentKey } from '../lib/coding-agents.tsx'
|
|
5
8
|
|
|
6
|
-
/* ── Preset logos (shared
|
|
9
|
+
/* ── Preset logos (shared CodingAgentIcon) ────────────────── */
|
|
7
10
|
|
|
8
11
|
type IconProps = { className?: string; style?: React.CSSProperties }
|
|
9
12
|
|
|
10
|
-
function
|
|
11
|
-
function
|
|
12
|
-
return <
|
|
13
|
+
function makeAgentLogo(agentKey: CodingAgentKey): React.ComponentType<IconProps> {
|
|
14
|
+
function AgentLogo({ className, style }: IconProps) {
|
|
15
|
+
return <CodingAgentIcon agent={agentKey} className={className} style={style} />
|
|
13
16
|
}
|
|
14
|
-
return
|
|
17
|
+
return AgentLogo
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/* ── Preset types & data ──────────────────────────────────── */
|
|
@@ -24,12 +27,14 @@ export type CodingToolPresetConfig = CodingToolId | {
|
|
|
24
27
|
disabled?: boolean
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
// Brand colors use Tailwind accent hex values (violet-400, orange-500, blue-400, cyan-400, neutral-400)
|
|
31
|
+
// applied via inline style because the color is dynamic per-item
|
|
27
32
|
const CODING_TOOL_PRESETS: Record<CodingToolId, { name: string; description: string; color: string; badgeColor: ConfirmBadgeColor; Logo: React.ComponentType<IconProps> }> = {
|
|
28
|
-
'claude-code': { name:
|
|
29
|
-
'github-copilot': { name:
|
|
30
|
-
'codex-cli': { name:
|
|
31
|
-
'gemini-cli': { name:
|
|
32
|
-
'opencode': { name:
|
|
33
|
+
'claude-code': { name: CODING_AGENT_NAMES.claude, description: 'Security & architecture focus', color: '#a78bfa', badgeColor: 'violet', Logo: makeAgentLogo('claude') },
|
|
34
|
+
'github-copilot': { name: CODING_AGENT_NAMES.copilot, description: 'Code quality & patterns', color: '#f97316', badgeColor: 'orange', Logo: makeAgentLogo('copilot') },
|
|
35
|
+
'codex-cli': { name: CODING_AGENT_NAMES.codex, description: 'Performance & optimization', color: '#60a5fa', badgeColor: 'blue', Logo: makeAgentLogo('codex') },
|
|
36
|
+
'gemini-cli': { name: CODING_AGENT_NAMES.gemini, description: 'Best practices & docs', color: '#22d3ee', badgeColor: 'cyan', Logo: makeAgentLogo('gemini') },
|
|
37
|
+
'opencode': { name: CODING_AGENT_NAMES.opencode, description: 'Provider-agnostic AI coding agent', color: '#a1a1aa', badgeColor: 'neutral', Logo: makeAgentLogo('opencode') },
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function resolvePresets(presets: CodingToolPresetConfig[]): SelectionCardItem[] {
|
|
@@ -85,16 +90,11 @@ export interface SelectionGridProps {
|
|
|
85
90
|
/** Number of columns for grid layout (auto if not set) */
|
|
86
91
|
columns?: number
|
|
87
92
|
className?: string
|
|
93
|
+
accentColor?: FormColor
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
/* ── Helpers ───────────────────────────────────────────────── */
|
|
91
97
|
|
|
92
|
-
const DEFAULT_COLOR = 'blue-400'
|
|
93
|
-
|
|
94
|
-
function resolveColor(color?: string): string {
|
|
95
|
-
return color || DEFAULT_COLOR
|
|
96
|
-
}
|
|
97
|
-
|
|
98
98
|
function autoColumns(itemCount: number): number {
|
|
99
99
|
return Math.min(itemCount, 5)
|
|
100
100
|
}
|
|
@@ -110,7 +110,10 @@ export function SelectionGrid({
|
|
|
110
110
|
layout = 'grid',
|
|
111
111
|
columns,
|
|
112
112
|
className,
|
|
113
|
+
accentColor,
|
|
113
114
|
}: SelectionGridProps) {
|
|
115
|
+
const contextAccent = useAccentColor()
|
|
116
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
114
117
|
const items = itemsProp || (presets ? resolvePresets(presets) : [])
|
|
115
118
|
|
|
116
119
|
function handleClick(item: SelectionCardItem) {
|
|
@@ -132,6 +135,7 @@ export function SelectionGrid({
|
|
|
132
135
|
|
|
133
136
|
if (layout === 'list') {
|
|
134
137
|
return (
|
|
138
|
+
<AccentColorProvider value={effectiveColor}>
|
|
135
139
|
<div
|
|
136
140
|
className={cn('grid gap-3', className)}
|
|
137
141
|
style={{ gridTemplateColumns: `repeat(${columns || 1}, 1fr)` }}
|
|
@@ -145,10 +149,12 @@ export function SelectionGrid({
|
|
|
145
149
|
/>
|
|
146
150
|
))}
|
|
147
151
|
</div>
|
|
152
|
+
</AccentColorProvider>
|
|
148
153
|
)
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
return (
|
|
157
|
+
<AccentColorProvider value={effectiveColor}>
|
|
152
158
|
<div
|
|
153
159
|
className={cn('grid gap-3', className)}
|
|
154
160
|
style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }}
|
|
@@ -162,6 +168,7 @@ export function SelectionGrid({
|
|
|
162
168
|
/>
|
|
163
169
|
))}
|
|
164
170
|
</div>
|
|
171
|
+
</AccentColorProvider>
|
|
165
172
|
)
|
|
166
173
|
}
|
|
167
174
|
|
|
@@ -174,7 +181,8 @@ interface CardProps {
|
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
function GridCard({ item, selected, onClick }: CardProps) {
|
|
177
|
-
const
|
|
184
|
+
const accent = useAccentColor() ?? 'blue'
|
|
185
|
+
const accentStyles = FORM_COLORS[accent]
|
|
178
186
|
const Icon = item.IconComponent || (item.icon ? iconMap[item.icon] : undefined)
|
|
179
187
|
|
|
180
188
|
return (
|
|
@@ -187,21 +195,16 @@ function GridCard({ item, selected, onClick }: CardProps) {
|
|
|
187
195
|
className={cn(
|
|
188
196
|
'relative p-3 rounded-lg text-center transition-all border-2 flex flex-col items-center',
|
|
189
197
|
item.disabled
|
|
190
|
-
? 'opacity-30 cursor-not-allowed border-neutral-
|
|
198
|
+
? 'opacity-30 cursor-not-allowed border-neutral-960 bg-[var(--background)]'
|
|
191
199
|
: selected
|
|
192
|
-
?
|
|
193
|
-
: 'bg-neutral-
|
|
200
|
+
? `cursor-pointer ${accentStyles.selectedBg} ${accentStyles.border}`
|
|
201
|
+
: 'bg-neutral-960/50 border-neutral-700 hover:bg-[var(--surface-hover)] hover:border-neutral-600 cursor-pointer',
|
|
194
202
|
)}
|
|
195
|
-
style={
|
|
196
|
-
selected && !item.disabled
|
|
197
|
-
? { borderColor: color }
|
|
198
|
-
: undefined
|
|
199
|
-
}
|
|
200
203
|
>
|
|
201
204
|
{Icon && (
|
|
202
205
|
<Icon
|
|
203
206
|
className="w-7 h-7 mb-1"
|
|
204
|
-
style={{ color: item.disabled ? undefined :
|
|
207
|
+
style={{ color: item.disabled ? undefined : item.color }}
|
|
205
208
|
/>
|
|
206
209
|
)}
|
|
207
210
|
|
|
@@ -215,7 +218,8 @@ function GridCard({ item, selected, onClick }: CardProps) {
|
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
function ListCard({ item, selected, onClick }: CardProps) {
|
|
218
|
-
const
|
|
221
|
+
const accent = useAccentColor() ?? 'blue'
|
|
222
|
+
const accentStyles = FORM_COLORS[accent]
|
|
219
223
|
const Icon = item.IconComponent || (item.icon ? iconMap[item.icon] : undefined)
|
|
220
224
|
|
|
221
225
|
return (
|
|
@@ -228,21 +232,16 @@ function ListCard({ item, selected, onClick }: CardProps) {
|
|
|
228
232
|
className={cn(
|
|
229
233
|
'relative p-3 rounded-lg transition-all border-2 flex items-center gap-3 text-left',
|
|
230
234
|
item.disabled
|
|
231
|
-
? 'opacity-30 cursor-not-allowed border-neutral-
|
|
235
|
+
? 'opacity-30 cursor-not-allowed border-neutral-960 bg-[var(--background)]'
|
|
232
236
|
: selected
|
|
233
|
-
?
|
|
234
|
-
: 'bg-neutral-
|
|
237
|
+
? `cursor-pointer ${accentStyles.selectedBg} ${accentStyles.border}`
|
|
238
|
+
: 'bg-neutral-960/50 border-neutral-700 hover:bg-[var(--surface-hover)] hover:border-neutral-600 cursor-pointer',
|
|
235
239
|
)}
|
|
236
|
-
style={
|
|
237
|
-
selected && !item.disabled
|
|
238
|
-
? { borderColor: color }
|
|
239
|
-
: undefined
|
|
240
|
-
}
|
|
241
240
|
>
|
|
242
241
|
{Icon && (
|
|
243
242
|
<Icon
|
|
244
243
|
className="w-5 h-5 flex-shrink-0"
|
|
245
|
-
style={{ color: item.disabled ? undefined :
|
|
244
|
+
style={{ color: item.disabled ? undefined : item.color }}
|
|
246
245
|
/>
|
|
247
246
|
)}
|
|
248
247
|
|
|
@@ -27,7 +27,7 @@ interface SettingRowToggle extends SettingRowBase {
|
|
|
27
27
|
type: 'toggle'
|
|
28
28
|
checked: boolean
|
|
29
29
|
onChange: (checked: boolean) => void
|
|
30
|
-
|
|
30
|
+
accentColor?: ToggleColor
|
|
31
31
|
size?: ToggleSize
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -65,7 +65,7 @@ export function SettingRow(props: SettingRowProps) {
|
|
|
65
65
|
checked={props.checked}
|
|
66
66
|
onChange={props.onChange}
|
|
67
67
|
disabled={disabled}
|
|
68
|
-
|
|
68
|
+
accentColor={props.accentColor}
|
|
69
69
|
size={props.size}
|
|
70
70
|
aria-label={label}
|
|
71
71
|
/>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { cn } from '../lib/cn.ts'
|
|
2
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
3
|
+
import { useAccentColor, AccentColorProvider } from '../lib/accent-context.ts'
|
|
2
4
|
|
|
3
5
|
export interface SettingsCardProps {
|
|
4
6
|
children: React.ReactNode
|
|
@@ -6,12 +8,15 @@ export interface SettingsCardProps {
|
|
|
6
8
|
title?: string
|
|
7
9
|
description?: string
|
|
8
10
|
testId?: string
|
|
11
|
+
accentColor?: FormColor
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
export function SettingsCard({ children, className, title, description, testId }: SettingsCardProps) {
|
|
14
|
+
export function SettingsCard({ children, className, title, description, testId, accentColor }: SettingsCardProps) {
|
|
15
|
+
const contextAccent = useAccentColor()
|
|
16
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
12
17
|
return (
|
|
13
18
|
<div
|
|
14
|
-
className={cn('bg-neutral-
|
|
19
|
+
className={cn('bg-neutral-980 border border-neutral-960 rounded-lg p-4 space-y-4', className)}
|
|
15
20
|
data-testid={testId}
|
|
16
21
|
>
|
|
17
22
|
{title && (
|
|
@@ -21,7 +26,9 @@ export function SettingsCard({ children, className, title, description, testId }
|
|
|
21
26
|
</div>
|
|
22
27
|
)}
|
|
23
28
|
{!title && description && <p className="text-md text-neutral-500">{description}</p>}
|
|
24
|
-
{
|
|
29
|
+
<AccentColorProvider value={effectiveColor}>
|
|
30
|
+
{children}
|
|
31
|
+
</AccentColorProvider>
|
|
25
32
|
</div>
|
|
26
33
|
)
|
|
27
34
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Info, AlertTriangle, CheckCircle, AlertCircle } from 'lucide-react'
|
|
2
|
-
import { ACCENT_TEXT, type AccentColor } from '../lib/form-colors.ts'
|
|
2
|
+
import { ACCENT_TEXT, type AccentColor, type FormColor } from '../lib/form-colors.ts'
|
|
3
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
3
4
|
import { cn } from '../lib/cn.ts'
|
|
4
5
|
|
|
5
6
|
export type SettingsInfoBoxColor = AccentColor
|
|
@@ -7,6 +8,7 @@ export type SettingsInfoBoxColor = AccentColor
|
|
|
7
8
|
export interface SettingsInfoBoxProps {
|
|
8
9
|
children: React.ReactNode
|
|
9
10
|
color?: SettingsInfoBoxColor
|
|
11
|
+
accentColor?: FormColor
|
|
10
12
|
className?: string
|
|
11
13
|
testId?: string
|
|
12
14
|
}
|
|
@@ -47,15 +49,17 @@ const borderColorMap: Record<SettingsInfoBoxColor, string> = {
|
|
|
47
49
|
teal: 'border-l-teal-500',
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
export function SettingsInfoBox({ children, color
|
|
51
|
-
const
|
|
52
|
+
export function SettingsInfoBox({ children, color, accentColor, className, testId }: SettingsInfoBoxProps) {
|
|
53
|
+
const contextAccent = useAccentColor()
|
|
54
|
+
const effectiveColor = (color ?? accentColor ?? contextAccent ?? 'neutral') as SettingsInfoBoxColor
|
|
55
|
+
const Icon = INFO_BOX_ICONS[effectiveColor]
|
|
52
56
|
|
|
53
57
|
return (
|
|
54
58
|
<div
|
|
55
|
-
className={cn('flex items-start gap-3 border-l-2 pl-2.5', borderColorMap[
|
|
59
|
+
className={cn('flex items-start gap-3 border-l-2 pl-2.5', borderColorMap[effectiveColor], className)}
|
|
56
60
|
data-testid={testId}
|
|
57
61
|
>
|
|
58
|
-
<Icon className={cn('w-4 h-4 mt-0.5 shrink-0', ACCENT_TEXT[
|
|
62
|
+
<Icon className={cn('w-4 h-4 mt-0.5 shrink-0', ACCENT_TEXT[effectiveColor])} />
|
|
59
63
|
<div className="text-md text-neutral-500">{children}</div>
|
|
60
64
|
</div>
|
|
61
65
|
)
|
|
@@ -6,16 +6,28 @@
|
|
|
6
6
|
* - Configuration panels - section dividers
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { ACCENT_TEXT, type AccentColor, type FormColor } from '../lib/form-colors.ts'
|
|
10
|
+
import { useAccentColor } from '../lib/accent-context.ts'
|
|
11
|
+
import { cn } from '../lib/cn.ts'
|
|
12
|
+
|
|
9
13
|
export interface SettingsSectionTitleProps {
|
|
10
14
|
children: React.ReactNode
|
|
11
15
|
className?: string
|
|
12
16
|
testId?: string
|
|
17
|
+
accentColor?: FormColor
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
export function SettingsSectionTitle({ children, className
|
|
20
|
+
export function SettingsSectionTitle({ children, className, testId, accentColor }: SettingsSectionTitleProps) {
|
|
21
|
+
const contextAccent = useAccentColor()
|
|
22
|
+
const effectiveColor = accentColor ?? contextAccent
|
|
23
|
+
|
|
16
24
|
return (
|
|
17
25
|
<h3
|
|
18
|
-
className={
|
|
26
|
+
className={cn(
|
|
27
|
+
'text-sm font-medium uppercase tracking-wider',
|
|
28
|
+
effectiveColor ? ACCENT_TEXT[effectiveColor as AccentColor] : 'text-neutral-400',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
19
31
|
data-testid={testId}
|
|
20
32
|
>
|
|
21
33
|
{children}
|
|
@@ -4,6 +4,8 @@ import { RefreshCw, Eye } from 'lucide-react'
|
|
|
4
4
|
import { cn } from '../lib/cn.ts'
|
|
5
5
|
import { Label, type LabelColor } from './label.tsx'
|
|
6
6
|
import type { IconName } from './icon-button.tsx'
|
|
7
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
8
|
+
import { useAccentColor, AccentColorProvider } from '../lib/accent-context.ts'
|
|
7
9
|
|
|
8
10
|
type SnapshotStatus = 'synced' | 'pending' | 'conflict' | 'outdated'
|
|
9
11
|
|
|
@@ -16,6 +18,7 @@ export interface SnapshotCardProps {
|
|
|
16
18
|
onSync?: () => void
|
|
17
19
|
onView?: () => void
|
|
18
20
|
className?: string
|
|
21
|
+
accentColor?: FormColor
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
const statusStripeColor: Record<SnapshotStatus, string> = {
|
|
@@ -41,9 +44,13 @@ export function SnapshotCard({
|
|
|
41
44
|
onSync,
|
|
42
45
|
onView,
|
|
43
46
|
className,
|
|
47
|
+
accentColor: accentColorProp,
|
|
44
48
|
}: SnapshotCardProps) {
|
|
49
|
+
const contextAccent = useAccentColor()
|
|
50
|
+
const effectiveColor = accentColorProp ?? contextAccent ?? 'blue'
|
|
45
51
|
return (
|
|
46
|
-
<
|
|
52
|
+
<AccentColorProvider value={effectiveColor}>
|
|
53
|
+
<div className={cn('rounded-lg border border-neutral-700 bg-neutral-960 overflow-hidden', className)}>
|
|
47
54
|
<div className={cn('h-1', statusStripeColor[status])} />
|
|
48
55
|
|
|
49
56
|
<div className="p-4">
|
|
@@ -51,7 +58,7 @@ export function SnapshotCard({
|
|
|
51
58
|
<h3 className="text-md font-medium text-neutral-200 truncate">{title}</h3>
|
|
52
59
|
<Label
|
|
53
60
|
text={statusLabelConfig[status].text}
|
|
54
|
-
|
|
61
|
+
accentColor={statusLabelConfig[status].color}
|
|
55
62
|
icon={statusLabelConfig[status].icon}
|
|
56
63
|
tooltip={{ description: statusLabelConfig[status].tooltip }}
|
|
57
64
|
size="xs"
|
|
@@ -103,5 +110,6 @@ export function SnapshotCard({
|
|
|
103
110
|
)}
|
|
104
111
|
</div>
|
|
105
112
|
</div>
|
|
113
|
+
</AccentColorProvider>
|
|
106
114
|
)
|
|
107
115
|
}
|
|
@@ -61,7 +61,7 @@ export function SnippetsPanel({
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
return (
|
|
64
|
-
<div className={cn('flex flex-col bg-neutral-
|
|
64
|
+
<div className={cn('flex flex-col bg-neutral-960 rounded-lg overflow-hidden', className)}>
|
|
65
65
|
<div className="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
|
|
66
66
|
<span className="text-xs font-semibold uppercase tracking-wider text-neutral-500">Snippets</span>
|
|
67
67
|
<span className="text-xs text-neutral-500">{snippets.length} snippets</span>
|
|
@@ -107,7 +107,7 @@ function SnippetCard({ snippet, onInsert, onCopy }: SnippetCardProps) {
|
|
|
107
107
|
const langColor = snippet.language ? LANGUAGE_COLORS[snippet.language.toLowerCase()] ?? '#6b7280' : '#6b7280'
|
|
108
108
|
|
|
109
109
|
return (
|
|
110
|
-
<div className="rounded-md border border-neutral-700 bg-neutral-
|
|
110
|
+
<div className="rounded-md border border-neutral-700 bg-neutral-980 hover:border-neutral-600 transition-colors">
|
|
111
111
|
<div className="flex items-center justify-between gap-2 px-2.5 py-1.5">
|
|
112
112
|
<div className="flex items-center gap-2 min-w-0">
|
|
113
113
|
<span className="text-sm font-medium text-neutral-200 truncate">{snippet.label}</span>
|
|
@@ -126,6 +126,7 @@ function SnippetCard({ snippet, onInsert, onCopy }: SnippetCardProps) {
|
|
|
126
126
|
icon="arrow-down-to-line"
|
|
127
127
|
onClick={() => onInsert(snippet.id)}
|
|
128
128
|
size="xss"
|
|
129
|
+
accentColor="neutral"
|
|
129
130
|
tooltip={{ title: 'Insert snippet', description: 'Insert this snippet' }}
|
|
130
131
|
/>
|
|
131
132
|
)}
|
|
@@ -134,6 +135,7 @@ function SnippetCard({ snippet, onInsert, onCopy }: SnippetCardProps) {
|
|
|
134
135
|
icon="copy"
|
|
135
136
|
onClick={() => onCopy(snippet.id)}
|
|
136
137
|
size="xss"
|
|
138
|
+
accentColor="neutral"
|
|
137
139
|
tooltip={{ title: 'Copy snippet', description: 'Copy to clipboard' }}
|
|
138
140
|
/>
|
|
139
141
|
)}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { ArrowUp, ArrowDown, ChevronDown, Check } 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 VARIANT_CLASSES = {
|
|
8
|
-
filled: { bg: 'bg-neutral-
|
|
10
|
+
filled: { bg: 'bg-neutral-960', hoverBg: 'hover:bg-neutral-700' },
|
|
9
11
|
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700' },
|
|
10
12
|
}
|
|
11
13
|
|
|
@@ -25,7 +27,7 @@ export interface SortDropdownProps {
|
|
|
25
27
|
onToggleDirection: () => void
|
|
26
28
|
fields: SortField[]
|
|
27
29
|
variant?: 'filled' | 'outline'
|
|
28
|
-
|
|
30
|
+
accentColor?: FormColor
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function SortDropdown({
|
|
@@ -35,15 +37,16 @@ export function SortDropdown({
|
|
|
35
37
|
onToggleDirection,
|
|
36
38
|
fields,
|
|
37
39
|
variant = 'outline',
|
|
38
|
-
|
|
40
|
+
accentColor,
|
|
39
41
|
}: SortDropdownProps) {
|
|
42
|
+
const contextAccent = useAccentColor()
|
|
43
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
40
44
|
const [isOpen, setIsOpen] = useState(false)
|
|
41
45
|
const [highlightIdx, setHighlightIdx] = useState(-1)
|
|
42
|
-
const
|
|
43
|
-
const menuRef = useDropdownMaxHeight<HTMLDivElement>(isOpen)
|
|
46
|
+
const { triggerRef, menuRef, position } = useDropdownPortal(isOpen)
|
|
44
47
|
const v = VARIANT_CLASSES[variant]
|
|
45
48
|
|
|
46
|
-
useClickOutside(
|
|
49
|
+
useClickOutside([triggerRef, menuRef], isOpen, () => setIsOpen(false))
|
|
47
50
|
|
|
48
51
|
useEffect(() => {
|
|
49
52
|
if (isOpen) setHighlightIdx(fields.findIndex((f) => f.value === field))
|
|
@@ -70,18 +73,42 @@ export function SortDropdown({
|
|
|
70
73
|
const current = fields.find((f) => f.value === field) ?? fields[0]
|
|
71
74
|
const DirIcon = ascending ? ArrowUp : ArrowDown
|
|
72
75
|
|
|
76
|
+
const menu = isOpen && createPortal(
|
|
77
|
+
<div
|
|
78
|
+
ref={menuRef}
|
|
79
|
+
role="listbox"
|
|
80
|
+
style={{ position: 'fixed', top: position.top, left: position.left, minWidth: position.minWidth, zIndex: 9999 }}
|
|
81
|
+
className={`whitespace-nowrap bg-[var(--popover)] border ${FORM_COLORS[effectiveColor].border} rounded-lg shadow-lg overflow-hidden`}
|
|
82
|
+
>
|
|
83
|
+
{fields.map((f, idx) => (
|
|
84
|
+
<button
|
|
85
|
+
key={f.value}
|
|
86
|
+
onClick={() => { onFieldChange(f.value); setIsOpen(false) }}
|
|
87
|
+
onPointerEnter={() => setHighlightIdx(idx)}
|
|
88
|
+
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
89
|
+
idx === highlightIdx ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200` : field === f.value ? `${FORM_COLORS[effectiveColor].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
<Check className={`w-3 h-3 shrink-0 ${field === f.value ? FORM_COLORS[effectiveColor].accent : 'invisible'}`} />
|
|
93
|
+
<span>{f.label}</span>
|
|
94
|
+
</button>
|
|
95
|
+
))}
|
|
96
|
+
</div>,
|
|
97
|
+
document.body,
|
|
98
|
+
)
|
|
99
|
+
|
|
73
100
|
return (
|
|
74
|
-
<div className="relative flex items-center" ref={
|
|
101
|
+
<div className="relative flex items-center" ref={triggerRef} onKeyDown={handleKeyDown}>
|
|
75
102
|
<button
|
|
76
103
|
aria-expanded={isOpen}
|
|
77
104
|
aria-haspopup="listbox"
|
|
78
105
|
onClick={() => setIsOpen(!isOpen)}
|
|
79
|
-
className={`flex items-center gap-1.5
|
|
106
|
+
className={`flex items-center gap-1.5 py-1 px-2 rounded-md border ${v.bg} text-sm transition-colors cursor-pointer ${FORM_COLORS[effectiveColor].border} text-neutral-200 ${FORM_COLORS[effectiveColor].hover}`}
|
|
80
107
|
>
|
|
81
108
|
<button
|
|
82
109
|
type="button"
|
|
83
110
|
aria-label={ascending ? 'Sort descending' : 'Sort ascending'}
|
|
84
|
-
className={`${FORM_COLORS[
|
|
111
|
+
className={`${FORM_COLORS[effectiveColor].accent} hover:brightness-125 transition-colors cursor-pointer`}
|
|
85
112
|
onClick={(e) => { e.stopPropagation(); onToggleDirection() }}
|
|
86
113
|
>
|
|
87
114
|
<DirIcon className="w-3 h-3" />
|
|
@@ -89,24 +116,7 @@ export function SortDropdown({
|
|
|
89
116
|
<span className="whitespace-nowrap">{current.label}</span>
|
|
90
117
|
<ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
91
118
|
</button>
|
|
92
|
-
|
|
93
|
-
{isOpen && (
|
|
94
|
-
<div ref={menuRef} role="listbox" className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] bg-[var(--popover)] border ${FORM_COLORS[color].border} rounded-lg shadow-lg overflow-hidden`}>
|
|
95
|
-
{fields.map((f, idx) => (
|
|
96
|
-
<button
|
|
97
|
-
key={f.value}
|
|
98
|
-
onClick={() => { onFieldChange(f.value); setIsOpen(false) }}
|
|
99
|
-
onPointerEnter={() => setHighlightIdx(idx)}
|
|
100
|
-
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer ${
|
|
101
|
-
idx === highlightIdx ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : field === f.value ? `${FORM_COLORS[color].selectedBg} text-neutral-200` : `text-neutral-400 ${v.hoverBg}`
|
|
102
|
-
}`}
|
|
103
|
-
>
|
|
104
|
-
<Check className={`w-3 h-3 shrink-0 ${field === f.value ? FORM_COLORS[color].accent : 'invisible'}`} />
|
|
105
|
-
<span>{f.label}</span>
|
|
106
|
-
</button>
|
|
107
|
-
))}
|
|
108
|
-
</div>
|
|
109
|
-
)}
|
|
119
|
+
{menu}
|
|
110
120
|
</div>
|
|
111
121
|
)
|
|
112
122
|
}
|