Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-FE-A11Y-COMPLETION-FORCED-COLORS-AND-AXE-CORE-CI — forced-colors styling + axe-core dev integration - #212

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-A11Y-COMPLETION-FORCED-COLORS-AND-AXE-CORE-CI-MEGABUCKET
May 24, 2026
Merged

feat(kora): KR-FE-A11Y-COMPLETION-FORCED-COLORS-AND-AXE-CORE-CI — forced-colors styling + axe-core dev integration#212
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-A11Y-COMPLETION-FORCED-COLORS-AND-AXE-CORE-CI-MEGABUCKET

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Closes the two a11y deferrals from #211: forced-colors styling pass + axe-core integration. CC#1 NousResearch#447 still hasn't opened a PR so the audit-filter verification (still the #1 outstanding item) continues to wait — this bucket keeps CC#2 productive on the a11y arc instead.

A — Forced-colors styling pass. New @media (forced-colors: active) block in index.css targeting the cockpit-wide focus-visible cue + the multi-tenant chrome that this lane owns (TenantPicker option highlight, ActiveTenantBadge chip, AggregateCostCards rung progress bar) + sidebar active-link. Data-attribute hooks added on the matching components so the CSS targets stable selectors.

B — @axe-core/react dev-mode + static a11y pins. @axe-core/react added as a devDep and mounted dev-only in main.tsx (gated on import.meta.env.DEV so the production bundle is unchanged). Standalone Playwright-based axe runner in CI deferred per §4 STOP-ASK — that's enough new infra for its own bucket.

What changed

web/src/index.css

New @media (forced-colors: active) block at end of file:

