Skip to content

Commit 4a0846a

Browse files
committed
refactor(a11y): single-page panel, compact rows, icon + category fixes
- Collapse the Dashboard/Violations tabs into one scrolling page: a sticky summary band (severity chips + counts + auto-scan/best-practice/clear controls) over the grouped, per-route violation list. - Tighten the violation rows, chips, and group headers for a denser list. - Fix icon rendering: pair every icon with a companion utility class (and mark it aria-hidden), matching how @antfu/design renders icons — a bare single-class `i-ph-*` span isn't extracted by UnoCSS, so Rescan/pin/clear icons were blank. - Show the messages-feed entries under a short `a11y` category instead of the `devframes_plugin_a11y` dock id. Co-authored-with an agent.
1 parent 56513fa commit 4a0846a

10 files changed

Lines changed: 260 additions & 480 deletions

File tree

plugins/a11y/src/inject/messages.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export interface HubMessageInput {
3131
message: string
3232
description?: string
3333
level: 'info' | 'warn' | 'error' | 'success' | 'debug'
34+
/**
35+
* Grouping category shown in the messages panel. Set explicitly to a short
36+
* `'a11y'` label — the hub client host otherwise defaults it to the dock id
37+
* (e.g. `devframes_plugin_a11y`), and input fields win over that default.
38+
*/
39+
category?: string
3440
labels?: string[]
3541
elementPosition?: {
3642
selector?: string
@@ -55,6 +61,8 @@ export interface A11yAgentContext {
5561
messages?: HubMessagesClient
5662
}
5763

64+
/** Short, human-facing grouping label shown in the messages panel. */
65+
const MESSAGE_CATEGORY = 'a11y'
5866
/** One deduplicated summary entry tracks the scan lifecycle. */
5967
const SCAN_MESSAGE_ID = 'devframes:plugin:a11y:scan'
6068
/** One deduplicated entry per violated rule; removed when the rule clears. */
@@ -104,7 +112,9 @@ export function createMessagesReporter(
104112
): MessagesReporter {
105113
let reportedRules = new Set<string>()
106114
// Fire-and-forget: the feed is a mirror, never a gate for the scan loop.
107-
const send = (input: HubMessageInput) => void messages.add(input).catch(() => {})
115+
// Every entry is grouped under the short `a11y` category.
116+
const send = (input: HubMessageInput) =>
117+
void messages.add({ category: MESSAGE_CATEGORY, ...input }).catch(() => {})
108118
const drop = (id: string) => void messages.remove(id).catch(() => {})
109119
const dockId = () => options.dockId?.() ?? A11Y_DEFAULT_DOCK_ID
110120

plugins/a11y/src/spa/app.tsx

Lines changed: 49 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { Impact, PinTarget, ScanReport, Violation, ViolationNode } from '../shared/protocol.ts'
2-
import type { TabId } from './components/header.tsx'
32
import type { PinsApi, RouteGroupModel } from './components/violations.tsx'
43
import { batch, createEffect, createMemo, createSignal, Match, on, Show, Switch } from 'solid-js'
54
import { emptyCounts } from '../shared/protocol.ts'
6-
import { Dashboard } from './components/dashboard.tsx'
75
import { Header, MetaLine } from './components/header.tsx'
86
import { CheckCircle, PlugIcon } from './components/icons.tsx'
7+
import { SummaryBar } from './components/summary.tsx'
98
import { ruleCardId, ViolationList } from './components/violations.tsx'
109
import { createA11yChannel } from './lib/channel.ts'
1110
import { connectDevframeState } from './lib/devframe.ts'
@@ -24,7 +23,6 @@ export function App() {
2423
const channel = createA11yChannel()
2524
const devframe = connectDevframeState()
2625

27-
const [activeTab, setActiveTab] = createSignal<TabId>('dashboard')
2826
const [filter, setFilter] = createSignal<Impact | null>(null)
2927
const [showBestPractice, setShowBestPractice] = createSignal(true)
3028
const [expandedRoutes, setExpandedRoutes] = createSignal<Set<string>>(new Set())
@@ -68,16 +66,7 @@ export function App() {
6866
const totalRules = createMemo(() =>
6967
routes().reduce((n, r) => n + r.violations.filter(includeViolation).length, 0))
7068

71-
const routeSummaries = createMemo(() =>
72-
routes().map(r => ({
73-
route: r.route,
74-
active: r.route === activeRoute(),
75-
ruleCount: r.violations.filter(includeViolation).length,
76-
nodeCount: r.violations.filter(includeViolation).reduce((m, v) => m + v.nodes.length, 0),
77-
})))
78-
79-
// Grouped, filtered violations for the Violations tab — active route first,
80-
// empty groups dropped.
69+
// Grouped, filtered violations — active route first, empty groups dropped.
8170
const groups = createMemo<RouteGroupModel[]>(() => {
8271
const models = routes().map(r => ({
8372
report: r,
@@ -183,14 +172,15 @@ export function App() {
183172
if (cfg && act.dockId !== cfg.dockId)
184173
return
185174
const params = act.params ?? {}
186-
const tab = params.tab === 'dashboard' ? 'dashboard' : 'violations'
187-
setActiveTab(tab)
188-
if (tab === 'dashboard')
189-
return
190175
const route = typeof params.route === 'string' ? params.route : undefined
191176
const ruleId = typeof params.ruleId === 'string' ? params.ruleId : undefined
192-
if (!route)
177+
178+
// A dashboard/summary activation (or one with no target) just scrolls to top.
179+
if (params.tab === 'dashboard' || !route) {
180+
document.querySelector('.scroll')?.scrollTo({ top: 0, behavior: 'smooth' })
193181
return
182+
}
183+
194184
batch(() => {
195185
setExpandedRoutes(prev => new Set(prev).add(route))
196186
if (ruleId)
@@ -205,7 +195,6 @@ export function App() {
205195
return [...prev, ...v.nodes.filter(n => !have.has(n.id)).map(n => nodePin(v, n))]
206196
})
207197
}
208-
// Scroll the card into view once it has rendered.
209198
setTimeout(() => document.getElementById(ruleCardId(route, ruleId))?.scrollIntoView({ block: 'center', behavior: 'smooth' }), 60)
210199
}
211200
}, { defer: true }))
@@ -234,11 +223,6 @@ export function App() {
234223
return next
235224
})
236225
}
237-
function selectRoute(route: string) {
238-
setActiveTab('violations')
239-
setExpandedRoutes(prev => new Set(prev).add(route))
240-
setTimeout(() => document.getElementById(`group-${encodeURIComponent(route)}`)?.scrollIntoView({ block: 'start', behavior: 'smooth' }), 60)
241-
}
242226

243227
const announce = () => {
244228
if (!channel.state())
@@ -251,8 +235,6 @@ export function App() {
251235
<Header
252236
agentReady={channel.agentReady()}
253237
scanning={channel.scanning()}
254-
activeTab={activeTab()}
255-
onTab={setActiveTab}
256238
onRescan={channel.rescan}
257239
/>
258240
<MetaLine
@@ -289,25 +271,7 @@ export function App() {
289271
</div>
290272
</Match>
291273

292-
{/* Dashboard. */}
293-
<Match when={activeTab() === 'dashboard'}>
294-
<Dashboard
295-
counts={chipCounts()}
296-
filter={filter()}
297-
onToggleFilter={toggleFilter}
298-
totalNodes={totalNodes()}
299-
totalRules={totalRules()}
300-
routes={routeSummaries()}
301-
onSelectRoute={selectRoute}
302-
autoScan={autoScan()}
303-
onToggleAutoScan={setAutoScan}
304-
showBestPractice={showBestPractice()}
305-
onToggleBestPractice={setShowBestPractice}
306-
onClearAll={channel.clearAll}
307-
/>
308-
</Match>
309-
310-
{/* Violations — clean. */}
274+
{/* Report in, nothing to flag. */}
311275
<Match when={totalFilterable() === 0}>
312276
<div class="state state--clean">
313277
<CheckCircle class="state__glyph" />
@@ -319,43 +283,48 @@ export function App() {
319283
</div>
320284
</Match>
321285

322-
{/* Violations — filtered to empty. */}
323-
<Match when={shownViolations() === 0}>
324-
<div class="state">
325-
<CheckCircle class="state__glyph" />
326-
<p class="state__title">Nothing matches the filter</p>
327-
<p class="state__body">
328-
{totalFilterable()}
329-
{' '}
330-
{totalFilterable() === 1 ? 'rule' : 'rules'}
331-
{' '}
332-
at other severities. Clear the filter to see them.
333-
</p>
334-
</div>
335-
</Match>
286+
{/* Single page: summary band + grouped violations. */}
287+
<Match when={totalFilterable() > 0}>
288+
<SummaryBar
289+
counts={chipCounts()}
290+
filter={filter()}
291+
onToggleFilter={toggleFilter}
292+
totalNodes={totalNodes()}
293+
totalRules={totalRules()}
294+
routeCount={routes().length}
295+
autoScan={autoScan()}
296+
onToggleAutoScan={setAutoScan}
297+
showBestPractice={showBestPractice()}
298+
onToggleBestPractice={setShowBestPractice}
299+
onClearAll={channel.clearAll}
300+
/>
336301

337-
{/* Violations — grouped list. */}
338-
<Match when={shownViolations() > 0}>
339-
<Show when={shownViolations() !== totalFilterable()}>
340-
<p class="filtered-note">
341-
Showing
342-
{' '}
343-
{shownViolations()}
344-
{' of '}
345-
{totalFilterable()}
346-
{' rules'}
347-
</p>
302+
<Show
303+
when={shownViolations() > 0}
304+
fallback={(
305+
<div class="state">
306+
<CheckCircle class="state__glyph" />
307+
<p class="state__title">Nothing matches the filter</p>
308+
<p class="state__body">
309+
{totalFilterable()}
310+
{' '}
311+
{totalFilterable() === 1 ? 'rule' : 'rules'}
312+
{' at other severities. Clear the filter to see them.'}
313+
</p>
314+
</div>
315+
)}
316+
>
317+
<ViolationList
318+
groups={groups()}
319+
collapsedRoutes={collapsedRoutes()}
320+
onToggleGroup={toggleGroup}
321+
onClearRoute={channel.clearRoute}
322+
expandedRules={expandedRules()}
323+
onToggleRule={toggleRule}
324+
channel={channel}
325+
pins={pinsApi}
326+
/>
348327
</Show>
349-
<ViolationList
350-
groups={groups()}
351-
collapsedRoutes={collapsedRoutes()}
352-
onToggleGroup={toggleGroup}
353-
onClearRoute={channel.clearRoute}
354-
expandedRules={expandedRules()}
355-
onToggleRule={toggleRule}
356-
channel={channel}
357-
pins={pinsApi}
358-
/>
359328
</Match>
360329
</Switch>
361330
</div>

plugins/a11y/src/spa/components/dashboard.stories.tsx

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)