Skip to content

ADR-012: UI/UX system - #309

Merged
dovvnloading merged 7 commits into
mainfrom
adr012-ui-ux-system
Aug 10, 2026
Merged

ADR-012: UI/UX system#309
dovvnloading merged 7 commits into
mainfrom
adr012-ui-ux-system

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

Several long-standing UI/UX gaps: no real light/dark theming, incomplete keyboard reachability on the canvas (node menus, focus indication), no prefers-reduced-motion support, every node kind hand-rolling the same wrapper boilerplate, no in-canvas search highlighting or node filtering, and no first-run path for a fresh install (no onboarding, no sample content, a blank canvas with no explanation). Three Settings surfaces were also owed by earlier ADRs: a working provider-mode switcher (the intent existed, nothing called it), an MCP servers panel (ADR-007's own "Consequences" section deferred this here), and reachable resource-limit disclosure (previously only shown mid code-execution-approval).

Change

Six stages, one branch:

  • 12.1 — real light/dark token palette replacing the placeholder single-theme values, with a drift/contrast regression test.
  • 12.2 — theme application (light/dark/system) wired into Settings and applied to the DOM on load and on change.
  • 12.3 — keyboard-complete node context menus (Shift+F10/ContextMenu key opens them via a synthetic dispatch, full arrow/Home/End roving focus, Escape/activation restores focus), a visible focus ring on canvas nodes, a skip-link past the canvas to the composer, and an aria-live region announcing streaming/run-status transitions. Also brought eslint-plugin-jsx-a11y in as a green gate, fixing every violation it surfaced.
  • 12.4prefers-reduced-motion respected across CSS transitions and React Flow's own imperative viewport pans/zooms.
  • 12.5NodeShell.tsx, a shared wrapper now used by 15 of 16 node kinds (the wrapper div, both handles, the context-menu wiring, and the collapse gate, previously duplicated in every *NodeView.tsx); canvas search-match highlighting (the same highlight plugin Document View uses, now live on every node card as you type in the search overlay); and a node filter-by-kind/status control in the View popover.
  • 12.6 — a first-run onboarding wizard (checks real provider readiness, offers a small bundled sample workspace, gets out of the way, reachable again later from Settings), a non-blank empty-canvas hint, and the three owed Settings surfaces: the provider-mode switcher, an MCP Servers page (list/add/remove/enable), and a read-only Resource Limits disclosure section.

Test plan

  • Full backend suite: python -m pytest -q — 2418 passed, 17 skipped, 5 failed (all in test_native_dialogs.py, a pre-existing/environmental pywebview issue unrelated to this branch — confirmed via empty diff on that file and its test).
  • Full frontend suite: npx vitest run — 1843 passed across 77 files.
  • npx tsc --noEmit -p . — clean.
  • npx eslint . — 0 errors (43 pre-existing warnings, none introduced).
  • python contracts/codegen.py --check — generated wire-contract artifacts in sync.
  • pytest tests/test_undo_classification_gate.py — every new intent classified.
  • npm run build + bundle-size gate — clean.
  • Live browser verification against the real backend: onboarding wizard through all 4 steps, sample workspace loading 3 real nodes end-to-end, Resource Limits/MCP Servers/provider-switcher Settings pages all rendering live backend-computed data.

dovvnloading and others added 7 commits August 10, 2026 13:36
…ift/contrast test

Both token files (gl-vars-dev.css, gl-theme.css) still carried "GENERATED -
do not hand-edit" headers pointing at graphlink_styles.py and a Qt island-
injection mechanism, both deleted in the Qt-removal cutover - there has been
no generator and no other consumer for a while, so the files were already
being hand-edited against their own header's claim. Made that honest, and
used the now-genuine hand-authored status to add what was actually missing:
a real light palette alongside dark.

The light values are a systematic per-token derivation, not free-picked:
every solid color in this app is a pure gray (a deliberate monochrome
"graphite" design), so light is a per-channel inversion of dark, with a
distinct rule for alpha-wash tokens (highlight/border washes flip polarity,
drop shadows keep black but reduce opacity, neutral grays keep their RGB
and bump alpha) - spot-checked against real WCAG relative-luminance
contrast, not raw RGB distance, catching one token (text-muted) that needed
a manual correction to match its own dark-mode counterpart's contrast
ratio. Switching is data-theme + prefers-color-scheme, per the ADR; base.css's
color-scheme property now follows the same cascade so native form controls
(scrollbars, etc.) track the active theme too.

The deleted Qt-era Python drift test (test_gl_vars_dev_css.py, which checked
against the generator) is replaced by a real frontend one,
token-theme-contrast.test.ts: name parity between the two themes, the two
duplicated light blocks (media query + explicit attribute) staying
identical, and WCAG AA contrast for every surface/text pair that carries
real body copy.

Verified visually via computed styles in a live browser session: the base
(no data-theme set) cascade renders byte-identical to before this stage -
theme *application* (stamping data-theme from a real Settings field) is
stage 12.2's job, not this one's.

Co-Authored-By: Claude <noreply@anthropic.com>
Wires a new setTheme (system|light|dark) app-settings intent end to end:
SettingsManager persistence, wire contract, WS intent + undo classification,
a Theme control in Settings' General page, and an App.tsx effect that
stamps/clears [data-theme] on <html> so stage 12.1's light/dark token
cascade actually applies. Verified live in-browser: toggling the control
re-themes the whole app immediately, no reload, for all three states.

Co-Authored-By: Claude <noreply@anthropic.com>
…nk, aria-live

Full WAI-ARIA menu keyboard pattern lives once in NodeMenu.tsx (initial
focus, Arrow/Home/End roving, Escape/activate-restores-focus-to-trigger),
so all 11 node kinds with a context menu get it for free. Shift+F10/the
ContextMenu key opens a node's menu by dispatching a real contextmenu event
at the focused node - the actually-focused element is React Flow's own
node wrapper, an ancestor of .scene-node, so no per-node-view change was
needed. The 5 kinds with no context menu (Chart/Group/Html/Note/Plan)
already expose their actions as plain buttons and needed nothing.

A visible :focus-visible outline (new --gl-focus-ring token, both themes)
is now distinct from .selected's own box-shadow treatment. A skip-link
jumps a keyboard user straight to the composer, bypassing every node on
the canvas. One aria-live region (announcer.ts) announces assistant
streaming start/finish and per-node run status transitions, fed by
composerStore.ts and sceneStore.ts.

Added eslint-plugin-jsx-a11y to the lint gate and fixed the 12 pre-existing
violations it surfaced across the app (real fixes where possible - alt
text, real keyboard handlers, aria-selected; scoped, justified suppressions
where the existing pattern was already correct, e.g. CommandPalette's
input-driven virtual selection).

Co-Authored-By: Claude <noreply@anthropic.com>
…rt pans

One blanket @media (prefers-reduced-motion: reduce) rule in base.css covers
styles.css's ~17 CSS transitions (and any future one, and the animations
this app doesn't have yet) - a near-zero duration rather than `transition:
none` so transitionend listeners still fire. React Flow's own zoom/pan
animations (fitView/zoomIn/zoomOut/setCenter/setViewport) aren't CSS and
can't be reached from there; each of their 9 call sites now runs its
requested duration through a new motionDuration() helper that collapses to
0 when the OS preference is set.

jsdom implements no window.matchMedia at all - added the standard stub to
vitest.setup.ts (same posture as the existing ResizeObserver/scrollIntoView
stubs) so tests exercising any of these call sites don't crash.

Co-Authored-By: Claude <noreply@anthropic.com>
…node filter

Adds NodeShell.tsx, the wrapper every content-card node kind (all 16 but
GroupNodeView) now renders through instead of hand-rolling its own
.scene-node div, Handle pair, and collapse gate - migrated all 15
applicable kinds (CodeNodeView as the reference, then Artifact/Thinking/
PyCoder/WebResearch/Image/Document/Gitlink/CodeSandbox/Chart/Html/Note/
Plan/Chat/Conversation via a verified Workflow pass plus Chart/Note by
hand once NodeShell needed a style/resizer/onBodyDoubleClick escape hatch
for their outlier needs).

Adds canvas search-match highlighting: CanvasSearchContext threads
SearchOverlay's live query into NodeMarkdown, reusing the existing
Document View highlight plugin so every visible node's body highlights
matches as you type.

Adds a node filter-by-kind/status control in ViewPopover's own FILTER
section, backed by two new sceneStore fields (filterKinds/filterStatuses)
composed into toFlowNodes' existing dimming mechanism - required wiring
isDimmed into 10 node-kind branches that never carried it (only the 5
branch-focus kinds did), since the filter needed to dim every content
kind, not just those five.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…aces

Adds a first-run onboarding wizard (OnboardingDialog.tsx) that auto-opens
once on a fresh machine, checks real provider readiness via the composer's
existing route.available signal, offers a small bundled sample workspace
(backend/api/intents_onboarding.py's loadSampleWorkspace - a fixed,
deterministic 3-node fixture, no LLM call, reused by both the wizard and
the canvas's own new empty-state hint), and gets out of the way. Persisted
via a new hasCompletedOnboarding setting, reopenable later from Settings.

Adds a non-blank empty-canvas hint in SceneCanvas.tsx, offering the same
sample-workspace load when the scene has zero nodes.

Adds 3 Settings surfaces this ADR's own stage table called for:
- A working provider-mode switcher (ProviderModeSwitch), wiring the
  frontend to the setProviderMode intent that already existed but nothing
  called - placed on each of the 3 per-mode pages rather than reviving the
  cramped toolbar control removed earlier this ADR. Corrects that removal's
  own now-stale doc comment in AppBar.tsx.
- An MCP Servers page (list/add/remove/enable), plus the setMcpServers
  intent and wire-contract field ADR-007's own docstring had explicitly
  deferred to this stage - MCP server config was previously edit-the-JSON-
  by-hand only.
- A read-only Resource Limits section surfacing the same backend-computed
  disclosure CodeExecutionApprovalPanel.tsx already shows mid-approval,
  now reachable ahead of time. Found and fixed a real bug in the process:
  ExecutionLimitsProvider only wrapped SceneCanvas, not the Settings
  dialog sibling, so the new section would have silently read blank text.

The DPAPI/secret-visibility badge (ADR-004) was already fully shipped;
confirmed via recon, no new work needed for it in this stage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
eslint-plugin-jsx-a11y's published peer range still tops out at ESLint
^9 (no ^10 support yet upstream, confirmed against the latest published
version), so a plain `npm ci` - what CI actually runs, no --legacy-peer-
deps - hits an ERESOLVE conflict against this repo's eslint@^10.8.0 and
fails before any check ever runs. Locally this was masked by installing
with --legacy-peer-deps, which was never carried into the CI workflow.

Fixes it with a targeted `overrides` entry pinning just this one peer
resolution to the project's actual eslint version, rather than relaxing
peer-dependency enforcement for the whole install. Regenerated
package-lock.json from a clean install; `npm ci` now succeeds and the
full suite (1843 frontend tests, tsc, eslint) passes unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit 7c2f0fe into main Aug 10, 2026
3 checks passed
@dovvnloading
dovvnloading deleted the adr012-ui-ux-system branch August 10, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant