diff --git a/.gitignore b/.gitignore index ede314f..31f5113 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,5 @@ Thumbs.db # Temporary working files (specs, scratch) — never commit tmp/ +verification-screenshots/ ArchFlow.iml diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 91c7aa0..ae26355 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -27,6 +27,7 @@ import { useAuthStore } from './stores/auth-store' import { useWorkspaceStore } from './stores/workspace-store' import { useWorkspaceSocket } from './hooks/use-realtime' import { ChatBubble } from './components/agent-chat/ChatBubble' +import { ThemeProvider } from './components/theme/ThemeProvider' import './index.css' const queryClient = new QueryClient({ @@ -74,10 +75,11 @@ function App() { return ( - {isAuthenticated && } - {isAuthenticated && workspaceId && } - - + + {isAuthenticated && } + {isAuthenticated && workspaceId && } + + : } @@ -213,11 +215,12 @@ function App() { : } /> - - {/* Agent chat bubble — floats over all workspace pages, outside route - layout but inside the Router so useNavigate() (in useViewChange) works. */} - {isAuthenticated && } - + + {/* Agent chat bubble — floats over all workspace pages, outside route + layout but inside the Router so useNavigate() (in useViewChange) works. */} + {isAuthenticated && } + + ) } diff --git a/frontend/src/components/agents-settings/AnalyticsConsentModal.tsx b/frontend/src/components/agents-settings/AnalyticsConsentModal.tsx index 6e66641..3f18f8a 100644 --- a/frontend/src/components/agents-settings/AnalyticsConsentModal.tsx +++ b/frontend/src/components/agents-settings/AnalyticsConsentModal.tsx @@ -46,20 +46,20 @@ function ModalBody({ >
e.stopPropagation()} - className="w-[520px] max-h-[85vh] overflow-y-auto rounded-lg border border-neutral-800 bg-neutral-900 text-neutral-100 shadow-2xl" + className="w-[520px] max-h-[85vh] overflow-y-auto rounded-lg border border-border-base bg-panel text-text-base shadow-popup" > -
+

Включити аналітику агентів?

-
+

Це допомагає нам зробити агентів кращими: ми бачимо які запити погано спрацьовують і покращуємо логіку.

-

+

Що збирається

    @@ -70,7 +70,7 @@ function ModalBody({
-

+

Що НЕ збирається

    @@ -81,7 +81,7 @@ function ModalBody({
-

+

Куди йде

    @@ -92,7 +92,7 @@ function ModalBody({
-

+

Виберіть рівень

@@ -121,18 +121,18 @@ function ModalBody({
-
+
@@ -165,8 +165,8 @@ function ConsentOption({ className="mt-0.5" /> - {label} - — {hint} + {label} + — {hint} ) diff --git a/frontend/src/components/agents-settings/ModelPricingTable.tsx b/frontend/src/components/agents-settings/ModelPricingTable.tsx index f632599..7fbcf48 100644 --- a/frontend/src/components/agents-settings/ModelPricingTable.tsx +++ b/frontend/src/components/agents-settings/ModelPricingTable.tsx @@ -34,11 +34,11 @@ export function ModelPricingTable({ pricing, onChange }: Props) { return (
- + @@ -50,7 +50,7 @@ export function ModelPricingTable({ pricing, onChange }: Props) { @@ -60,9 +60,9 @@ export function ModelPricingTable({ pricing, onChange }: Props) { - ))} {/* Add row */} - +
Model Input ($/1M tokens) Output ($/1M tokens)
No pricing overrides — falling back to LiteLLM defaults.
+ {modelId} @@ -77,7 +77,7 @@ export function ModelPricingTable({ pricing, onChange }: Props) { }) } data-testid={`pricing-${modelId}-input`} - className="w-28 bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="w-28 bg-surface border border-border-base rounded px-2 py-1 text-xs text-text-base outline-none focus:border-border-hi" /> @@ -92,7 +92,7 @@ export function ModelPricingTable({ pricing, onChange }: Props) { }) } data-testid={`pricing-${modelId}-output`} - className="w-28 bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="w-28 bg-surface border border-border-base rounded px-2 py-1 text-xs text-text-base outline-none focus:border-border-hi" /> @@ -108,7 +108,7 @@ export function ModelPricingTable({ pricing, onChange }: Props) {
setNewId(e.target.value)} placeholder="claude-haiku-3-5" data-testid="pricing-new-id" - className="w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="w-full bg-panel border border-border-base rounded px-2 py-1 text-xs text-text-base placeholder:text-text-4 outline-none focus:border-border-hi" /> @@ -127,7 +127,7 @@ export function ModelPricingTable({ pricing, onChange }: Props) { onChange={(e) => setNewInput(e.target.value)} placeholder="0.80" data-testid="pricing-new-input" - className="w-28 bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="w-28 bg-panel border border-border-base rounded px-2 py-1 text-xs text-text-base placeholder:text-text-4 outline-none focus:border-border-hi" /> @@ -138,7 +138,7 @@ export function ModelPricingTable({ pricing, onChange }: Props) { onChange={(e) => setNewOutput(e.target.value)} placeholder="4.00" data-testid="pricing-new-output" - className="w-28 bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="w-28 bg-panel border border-border-base rounded px-2 py-1 text-xs text-text-base placeholder:text-text-4 outline-none focus:border-border-hi" /> @@ -147,7 +147,7 @@ export function ModelPricingTable({ pricing, onChange }: Props) { onClick={addRow} disabled={!newId.trim()} data-testid="pricing-add" - className="text-xs text-blue-400 hover:text-blue-300 disabled:opacity-40 disabled:cursor-not-allowed" + className="text-xs text-coral hover:text-coral-2 disabled:opacity-40 disabled:cursor-not-allowed" > + Add row diff --git a/frontend/src/components/agents-settings/PerAgentOverrideTable.tsx b/frontend/src/components/agents-settings/PerAgentOverrideTable.tsx index b1adb27..ada8c28 100644 --- a/frontend/src/components/agents-settings/PerAgentOverrideTable.tsx +++ b/frontend/src/components/agents-settings/PerAgentOverrideTable.tsx @@ -32,11 +32,11 @@ export function PerAgentOverrideTable({ agents, defaultModel, onChange }: Props) return (
- + @@ -51,9 +51,9 @@ export function PerAgentOverrideTable({ agents, defaultModel, onChange }: Props) -
Agent Model Turn limit
+ {agentId} @@ -69,7 +69,7 @@ export function PerAgentOverrideTable({ agents, defaultModel, onChange }: Props) ) } data-testid={`agent-${agentId}-model`} - className="w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="w-full bg-surface border border-border-base rounded px-2 py-1 text-xs text-text-base placeholder:text-text-4 outline-none focus:border-border-hi" /> @@ -86,7 +86,7 @@ export function PerAgentOverrideTable({ agents, defaultModel, onChange }: Props) ) } data-testid={`agent-${agentId}-turn_limit`} - className="w-20 bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="w-20 bg-surface border border-border-base rounded px-2 py-1 text-xs text-text-base placeholder:text-text-4 outline-none focus:border-border-hi" /> @@ -103,7 +103,7 @@ export function PerAgentOverrideTable({ agents, defaultModel, onChange }: Props) ) } data-testid={`agent-${agentId}-budget_usd`} - className="w-24 bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="w-24 bg-surface border border-border-base rounded px-2 py-1 text-xs text-text-base placeholder:text-text-4 outline-none focus:border-border-hi" /> @@ -117,7 +117,7 @@ export function PerAgentOverrideTable({ agents, defaultModel, onChange }: Props) ) } data-testid={`agent-${agentId}-budget_scope`} - className="bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-xs outline-none focus:border-neutral-500" + className="bg-surface border border-border-base rounded px-2 py-1 text-xs text-text-base outline-none focus:border-border-hi" > diff --git a/frontend/src/components/canvas/ArchFlowCanvas.test.tsx b/frontend/src/components/canvas/ArchFlowCanvas.test.tsx new file mode 100644 index 0000000..3f44e08 --- /dev/null +++ b/frontend/src/components/canvas/ArchFlowCanvas.test.tsx @@ -0,0 +1,145 @@ +import { render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const h = vi.hoisted(() => ({ + reactFlowProps: null as null | Record, + backgroundProps: null as null | Record, + miniMapProps: null as null | Record, + commentComposeType: null as null | string, + dependenciesFocusId: null as null | string, + allObjects: [] as Array<{ id: string; name: string; type: string }>, +})) + +vi.mock('@xyflow/react', () => ({ + ReactFlow: ({ children, ...props }: { children?: React.ReactNode }) => { + h.reactFlowProps = props as Record + return
{children}
+ }, + Background: (props: Record) => { + h.backgroundProps = props + return
+ }, + Controls: () =>
, + MiniMap: (props: Record) => { + h.miniMapProps = props + return
+ }, + ConnectionMode: { Loose: 'Loose' }, + MarkerType: { ArrowClosed: 'ArrowClosed' }, + useReactFlow: () => ({ + setNodes: vi.fn(), + setEdges: vi.fn(), + getNodes: () => [], + getEdges: () => [], + screenToFlowPosition: ({ x, y }: { x: number; y: number }) => ({ x, y }), + fitView: vi.fn(), + }), +})) + +vi.mock('../../hooks/use-api', () => ({ + useConnections: () => ({ data: [] }), + useCreateComment: () => ({ mutate: vi.fn() }), + useCreateConnection: () => ({ mutate: vi.fn() }), + useDeleteConnection: () => ({ mutate: vi.fn() }), + useDiagramObjects: () => ({ data: [] }), + useFlows: () => ({ data: [] }), + useObjects: () => ({ data: h.allObjects }), + useRemoveObjectFromDiagram: () => ({ mutate: vi.fn() }), + useSaveDiagramPosition: () => ({ mutate: vi.fn() }), + useUpdateObject: () => ({ mutate: vi.fn() }), +})) + +vi.mock('../../hooks/use-diagrams', () => ({ + useDiagram: () => ({ data: null }), +})) + +vi.mock('../../hooks/use-realtime', () => ({ + useDiagramSocket: () => ({ + cursors: {}, + selections: {}, + presence: [], + sendCursor: vi.fn(), + sendSelection: vi.fn(), + }), +})) + +vi.mock('../../hooks/use-undo', () => ({ + useUndoController: vi.fn(), + useUndoMutation: () => ({ mutate: vi.fn() }), + useRedoMutation: () => ({ mutate: vi.fn() }), +})) + +vi.mock('../../lib/canvas-events', () => ({ + useFocusObjectListener: vi.fn(), + useFocusConnectionListener: vi.fn(), +})) + +vi.mock('../../stores/canvas-store', () => ({ + useCanvasStore: () => ({ + selectNode: vi.fn(), + selectEdge: vi.fn(), + dependenciesFocusId: h.dependenciesFocusId, + setDependenciesFocus: vi.fn(), + activeFilter: 'status', + activeFilterValue: null, + playingFlowId: null, + playingStepIdx: 0, + activeBranch: 'main', + commentComposeType: h.commentComposeType, + setCommentComposeType: vi.fn(), + setRemoteNodeEditors: vi.fn(), + setPresenceUsers: vi.fn(), + }), +})) + +vi.mock('./CanvasComments', () => ({ + CanvasComments: () => null, +})) + +vi.mock('./CursorsOverlay', () => ({ + CursorsOverlay: () => null, + RemoteSelectionsOverlay: () => null, +})) + +vi.mock('./UndoToolbarButtons', () => ({ + UndoToolbarButtons: () => null, +})) + +import { ArchFlowCanvas } from './ArchFlowCanvas' + +describe('ArchFlowCanvas theming', () => { + beforeEach(() => { + h.reactFlowProps = null + h.backgroundProps = null + h.miniMapProps = null + h.commentComposeType = null + h.dependenciesFocusId = null + h.allObjects = [] + }) + + it('uses semantic theme variables for the canvas, grid, and minimap mask', () => { + render() + + expect(h.reactFlowProps?.style).toMatchObject({ background: 'var(--color-bg)' }) + expect(h.backgroundProps).toMatchObject({ color: 'var(--canvas-grid)' }) + expect(h.miniMapProps).toMatchObject({ maskColor: 'var(--minimap-mask)' }) + }) + + it('uses semantic theme variables for floating canvas notices', () => { + h.commentComposeType = 'risk' + h.dependenciesFocusId = 'obj-1' + h.allObjects = [{ id: 'obj-1', name: 'Checkout', type: 'system' }] + + render() + + const composeNotice = screen.getByText('Click on the canvas to place a risk pin').closest('div') + const dependencyNotice = screen.getByText(/Dependencies of/).closest('div') + + expect(composeNotice?.getAttribute('style')).toContain('background: var(--color-panel)') + expect(composeNotice?.getAttribute('style')).toContain('border: 1px solid var(--color-accent-blue)') + expect(composeNotice?.getAttribute('style')).toContain('color: var(--color-text-base)') + expect(dependencyNotice?.getAttribute('style')).toContain('background: var(--color-panel)') + expect(dependencyNotice?.getAttribute('style')).toContain('border: 1px solid var(--color-accent-blue)') + expect(dependencyNotice?.getAttribute('style')).toContain('color: var(--color-text-base)') + }) +}) diff --git a/frontend/src/components/canvas/ArchFlowCanvas.tsx b/frontend/src/components/canvas/ArchFlowCanvas.tsx index 5aaf96d..9b10608 100644 --- a/frontend/src/components/canvas/ArchFlowCanvas.tsx +++ b/frontend/src/components/canvas/ArchFlowCanvas.tsx @@ -755,15 +755,15 @@ function CanvasInner({ diagramId }: ArchFlowCanvasProps) { transform: 'translateX(-50%)', zIndex: 25, padding: '6px 12px', - background: '#171717', - border: '1px solid #3b82f6', + background: 'var(--color-panel)', + border: '1px solid var(--color-accent-blue)', borderRadius: 8, fontSize: 12, - color: '#e5e5e5', + color: 'var(--color-text-base)', display: 'flex', alignItems: 'center', gap: 10, - boxShadow: '0 4px 12px rgba(0,0,0,0.4)', + boxShadow: 'var(--shadow-popup)', }} > Click on the canvas to place a {commentComposeType} pin @@ -771,8 +771,8 @@ function CanvasInner({ diagramId }: ArchFlowCanvasProps) { onClick={() => setCommentComposeType(null)} style={{ background: 'transparent', - border: '1px solid #404040', - color: '#a3a3a3', + border: '1px solid var(--color-border-base)', + color: 'var(--color-text-2)', borderRadius: 4, padding: '2px 8px', cursor: 'pointer', @@ -796,25 +796,25 @@ function CanvasInner({ diagramId }: ArchFlowCanvasProps) { alignItems: 'center', gap: 12, padding: '8px 14px', - background: '#171717', - border: '1px solid #3b82f6', + background: 'var(--color-panel)', + border: '1px solid var(--color-accent-blue)', borderRadius: 8, - color: '#e5e5e5', + color: 'var(--color-text-base)', fontSize: 12, - boxShadow: '0 4px 12px rgba(0,0,0,0.4)', + boxShadow: 'var(--shadow-popup)', }} > - 🔗 + 🔗 Dependencies of{' '} - {focusObject.name} + {focusObject.name} ))}
)} {filteredObjects.length > 0 && ( -
-
+
+
Objects
{filteredObjects.slice(0, 10).map((o) => ( -
{ onClose() }} - style={{ - padding: '8px 16px', cursor: 'pointer', fontSize: 13, - display: 'flex', alignItems: 'center', gap: 8, - }} - onMouseEnter={(e) => (e.currentTarget.style.background = '#262626')} - onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} + className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-left text-[13px] text-text-base transition-colors hover:bg-surface-hi focus-visible:bg-surface-hi focus-visible:outline-none" > - {TYPE_ICONS[o.type as ObjectType]} - {o.name} + {TYPE_ICONS[o.type as ObjectType]} + {o.name} {(() => { const techs = (o.technology_ids ?? []) .map((id) => catalogMap.get(id)) .filter((t): t is NonNullable => Boolean(t)) if (techs.length === 0) return null return ( - + {techs.slice(0, 3).map((t) => ( ))} ) })()} -
+ ))}
)} {query && filteredObjects.length === 0 && filteredDiagrams.length === 0 && ( -
- No results for "{query}" +
+ No results for "{query}"
)}
diff --git a/frontend/src/components/nav/WorkspaceSwitcher.tsx b/frontend/src/components/nav/WorkspaceSwitcher.tsx index b721297..9b46961 100644 --- a/frontend/src/components/nav/WorkspaceSwitcher.tsx +++ b/frontend/src/components/nav/WorkspaceSwitcher.tsx @@ -128,7 +128,7 @@ export function WorkspaceSwitcher() { className="w-full flex items-center justify-between px-3 py-2 rounded-md border border-border-base bg-surface hover:bg-surface-hi transition-all duration-[120ms]" >
-
+
{wsInitials.slice(0, 1)}
diff --git a/frontend/src/components/sidebar/EdgeSidebar.tsx b/frontend/src/components/sidebar/EdgeSidebar.tsx index f3e2449..1f5ac32 100644 --- a/frontend/src/components/sidebar/EdgeSidebar.tsx +++ b/frontend/src/components/sidebar/EdgeSidebar.tsx @@ -140,7 +140,7 @@ export function EdgeSidebar({ diagramId }: EdgeSidebarProps) { className={cn( 'flex-1 flex flex-col items-center px-2 py-1 rounded text-[11px] transition-colors', conn.direction === d.value - ? 'bg-coral text-bg font-medium' + ? 'bg-coral text-on-accent font-medium' : 'text-text-3 hover:text-text-2', )} > @@ -173,7 +173,7 @@ export function EdgeSidebar({ diagramId }: EdgeSidebarProps) { className={cn( 'flex-1 flex flex-col items-center px-2 py-1 rounded text-[11px] transition-colors', conn.shape === s.value - ? 'bg-coral text-bg font-medium' + ? 'bg-coral text-on-accent font-medium' : 'text-text-3 hover:text-text-2', )} > diff --git a/frontend/src/components/theme/ThemeProvider.tsx b/frontend/src/components/theme/ThemeProvider.tsx new file mode 100644 index 0000000..4795b11 --- /dev/null +++ b/frontend/src/components/theme/ThemeProvider.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react' +import { ThemeContext, type ThemeMode } from './theme-context' + +const STORAGE_KEY = 'archflow.theme' +const DEFAULT_THEME: ThemeMode = 'dark' + +function isThemeMode(value: string | null): value is ThemeMode { + return value === 'dark' || value === 'light' +} + +function readStoredTheme(): ThemeMode { + if (typeof window === 'undefined') return DEFAULT_THEME + + try { + const stored = window.localStorage.getItem(STORAGE_KEY) + return isThemeMode(stored) ? stored : DEFAULT_THEME + } catch { + return DEFAULT_THEME + } +} + +function applyTheme(theme: ThemeMode) { + if (typeof document === 'undefined') return + document.documentElement.dataset.theme = theme + document.documentElement.classList.toggle('light', theme === 'light') + document.documentElement.classList.toggle('dark', theme === 'dark') +} + +applyTheme(readStoredTheme()) + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setThemeState] = useState(readStoredTheme) + + const setTheme = (nextTheme: ThemeMode) => { + setThemeState(nextTheme) + try { + window.localStorage.setItem(STORAGE_KEY, nextTheme) + } catch { + // Keep the in-session theme even if storage is unavailable. + } + applyTheme(nextTheme) + } + + const toggleTheme = () => { + setTheme(theme === 'dark' ? 'light' : 'dark') + } + + useEffect(() => { + applyTheme(theme) + }, [theme]) + + return ( + + {children} + + ) +} diff --git a/frontend/src/components/theme/ThemeToggle.tsx b/frontend/src/components/theme/ThemeToggle.tsx new file mode 100644 index 0000000..2f7c176 --- /dev/null +++ b/frontend/src/components/theme/ThemeToggle.tsx @@ -0,0 +1,43 @@ +import { Button } from '../ui/Button' +import { useOptionalTheme } from './theme-context' + +function SunIcon() { + return ( + + ) +} + +function MoonIcon() { + return ( + + ) +} + +export function ThemeToggle() { + const themeContext = useOptionalTheme() + if (themeContext == null) return null + + const { theme, toggleTheme } = themeContext + const isDark = theme === 'dark' + const nextThemeLabel = isDark ? 'light' : 'dark' + + return ( + + ) +} diff --git a/frontend/src/components/theme/theme-context.ts b/frontend/src/components/theme/theme-context.ts new file mode 100644 index 0000000..4959ec8 --- /dev/null +++ b/frontend/src/components/theme/theme-context.ts @@ -0,0 +1,23 @@ +import { createContext, useContext } from 'react' + +export type ThemeMode = 'dark' | 'light' + +export interface ThemeContextValue { + theme: ThemeMode + setTheme: (theme: ThemeMode) => void + toggleTheme: () => void +} + +export const ThemeContext = createContext(null) + +export function useOptionalTheme() { + return useContext(ThemeContext) +} + +export function useTheme() { + const context = useOptionalTheme() + if (context == null) { + throw new Error('useTheme must be used within ThemeProvider') + } + return context +} diff --git a/frontend/src/components/toolbar/ExportToolbar.tsx b/frontend/src/components/toolbar/ExportToolbar.tsx index ee78dd7..426018c 100644 --- a/frontend/src/components/toolbar/ExportToolbar.tsx +++ b/frontend/src/components/toolbar/ExportToolbar.tsx @@ -119,9 +119,9 @@ export function ExportToolbar({ diagramId }: ExportToolbarProps) { height: 32, padding: '0 12px', borderRadius: 8, - background: open ? '#262626' : '#171717', - border: `1px solid ${open ? '#525252' : '#333'}`, - color: '#d4d4d4', + background: open ? 'var(--control-button-hover)' : 'var(--control-button-bg)', + border: `1px solid ${open ? 'var(--color-border-hi)' : 'var(--control-border)'}`, + color: 'var(--color-text-base)', cursor: 'pointer', fontSize: 12, display: 'flex', @@ -145,8 +145,8 @@ export function ExportToolbar({ diagramId }: ExportToolbarProps) { right: 0, top: 38, width: 280, - background: '#171717', - border: '1px solid #333', + background: 'var(--color-panel)', + border: '1px solid var(--color-border-base)', borderRadius: 8, zIndex: 10, overflow: 'hidden', @@ -155,9 +155,9 @@ export function ExportToolbar({ diagramId }: ExportToolbarProps) {
-
+
{f.label}
-
+
{flashed ? flashed.text : `${f.hint} · .${f.ext}`} @@ -227,9 +227,9 @@ function FormatActionButton({ active?: boolean error?: boolean }) { - let bg = '#262626' - let border = '#333' - let color = '#d4d4d4' + let bg = 'var(--control-button-hover)' + let border = 'var(--control-border)' + let color = 'var(--color-text-base)' if (active) { bg = '#1f3a23' border = '#2f6b3a' diff --git a/frontend/src/components/toolbar/FilterToolbar.tsx b/frontend/src/components/toolbar/FilterToolbar.tsx index b4a8704..9c2fb7c 100644 --- a/frontend/src/components/toolbar/FilterToolbar.tsx +++ b/frontend/src/components/toolbar/FilterToolbar.tsx @@ -32,11 +32,11 @@ export function FilterToolbar() { {/* Legend — values with their color swatches for the active dimension */} {activeFilter !== 'none' && (
{legend.length === 0 ? ( - + No data for this dimension ) : ( @@ -59,7 +59,7 @@ export function FilterToolbar() { {/* Tab bar */}
{TABS.map((tab) => { @@ -69,9 +69,9 @@ export function FilterToolbar() { key={tab.id} onClick={() => setActiveFilter(active ? 'none' : tab.id)} style={{ - background: active ? '#262626' : 'transparent', + background: active ? 'var(--color-surface-hi)' : 'transparent', border: 'none', borderRadius: 6, padding: '4px 10px', - color: active ? '#f5f5f5' : '#737373', + color: active ? 'var(--color-text-base)' : 'var(--color-text-3)', cursor: 'pointer', fontSize: 12, display: 'flex', alignItems: 'center', gap: 4, }} > diff --git a/frontend/src/components/toolbar/FlowsPanel.tsx b/frontend/src/components/toolbar/FlowsPanel.tsx index 811294a..0a8c34e 100644 --- a/frontend/src/components/toolbar/FlowsPanel.tsx +++ b/frontend/src/components/toolbar/FlowsPanel.tsx @@ -40,9 +40,9 @@ export function FlowsPanel({ diagramId }: FlowsPanelProps) { onClick={() => setOpen((v) => !v)} style={{ width: 40, height: 40, borderRadius: 8, - background: open ? '#333' : '#262626', - border: '1px solid #404040', - color: '#d4d4d4', cursor: 'pointer', fontSize: 16, + background: open ? 'var(--control-button-hover)' : 'var(--control-button-bg)', + border: '1px solid var(--control-border)', + color: 'var(--color-text-base)', cursor: 'pointer', fontSize: 16, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 8px rgba(0,0,0,0.3)', }} @@ -64,8 +64,8 @@ export function FlowsPanel({ diagramId }: FlowsPanelProps) { onClick={(e) => e.stopPropagation()} style={{ position: 'absolute', right: 52, top: 0, width: 320, - background: '#171717', border: '1px solid #333', borderRadius: 8, - boxShadow: '0 8px 24px rgba(0,0,0,0.5)', zIndex: 10, + background: 'var(--color-panel)', border: '1px solid var(--color-border-base)', borderRadius: 8, + boxShadow: 'var(--shadow-popup)', zIndex: 10, display: 'flex', flexDirection: 'column', overflow: 'hidden', maxHeight: '70vh', }} @@ -78,15 +78,15 @@ export function FlowsPanel({ diagramId }: FlowsPanelProps) { /> ) : ( <> -
-
+
+
Flows
{flows.length === 0 ? ( -
+
No flows yet. Create one to document user journeys.
) : ( @@ -146,14 +146,14 @@ function FlowRow({ }, [flow.steps]) return ( -
+
-
+
{isPlaying && } {flow.name}
-
+
{flow.steps.length} step{flow.steps.length === 1 ? '' : 's'} {branches.length > 0 && ` · ${branches.length + 1} branches`}
@@ -176,10 +176,10 @@ function IconButton({ children, title, onClick, danger }: { children: React.Reac style={{ width: 24, height: 24, padding: 0, fontSize: 11, background: 'transparent', border: 'none', borderRadius: 4, - color: danger ? '#f87171' : '#a3a3a3', + color: danger ? 'var(--context-menu-danger)' : 'var(--color-text-2)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', }} - onMouseEnter={(e) => (e.currentTarget.style.background = '#262626')} + onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--color-surface-hi)')} onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} > {children} @@ -256,29 +256,29 @@ function FlowEditor({ return ( <> -
+
setName(e.target.value)} style={{ flex: 1, background: 'transparent', border: 'none', - color: '#f5f5f5', fontSize: 13, fontWeight: 600, outline: 'none', + color: 'var(--color-text-base)', fontSize: 13, fontWeight: 600, outline: 'none', }} />
-
+
Steps ({steps.length})
{steps.length === 0 ? ( -
+
No steps yet. Pick connections below to add.
) : ( @@ -292,13 +292,13 @@ function FlowEditor({ key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 6, - padding: '4px 6px', fontSize: 11, color: '#d4d4d4', - borderLeft: '2px solid #3b82f6', marginBottom: 4, + padding: '4px 6px', fontSize: 11, color: 'var(--color-text-base)', + borderLeft: '2px solid var(--color-accent-blue)', marginBottom: 4, }} > {idx + 1} @@ -311,8 +311,8 @@ function FlowEditor({ onChange={(e) => setStepBranch(s.id, e.target.value || null)} placeholder="branch" style={{ - width: 60, background: '#262626', border: '1px solid #333', - borderRadius: 3, padding: '1px 4px', color: '#a3a3a3', fontSize: 10, + width: 60, background: 'var(--color-surface)', border: '1px solid var(--color-border-base)', + borderRadius: 3, padding: '1px 4px', color: 'var(--color-text-2)', fontSize: 10, }} /> moveStep(s.id, -1)}>▲ @@ -324,11 +324,11 @@ function FlowEditor({
)} -
+
Connections in diagram
{connections.length === 0 ? ( -
No connections yet.
+
No connections yet.
) : ( connections.map((c) => { const source = objectMap.get(c.source_id) @@ -341,30 +341,30 @@ function FlowEditor({ display: 'flex', alignItems: 'center', gap: 6, width: '100%', padding: '4px 6px', marginBottom: 2, background: 'transparent', border: 'none', borderRadius: 4, - color: stepConnectionIds.has(c.id) ? '#525252' : '#d4d4d4', + color: stepConnectionIds.has(c.id) ? 'var(--color-text-4)' : 'var(--color-text-base)', fontSize: 11, cursor: 'pointer', textAlign: 'left', }} - onMouseEnter={(e) => (e.currentTarget.style.background = '#262626')} + onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--color-surface-hi)')} onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} > - + + + {source?.name || '?'} → {target?.name || '?'} - {c.label && {c.label}} + {c.label && {c.label}} ) }) )}
-
+
@@ -659,7 +659,7 @@ export function AgentsSettingsPage() { onClick={onSave} disabled={!dirty || update.isPending} data-testid="save-btn" - className="bg-blue-600 hover:bg-blue-500 text-white text-xs font-medium rounded px-4 py-1.5 disabled:opacity-40" + className="bg-coral hover:bg-coral-2 text-on-accent text-xs font-medium rounded px-4 py-1.5 disabled:bg-surface-hi disabled:text-text-3 disabled:border disabled:border-border-base disabled:opacity-100 disabled:cursor-not-allowed" > {update.isPending ? 'Saving…' : 'Save'} @@ -713,7 +713,7 @@ const EDITS_POLICY_OPTIONS: { // ─── Layout primitives ────────────────────────────────────────────────────── const inputCls = - 'w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1.5 text-sm outline-none focus:border-neutral-500' + 'w-full bg-surface border border-border-base rounded px-2 py-1.5 text-sm text-text-base placeholder:text-text-4 outline-none focus:border-border-hi' function Section({ title, @@ -727,7 +727,7 @@ function Section({ return (

{title}

- {hint &&

{hint}

} + {hint &&

{hint}

}
{children}
) @@ -742,7 +742,7 @@ function Field({ }) { return (
- + {children}
) @@ -769,8 +769,8 @@ function CardRadio({ ) diff --git a/frontend/src/pages/ConnectionsPage.tsx b/frontend/src/pages/ConnectionsPage.tsx index 4e2c96a..f80a1f2 100644 --- a/frontend/src/pages/ConnectionsPage.tsx +++ b/frontend/src/pages/ConnectionsPage.tsx @@ -56,7 +56,7 @@ export function ConnectionsPage() { value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search connections…" - className="w-72 bg-neutral-900 border border-neutral-800 rounded px-3 py-1.5 text-sm outline-none focus:border-neutral-600" + className="w-72 bg-surface border border-border-base rounded px-3 py-1.5 text-sm text-text-base placeholder:text-text-4 outline-none focus:border-border-hi" />
@@ -67,10 +67,10 @@ export function ConnectionsPage() {
)} -
+
- + @@ -80,23 +80,23 @@ export function ConnectionsPage() { {filtered.map((r) => ( - - - - + + + - +
Source Target Direction
{r.source}{r.target} +
{r.source}{r.target} {r.direction === 'bidirectional' ? '⇄ bidirectional' : '→ outgoing'} {r.label || '—'}{r.label || '—'} {r.protocols.length === 0 ? ( - + ) : (
{r.protocols.slice(0, 4).map((p) => ( ))} {r.protocols.length > 4 && ( - + +{r.protocols.length - 4} )} diff --git a/frontend/src/pages/ObjectsPage.tsx b/frontend/src/pages/ObjectsPage.tsx index ea49f26..cf4e1b6 100644 --- a/frontend/src/pages/ObjectsPage.tsx +++ b/frontend/src/pages/ObjectsPage.tsx @@ -60,7 +60,7 @@ export function ObjectsPage() { value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search objects…" - className="w-72 bg-neutral-900 border border-neutral-800 rounded px-3 py-1.5 text-sm outline-none focus:border-neutral-600" + className="w-72 bg-surface border border-border-base rounded px-3 py-1.5 text-sm text-text-base placeholder:text-text-4 outline-none focus:border-border-hi" />
@@ -71,10 +71,10 @@ export function ObjectsPage() { )} -
+
- + @@ -124,12 +124,12 @@ function ObjectRow({ .filter((t): t is Technology => Boolean(t)) return ( - + - + - +
Name Type Status
{TYPE_ICONS[obj.type]} - {obj.name} + {obj.name} {TYPE_LABELS[obj.type]}{TYPE_LABELS[obj.type]} {technologies.length === 0 ? ( - + ) : (
{technologies.slice(0, 4).map((t) => ( ))} {technologies.length > 4 && ( - +{technologies.length - 4} + +{technologies.length - 4} )}
)}
{obj.owner_team || '—'}{obj.owner_team || '—'} {diagrams.length === 0 ? ( - + ) : (
{diagrams.slice(0, 2).map((d) => ( @@ -172,7 +172,7 @@ function ObjectRow({ ))} {diagrams.length > 2 && ( - +{diagrams.length - 2} + +{diagrams.length - 2} )}
)} @@ -183,7 +183,7 @@ function ObjectRow({ e.stopPropagation() onEdit(obj.id) }} - className="px-2 py-1 text-neutral-500 hover:text-neutral-200 hover:bg-neutral-800 rounded text-base leading-none" + className="px-2 py-1 text-text-3 hover:text-text-base hover:bg-surface-hi rounded text-base leading-none" title="Edit object" > ⋯ diff --git a/frontend/src/pages/OverviewPage.tsx b/frontend/src/pages/OverviewPage.tsx index 60a9b5e..faa6ee1 100644 --- a/frontend/src/pages/OverviewPage.tsx +++ b/frontend/src/pages/OverviewPage.tsx @@ -330,7 +330,7 @@ export function OverviewPage() {