From b5e980d467cb8dc8bc895627871d5d94031cad70 Mon Sep 17 00:00:00 2001 From: Sergio Marcelino Date: Wed, 19 Aug 2026 10:21:26 -0300 Subject: [PATCH 1/5] (MOT-4472) feat(console): unify shared UI patterns across workers --- browser/ui/src/configuration/index.tsx | 108 +- .../function-trigger-message/BrowserViews.tsx | 15 +- .../ui/src/function-trigger-message/index.tsx | 6 +- browser/ui/src/lib/errors.tsx | 6 +- browser/ui/src/lib/icons.tsx | 2 +- browser/ui/src/page/SessionRail.tsx | 28 +- browser/ui/src/page/SessionView.tsx | 174 +- browser/ui/src/page/index.tsx | 87 +- browser/ui/styles.css | 95 +- canvas/ui/src/page/ExportMenu.tsx | 4 +- canvas/ui/src/page/MermaidPane.tsx | 19 +- canvas/ui/src/page/icons.tsx | 2 +- canvas/ui/src/page/index.tsx | 8 +- computer/ui/src/page/index.tsx | 16 +- computer/ui/styles.css | 4 +- console/DESIGN.md | 389 ++-- console/PRODUCT.md | 2 +- console/SKILL.md | 102 +- console/skills/design-console-ui/SKILL.md | 71 +- console/ui/src/catalog/ActivityFeed.tsx | 4 +- console/ui/src/catalog/FunctionsPage.tsx | 26 +- console/ui/src/catalog/HttpTester.tsx | 6 +- console/ui/src/catalog/InvokePanel.tsx | 6 +- console/ui/src/catalog/SchemaTable.tsx | 110 +- console/ui/src/catalog/TriggersPage.tsx | 24 +- console/ui/src/catalog/live.tsx | 6 +- console/ui/src/catalog/widgets.tsx | 25 +- console/ui/src/injectable-ui-form/index.tsx | 15 +- console/ui/styles.css | 120 +- console/web/DESIGN.md | 394 ++-- console/web/README.md | 26 +- console/web/package.json | 1 + console/web/public/vendor/console-ui.js | 25 + console/web/scripts/generate-vendor-shims.mjs | 9 +- console/web/src/App.tsx | 16 +- console/web/src/components/CommandPalette.tsx | 32 +- .../src/components/chat/AttachmentChip.tsx | 4 +- .../src/components/chat/ChatSettingsSheet.tsx | 2 +- console/web/src/components/chat/ChatView.tsx | 16 +- .../web/src/components/chat/ContextUsage.tsx | 6 +- .../src/components/chat/DirectoryPicker.tsx | 31 +- .../web/src/components/chat/EmptyState.tsx | 22 +- .../web/src/components/chat/MemoryChip.tsx | 2 +- console/web/src/components/chat/Message.tsx | 34 +- .../web/src/components/chat/ModePicker.tsx | 18 +- .../web/src/components/chat/ModelPicker.tsx | 24 +- .../components/chat/ProviderSettingsForm.tsx | 2 +- .../components/chat/SessionAddonsPicker.tsx | 18 +- .../src/components/chat/SessionTriggers.tsx | 30 +- .../chat/SystemPromptPicker.test.tsx | 10 +- .../components/chat/SystemPromptPicker.tsx | 36 +- .../components/chat/ThoughtMessage.test.tsx | 2 +- .../src/components/chat/ThoughtMessage.tsx | 8 +- .../web/src/components/chat/WorktreeBadge.tsx | 2 +- .../components/chat/coder/CreateFileView.tsx | 4 +- .../components/chat/coder/DeleteFileView.tsx | 120 +- .../src/components/chat/coder/InfoView.tsx | 59 +- .../components/chat/coder/ListFolderView.tsx | 72 +- .../src/components/chat/coder/MoveView.tsx | 168 +- .../chat/coder/OpenInEditorButton.tsx | 2 +- .../components/chat/coder/ReadFileView.tsx | 4 +- .../src/components/chat/coder/SearchView.tsx | 16 +- .../src/components/chat/coder/TreeView.tsx | 2 +- .../components/chat/coder/UpdateFileView.tsx | 83 +- .../src/components/chat/coder/entryShared.tsx | 2 +- .../chat/engine/RegisterTriggerView.tsx | 10 +- .../chat/engine/__tests__/parsers.test.ts | 2 +- .../web/src/components/chat/engine/shared.tsx | 2 +- .../src/components/chat/harness/SpawnView.tsx | 10 +- .../chat/lexical/FileMentionNode.tsx | 4 +- .../sandbox/terminal/CopyCommandButton.tsx | 2 +- .../components/chat/scrapling/CrawlView.tsx | 2 +- .../components/chat/scrapling/FetchView.tsx | 16 +- .../chat/scrapling/MarkdownView.tsx | 4 +- .../components/chat/scrapling/ParseViews.tsx | 6 +- .../chat/scrapling/ScreenshotView.tsx | 6 +- .../components/chat/scrapling/SearchViews.tsx | 44 +- .../chat/scrapling/SessionViews.tsx | 59 +- .../web/src/components/chat/web/FetchView.tsx | 50 +- .../components/chat/worker/WorkerOpView.tsx | 6 +- .../chat/worker/WorkerStatusView.tsx | 6 +- .../chat/workflow/NodeResultView.tsx | 2 +- .../components/chat/workflow/StatusView.tsx | 4 +- .../src/components/chat/workflow/shared.tsx | 8 +- .../function-trigger/FunctionTriggerCard.tsx | 20 +- .../function-trigger/redact-raw.test.tsx | 2 +- .../permissions/FilesystemAccessDialog.tsx | 10 +- .../permissions/FunctionAllowlistTree.tsx | 2 +- .../components/sidebar/ConversationRow.tsx | 4 +- console/web/src/components/ui/Badge.tsx | 2 +- console/web/src/components/ui/BottomSheet.tsx | 47 +- .../web/src/components/ui/Button.stories.tsx | 4 +- console/web/src/components/ui/Button.tsx | 2 +- console/web/src/components/ui/Cell.tsx | 4 +- console/web/src/components/ui/Chip.tsx | 23 + console/web/src/components/ui/Dialog.tsx | 49 +- .../web/src/components/ui/DropdownMenu.tsx | 30 +- console/web/src/components/ui/EmptyState.tsx | 4 +- console/web/src/components/ui/IconButton.tsx | 46 + console/web/src/components/ui/Input.tsx | 13 +- console/web/src/components/ui/List.tsx | 90 + .../src/components/ui/ModeToggle.stories.tsx | 30 +- .../web/src/components/ui/ModeToggle.test.tsx | 44 + console/web/src/components/ui/ModeToggle.tsx | 66 +- console/web/src/components/ui/PageChrome.tsx | 6 +- console/web/src/components/ui/Pagination.tsx | 12 +- console/web/src/components/ui/Select.tsx | 155 +- .../src/components/ui/Selector.stories.tsx | 124 ++ .../web/src/components/ui/Selector.test.ts | 52 + console/web/src/components/ui/Selector.tsx | 551 ++++++ .../ui/SharedPrimitives.stories.tsx | 56 + .../web/src/components/ui/SheetNavigation.tsx | 2 +- console/web/src/components/ui/StatusPanel.tsx | 9 +- console/web/src/components/ui/Surface.tsx | 61 + console/web/src/components/ui/TabIcon.tsx | 121 ++ .../web/src/components/ui/Table.stories.tsx | 132 ++ console/web/src/components/ui/Table.test.tsx | 62 + console/web/src/components/ui/Table.tsx | 121 ++ .../web/src/components/ui/Tabs.stories.tsx | 33 + console/web/src/components/ui/Tabs.tsx | 38 +- .../src/components/ui/TerminalCommandLine.tsx | 2 +- .../web/src/components/ui/Tooltip.stories.tsx | 41 + console/web/src/components/ui/Tooltip.tsx | 49 +- console/web/src/components/ui/Wordmark.tsx | 2 +- .../src/components/workspace/EmptyPane.tsx | 42 +- .../workspace/MobileWorkspaceMenu.tsx | 4 +- .../web/src/components/workspace/TabStrip.tsx | 6 +- .../components/workspace/pane-controls.tsx | 2 +- console/web/src/index.css | 52 +- console/web/src/lib/console-api.ts | 75 +- .../src/lib/console-ui-conformance.test.ts | 113 ++ .../web/src/lib/icon-size-conformance.test.ts | 62 + console/web/src/lib/markdown-table.test.tsx | 23 + console/web/src/lib/markdown.tsx | 54 +- console/web/src/lib/monaco.ts | 14 +- .../web/src/lib/selection-conformance.test.ts | 122 ++ console/web/src/lib/table-conformance.test.ts | 35 + .../src/lib/typography-conformance.test.ts | 71 + console/web/src/lib/ui-loader.test.tsx | 1 + console/web/src/lib/ui-loader.tsx | 18 +- console/web/src/lib/ui-scope.test.tsx | 26 + console/web/src/lib/ui-scope.tsx | 36 + console/web/src/pages/Configuration/index.tsx | 4 +- .../tabs/WorkersTab/EmptyState.tsx | 11 +- .../Configuration/tabs/WorkersTab/SaveBar.tsx | 4 +- .../tabs/WorkersTab/WorkersList.tsx | 10 +- .../tabs/WorkersTab/schema-form/RowChrome.tsx | 4 +- .../env-lexical/EnvPlaceholderNode.tsx | 4 +- console/web/src/pages/Ext/index.test.tsx | 4 +- console/web/src/pages/Ext/index.tsx | 10 +- .../TracesV2/components/AttributesFilter.tsx | 8 +- .../TracesV2/components/GroupByPicker.tsx | 222 +-- .../TracesV2/components/GroupedTraceList.tsx | 10 +- .../TracesV2/components/SpanBaggageTab.tsx | 4 +- .../TracesV2/components/SpanErrorsTab.tsx | 6 +- .../pages/TracesV2/components/SpanInfoTab.tsx | 6 +- .../TracesV2/components/SpanLinksTab.tsx | 2 +- .../TracesV2/components/SpanOtelLogsTab.tsx | 6 +- .../pages/TracesV2/components/SpanPanel.tsx | 26 +- .../pages/TracesV2/components/SpanTagsTab.tsx | 6 +- .../components/TraceDetailSkeleton.tsx | 10 +- .../TracesV2/components/TraceFilters.tsx | 28 +- .../pages/TracesV2/components/TraceHeader.tsx | 12 +- .../TracesV2/components/TraceListRow.tsx | 8 +- .../TracesV2/components/ViewsDropdown.tsx | 12 +- .../TracesV2/components/WaterfallChart.tsx | 16 +- .../TracesV2/components/WorkerBreakdown.tsx | 2 +- .../components/timeline/SpanFilterMenu.tsx | 6 +- .../components/timeline/SpanHoverCard.tsx | 6 +- .../components/timeline/TimelineStrip.tsx | 6 +- .../components/timeline/TraceTimeline.tsx | 2 +- .../components/timeline/spanVisuals.tsx | 6 +- console/web/src/pages/TracesV2/index.tsx | 4 +- .../web/src/pages/TracesV2/lib/traceColors.ts | 2 +- .../components/WorkerConfigurationDialog.tsx | 4 +- .../components/WorkerConfigurationPanel.tsx | 4 +- .../Workers/components/WorkerSurface.tsx | 8 +- .../Workers/components/WorkersFilters.tsx | 4 +- .../pages/Workers/components/WorkersTable.tsx | 4 +- console/web/src/pages/Workers/index.tsx | 4 +- console/web/src/styles/ui-recipes.css | 586 ++++++ console/web/src/types/injectable-ui.ts | 4 + database/ui/src/configuration/index.tsx | 118 +- .../ui/src/function-trigger-message/index.tsx | 86 +- database/ui/src/lib/rpc.ts | 184 +- database/ui/src/page/CellInspector.tsx | 43 +- database/ui/src/page/ChangesPanel.tsx | 60 +- database/ui/src/page/ColumnStatsPanel.tsx | 62 +- database/ui/src/page/ErdPanel.tsx | 146 +- database/ui/src/page/FilterBar.tsx | 107 +- database/ui/src/page/HealthPanel.tsx | 180 +- database/ui/src/page/JsonTree.tsx | 75 +- database/ui/src/page/PlanTree.tsx | 87 +- database/ui/src/page/RowDetail.tsx | 43 +- database/ui/src/page/SchemaTree.tsx | 94 +- database/ui/src/page/SqlPanel.tsx | 107 +- database/ui/src/page/TableDataPanel.tsx | 127 +- database/ui/src/page/icons.tsx | 2 +- database/ui/src/page/index.tsx | 266 ++- database/ui/src/page/pagination.tsx | 16 +- database/ui/src/page/result-grid.tsx | 143 +- database/ui/styles.css | 1620 ++++++++++++----- docs/sops/console-ui-conformance.md | 115 ++ docs/sops/injectable-console-ui.md | 98 +- editor/ui/src/page/index.tsx | 447 +++-- editor/ui/styles.css | 223 +-- eval/ui/src/page/EvaluationDetail.tsx | 221 ++- eval/ui/src/page/NewEvaluationForm.tsx | 328 +++- eval/ui/styles.css | 54 +- github/ui/src/page/GitGraph.tsx | 165 +- github/ui/src/page/GithubPage.tsx | 47 +- github/ui/src/page/icons.tsx | 2 +- github/ui/src/page/index.tsx | 76 +- github/ui/src/page/result-views.tsx | 64 +- github/ui/styles.css | 88 +- harness/ui/styles.css | 8 +- iii-directory/ui/src/configuration/index.tsx | 4 +- .../ui/src/function-trigger/DownloadView.tsx | 8 +- .../ui/src/function-trigger/RegistryViews.tsx | 8 +- iii-directory/ui/src/page/browser.tsx | 228 ++- iii-directory/ui/src/page/index.tsx | 99 +- iii-directory/ui/src/session-chip/index.tsx | 4 +- iii-directory/ui/styles.css | 102 +- memory/ui/src/page/BankRail.tsx | 8 +- memory/ui/src/page/MemoriesPanel.tsx | 38 +- memory/ui/src/page/MemoryGraph.tsx | 10 +- memory/ui/src/page/ModeToggle.tsx | 50 - memory/ui/src/page/RecallPanel.tsx | 20 +- memory/ui/src/page/RulesPanel.tsx | 6 +- memory/ui/src/page/icons.tsx | 2 +- memory/ui/src/page/index.tsx | 30 +- memory/ui/styles.css | 55 +- packages/console-ui/README.md | 61 +- packages/console-ui/component-names.mjs | 24 + packages/console-ui/index.d.ts | 241 ++- packages/console-ui/package.json | 19 +- packages/console-ui/token-names.d.mts | 2 + packages/console-ui/token-names.mjs | 67 + packages/console-ui/ui-classes.d.mts | 45 + packages/console-ui/ui-classes.mjs | 52 + pdf/ui/src/page/index.tsx | 32 +- pdf/ui/styles.css | 78 +- pnpm-lock.yaml | 3 + .../ui/src/page/SandboxPage.tsx | 35 +- sandbox-code-runner/ui/src/page/icons.tsx | 16 +- sandbox-code-runner/ui/src/styles/family.css | 59 +- sandbox-code-runner/ui/src/styles/page.css | 27 +- shell/ui/page.tsx | 18 +- .../ui/src/function-trigger/AgentRunView.tsx | 4 +- shell/ui/src/page/HoverTip.tsx | 68 +- shell/ui/src/page/TerminalPane.tsx | 12 +- shell/ui/src/page/WorkspaceBrowser.tsx | 2 +- .../terminal-router-lifecycle.test.ts | 52 + .../__tests__/terminal-workspace.test.tsx | 8 +- shell/ui/src/page/index.tsx | 11 +- shell/ui/styles.css | 550 ++++-- state/ui/src/page/index.tsx | 11 +- state/ui/styles.css | 126 +- storage/ui/src/page.tsx | 429 ++++- storage/ui/styles.css | 17 +- worktree/ui/src/page/WorktreeDetailPanel.tsx | 8 +- worktree/ui/src/page/WorktreeGraph.tsx | 25 +- worktree/ui/src/page/icons.tsx | 2 +- worktree/ui/src/page/index.tsx | 25 +- worktree/ui/styles.css | 12 +- 265 files changed, 11135 insertions(+), 4167 deletions(-) create mode 100644 console/web/src/components/ui/Chip.tsx create mode 100644 console/web/src/components/ui/IconButton.tsx create mode 100644 console/web/src/components/ui/List.tsx create mode 100644 console/web/src/components/ui/ModeToggle.test.tsx create mode 100644 console/web/src/components/ui/Selector.stories.tsx create mode 100644 console/web/src/components/ui/Selector.test.ts create mode 100644 console/web/src/components/ui/Selector.tsx create mode 100644 console/web/src/components/ui/SharedPrimitives.stories.tsx create mode 100644 console/web/src/components/ui/Surface.tsx create mode 100644 console/web/src/components/ui/TabIcon.tsx create mode 100644 console/web/src/components/ui/Table.stories.tsx create mode 100644 console/web/src/components/ui/Table.test.tsx create mode 100644 console/web/src/components/ui/Table.tsx create mode 100644 console/web/src/components/ui/Tabs.stories.tsx create mode 100644 console/web/src/components/ui/Tooltip.stories.tsx create mode 100644 console/web/src/lib/icon-size-conformance.test.ts create mode 100644 console/web/src/lib/markdown-table.test.tsx create mode 100644 console/web/src/lib/selection-conformance.test.ts create mode 100644 console/web/src/lib/table-conformance.test.ts create mode 100644 console/web/src/lib/typography-conformance.test.ts create mode 100644 console/web/src/lib/ui-scope.test.tsx create mode 100644 console/web/src/lib/ui-scope.tsx create mode 100644 console/web/src/styles/ui-recipes.css create mode 100644 docs/sops/console-ui-conformance.md delete mode 100644 memory/ui/src/page/ModeToggle.tsx create mode 100644 packages/console-ui/token-names.d.mts create mode 100644 packages/console-ui/token-names.mjs create mode 100644 packages/console-ui/ui-classes.d.mts create mode 100644 packages/console-ui/ui-classes.mjs create mode 100644 shell/ui/src/page/__tests__/terminal-router-lifecycle.test.ts diff --git a/browser/ui/src/configuration/index.tsx b/browser/ui/src/configuration/index.tsx index ea3684d16..8dd7d9f95 100644 --- a/browser/ui/src/configuration/index.tsx +++ b/browser/ui/src/configuration/index.tsx @@ -4,7 +4,12 @@ * unsaved-change guard; this component only edits the JSON draft. */ -import { type ConfigFormProps, Input, type JsonValue, StatusPanel } from '@iii-dev/console-ui' +import { + type ConfigFormProps, + Input, + type JsonValue, + StatusPanel, +} from '@iii-dev/console-ui' import { type ReactNode, useEffect, useRef, useState } from 'react' import { ChevronLeftIcon, GlobeIcon, useContainerNarrow } from '../lib/widgets' @@ -49,7 +54,9 @@ const FIELD_SECTION: Record = { } function asObject(value: JsonValue | undefined): JsonObject { - return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {} + return value && typeof value === 'object' && !Array.isArray(value) + ? { ...value } + : {} } function stringValue(value: JsonValue | undefined, fallback = ''): string { @@ -60,7 +67,10 @@ function numberValue(value: JsonValue | undefined, fallback: number): number { return typeof value === 'number' ? value : fallback } -function booleanValue(value: JsonValue | undefined, fallback: boolean): boolean { +function booleanValue( + value: JsonValue | undefined, + fallback: boolean, +): boolean { return typeof value === 'boolean' ? value : fallback } @@ -70,7 +80,12 @@ function pointer(field: string) { function fieldError(errors: ConfigFormProps['errors'], field: string) { const base = pointer(field) - return errors?.get(base) ?? [...(errors?.entries() ?? [])].find(([path]) => path.startsWith(`${base}/`))?.[1] + return ( + errors?.get(base) ?? + [...(errors?.entries() ?? [])].find(([path]) => + path.startsWith(`${base}/`), + )?.[1] + ) } function formatCount(value: number) { @@ -131,7 +146,11 @@ function TextField({ }) { const id = `br-cfg-${field}` return ( - {label}} hint={hint} error={error}> + {label}} + hint={hint} + error={error} + > {label}} hint={hint} error={error}> + {label}} + hint={hint} + error={error} + > Allowed URL schemes} + label={ + + } hint="Enter a comma-separated list without ://. Keep this list as narrow as your workflows allow." error={error} > @@ -274,7 +299,13 @@ function SchemesField({ ) } -function SectionHeader({ title, description }: { title: string; description: string }) { +function SectionHeader({ + title, + description, +}: { + title: string + description: string +}) { return (
@@ -298,9 +329,18 @@ function ConfigNav({ const height = numberValue(value.viewport_height, DEFAULTS.viewport_height) const maxSessions = numberValue(value.max_sessions, DEFAULTS.max_sessions) const headless = booleanValue(value.headless, DEFAULTS.headless) - const consoleBuffer = numberValue(value.console_buffer, DEFAULTS.console_buffer) - const networkBuffer = numberValue(value.network_buffer, DEFAULTS.network_buffer) - const timeout = numberValue(value.default_timeout_ms, DEFAULTS.default_timeout_ms) + const consoleBuffer = numberValue( + value.console_buffer, + DEFAULTS.console_buffer, + ) + const networkBuffer = numberValue( + value.network_buffer, + DEFAULTS.network_buffer, + ) + const timeout = numberValue( + value.default_timeout_ms, + DEFAULTS.default_timeout_ms, + ) const idle = numberValue(value.idle_stop_ms, DEFAULTS.idle_stop_ms) const sections: Array<{ @@ -338,7 +378,7 @@ function ConfigNav({ return (
{booleanValue(value.allow_attach, DEFAULTS.allow_attach) ? (
- Attach mode is enabled. Only connect to browser instances you trust. + Attach mode is enabled. Only connect to browser instances you + trust.
) : null} @@ -558,8 +609,12 @@ function ConfigEditor({ }} > - {numberValue(value.viewport_width, DEFAULTS.viewport_width)} ×{' '} - {numberValue(value.viewport_height, DEFAULTS.viewport_height)} + {numberValue(value.viewport_width, DEFAULTS.viewport_width)}{' '} + ×{' '} + {numberValue( + value.viewport_height, + DEFAULTS.viewport_height, + )}

Aspect-ratio preview for newly launched sessions.

@@ -676,11 +731,16 @@ function ConfigEditor({ typeof scheme === 'string') + ? value.allowed_schemes.filter( + (scheme): scheme is string => + typeof scheme === 'string', + ) : [...DEFAULTS.allowed_schemes] } error={fieldError(errors, 'allowed_schemes')} - onChange={(schemes) => onChange({ ...value, allowed_schemes: schemes })} + onChange={(schemes) => + onChange({ ...value, allowed_schemes: schemes }) + } /> @@ -718,7 +778,9 @@ export function BrowserConfigForm(props: ConfigFormProps) { useEffect(() => { if (!focusKey || !domRef.current) return const field = props.focusField?.[0] ?? focusKey - const target = domRef.current.querySelector(`[data-field="${CSS.escape(field)}"]`) + const target = domRef.current.querySelector( + `[data-field="${CSS.escape(field)}"]`, + ) target?.focus() target?.scrollIntoView({ block: 'center' }) }, [focusKey, selection]) @@ -729,7 +791,9 @@ export function BrowserConfigForm(props: ConfigFormProps) { return (
- {showNav ? : null} + {showNav ? ( + + ) : null} {showEditor ? ( {formatTime(entry.timestamp)} - + {entry.level} @@ -304,9 +307,7 @@ export function ConsoleReadView({ export function NetworkEntryRow({ entry }: { entry: BrowserNetworkEntry }) { return (
  • - + {entry.status ?? (entry.failed ? 'err' : '...')} {entry.method} @@ -338,7 +339,7 @@ export function NetworkReadView({ variant={res.entries.length > 0 ? 'accent' : 'default'} /> {req?.failed_only ? ( - failed only + Failed only ) : null} {req?.pattern ? /{req.pattern}/ : null} {res.dropped > 0 ? ( @@ -513,7 +514,9 @@ export function EvaluateView({
    · undefined
    ) : (
    - +
    ) ) : ( diff --git a/browser/ui/src/function-trigger-message/index.tsx b/browser/ui/src/function-trigger-message/index.tsx index a458c528e..9ffbe9896 100644 --- a/browser/ui/src/function-trigger-message/index.tsx +++ b/browser/ui/src/function-trigger-message/index.tsx @@ -165,13 +165,13 @@ function BrowserCallView({ message }: { message: FunctionTriggerMessage }) { )} {sessionId ? ( - + open in browser tab ) : null}
  • {running && message.output == null ? ( -

    running...

    +

    Running...

    ) : body ? ( body ) : fallback != null ? ( @@ -179,7 +179,7 @@ function BrowserCallView({ message }: { message: FunctionTriggerMessage }) {
    ) : ( -

    no result

    +

    No result

    )} ) diff --git a/browser/ui/src/lib/errors.tsx b/browser/ui/src/lib/errors.tsx index e211838fe..71b9c51b4 100644 --- a/browser/ui/src/lib/errors.tsx +++ b/browser/ui/src/lib/errors.tsx @@ -147,7 +147,9 @@ function denialToInvocation( ? 'Denied' : 'Trigger failed' const message = - denial.reason ?? fallbackMessage ?? 'The browser trigger could not complete.' + denial.reason ?? + fallbackMessage ?? + 'The browser trigger could not complete.' return { title, message, @@ -344,7 +346,7 @@ function DispatchDeniedView({ denial }: { denial: InfraDispatchDenial }) { denied - dispatch policy + Dispatch policy {fn ? (
    diff --git a/browser/ui/src/lib/icons.tsx b/browser/ui/src/lib/icons.tsx index b3a3eff66..345191d0e 100644 --- a/browser/ui/src/lib/icons.tsx +++ b/browser/ui/src/lib/icons.tsx @@ -18,7 +18,7 @@ export interface IconProps { } function Svg({ - size = 14, + size = 16, children, className, style, diff --git a/browser/ui/src/page/SessionRail.tsx b/browser/ui/src/page/SessionRail.tsx index 20e399ed9..13cd4bd24 100644 --- a/browser/ui/src/page/SessionRail.tsx +++ b/browser/ui/src/page/SessionRail.tsx @@ -29,7 +29,12 @@ function hostOf(url: string): string { } } -export function SessionRail({ sessions, selectedId, loading, onSelect }: SessionRailProps) { +export function SessionRail({ + sessions, + selectedId, + loading, + onSelect, +}: SessionRailProps) { if (sessions.length === 0) { if (loading) { return ( @@ -46,7 +51,10 @@ export function SessionRail({ sessions, selectedId, loading, onSelect }: Session return (

    No sessions yet.

    -

    Sessions started by agents appear in this list live; new session starts one now.

    +

    + Sessions started by agents appear in this list live; new session + starts one now. +

    ) } @@ -65,18 +73,24 @@ export function SessionRail({ sessions, selectedId, loading, onSelect }: Session className={cn('br-ui-rail-row', selected && 'active')} > - + - {session.title?.trim() || hostOf(session.url) || 'about:blank'} + {session.title?.trim() || + hostOf(session.url) || + 'about:blank'} + + + {session.headless ? 'headless' : 'headful'} - {session.headless ? 'headless' : 'headful'} {session.url} - live + Live · - {formatMtime(Math.floor(session.last_used_ms / 1000))} + + {formatMtime(Math.floor(session.last_used_ms / 1000))} + diff --git a/browser/ui/src/page/SessionView.tsx b/browser/ui/src/page/SessionView.tsx index 7dc495f63..414dd309d 100644 --- a/browser/ui/src/page/SessionView.tsx +++ b/browser/ui/src/page/SessionView.tsx @@ -18,7 +18,7 @@ * here — url draft, pick mode, type buffer, pane choices — is session-local. */ -import { Button, type Host, Input } from '@iii-dev/console-ui' +import { Button, type Host, Input, SegmentedControl } from '@iii-dev/console-ui' import { useCallback, useEffect, useRef, useState } from 'react' import { BROWSER_PICKED_TRIGGER, @@ -122,7 +122,9 @@ export function SessionView({ readStored(dockStoreKey) === 'network' ? 'network' : 'console', ) const dockCollapsedStoreKey = `browser-ui:${tabId || 'page'}:dock-collapsed` - const [dockCollapsed, setDockCollapsedState] = useState(() => readStored(dockCollapsedStoreKey) === 'true') + const [dockCollapsed, setDockCollapsedState] = useState( + () => readStored(dockCollapsedStoreKey) === 'true', + ) const setDockPane = (pane: FeedPane) => { setDockPaneState(pane) writeStored(dockStoreKey, pane) @@ -194,7 +196,8 @@ export function SessionView({ const openCurrentPage = useCallback(() => { let url = urlDraft.trim() || session.url - if (url && !/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(url)) url = `https://${url}` + if (url && !/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(url)) + url = `https://${url}` if (url) window.open(url, '_blank', 'noopener,noreferrer') }, [session.url, urlDraft]) @@ -252,7 +255,9 @@ export function SessionView({ setLastPicked(evt) // No composer slot in injected UI: copy the summary for the user to // paste into chat. - void navigator.clipboard?.writeText(formatPickedElement(evt)).catch(() => {}) + void navigator.clipboard + ?.writeText(formatPickedElement(evt)) + .catch(() => {}) setPicking(false) }, }) @@ -341,22 +346,39 @@ export function SessionView({ }) }, [host, sessionId, runAction, onSessionsRefresh, onStopped]) - const displayName = session.title?.trim() || hostOf(session.url) || 'about:blank' - const feedPane: FeedPane = narrow ? (narrowPane === 'network' ? 'network' : 'console') : dockPane + const displayName = + session.title?.trim() || hostOf(session.url) || 'about:blank' + const feedPane: FeedPane = narrow + ? narrowPane === 'network' + ? 'network' + : 'console' + : dockPane const browserMajor = chromiumVersion?.match(/\d+/)?.[0] const browserLabel = browserMajor ? `Chromium ${browserMajor}` : null return ( -
    +
    - {narrow ? : null} + {narrow ? ( + + ) : null}
    - + {displayName} - {session.headless ? 'headless' : 'headful'} - {!narrow && browserLabel ? {browserLabel} : null} + + {session.headless ? 'headless' : 'headful'} + + {!narrow && browserLabel ? ( + {browserLabel} + ) : null}
    {session.url} @@ -368,7 +390,9 @@ export function SessionView({ {!narrow ? ( <> · - started {formatMtime(Math.floor(session.created_ms / 1000))} + + started {formatMtime(Math.floor(session.created_ms / 1000))} + ) : null} @@ -385,10 +409,15 @@ export function SessionView({ } className={cn('br-ui-pick-btn', picking && 'is-on')} > - + {picking ? 'Inspecting...' : 'Inspect'} -
    @@ -396,27 +425,36 @@ export function SessionView({ {lastPicked ? (
    - picked - + Picked + {lastPicked.element.ref} - {pickedSelector(lastPicked.element)} + + {pickedSelector(lastPicked.element)} + - copied to clipboard + Copied to clipboard
    ) : null} {actionError ? (
    {actionError} -
    @@ -424,20 +462,21 @@ export function SessionView({ {narrow ? (
    - {/* biome-ignore lint/a11y/useSemanticElements: segmented control of buttons; fieldset chrome (min-content sizing) breaks the row */} -
    - {NARROW_PANES.map((pane) => ( - - ))} -
    + + value={narrowPane} + onChange={setNarrowPane} + options={NARROW_PANES.map((pane) => ({ + value: pane, + label: + pane === 'console' + ? 'Console' + : pane === 'network' + ? 'Network' + : 'Viewport', + }))} + className="br-ui-tabs" + aria-label="Session view" + />
    ) : null} @@ -451,7 +490,10 @@ export function SessionView({ submitUrl() }} > -
    +
    - + - @@ -538,26 +584,26 @@ export function SessionView({ {!narrow ? (
    - {/* biome-ignore lint/a11y/useSemanticElements: segmented control of buttons; fieldset chrome (min-content sizing) breaks the row */} -
    - {FEED_PANES.map((pane) => ( - - ))} -
    + + value={dockPane} + onChange={setDockPane} + options={FEED_PANES.map((pane) => ({ + value: pane, + label: pane === 'console' ? 'Console' : 'Network', + }))} + className="br-ui-tabs" + aria-label="Session feeds" + />
    {!dockCollapsed ? (
    {dockPane === 'console' ? ( - + ) : ( - + )}
    ) : null} @@ -590,7 +644,9 @@ export function SessionView({ ) : ( Viewport: — )} - {session.headless ? 'Headless' : 'Headful'} + + {session.headless ? 'Headless' : 'Headful'} + {browserLabel ? {browserLabel} : null} @@ -599,7 +655,9 @@ export function SessionView({ {viewportShown ? ( picking ? ( - pick mode: click an element to copy it — esc cancels + + pick mode: click an element to copy it — esc cancels + ) : ( <> Click to focus diff --git a/browser/ui/src/page/index.tsx b/browser/ui/src/page/index.tsx index 8ea9dc71a..34a84ba14 100644 --- a/browser/ui/src/page/index.tsx +++ b/browser/ui/src/page/index.tsx @@ -20,12 +20,27 @@ * SessionView), so a narrow pane parked on the list streams nothing. */ -import { Button, type Host, PageHeader, type PageRenderProps, PageShell } from '@iii-dev/console-ui' +import { + Button, + type Host, + PageHeader, + type PageRenderProps, + PageShell, +} from '@iii-dev/console-ui' import type { ComponentType } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { errorMessage, readBrowserDoctor, startBrowserSession } from '../lib/browser' +import { + errorMessage, + readBrowserDoctor, + startBrowserSession, +} from '../lib/browser' import { Plus } from '../lib/icons' -import { GlobeIcon, LivePill, RefreshButton, useContainerNarrow } from '../lib/widgets' +import { + GlobeIcon, + LivePill, + RefreshButton, + useContainerNarrow, +} from '../lib/widgets' import { SessionRail } from './SessionRail' import { SessionView } from './SessionView' import { useBrowserSessionsLive } from './useBrowserSessionsLive' @@ -47,7 +62,10 @@ export function BrowserPage({ onClose: () => void }> | undefined - const { sessions, loading, error, live, refresh } = useBrowserSessionsLive(host, true) + const { sessions, loading, error, live, refresh } = useBrowserSessionsLive( + host, + true, + ) const [chromiumVersion, setChromiumVersion] = useState(null) useEffect(() => { @@ -91,7 +109,10 @@ export function BrowserPage({ }) }, [loading, sessions]) - const selected = useMemo(() => sessions.find((s) => s.session_id === selectedId) ?? null, [sessions, selectedId]) + const selected = useMemo( + () => sessions.find((s) => s.session_id === selectedId) ?? null, + [sessions, selectedId], + ) // The drilled-into session can die underneath us (stopped from chat or // another tab): drill back out to the list rather than silently showing @@ -138,12 +159,16 @@ export function BrowserPage({ } title="Browser" - description="live Chromium sessions you can watch and drive" + description="Live Chromium sessions you can watch and drive" actions={
    {ConfigurationDialog ? ( - ) : null} @@ -164,7 +189,10 @@ export function BrowserPage({
    ) : null} -
    +
    {railVisible ? (
    - {startError ?

    {startError}

    : null} + {startError ? ( +

    {startError}

    + ) : null}
    - active now + Active now - {loading && sessions.length === 0 ? null : {sessions.length}} - + {loading && sessions.length === 0 ? null : ( + {sessions.length} + )} +
    - +
    ) : null} @@ -217,8 +264,9 @@ export function BrowserPage({

    No browser sessions

    - Sessions started by agents appear here automatically. Start one from the session rail, or ask an agent - to call browser::sessions::start. + Sessions started by agents appear here automatically. Start + one from the session rail, or ask an agent to call{' '} + browser::sessions::start.

    @@ -226,7 +274,10 @@ export function BrowserPage({ ) : null}
    {ConfigurationDialog ? ( - setConfigOpen(false)} /> + setConfigOpen(false)} + /> ) : null} ) diff --git a/browser/ui/styles.css b/browser/ui/styles.css index 8d02bdacc..95b881952 100644 --- a/browser/ui/styles.css +++ b/browser/ui/styles.css @@ -254,8 +254,8 @@ outline-offset: -2px; } [data-iii-ui="browser"] .br-ui-iconbtn-icon { - width: 13px; - height: 13px; + width: 16px; + height: 16px; } [data-iii-ui="browser"] .br-ui-rail-scroll { @@ -291,17 +291,21 @@ font-family: inherit; } [data-iii-ui="browser"] .br-ui-rail-row:hover { - border-color: color-mix(in srgb, var(--color-ink-ghost) 58%, var(--color-edge)); + border-color: color-mix( + in srgb, + var(--color-ink-ghost) 58%, + var(--color-edge) + ); background: var(--color-surface-hover); } [data-iii-ui="browser"] .br-ui-rail-row:focus-visible { outline: 2px solid var(--color-rule-focus); outline-offset: -2px; } -/* Selection = wash + accent indicator + stronger title, not color alone. */ +/* Selection stays neutral: wash + edge + stronger title. */ [data-iii-ui="browser"] .br-ui-rail-row.active { - border-color: var(--color-accent); - background: color-mix(in srgb, var(--color-accent) 10%, var(--color-panel-raised)); + border-color: var(--color-edge); + background: var(--color-surface-selected); } [data-iii-ui="browser"] .br-ui-rail-row.active::before { content: none; @@ -317,7 +321,7 @@ color: var(--color-ink-ghost); } [data-iii-ui="browser"] .br-ui-rail-row.active .br-ui-rail-icon { - color: var(--color-accent); + color: var(--color-ink-faint); } [data-iii-ui="browser"] .br-ui-rail-title { min-width: 0; @@ -527,7 +531,8 @@ flex-shrink: 0; border-radius: 50%; background: var(--color-ok, var(--color-accent)); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-ok, var(--color-accent)) 12%, transparent); + box-shadow: 0 0 0 2px + color-mix(in srgb, var(--color-ok, var(--color-accent)) 12%, transparent); } [data-iii-ui="browser"] .br-ui-doc-actions { display: flex; @@ -595,7 +600,8 @@ [data-iii-ui="browser"] .br-ui-stop-btn { min-height: 36px; padding-inline: 13px; - border: 1px solid color-mix(in srgb, var(--color-alert) 80%, var(--color-edge)); + border: 1px solid + color-mix(in srgb, var(--color-alert) 80%, var(--color-edge)); color: var(--color-alert); } @@ -772,55 +778,19 @@ font-variant-numeric: tabular-nums; } -/* segmented control — one control, mutually exclusive options */ -[data-iii-ui="browser"] .br-ui-seg { - display: inline-flex; - align-items: center; - gap: 2px; - padding: 2px; - background: var(--color-surface); - border-radius: 6px; +/* Shared line tabs own typography, icon sizing and active underline. */ +[data-iii-ui="browser"] .br-ui-tabs { flex-shrink: 0; } -[data-iii-ui="browser"] .br-ui-seg.block { - display: flex; -} -[data-iii-ui="browser"] .br-ui-seg-btn { - appearance: none; - border: 0; - background: transparent; - height: 26px; - padding: 0 12px; - border-radius: 4px; - font-family: var(--font-mono, ui-monospace, monospace); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-ink-faint); - cursor: pointer; -} -[data-iii-ui="browser"] .br-ui-seg-btn:hover { - color: var(--color-ink); - background: var(--color-surface-hover); -} -[data-iii-ui="browser"] .br-ui-seg-btn.active { - color: var(--color-ink); - background: var(--color-panel-raised); - box-shadow: 0 0 0 1px var(--color-edge); -} -[data-iii-ui="browser"] .br-ui-seg-btn:focus-visible { - outline: 2px solid var(--color-rule-focus); - outline-offset: -2px; -} -[data-iii-ui="browser"] .br-ui-seg.block .br-ui-seg-btn { - flex: 1; -} /* narrow-mode viewport | console | network switcher row */ [data-iii-ui="browser"] .br-ui-view-row { padding: 8px 12px; flex-shrink: 0; border-bottom: 1px solid var(--color-edge); } +[data-iii-ui="browser"] .br-ui-view-row .br-ui-tabs { + width: 100%; +} /* ── viewport — one browser frame, sized from the live screencast ───── */ @@ -973,27 +943,8 @@ border-bottom: 1px solid var(--color-edge); } -[data-iii-ui="browser"] .br-ui-dock .br-ui-seg { +[data-iii-ui="browser"] .br-ui-dock .br-ui-tabs { align-self: stretch; - gap: 0; - padding: 0; - border-radius: 0; - background: transparent; -} -[data-iii-ui="browser"] .br-ui-dock .br-ui-seg-btn { - position: relative; - height: 100%; - padding: 0 16px; - border-radius: 0; - text-transform: none; - letter-spacing: 0; - font-family: var(--font-sans, system-ui, sans-serif); - font-size: 12px; -} -[data-iii-ui="browser"] .br-ui-dock .br-ui-seg-btn.active { - background: var(--color-surface-selected); - color: var(--color-accent); - box-shadow: inset 0 -2px 0 var(--color-accent); } [data-iii-ui="browser"] .br-ui-dock.collapsed .br-ui-dock-head { @@ -1033,7 +984,7 @@ height: 16px; flex-shrink: 0; transform: rotate(-90deg); - transition: transform 120ms ease; + transition: transform var(--motion-duration-fast) var(--motion-ease-standard); } [data-iii-ui="browser"] .br-ui-dock.collapsed .br-ui-dock-toggle-icon { @@ -1544,7 +1495,7 @@ inset: 7px auto 7px -8px; width: 2px; border-radius: 0 2px 2px 0; - background: var(--color-accent); + background: var(--color-edge); content: ""; } diff --git a/canvas/ui/src/page/ExportMenu.tsx b/canvas/ui/src/page/ExportMenu.tsx index a816b96ca..5d80fd155 100644 --- a/canvas/ui/src/page/ExportMenu.tsx +++ b/canvas/ui/src/page/ExportMenu.tsx @@ -57,7 +57,7 @@ export function ExportMenu({ theme, disabled, onExport }: ExportMenuProps) { disabled={disabled} title="download as svg" > - + svg diff --git a/canvas/ui/src/page/MermaidPane.tsx b/canvas/ui/src/page/MermaidPane.tsx index e103ec835..c21c4ff70 100644 --- a/canvas/ui/src/page/MermaidPane.tsx +++ b/canvas/ui/src/page/MermaidPane.tsx @@ -80,7 +80,6 @@ interface View { z: number } - interface MermaidPaneProps { host: Host record: CanvasRecord @@ -217,7 +216,10 @@ export function MermaidPane({ initMermaidOnce(mermaid, theme) await mermaid.parse(source) if (seqRef.current !== seq) return - const { svg } = await mermaid.render(`cv-mmd-${record.id}-${seq}`, source) + const { svg } = await mermaid.render( + `cv-mmd-${record.id}-${seq}`, + source, + ) setPreview((s) => renderSucceeded(s, seq, svg)) } catch (err) { setPreview((s) => renderFailed(s, seq, errorMessage(err))) @@ -254,7 +256,8 @@ export function MermaidPane({ const svg = el.querySelector('svg') if (!svg) return const box = svg.viewBox.baseVal - const w = box && box.width > 0 ? box.width : svg.getBoundingClientRect().width + const w = + box && box.width > 0 ? box.width : svg.getBoundingClientRect().width const h = box && box.height > 0 ? box.height : svg.getBoundingClientRect().height if (w > 0 && h > 0) { @@ -378,7 +381,11 @@ export function MermaidPane({ const d = dragRef.current if (!d) return const v = viewRef.current - viewRef.current = { ...v, x: v.x + (e.clientX - d.px), y: v.y + (e.clientY - d.py) } + viewRef.current = { + ...v, + x: v.x + (e.clientX - d.px), + y: v.y + (e.clientY - d.py), + } dragRef.current = { px: e.clientX, py: e.clientY } schedule() } @@ -465,7 +472,7 @@ export function MermaidPane({ disabled={!contentSize} title="fit the diagram in the frame" > - + fit @@ -477,7 +477,7 @@ export function CanvasPage({ title="delete" onClick={() => remove(c)} > - + ))} diff --git a/computer/ui/src/page/index.tsx b/computer/ui/src/page/index.tsx index 5c3c848e9..5179a207a 100644 --- a/computer/ui/src/page/index.tsx +++ b/computer/ui/src/page/index.tsx @@ -52,7 +52,9 @@ const NARROW_BELOW = 720 * gives it. Measures synchronously on mount to avoid a wide-mode flash; * zero widths (display:none) are ignored so a hidden page keeps its last * real layout. */ -function useContainerNarrow(threshold: number): [(node: HTMLDivElement | null) => void, boolean] { +function useContainerNarrow( + threshold: number, +): [(node: HTMLDivElement | null) => void, boolean] { const [narrow, setNarrow] = useState(false) const observerRef = useRef(null) const refCb = useCallback( @@ -206,8 +208,8 @@ export function ComputerPage({ } - title="computer" - description="live desktops you can watch and drive" + title="Computer" + description="Live desktops you can watch and drive" actions={} onClose={onRequestClose} /> @@ -322,7 +324,9 @@ export function ComputerPage({ click to focus — clicks, scroll, typing and shortcuts forward as act - shift+esc leaves the surface + + shift+esc leaves the surface + ) : ( @@ -330,8 +334,8 @@ export function ComputerPage({

    no desktop yet

    - start a session to drive this machine, a sandboxed desktop, - or a remote one. + start a session to drive this machine, a sandboxed desktop, or + a remote one.

    )} diff --git a/computer/ui/styles.css b/computer/ui/styles.css index 8ff28a0aa..df6ec8f87 100644 --- a/computer/ui/styles.css +++ b/computer/ui/styles.css @@ -214,7 +214,7 @@ [data-iii-ui="computer"] .cp-ui-rail-row:hover { background: var(--color-surface-hover); } -/* Selection = wash + accent indicator + stronger id, not color alone. */ +/* Selection stays neutral: wash + edge indicator + stronger id. */ [data-iii-ui="computer"] .cp-ui-rail-row.active { background: var(--color-surface-selected); } @@ -226,7 +226,7 @@ bottom: 7px; width: 2px; border-radius: 1px; - background: var(--color-accent); + background: var(--color-edge); } [data-iii-ui="computer"] .cp-ui-rail-pick { flex: 1; diff --git a/console/DESIGN.md b/console/DESIGN.md index c60b9b3de..10beb74b2 100644 --- a/console/DESIGN.md +++ b/console/DESIGN.md @@ -1,7 +1,7 @@ --- version: beta name: iii Schematic -description: A minimal, engineering-document design system for the iii engine. The system draws no lines — hierarchy comes entirely from layered surfaces and alpha-gray fills; warm cream paper in light, neutral grays from black in dark, with a single rationed accent (burnt orange on cream, electric blue on dark), one 6px corner radius everywhere, and a mono voice for technical data. The only sanctioned strokes are the focus ring and the very subtle `edge` frame around the floating workspace panels. +description: A minimal, engineering-document design system for the iii engine. Hierarchy comes from layered surfaces and alpha-gray fills; warm cream paper in light, neutral grays from black in dark, with a single rationed accent (burnt orange on cream, electric blue on dark), one 6px corner radius everywhere, and a mono voice for technical data. Strokes are limited to focus, the subtle workspace frame, and an optional neutral selection edge. colors: bg: "#f2f0ed" sidebar: "#edeae5" @@ -11,7 +11,7 @@ colors: paper-2: "#ebe8e3" surface: "rgba(20, 16, 8, 0.055)" surface-hover: "rgba(20, 16, 8, 0.085)" - surface-selected: "rgba(184, 66, 15, 0.12)" + surface-selected: "rgba(20, 16, 8, 0.12)" surface-active: "rgba(20, 16, 8, 0.12)" ink: "#0a0a0a" ink-2: "#1a1a1a" @@ -44,7 +44,7 @@ colors: paper-2-dark: "#171717" surface-dark: "rgba(255, 255, 255, 0.055)" surface-hover-dark: "rgba(255, 255, 255, 0.085)" - surface-selected-dark: "rgba(40, 168, 247, 0.14)" + surface-selected-dark: "rgba(255, 255, 255, 0.12)" surface-active-dark: "rgba(255, 255, 255, 0.12)" ink-dark: "#ededed" ink-faint-dark: "#a6a6a6" @@ -373,7 +373,7 @@ The full design-token stylesheet — drop this in as your global CSS entrypoint: --color-paper-2: #ebe8e3; --color-surface: rgba(20, 16, 8, 0.055); --color-surface-hover: rgba(20, 16, 8, 0.085); - --color-surface-selected: rgba(184, 66, 15, 0.12); + --color-surface-selected: rgba(20, 16, 8, 0.12); --color-surface-active: rgba(20, 16, 8, 0.12); /* ── Ink ramp ──────────────────────────────────────────────────────── */ @@ -434,7 +434,14 @@ The full design-token stylesheet — drop this in as your global CSS entrypoint: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 12px 32px rgba(0, 0, 0, 0.28); /* ── Motion ─────────────────────────────────────────────────────────── */ - --ease-glide: cubic-bezier(0.2, 0.8, 0.2, 1); + --motion-duration-instant: 0ms; + --motion-duration-fast: 120ms; + --motion-duration-control: 160ms; + --motion-duration-panel: 220ms; + --motion-ease-standard: cubic-bezier(0.2, 0, 0, 1); + --motion-ease-enter: cubic-bezier(0.16, 1, 0.3, 1); + --motion-ease-exit: cubic-bezier(0.4, 0, 1, 1); + --ease-glide: var(--motion-ease-enter); /* spacing scale (carried from the YAML) */ --spacing-gutter: 24px; @@ -447,8 +454,8 @@ The full design-token stylesheet — drop this in as your global CSS entrypoint: [data-theme="dark"] { /* Neutral grays derived from black — no blue cast in the base ramp. The component fills (surface*) are white-alpha so a step reads identically - over any base layer; the ONLY chromatic surface is surface-selected, - the blue selection tint. Borders are gone: rule/rule-2/rule-strong + over any base layer, including selection. Borders are gone: + rule/rule-2/rule-strong resolve to transparent, and hierarchy is carried entirely by fills. */ --color-bg: #0a0a0a; --color-sidebar: #0e0e0e; @@ -457,7 +464,7 @@ The full design-token stylesheet — drop this in as your global CSS entrypoint: --color-paper-2: #171717; --color-surface: rgba(255, 255, 255, 0.055); --color-surface-hover: rgba(255, 255, 255, 0.085); - --color-surface-selected: rgba(40, 168, 247, 0.14); + --color-surface-selected: rgba(255, 255, 255, 0.12); --color-surface-active: rgba(255, 255, 255, 0.12); --color-ink: #ededed; --color-ink-faint: #a6a6a6; @@ -607,18 +614,19 @@ looking boxed. - The page is built from **layered surfaces**: hierarchy comes from a one-step background difference, full stop. The system draws **no lines** — no outlines on controls, no dividers between rows or regions. Exactly - two strokes are sanctioned: the focus indicator (`rule-focus` - border/ring) on a focused control, and the very subtle `edge` frame - around the floating workspace panels. + three strokes are sanctioned: the focus indicator (`rule-focus`), the + very subtle `edge` frame around floating workspace panels, and an optional + neutral `edge` on a selected row/card when fill alone is too subtle. - Dark mode is a **first-class layered system** — neutral grays derived from black (`#0a0a0a → #171717`, no blue cast) with white-alpha component fills, not an inverted paper ramp. - One corner radius: **6px everywhere** (every Tailwind radius step resolves to it); the shapes stay disciplined, not soft or - consumer-playful. Lowercase voice throughout. + consumer-playful. Natural sentence/title case throughout. - Color is rationed: the palette is essentially **ink-on-surface**, broken by a single accent (burnt orange on cream, electric blue on dark) reserved - for selected, focused, active, and live states. + for primary actions, form focus, live/running state, and semantic data. + Selection itself remains neutral in both themes. - The personality is **technical but unintimidating** — the same energy as a well-kept lab notebook or a hand-drawn architecture diagram. It must feel built by engineers, for engineers, and for the agents working alongside @@ -629,8 +637,10 @@ looking boxed. ### Voice -- All UI copy is **lowercase**, including headlines, buttons, and nav items. -- Headlines treat sentence fragments as visual blocks (e.g. *"any task. one +- Author human-facing UI copy in **natural sentence/title case**, including + headlines, buttons, tabs, menus, fields, and navigation. Never force case + with CSS text transforms. +- Headlines treat sentence fragments as visual blocks (e.g. *"Any task. One experience."*). - Numbers and metadata always use **tabular monospace**, never proportional figures. @@ -644,18 +654,17 @@ looking boxed. ## 2. Typography — two families (Geist + Geist Mono) -**Geist** (`--font-sans`) carries the UI: navigation, conversation titles, -buttons, inputs, chat content, empty states, headings, labels. **Geist -Mono** (`--font-mono`) carries everything technical: trace names, worker -names, function names, IDs, timestamps, metrics, span labels, filter -expressions, code-like values. +**Geist** (`--font-sans`) carries all human-facing UI chrome: navigation, +conversation titles, buttons, inputs, tabs, menus, chat content, empty states, +headings, and labels. **Geist Mono** (`--font-mono`) is reserved for +machine-readable values: trace names, worker names, function names, IDs, +timestamps, metrics, span labels, filter expressions, and code-like values. **Rule:** if a human wrote it, it's sans; if the machine produced it (or a machine will parse it), it's mono. Don't add a third family — variety comes -from weight, scale, case, and letter-spacing, not more fonts. Chat and -configuration surfaces remap incidental `.font-mono` chrome back to Geist -(see the `.chat-surface` / `.configuration-surface` rules in `index.css`); -function-trigger cards run Geist Mono throughout. +from weight, scale, case, and letter-spacing, not more fonts. Do not apply +mono to an entire panel or function-trigger card; technical values inside +those surfaces opt into mono individually. Decorative ligatures are explicitly disabled on mono surfaces (`liga 0, clig 0, calt 0, dlig 0`) to preserve the schematic feel. @@ -682,10 +691,9 @@ Decorative ligatures are explicitly disabled on mono surfaces - Any numeric or timestamp cell uses `tabular-nums` so columns align. See the duration column in `Trace` and the version row in `WorkerCard` (§10). -- The **only** uppercase text in the system is the `label-caps-*` set - (uppercase + tracking). Used for: tab strips, status pills, table headers, - code-block chrome, section eyebrows. Never capitalize a sentence to "fix" a - heading — rewrite it instead. +- Human-facing labels keep their authored casing. Do not use CSS `uppercase` + or `lowercase` transforms on tabs, buttons, menus, fields, or navigation. + Preserve acronyms and machine identifiers exactly as provided. --- @@ -699,9 +707,8 @@ raw CSS variable. The base layers (`bg` → `panel-raised`) are solid neutral tones; the component fills (`surface*`) are **alpha grays**, so one step reads -identically over any base layer. `surface-selected` is the only chromatic -surface — the accent-tinted selection fill (blue in dark, burnt orange in -light). +identically over any base layer. `surface-selected` is a stronger neutral +alpha fill in both themes; it never inherits the orange/blue accent. | Token | Use | | ------------------ | ---------------------------------------------------------- | @@ -712,7 +719,7 @@ light). | `paper-2` | Legacy alias for `panel-raised` (kept for existing code) | | `surface` | Inputs, controls, pills, chips, secondary cards | | `surface-hover` | Hover state on rows, items, and ghost controls | -| `surface-selected` | Selected conversation, trace row, or list item (accent tint) | +| `surface-selected` | Selected conversation, trace row, list item, card, tab, or chip (neutral wash) | | `surface-active` | Strong active/pressed state | ### Ink (4-step contrast) @@ -737,16 +744,15 @@ breaking. Never design with them. | `rule-2` | transparent | Legacy subtle divider — draws nothing | | `rule-strong` | transparent | Legacy emphasis border — draws nothing | | `rule-focus` | accent ~60–70% alpha | The interactive stroke: the focus indicator on inputs and controls | -| `edge` | ink ~7–8% alpha | The structural stroke: the VERY subtle frame around the floating workspace panels (the tab columns) — never used inside a panel | +| `edge` | ink ~7–8% alpha | The structural stroke: workspace frames and optional neutral selected edges/rails | ### Accent (single hero — burnt orange on cream, electric blue on dark) `accent`, `accent-fg`, `accent-hover`, `accent-muted` (10–12%-alpha fill), -`accent-border` (35%-alpha, legacy — prefer `accent-muted` fills). Reserved -for: selected navigation and segmented controls (`accent-muted` fill), -focused inputs (`rule-focus`), active filters, live state, the selected -conversation/trace (`surface-selected` fill), the primary technical action, -the `$` prompt. **Never** for body text, large fills, or outlines. +`accent-border` (35%-alpha, legacy). Reserved for focused form controls +(`rule-focus`), live/running state, primary technical actions, semantic chart +data, and the `$` prompt. **Never** use accent for selection labels, rails, +borders, tab underlines, or selected card fills. ### Status @@ -757,7 +763,7 @@ stripe or outline. | Token | Use | | -------- | ----------------------------------------- | -| `accent` | live / running / focused / selected | +| `accent` | live / running / focused / primary action | | `ok` | success, completed calls, diff additions | | `alert` | error states (traces, status panels) | | `warn` | warning states, pending approval | @@ -768,9 +774,9 @@ Override the same tokens inside a `[data-theme="dark"]` block (see §0). Dark is a neutral gray ramp derived from black (`#0a0a0a → #0e0e0e → #111111 → #171717` — no blue cast), white-alpha component fills (`rgba(255,255,255,0.055 → 0.12)`), neutral light ink (`#ededed → #a6a6a6 → -#6f6f6f`), and the accent swapped to electric blue (`#28a8f7`) — which also -tints `surface-selected`. Same structural logic; the blue lives only in -state, never in the grays. +#6f6f6f`), and the accent swapped to electric blue (`#28a8f7`). Selection +remains a white-alpha neutral wash; blue is reserved for meaningful state, +never the neutral gray ramp. To follow the OS, set the attribute on load: @@ -783,9 +789,9 @@ document.documentElement.dataset.theme = isDark ? 'dark' : 'light' ## 4. The "surfaces, not borders" rule (key composition pattern) -> **A one-step surface difference defines a region. The system draws no -> lines — exactly two strokes are sanctioned: the focus ring, and the -> `edge` frame around the floating workspace panels.** +> **A one-step surface difference defines a region. Strokes are limited to +> focus, the workspace `edge` frame, and a neutral selected edge when a wash +> alone is not sufficiently clear.** Structure comes from the layered surface ramp (§3): a new region means a new background step, never an outline and never a divider. Controls are @@ -793,7 +799,7 @@ alpha-gray fills; rows separate by their hover/selected fills; regions separate by base-layer steps; overlays separate by `panel-raised` + `shadow-floating`. -Two strokes are allowed, each with one job: +Three strokes are allowed, each with one job: 1. **Focus** — a focused field swaps its (transparent) border to `rule-focus` and gains a soft 3px accent ring; keyboard focus on @@ -802,8 +808,10 @@ Two strokes are allowed, each with one job: workspace-tab column) float on the canvas as `rounded-sm border border-edge bg-panel` with 6px gutters; the `edge` stroke is a VERY subtle ink-alpha frame that keeps a panel readable - against the canvas. It is never used inside a panel — interior - hierarchy stays fill-only. + against the canvas. +3. **Selection edge** — dense lists/cards may pair `surface-selected` with + an `edge` border or 2px inset rail. The edge stays neutral and the label + stays `ink`; never substitute `accent`. Nothing else in the chrome may draw a line. (Data visualizations are exempt: charts may draw connector and grid lines with explicit alpha-ink @@ -814,8 +822,8 @@ trace timeline.) ```tsx
    -
    - title +
    + Title
    {/* body */}
    @@ -826,9 +834,10 @@ separator. No divider, no outer outline. ### Selection and severity -- Selected row, card, conversation, or trace: `bg-surface-selected` — the - accent-tinted fill. **No left rail, no outline.** -- Active segment / tab / filter: `bg-accent-muted` fill. +- Selected row, card, conversation, or trace: `bg-surface-selected` with + `text-ink`; optionally add a subtle `edge` border/inset rail for dense + navigation. Never recolor the label or rail with accent. +- Active segment / tab / filter: `bg-surface-selected` with `text-ink`. - Row severity: the status's `-muted` tinted fill (e.g. `bg-alert-muted`) plus the status text color and dot — no stripe. - Legacy `divide-y divide-rule-2` / `border-b border-rule-2` classes are @@ -842,6 +851,25 @@ dropdowns, tooltips). `.deal-shadow` remains a transient animation cue on the language-card stack. No heavy glows — the only glow is the live `pulse-dot`. +### Motion vocabulary + +Motion communicates continuity; it never decorates a settled screen or delays +input. Use the shared tokens instead of component-local milliseconds: + +| Token | Duration | Use | +| --- | ---: | --- | +| `motion-duration-instant` | 0ms | Streaming/high-frequency updates and direct manipulation | +| `motion-duration-fast` | 120ms | Tooltip/menu opacity and lightweight feedback | +| `motion-duration-control` | 160ms | Hover, selected, pressed, chevrons, segmented controls | +| `motion-duration-panel` | 220ms | Dialogs, sheets and panel/overlay entry/exit | + +Use `motion-ease-standard` for state changes, `motion-ease-enter` for mounting, +and `motion-ease-exit` for dismissal. Width/position updates driven by token +streaming, logs, traces, terminal output, drag, resize, or pointer movement are +instant: repeated updates must not queue animations. Under +`prefers-reduced-motion`, all three non-zero duration tokens resolve to zero and +non-essential keyframes run once without delay. + **When in doubt:** step the background one level. Never reach for a border. --- @@ -863,8 +891,8 @@ bg application canvas ``` Interactive containers walk the state sub-ramp (`surface → -surface-hover → surface-selected → surface-active`); nothing about their -edges ever changes except the focus ring. +surface-hover → surface-selected → surface-active`). Dense selected rows/cards +may also reveal the neutral `edge`; focus remains the only chromatic stroke. ### Where elevation actually shows up @@ -883,8 +911,8 @@ edges ever changes except the focus ring. system allows. Dark mode keeps the same surface-driven hierarchy — neutral grays stepping -up from black. The only visible edge anywhere is the workspace panels' -`edge` frame (§4). +up from black. Visible edges remain limited to workspace frames and the +optional neutral selected-state edge (§4). --- @@ -1001,9 +1029,10 @@ severity. background plus its text color and dot. Never a stripe, ring, or solid status background. See `Trace` (§10). -**Dot semantics:** green (`ok`) means completed; blue (`accent`) is reserved -for live/running (animated) and selected states; `warn` for pending; `alert` -for failed. For inline "live" emphasis, pair a `StatusDot` with the +**Dot semantics:** green (`ok`) means completed; `accent` is reserved for +live/running activity; `warn` for pending; `alert` for failed. Selection uses +the neutral surface recipe, never a status dot or chromatic state. For inline +"live" emphasis, pair a `StatusDot` with the `pulse-dot` utility — the only sanctioned glow. --- @@ -1070,8 +1099,8 @@ export function Caret({ className }: CaretProps) { ### `StatusDot` 6px circle. Optional `.pulse-dot` glow for "live" emphasis. Tone semantics -per §9: `ok` = completed, `accent` = live/selected, `warn` = pending, -`alert` = failed. +per §9: `ok` = completed, `accent` = live/running, `warn` = pending, +`alert` = failed. Selection is not a status tone. ```tsx import * as React from 'react' @@ -1131,7 +1160,7 @@ import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const buttonVariants = cva( - 'inline-flex items-center justify-center gap-x-2 whitespace-nowrap font-mono lowercase rounded-sm transition-[background-color,color,border-color] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rule-focus disabled:pointer-events-none disabled:opacity-40 select-none', + 'iii-ui-motion-control inline-flex items-center justify-center gap-x-2 whitespace-nowrap rounded-sm font-sans transition-[background-color,color,border-color] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rule-focus disabled:pointer-events-none disabled:opacity-40 select-none [&>svg]:size-4 [&>svg]:shrink-0', { variants: { variant: { @@ -1260,14 +1289,14 @@ export function StatusPanel({
    {headline}
    {detail ? ( -
    +
    {detail}
    ) : null} @@ -1330,7 +1359,7 @@ export function Terminal({ title, children, className }: TerminalProps) { return (
    {title ? ( -
    +
    {title}
    ) : null} @@ -1411,7 +1440,7 @@ export function Trace({ title, rows, totalMs, className }: TraceProps) { ) return (
    -
    +
    {title}
      @@ -1425,13 +1454,13 @@ export function Trace({ title, rows, totalMs, className }: TraceProps) { className="grid grid-cols-[auto_1fr_auto_auto] items-center gap-x-3 px-3.5 py-2 font-mono text-[12px]" > - {row.op} + {row.op} {row.durationMs}ms @@ -1473,11 +1502,11 @@ export function Cell({ title, children, className }: CellProps) { return (
      {title ? ( -
      +
      {title}
      ) : null} -
      +
      {children}
      @@ -1490,7 +1519,8 @@ export function Cell({ title, children, className }: CellProps) { 400px-wide ticker card with a name + version row, description, a `panel`-tinted command block, and a footer with a kind tag and check icon. Focused state switches the body fill from `surface` to `surface-selected` -(the accent tint) — no rail, no outline. +(a neutral wash) and may add the neutral `edge` when fill alone is too subtle. +It never changes the title to `accent`. ```tsx import * as React from 'react' @@ -1518,27 +1548,27 @@ export function WorkerCard({ return (
      -
      +
      {name}
      -
      +
      v{version}
      -
      +
      {description}
      {command}