Skip to content

Commit 75d663f

Browse files
committed
feat(a11y): rework inspector — routes, dashboard, pins, auto-scan, config
Rework the a11y inspector to track violations per route, present them through a Dashboard + grouped Violations tab, and highlight with a preview ring plus pinned, numbered badges (root-element targets get a corner notice). The agent detects routes framework-neutrally (History-API patch + popstate/hashchange), persists its route→report map in sessionStorage, broadens the axe tag set to WCAG 2.0–2.2 + best-practice (tagged and filterable), adds interaction-driven auto-scan, and logs newly-appeared violations to the console. Author options (auto-scan, logging, default-highlight, axe tags/options) flow through `get-config` to the panel, which forwards the runtime slice to the agent over the BroadcastChannel. Each mirrored messages-feed entry carries a navigation action back to the deep-linked rule/route, and the panel honours dock activations from other docks. Co-authored-with an agent.
1 parent d433b5b commit 75d663f

25 files changed

Lines changed: 2031 additions & 253 deletions

‎plugins/a11y/README.md‎

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,25 @@
55
> it stabilizes.
66
77
An accessibility inspector built on [devframe](../../packages/devframe). It runs
8-
[axe-core](https://github.com/dequelabs/axe-core) against a host application,
9-
lists the WCAG A/AA violations in a [Solid](https://www.solidjs.com/) panel, and
10-
**highlights the offending element in the page when you hover a warning**.
8+
[axe-core](https://github.com/dequelabs/axe-core) against a host application and
9+
surfaces the violations in a [Solid](https://www.solidjs.com/) panel:
10+
11+
- **Route-aware tracking** — buckets violations by `location.pathname` and tracks
12+
them as you navigate the app (History-API patched, framework-neutral), persisted
13+
in `sessionStorage` so history survives reloads within a tab session.
14+
- **Dashboard + grouped violations** — a Dashboard tab (totals, severity
15+
breakdown, per-route inventory, scan controls) and a Violations tab listing
16+
every tracked route, grouped, with the active route marked.
17+
- **Pin + numbered highlights** — hover previews the offending element; clicking a
18+
rule pins all its elements (clicking a single element toggles just that one)
19+
with globally-numbered badges drawn in the page. `<html>`/`<body>` targets get a
20+
corner notice instead of a viewport-filling ring.
21+
- **Constant scanning** — a DOM `MutationObserver` plus debounced
22+
mouse/keyboard/touch rescans, toggleable from the Dashboard.
23+
- **WCAG 2.0–2.2 + best-practice** — the broadened axe tag set, with best-practice
24+
rules tagged and filterable.
25+
- **Console logging** — newly-appeared violations are logged (deduped) to the
26+
browser console.
1127

1228
The scan + highlight loop works the same whether the plugin runs as a live dev
1329
server or as a baked static build.
@@ -18,17 +34,20 @@ Three pieces, two of them browser-side:
1834

1935
| Piece | Runs in | Role |
2036
|-------|---------|------|
21-
| **Agent** (`src/inject`) | the host app's page | runs axe-core, broadcasts the report, draws the highlight ring |
22-
| **Panel** (`src/spa`) | the devtools iframe | Solid SPA: lists violations, fires highlight/clear on hover |
23-
| **Node** (`src/index.ts`, `src/node`, `src/rpc`) | the devframe backend | `get-config` RPC (impact taxonomy) — live in dev, baked in a static build |
37+
| **Agent** (`src/inject`) | the host app's page | runs axe-core, tracks routes, broadcasts the aggregate state, draws the preview + pinned rings |
38+
| **Panel** (`src/spa`) | the devtools iframe | Solid SPA: Dashboard + grouped violations, fires preview/pin/rescan |
39+
| **Node** (`src/index.ts`, `src/node`, `src/rpc`) | the devframe backend | `get-config` RPC (impact taxonomy + runtime config) — live in dev, baked in a static build |
2440

2541
The agent and panel talk over a same-origin
2642
[`BroadcastChannel`](src/shared/protocol.ts), not the devframe RPC backend. That
2743
is what keeps the live loop working in **both modes**: neither half needs a
2844
server to reach the other, only a shared browser origin (host page + panel
29-
iframe). devframe RPC carries the data model on top — `get-config` is a `static`
30-
function, so it resolves over WebSocket in dev and from the baked dump in a
31-
static build.
45+
iframe). The agent owns the authoritative route → report map and broadcasts the
46+
whole aggregate on every change, so the panel stays a pure render of it. devframe
47+
RPC carries the data model on top — `get-config` is a `static` function, so it
48+
resolves over WebSocket in dev and from the baked dump in a static build; the
49+
panel forwards its runtime-config slice to the agent over the channel, keeping the
50+
agent itself free of any RPC dependency.
3251

3352
devframe deliberately provides no access to the host application's DOM, so the
3453
agent is the author-provided bridge into the page being checked. In a hub, the
@@ -37,14 +56,34 @@ dock's `clientScript` (resolved to an importable URL — `/@fs/…` under Vite,
3756
statically-served path) and the hub's client runtime (`createDevframeClientHost`
3857
from `@devframes/hub/client`) imports it into the host page and calls its
3958
default export with the client-script context. Booted that way, the agent also
40-
mirrors each scan into the hub's **messages feed** — a summary entry driven
41-
through the loading → idle lifecycle plus one entry per violated rule, carrying
42-
the impact-mapped level, WCAG tags as labels, and the first offending element's
43-
selector and bounding box (rendered by `@devframes/plugin-messages` when the
44-
hub mounts it). Both minimal hub examples do exactly this. Outside a hub, one
59+
mirrors the active route's scan into the hub's **messages feed** — a summary entry
60+
driven through the loading → idle lifecycle plus one entry per violated rule,
61+
carrying the impact-mapped level, WCAG tags as labels, and the first offending
62+
element's selector and bounding box (rendered by `@devframes/plugin-messages` when
63+
the hub mounts it). Each entry also carries a **navigation action**: clicking it
64+
in the messages panel activates the a11y dock (`hub:docks:activate`) deep-linked
65+
to the rule + route (or the Dashboard, for the summary). Both minimal hub examples
66+
do exactly this. Outside a hub, one
4567
`<script type="module">` for the same bundle does the job — the demo below
4668
shows it (no hub context, so the feed mirror simply stays off).
4769

70+
## Configuration
71+
72+
Pass options to `createA11yDevframe()` (surfaced through `get-config`, so they
73+
reach both the panel and the agent):
74+
75+
```ts
76+
createA11yDevframe({
77+
autoScan: true, // rescan on debounced interaction (default true)
78+
logIssues: true, // log new violations to the console (default true)
79+
defaultHighlight: false, // auto-pin a route's violations on first scan (default false)
80+
axe: {
81+
tags: ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa', 'best-practice'],
82+
runOptions: {}, // extra axe `run` options merged over the defaults
83+
},
84+
})
85+
```
86+
4887
## Run the demo
4988

5089
The demo serves an intentionally-broken host page and the panel from **one
@@ -74,7 +113,7 @@ pnpm -C plugins/a11y dev # from source: same, at /__devframes_plugin_a11
74113
| Path | Export | Purpose |
75114
|------|--------|---------|
76115
| `src/index.ts` | `.` | `createA11yDevframe()` + the default `DevframeDefinition`; `a11yAgentBundlePath` — the agent module a hub attaches as this dock's client script |
77-
| `src/node/index.ts` | `/node` | `setupA11y(ctx)` — registers the RPC functions |
116+
| `src/node/index.ts` | `/node` | `setupA11y(ctx, options?)` — registers the RPC functions with the runtime config |
78117
| `src/cli.ts` | `/cli` | `createA11yCli()` — backs the `devframes_plugin_a11y` bin |
79118
| `src/vite.ts` | `/vite` | `a11yVitePlugin()` — mounts the panel into a Vite host |
80119
| `src/client/index.ts` | `/client` | `connectA11y()` — typed browser RPC client wrapper |

‎plugins/a11y/src/index.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ export interface A11yDevframeOptions {
4040
basePath?: string
4141
/** Preferred standalone CLI port. */
4242
port?: number
43+
/**
44+
* Rescan on debounced user interaction (mouse/keyboard/touch), on top of the
45+
* DOM MutationObserver. Default `true`.
46+
*/
47+
autoScan?: boolean
48+
/** Log newly-appeared violations to the browser console. Default `true`. */
49+
logIssues?: boolean
50+
/**
51+
* Auto-pin all of a route's violations the first time it's scanned.
52+
* Default `false`.
53+
*/
54+
defaultHighlight?: boolean
55+
/** axe-core configuration. */
56+
axe?: {
57+
/** Rule tags to run (defaults to the broadened WCAG 2.0–2.2 + best-practice set). */
58+
tags?: string[]
59+
/** Extra axe `run` options merged over the defaults. */
60+
runOptions?: Record<string, unknown>
61+
}
4362
}
4463

4564
/**
@@ -70,7 +89,13 @@ export function createA11yDevframe(options: A11yDevframeOptions = {}): DevframeD
7089
},
7190
spa: { loader: 'none' },
7291
setup(ctx) {
73-
setupA11y(ctx)
92+
setupA11y(ctx, {
93+
dockId: id,
94+
autoScan: options.autoScan,
95+
logIssues: options.logIssues,
96+
defaultHighlight: options.defaultHighlight,
97+
axe: options.axe,
98+
})
7499
},
75100
})
76101
}

0 commit comments

Comments
 (0)