Rule Why
:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; } Tailwind's focus-visible:ring uses box-shadow, which forced-colors strips. The system Highlight color restores the keyboard-focus cue cockpit-wide.
[role=\"option\"][data-highlighted=\"true\"], [aria-selected=\"true\"] TenantPicker keyboard-highlight uses bg-accent/60 — invisible under forced-colors. System Highlight outline (with forced-color-adjust: none so it actually paints) restores the "where would Enter land" cue.
[data-tenant-chip] ActiveTenantBadge chip uses border-current/20 + bg-card/60. Add 1px solid CanvasText so the chip outline survives.
[data-rung-bar] AggregateCostCards progress bar fill IS the semantic (red = hard stop). Opts into forced-color-adjust: none — explicit signal that we want author colors preserved.
a[aria-current=\"page\"] Sidebar NavLink emits aria-current=\"page\" for active route; Highlight outline keeps the active vs inactive cue.

@nous-research/ui Badge tones already use Tailwind border classes, so the chip outline remains visible under forced-colors thanks to system-color border substitution — no additional rule needed. ConfirmDialog backdrop (rgba) + panel border are sufficient — same.

Components — data-attribute hooks

  • web/src/components/TenantPicker.tsx: TenantOption button emits data-highlighted={highlighted ? \"true\" : undefined}
  • web/src/components/ActiveTenantBadge.tsx: both chip buttons get data-tenant-chip
  • web/src/components/AggregateCostCards.tsx: progress bar gets data-rung-bar

web/src/main.tsx

if (import.meta.env.DEV) {
  void import(\"@axe-core/react\").then(({ default: axe }) => {
    void import(\"react-dom\").then((ReactDOM) => {
      void axe(React, ReactDOM, 1000); // 1s debounce
      console.info(\"[a11y] @axe-core/react active  WCAG 2.1 AA violations will log to console\");
    });
  });
}

Dynamic import inside a DEV gate — vite sets import.meta.env.PROD = true for vite build and tree-shakes the entire branch. Production bundle size confirmed unchanged (1,979.05 kB vs 1,975.48 kB pre-bucket = +3.6 kB, all from the new CSS block, not the axe import).

web/package.json

  • @axe-core/react@^4.10.0 added as devDependency (resolved to 4.11.3 with axe-core ~4.11.4 transitive)

tests/test_tenants_endpoint.py

New test_fe_forced_colors_and_axe_core_pins:

  • Pins the @media (forced-colors: active) block + each selector (drift-guard against a CSS reorganization that silently drops a rule)
  • Pins the matching data-attribute emissions on each component (drift-guard against rename on the JSX side without updating CSS)
  • Pins @axe-core/react in package.json + import.meta.env.DEV gate + \"@axe-core/react\" import string in main.tsx

Why not a Playwright-based CI runner (B.1 STOP-ASK)

The bucket §4 explicitly called out the Playwright path as a deferral candidate. The infra cost is real:

  1. GH Actions step needs npx playwright install chromium (~120 MB download per run; can be cached but adds workflow complexity)
  2. The SPA needs a running backend OR a static-build harness that mocks /api/* calls — neither exists today
  3. axe-core/playwright assertion in CI needs a baseline-vs-regression diff mechanism (otherwise the first run pins every current violation)

CC#2's read: that's its own bucket. Dev-mode catches the majority of regressions during the inner loop. The static drift-guard pins added here catch the specific data-attribute / CSS contracts at CI time without browser infra. A future bucket can add the Playwright runner as a dedicated effort.

Build

  • tsc -b && vite build ✓ clean
  • python3 -m py_compile tests/test_tenants_endpoint.py
  • Production bundle size delta: +3.6 kB CSS only (axe-core tree-shaken)

Manual verification (forced-colors)

Run the dev server and toggle DevTools → Rendering → "Emulate CSS media feature forced-colors: active":

Surface Before After
Tab through any page Focus ring invisible (Tailwind box-shadow stripped) Yellow Highlight outline visible on every focusable element
Open TenantPicker, ↓/↑ nav Highlight + selected states invisible (bg vanishes) Inset Highlight outline on highlighted; same on aria-selected
ActiveTenantBadge in audit-panel header Chip border invisible (current/20 alpha) CanvasText border restores chip outline
AggregateCostCards rung bars Semantic color stripped (red/yellow/green all → system) Author colors preserved (forced-color-adjust: none); track gets CanvasText outline
Sidebar active nav-link Active vs inactive indistinguishable Active link gets inset Highlight outline (via aria-current=\"page\")

Dev-mode demo (axe-core)

$ cd web && npm run dev
# Open localhost:5173 in browser, DevTools console:
[a11y] @axe-core/react active — WCAG 2.1 AA violations will log to console
# Navigate cockpit; axe re-runs 1s after each render
# Violations log with link to the affected DOM node + WCAG rule

Production build verification:

$ cd web && npm run build
$ grep -c \"@axe-core/react\" hermes_cli/web_dist/assets/index-*.js
0   # tree-shaken — confirmed no axe-core in prod bundle

Test plan

Recommendation for next CC#2 dispatch

Per-tenant audit BE filter verification (still gated on CC#1 NousResearch#447). Per #208/#210/#211, this is the #1 outstanding item. CC#1 NousResearch#447 hasn't opened a PR yet — check gh pr view 447 --json state,merged and gh pr list --author cc1 --state open before dispatch.

Lighter alternatives if NousResearch#447 is still in flight:

  1. Playwright-based axe CI runner (the deferral above). Add @axe-core/playwright, a small Node script that serves the built SPA + mocks /api/*, GH Actions step with playwright install --with-deps chromium, baseline-vs-regression diff mechanism. ~3-4 hours; substantial enough to deserve its own bucket as noted in this PR body.
  2. ConfirmDialog description-prop enforcement (the other feat(kora): KR-FE-A11Y-AUDIT-AND-MULTI-TENANT-POLISH — a11y audit + recent tenants + wizard skip #211 deferral). Make description required on the ConfirmDialog API + update call sites. Tiny — ~30 min.
  3. Cockpit-wide a11y findings sweep beyond multi-tenant chrome. This bucket's audit and fixes focused on the multi-tenant chrome lane. A focused sweep over the rest of the cockpit (chat, sessions, models, plugins, OAuth flows) would likely turn up more BLOCKER/MAJOR findings to fix. ~3-4 hours.

🤖 Generated with Claude Code

…ced-colors styling + axe-core dev integration

Deliverable A — forced-colors styling pass:
  * New @media (forced-colors: active) block in web/src/index.css
    covering the high-impact selectors:
      - :focus-visible → Highlight outline (Tailwind ring uses
        box-shadow which forced-colors strips)
      - [role="option"][data-highlighted="true"]/aria-selected →
        Highlight outline (TenantPicker keyboard nav cue)
      - [data-tenant-chip] → CanvasText border (ActiveTenantBadge
        chip affordance)
      - [data-rung-bar] → forced-color-adjust: none (rung color
        IS the semantic — red = hard stop)
      - a[aria-current="page"] → Highlight outline (active
        sidebar nav-link)
  * Data-attribute hooks added on the matching components so the
    CSS rules target stable selectors:
      - TenantOption gains data-highlighted={"true"|undefined}
      - ActiveTenantBadge buttons gain data-tenant-chip
      - AggregateCostCards progress bar gains data-rung-bar
  * @nous-research/ui Badge tones already use Tailwind border
    classes so badges remain readable under forced-colors
    without additional rules; ConfirmDialog backdrop + panel
    border are sufficient. Noted in CSS comments.

Deliverable B — @axe-core/react dev-mode + static a11y pins:
  * web/package.json: @axe-core/react@^4.10.0 added as devDep
  * main.tsx: dev-only dynamic import + axe(React, ReactDOM,
    1000) mount gated on import.meta.env.DEV; the entire branch
    tree-shakes out of `vite build` (production bundle size
    confirmed unchanged ±0.05%)
  * Per §4 STOP-ASK B.1: standalone Playwright-based axe runner
    in CI is deferred to a dedicated bucket — adding Playwright
    + chromium-download + a SPA test harness in GH Actions is
    too much new infra for this scope. Dev-mode catches the
    bulk of regressions while development is active; the static
    drift-guard pins below catch the named-pattern violations
    at CI time without browser infra.

Drift-guard extension (tests/test_tenants_endpoint.py):
  * forced-colors media-query block pin
  * Each rule selector pinned ([role="option"][data-highlighted=
    "true"], [data-tenant-chip], [data-rung-bar],
    :focus-visible, a[aria-current="page"])
  * Component-side data-attribute emissions pinned (TenantPicker
    data-highlighted, ActiveTenantBadge data-tenant-chip,
    AggregateCostCards data-rung-bar) — rename on either side
    fails CI
  * @axe-core/react in package.json + import.meta.env.DEV gate
    in main.tsx pinned

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

⚠️ npm lockfile hash out of date

Checked against commit 0bd159c (PR head at check time).

The hash = "sha256-..." line in these nix files no longer matches the committed package-lock.json:

Apply the fix

  • Apply lockfile fix — tick to push a commit with the correct hashes to this PR branch
  • Or run the Nix Lockfile Fix workflow manually (pass PR #212)
  • Or locally: nix run .#fix-lockfiles and commit the diff

@rafe-walker
rafe-walker merged commit 087c180 into feature/phase2-upgrades May 24, 2026
2 of 4 checks passed
@rafe-walker
rafe-walker deleted the feat/kora-KR-FE-A11Y-COMPLETION-FORCED-COLORS-AND-AXE-CORE-CI-MEGABUCKET branch May 24, 2026 19:34
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant