@toolr/ui-design 0.1.7 → 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/hooks/use-modal-behavior.ts +32 -3
- 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 +11 -10
- 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 +37 -35
- package/components/ui/ai-action-button.tsx +12 -11
- package/components/ui/ai-execution-action-buttons.tsx +13 -5
- package/components/ui/badge.tsx +17 -6
- package/components/ui/bottom-panel-header.tsx +9 -5
- package/components/ui/breadcrumb.tsx +14 -6
- package/components/ui/{extension-list-card.tsx → capability-list-card.tsx} +14 -6
- package/components/ui/checkbox.tsx +23 -14
- package/components/ui/collapsible-section.tsx +38 -28
- package/components/ui/confirm-badge.tsx +17 -6
- 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 +7 -5
- package/components/ui/files-panel.tsx +147 -27
- package/components/ui/filter-dropdown.tsx +88 -75
- package/components/ui/form-actions.tsx +21 -11
- package/components/ui/frontmatter-form-header.tsx +10 -2
- package/components/ui/icon-button.tsx +27 -14
- package/components/ui/input.tsx +15 -7
- package/components/ui/label.tsx +9 -5
- package/components/ui/layout-tab-bar.tsx +11 -9
- package/components/ui/modal.tsx +26 -8
- package/components/ui/nav-card.tsx +7 -4
- package/components/ui/navigation-bar.tsx +40 -12
- package/components/ui/number-input.tsx +14 -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 +7 -2
- package/components/ui/select.tsx +17 -11
- package/components/ui/selection-grid.tsx +40 -37
- package/components/ui/setting-row.tsx +6 -4
- package/components/ui/settings-card.tsx +12 -5
- package/components/ui/settings-info-box.tsx +9 -6
- 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 +45 -32
- package/components/ui/status-card.tsx +9 -1
- package/components/ui/tab-bar.tsx +26 -13
- package/components/ui/toggle.tsx +31 -17
- package/components/ui/tooltip.tsx +14 -6
- package/dist/content.js +8 -8
- package/dist/diagrams.d.ts +0 -1
- package/dist/index.d.ts +431 -186
- package/dist/index.js +3119 -1724
- 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 +9 -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
|
@@ -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
|
|
@@ -260,9 +271,9 @@ const statusIcons: Record<IconButtonStatus, LucideIcon> = {
|
|
|
260
271
|
|
|
261
272
|
const statusConfig = {
|
|
262
273
|
loading: { color: undefined, active: true, animation: 'animate-spin' },
|
|
263
|
-
success: { color: 'green' as const, active: true, animation: '
|
|
264
|
-
warning: { color: 'amber' as const, active: true, animation: '
|
|
265
|
-
error: { color: 'red' as const, active: true, animation: '
|
|
274
|
+
success: { color: 'green' as const, active: true, animation: '' },
|
|
275
|
+
warning: { color: 'amber' as const, active: true, animation: '' },
|
|
276
|
+
error: { color: 'red' as const, active: true, animation: '' },
|
|
266
277
|
}
|
|
267
278
|
|
|
268
279
|
function resolveIcon(icon: IconName | ReactNode, status: IconButtonStatus | undefined): LucideIcon | null {
|
|
@@ -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 : ''}
|
|
@@ -337,7 +350,7 @@ export function IconButton({
|
|
|
337
350
|
href={href}
|
|
338
351
|
target="_blank"
|
|
339
352
|
rel="noopener noreferrer"
|
|
340
|
-
aria-label={tooltip?.title}
|
|
353
|
+
aria-label={tooltip?.title || (typeof tooltip?.description === 'string' ? tooltip.description : undefined)}
|
|
341
354
|
data-testid={testId}
|
|
342
355
|
className={`${sharedClassName} cursor-pointer no-underline`}
|
|
343
356
|
>
|
|
@@ -348,7 +361,7 @@ export function IconButton({
|
|
|
348
361
|
type="button"
|
|
349
362
|
onClick={onClick}
|
|
350
363
|
disabled={disabled}
|
|
351
|
-
aria-label={tooltip?.title}
|
|
364
|
+
aria-label={tooltip?.title || (typeof tooltip?.description === 'string' ? tooltip.description : undefined)}
|
|
352
365
|
data-testid={testId}
|
|
353
366
|
className={`${sharedClassName} cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
354
367
|
>
|
|
@@ -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
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
* - Extends native input attributes
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { forwardRef, useEffect, useRef, useState, type InputHTMLAttributes, type ReactNode } from 'react'
|
|
20
|
+
import { forwardRef, useEffect, useId, useRef, useState, type InputHTMLAttributes, type ReactNode } from 'react'
|
|
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)
|
|
@@ -84,6 +87,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
84
87
|
const isSearch = type === 'search'
|
|
85
88
|
const isPassword = type === 'password'
|
|
86
89
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false)
|
|
90
|
+
const errorId = useId()
|
|
87
91
|
|
|
88
92
|
// Debounce state
|
|
89
93
|
const [internalValue, setInternalValue] = useState(value)
|
|
@@ -146,13 +150,16 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
146
150
|
onBlur={(e) => { setFocused(false); onBlurProp?.(e) }}
|
|
147
151
|
disabled={disabled}
|
|
148
152
|
data-testid={testId}
|
|
153
|
+
aria-invalid={hasError || undefined}
|
|
154
|
+
aria-describedby={typeof error === 'string' && error ? errorId : undefined}
|
|
155
|
+
{...(isSearch && !props['aria-label'] ? { 'aria-label': props.placeholder || 'Search' } : {})}
|
|
149
156
|
{...searchAutoProps}
|
|
150
157
|
className={`
|
|
151
158
|
w-full border rounded-lg text-neutral-200 placeholder-neutral-500
|
|
152
159
|
focus:outline-none transition-colors
|
|
153
160
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
154
161
|
${sizeClasses[size]}
|
|
155
|
-
${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}`}
|
|
156
163
|
${mono ? 'font-mono' : ''}
|
|
157
164
|
${isSearch ? 'pl-8 pr-8' : (hasAction || isPassword) ? 'pr-8' : ''}
|
|
158
165
|
${className}
|
|
@@ -162,6 +169,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
162
169
|
{showClear && (
|
|
163
170
|
<button
|
|
164
171
|
type="button"
|
|
172
|
+
aria-label="Clear search"
|
|
165
173
|
onClick={handleClear}
|
|
166
174
|
className="absolute right-2 top-1/2 -translate-y-1/2 w-[18px] h-[18px] flex items-center justify-center rounded-md text-neutral-400 hover:text-neutral-300 hover:bg-neutral-500/20 transition-colors z-10 cursor-pointer"
|
|
167
175
|
>
|
|
@@ -177,7 +185,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
177
185
|
<button
|
|
178
186
|
type="button"
|
|
179
187
|
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
|
180
|
-
|
|
188
|
+
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
|
|
181
189
|
className="absolute right-2 top-1/2 -translate-y-1/2 w-[18px] h-[18px] flex items-center justify-center rounded-md text-neutral-400 hover:text-neutral-300 hover:bg-neutral-500/20 transition-colors z-10 cursor-pointer"
|
|
182
190
|
>
|
|
183
191
|
{isPasswordVisible ? <EyeOff className="w-2.5 h-2.5" /> : <Eye className="w-2.5 h-2.5" />}
|
|
@@ -188,7 +196,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
188
196
|
)}
|
|
189
197
|
</div>
|
|
190
198
|
{typeof error === 'string' && error && (
|
|
191
|
-
<p className="text-sm text-red-400 mt-1 text-right">{error}</p>
|
|
199
|
+
<p id={errorId} className="text-sm text-red-400 mt-1 text-right" role="alert">{error}</p>
|
|
192
200
|
)}
|
|
193
201
|
</div>
|
|
194
202
|
)
|
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
|
]
|
|
@@ -125,7 +125,11 @@ export function Label({
|
|
|
125
125
|
<>
|
|
126
126
|
{hasProgress && (
|
|
127
127
|
<span
|
|
128
|
-
|
|
128
|
+
role="progressbar"
|
|
129
|
+
aria-valuenow={Math.min(progress, 100)}
|
|
130
|
+
aria-valuemin={0}
|
|
131
|
+
aria-valuemax={100}
|
|
132
|
+
className={`absolute inset-y-0 left-0 ${progressFillColors[accentColor]} rounded-[inherit]`}
|
|
129
133
|
style={{ width: `${Math.min(progress, 100)}%` }}
|
|
130
134
|
/>
|
|
131
135
|
)}
|
|
@@ -138,7 +142,7 @@ export function Label({
|
|
|
138
142
|
<span key={i} className={`${s.iconSize} flex-shrink-0 ${hasProgress ? 'relative' : ''}`}><Icon className={s.iconSize} /></span>
|
|
139
143
|
) : null
|
|
140
144
|
})}
|
|
141
|
-
<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}>
|
|
142
146
|
{transformText(text, textTransform)}
|
|
143
147
|
</span>
|
|
144
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={{
|
|
@@ -233,15 +235,15 @@ export function LayoutTabBar({ tabs, activeId, onSelect, onClose, onReorder, cla
|
|
|
233
235
|
|
|
234
236
|
{/* Close button */}
|
|
235
237
|
{tab.closable && onClose && (
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
aria-label={`Close ${tab.title}`}
|
|
238
241
|
tabIndex={-1}
|
|
239
242
|
onClick={(e) => { e.stopPropagation(); onClose(tab.id) }}
|
|
240
|
-
onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); onClose(tab.id) } }}
|
|
241
243
|
className="absolute right-1.5 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 hover:bg-neutral-700 rounded p-0.5 transition-opacity cursor-pointer"
|
|
242
244
|
>
|
|
243
245
|
<X className="w-3 h-3" />
|
|
244
|
-
</
|
|
246
|
+
</button>
|
|
245
247
|
)}
|
|
246
248
|
</button>
|
|
247
249
|
|
|
@@ -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-
|
|
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>
|
package/components/ui/modal.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { useRef, useState } from 'react'
|
|
1
|
+
import { useId, useRef, useState } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { Info, AlertTriangle, AlertCircle, Check } from 'lucide-react'
|
|
4
4
|
import { useModalBehavior } from '../hooks/use-modal-behavior.ts'
|
|
5
5
|
import { IconButton, type ActionItem } from './icon-button.tsx'
|
|
6
6
|
import { FormActions } from './form-actions.tsx'
|
|
7
7
|
import type { ReactNode } from 'react'
|
|
8
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
9
|
+
import { useAccentColor, AccentColorProvider } from '../lib/accent-context.ts'
|
|
8
10
|
|
|
9
11
|
export type ModalKind = 'info' | 'warning' | 'error' | 'orange' | 'success'
|
|
10
12
|
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
@@ -42,35 +44,39 @@ interface ModalProps {
|
|
|
42
44
|
|
|
43
45
|
function Modal({ isOpen, onClose, title, children, kind = 'info', size = 'md', hideCloseButton = false, headerActions, testId }: ModalProps) {
|
|
44
46
|
const modalRef = useRef<HTMLDivElement>(null)
|
|
47
|
+
const titleId = useId()
|
|
45
48
|
|
|
46
|
-
useModalBehavior(isOpen, onClose)
|
|
49
|
+
useModalBehavior(isOpen, onClose, modalRef)
|
|
47
50
|
|
|
48
51
|
if (!isOpen) return null
|
|
49
52
|
|
|
50
53
|
return createPortal(
|
|
51
54
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
52
|
-
<div className="absolute inset-0 bg-[var(--dialog-backdrop)]
|
|
55
|
+
<div className="absolute inset-0 bg-[var(--dialog-backdrop)]" onClick={onClose} aria-hidden="true" />
|
|
53
56
|
<div
|
|
54
57
|
ref={modalRef}
|
|
58
|
+
role="dialog"
|
|
59
|
+
aria-modal="true"
|
|
60
|
+
aria-labelledby={titleId}
|
|
55
61
|
data-testid={testId}
|
|
56
|
-
className={`relative bg-neutral-
|
|
62
|
+
className={`relative bg-neutral-980 border border-neutral-700 rounded-lg shadow-lg ${SIZE_CLASSES[size]} w-full mx-4 overflow-hidden`}
|
|
57
63
|
>
|
|
58
|
-
<div className="flex items-center gap-3 px-
|
|
64
|
+
<div className="flex items-center gap-3 px-4 py-4 border-b border-neutral-960">
|
|
59
65
|
{KIND_ICON[kind]}
|
|
60
|
-
<h3 className="text-lg font-semibold text-white flex-1">{title}</h3>
|
|
66
|
+
<h3 id={titleId} className="text-lg font-semibold text-white flex-1 min-w-0 truncate">{title}</h3>
|
|
61
67
|
{headerActions?.map((a, i) => <IconButton key={i} {...a} />)}
|
|
62
68
|
{!hideCloseButton && (
|
|
63
69
|
<IconButton
|
|
64
70
|
icon="x"
|
|
65
71
|
onClick={onClose}
|
|
66
72
|
size="sm"
|
|
67
|
-
|
|
73
|
+
accentColor="neutral"
|
|
68
74
|
tooltip={{ description: 'Close this modal' }}
|
|
69
75
|
testId="modal-close"
|
|
70
76
|
/>
|
|
71
77
|
)}
|
|
72
78
|
</div>
|
|
73
|
-
<div className="px-
|
|
79
|
+
<div className="px-4 py-4">{children}</div>
|
|
74
80
|
</div>
|
|
75
81
|
</div>,
|
|
76
82
|
document.body,
|
|
@@ -89,6 +95,7 @@ export interface ConfirmModalProps {
|
|
|
89
95
|
confirmColor?: 'red' | 'blue' | 'orange' | 'yellow'
|
|
90
96
|
isLoading?: boolean
|
|
91
97
|
confirmDisabled?: boolean
|
|
98
|
+
accentColor?: FormColor
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
export function ConfirmModal({
|
|
@@ -103,7 +110,10 @@ export function ConfirmModal({
|
|
|
103
110
|
confirmColor = 'blue',
|
|
104
111
|
isLoading = false,
|
|
105
112
|
confirmDisabled = false,
|
|
113
|
+
accentColor: accentColorProp,
|
|
106
114
|
}: ConfirmModalProps) {
|
|
115
|
+
const contextAccent = useAccentColor()
|
|
116
|
+
const effectiveColor = accentColorProp ?? contextAccent ?? 'blue'
|
|
107
117
|
const [isConfirming, setIsConfirming] = useState(false)
|
|
108
118
|
|
|
109
119
|
const isDisabled = isLoading || isConfirming || confirmDisabled
|
|
@@ -122,6 +132,7 @@ export function ConfirmModal({
|
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
return (
|
|
135
|
+
<AccentColorProvider value={effectiveColor}>
|
|
125
136
|
<Modal isOpen={isOpen} onClose={onClose} title={title} kind={kind} hideCloseButton>
|
|
126
137
|
<div className="text-neutral-300 mb-6">
|
|
127
138
|
{message}
|
|
@@ -150,6 +161,7 @@ export function ConfirmModal({
|
|
|
150
161
|
confirmStatus={isInProgress ? 'loading' : undefined}
|
|
151
162
|
/>
|
|
152
163
|
</Modal>
|
|
164
|
+
</AccentColorProvider>
|
|
153
165
|
)
|
|
154
166
|
}
|
|
155
167
|
|
|
@@ -159,6 +171,7 @@ export interface AlertModalProps {
|
|
|
159
171
|
title: string
|
|
160
172
|
message: string
|
|
161
173
|
kind?: ModalKind
|
|
174
|
+
accentColor?: FormColor
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
export function AlertModal({
|
|
@@ -167,8 +180,12 @@ export function AlertModal({
|
|
|
167
180
|
title,
|
|
168
181
|
message,
|
|
169
182
|
kind = 'info',
|
|
183
|
+
accentColor: accentColorProp,
|
|
170
184
|
}: AlertModalProps) {
|
|
185
|
+
const contextAccent = useAccentColor()
|
|
186
|
+
const effectiveColor = accentColorProp ?? contextAccent ?? 'blue'
|
|
171
187
|
return (
|
|
188
|
+
<AccentColorProvider value={effectiveColor}>
|
|
172
189
|
<Modal isOpen={isOpen} onClose={onClose} title={title} kind={kind} hideCloseButton>
|
|
173
190
|
<div className="text-neutral-300 mb-6">{message}</div>
|
|
174
191
|
<FormActions
|
|
@@ -178,5 +195,6 @@ export function AlertModal({
|
|
|
178
195
|
confirmTooltip="Dismiss this alert"
|
|
179
196
|
/>
|
|
180
197
|
</Modal>
|
|
198
|
+
</AccentColorProvider>
|
|
181
199
|
)
|
|
182
200
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { iconMap, type IconName } from './icon-button.tsx'
|
|
4
4
|
import { Label, type LabelColor } from './label.tsx'
|
|
5
5
|
import { cn } from '../lib/cn.ts'
|
|
6
|
+
import type { FormColor } from '../lib/form-colors.ts'
|
|
6
7
|
|
|
7
8
|
export interface NavCardProps {
|
|
8
9
|
title: string
|
|
@@ -11,11 +12,12 @@ export interface NavCardProps {
|
|
|
11
12
|
/** Custom icon component. Takes precedence over icon name. */
|
|
12
13
|
IconComponent?: React.ComponentType<{ className?: string }>
|
|
13
14
|
iconColor?: string
|
|
14
|
-
label?: { text: string; color: LabelColor; tooltip: { description: string } }
|
|
15
|
+
label?: { text: string; color: LabelColor; icon?: IconName; tooltip: { description: string } }
|
|
15
16
|
stats?: string
|
|
16
17
|
onClick?: () => void
|
|
17
18
|
disabled?: boolean
|
|
18
19
|
className?: string
|
|
20
|
+
accentColor?: FormColor
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function NavCard({
|
|
@@ -29,6 +31,7 @@ export function NavCard({
|
|
|
29
31
|
onClick,
|
|
30
32
|
disabled = false,
|
|
31
33
|
className,
|
|
34
|
+
accentColor: _accentColor,
|
|
32
35
|
}: NavCardProps) {
|
|
33
36
|
const Icon = IconComponent ?? (icon ? iconMap[icon] : undefined)
|
|
34
37
|
|
|
@@ -38,7 +41,7 @@ export function NavCard({
|
|
|
38
41
|
onClick={disabled ? undefined : onClick}
|
|
39
42
|
disabled={disabled}
|
|
40
43
|
className={cn(
|
|
41
|
-
'relative w-full text-left rounded-lg border border-neutral-700 bg-neutral-
|
|
44
|
+
'relative w-full text-left rounded-lg border border-neutral-700 bg-neutral-960 p-4 transition-all duration-200 cursor-pointer',
|
|
42
45
|
!disabled && 'hover:-translate-y-0.5 hover:border-neutral-600 hover:bg-neutral-700',
|
|
43
46
|
disabled && 'opacity-50 cursor-not-allowed',
|
|
44
47
|
className,
|
|
@@ -46,7 +49,7 @@ export function NavCard({
|
|
|
46
49
|
>
|
|
47
50
|
{label && (
|
|
48
51
|
<span className="absolute top-3 right-3">
|
|
49
|
-
<Label text={label.text}
|
|
52
|
+
<Label text={label.text} accentColor={label.color} icon={label.icon} size="xs" tooltip={label.tooltip} />
|
|
50
53
|
</span>
|
|
51
54
|
)}
|
|
52
55
|
|
|
@@ -59,7 +62,7 @@ export function NavCard({
|
|
|
59
62
|
</div>
|
|
60
63
|
)}
|
|
61
64
|
|
|
62
|
-
<h3 className="text-md font-medium text-neutral-200">{title}</h3>
|
|
65
|
+
<h3 className="text-md font-medium text-neutral-200 truncate">{title}</h3>
|
|
63
66
|
|
|
64
67
|
{description && (
|
|
65
68
|
<p className="mt-1 text-sm text-neutral-500 leading-relaxed line-clamp-2">{description}</p>
|