You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ui: fixes and improvements for the panel shell, popup and accessibility #353
Fixes and improvements from a code audit of the overlay, popup and panel shell, panel ux and accessibility, done in October 2026. Roadmap work lives in #340, #341, #342 and #343. Items marked (to confirm) were not reproduced; check them before fixing.
The overlay, popup and panel shell
Problems
Medium
No retry after a failed first connection:connectDevframe runs once, and on rejection the panel sets connectionFailed and asks the user to reload. A dev server that starts slowly or restarts needs a manual reload. Retry with backoff (1, 2, 5, 10 s) while connectionFailed is set and add a "Retry now" button. The popup's findTarget also only retries on the next open (app/src/app.ts:913-947, app/src/app.ts:372-375, packages/devtools/src/popup.ts:114-121).
Low
Popup accepts theme messages from any window:onThemeMessage only rejects e.source === window, so any iframe or script on the host page can flip the popup theme, and the value is saved to localStorage. Accept the message only from iframe.contentWindow (or a frame inside it) with a matching origin (packages/devtools/src/popup.ts:827-832).
Theme posted to every ancestor with '*':ThemeService walks window.parent and posts pangular:theme-change with target origin '*', which leaks to unknown origins when the panel is embedded cross-origin. Post to the parent's origin (from document.referrer or ancestorOrigins), or rely on the BroadcastChannel for same-origin cases (app/src/theme.service.ts:36-47).
Tab strip is not a tablist: the inspector tabs are buttons with aria-current="page", with no roving tabindex or arrow keys, so ten tabs are ten tab stops before the content. Use role=tablist/tab/tabpanel with aria-selected and roving tabindex, or keep the nav pattern and add arrow-key handling (app/src/app.ts:346-358).
Opening the panel leaves focus on the FAB:togglePanel only moves focus on close. When opened from the keyboard (click with detail === 0), focus the iframe or its main once loaded (packages/devtools/src/popup.ts:670-675).
Hub views listed twice:HUB_VIEWS is defined in both the panel and the package, and the pangular: dock prefix is repeated too. Adding a dock in one place breaks followHubDocks and view titles silently. Export HUB_VIEWS and dockId from one module and import them in the app (app/src/app.ts:43-50, packages/devtools/src/hub-docks.ts:9-16, packages/devtools/src/hub-docks.ts:48, app/src/hub-dock-sync.ts:2, app/src/app.ts:1088).
Overlay and popup probe different bases: the overlay tries ['./', '/__pangular/', '/__devframes/pangular/'] and the popup tries ['/__pangular/', '/__devframes/pangular/', '/__devframe/', '/'], so one can find a server the other misses. Keep one exported list in panel-frame.ts and use it in both (packages/devtools/src/overlay.ts:177, packages/devtools/src/popup.ts:61).
Untyped hub:docks:activate call: the fallback dock switch casts the RPC client as unknown and calls hub:docks:activate. If the client has no top-level call, the panel quietly switches to the fallback tab instead of the dock. Use the typed hub client API and add a test that the hub path activates the dock (app/src/app.ts:1073-1094) (to confirm).
Short page id: the page id is 4 base36 characters from Math.random, and the BroadcastChannel claim only catches collisions inside one browser. Pages from other devices (NativeScript, Angular Native, a phone) reporting to one hub can collide. Use crypto.randomUUID().slice(0, 8) or longer, staying under the server's 50-character limit (packages/devtools/src/overlay.ts:96, packages/devtools/src/devframe.ts:272).
Improvements
Move limits, redaction, refreshMs and the action toggles from config-file-only to ctx.settings, with a Settings view in the panel (packages/devtools/src/config.ts, packages/devtools/src/devframe.ts, app/src/app.ts).
Export a programmatic buildReport() next to the CLI build, so CI and Analog/Vite builds can produce the static report without the CLI wrapper (packages/devtools/src/cli.ts, packages/devtools/package.json).
Remember scroll position and selection per tab, not only the tab id, in sessionStorage keyed by pageId, so a popup or extension reload returns to the same item (app/src/tab-memory.ts, app/src/app.ts).
A minimized "pill" launcher that shows the render count and error count on the FAB (packages/devtools/src/popup.ts).
Error states without Retry: several error states tell the user to reload or reopen the panel and offer no button. Add a Retry button that calls the existing refresh() or load function, as pipes-inspector.ts:188-192 already does. In the shell, retry the devframe connection with backoff and show a "Reconnect" button (app/src/pages/network-inspector.ts:296-303, app/src/pages/dashboard.ts:234-241, app/src/app.ts:372-375).
Store has no loading state: before the first shared state arrives, the store page shows "No page is reporting NgRx state yet.", so NgRx users see a wrong message for a moment. Set a loaded flag on the first shared state value and show a role="status" "Loading…" state until then (app/src/pages/store-inspector.ts:130-143).
No arrow-key navigation in long lists: the network timeline (up to 2,000 rows), the store change and event lists and the signals node list need one Tab per row. Add roving tabindex with ArrowUp/Down and Home/End, like onListKey in pipes-inspector.ts:1341. A shared appRovingList directive would cover all of them (app/src/pages/network-inspector.ts:595-616, app/src/pages/store-inspector.ts:362, 499).
Low
Search fields differ from page to page: the guidelines ask for a search field with an icon, Escape to clear and a count. Only six pages have the search icon. It is missing on signal-inspector.ts:159, store-inspector.ts:100, forms-inspector.ts:358, route-tree.ts:115, route-timeline.ts:37 and route-inspector.ts:55. Placeholders mix "Filter" and "Find", and only some end with "…". Extract an <app-search-field> in app/src/ui with the icon, Escape, autocomplete="off", spellcheck="false" and an optional count, and pick one verb.
Route tree has no tree semantics: depth is shown only by padding-left, with no aria-level, aria-expanded or treegrid role, so screen readers lose the hierarchy. Use role="treegrid" with aria-level and aria-expanded, and support Left/Right to collapse and expand (app/src/pages/route-tree.ts:176-177).
Filtered lists tracked by $index: when the filter changes, rows are reused by position, so focus and selection jump. Track by a stable key such as route.file + route.fullPath, a finding's code and location, or a component id (app/src/pages/route-inspector.ts:118, route-current.ts:95, 167, route-lint.ts:65, forms-report.ts:162, cd-recording.ts:89).
Hex colors in the Analog view: the info and kind colors are hard-coded hex values outside the token system and the tint-contrast test. Move them to app/src/styles/_palette.scss or app/src/view-accents.ts with dark and light pairs, and add them to app/src/__tests__/tint-contrast.test.ts (app/src/pages/analog-inspector.ts:1133-1138, 1693-1725).
"Inspector turned off" gives no next step: the message names the inspectors key but has no docs link and no snippet. Add a link (as LimitNote does with LIMITS_DOCS_URL) and the exact option to turn it on, for example inspectors: { store: true } (app/src/app.ts:380-384).
No shortcut to focus search or switch tabs: there is no / to focus the current page's search and no key to switch tabs. Add a host listener in app.ts for / (expose each page's search through a small service) and Alt+1..9 for tabs, skipped while focus is in an input. The Cmd/Ctrl+K palette is tracked in phase 3 of 4: a shell you live in #342 (app/src/app.ts:149-153).
Improvements
One shared <app-state kind="loading|error|empty|no-match"> component with Retry and "Clear filters" slots, plus a spec that checks every page renders all four states (app/src/ui, app/src/pages/*.ts).
A spec that lists the pages and checks each one uses <app-search-field> and the roving-list directive (app/src/ui, app/src/pages/*.ts).
Resizable list and detail panes with a keyboard-accessible splitter (role="separator", aria-valuenow, arrow keys), width saved per view (app/src/pages/network-inspector.ts, store-inspector.ts, di-inspector.ts, app/src/tab-memory.ts).
"Pause live updates" and "Auto-scroll to newest" toggles on the network timeline, store change log and route timeline (app/src/pages/network-inspector.ts, store-inspector.ts, route-timeline.ts).
Sortable column headers with aria-sort on the network table (time, status, duration) and the route config table (app/src/pages/network-inspector.ts:580-592, app/src/pages/route-inspector.ts).
Saved filter chips per view, for example "Failed only" and "SSR" on the network page, stored in localStorage inside try/catch (app/src/pages/network-inspector.ts, app/src/tab-memory.ts).
Run pnpm test:axe on the loading, error and no-match states too, not only the populated static report.
Context
Fixes and improvements from a code audit of the overlay, popup and panel shell, panel ux and accessibility, done in October 2026. Roadmap work lives in #340, #341, #342 and #343. Items marked (to confirm) were not reproduced; check them before fixing.
The overlay, popup and panel shell
Problems
Medium
connectDevframeruns once, and on rejection the panel setsconnectionFailedand asks the user to reload. A dev server that starts slowly or restarts needs a manual reload. Retry with backoff (1, 2, 5, 10 s) whileconnectionFailedis set and add a "Retry now" button. The popup'sfindTargetalso only retries on the next open (app/src/app.ts:913-947,app/src/app.ts:372-375,packages/devtools/src/popup.ts:114-121).Low
onThemeMessageonly rejectse.source === window, so any iframe or script on the host page can flip the popup theme, and the value is saved tolocalStorage. Accept the message only fromiframe.contentWindow(or a frame inside it) with a matching origin (packages/devtools/src/popup.ts:827-832).'*':ThemeServicewalkswindow.parentand postspangular:theme-changewith target origin'*', which leaks to unknown origins when the panel is embedded cross-origin. Post to the parent's origin (fromdocument.referrerorancestorOrigins), or rely on theBroadcastChannelfor same-origin cases (app/src/theme.service.ts:36-47).aria-current="page", with no roving tabindex or arrow keys, so ten tabs are ten tab stops before the content. Userole=tablist/tab/tabpanelwitharia-selectedand roving tabindex, or keep the nav pattern and add arrow-key handling (app/src/app.ts:346-358).togglePanelonly moves focus on close. When opened from the keyboard (clickwithdetail === 0), focus the iframe or itsmainonce loaded (packages/devtools/src/popup.ts:670-675).HUB_VIEWSis defined in both the panel and the package, and thepangular:dock prefix is repeated too. Adding a dock in one place breaksfollowHubDocksand view titles silently. ExportHUB_VIEWSanddockIdfrom one module and import them in the app (app/src/app.ts:43-50,packages/devtools/src/hub-docks.ts:9-16,packages/devtools/src/hub-docks.ts:48,app/src/hub-dock-sync.ts:2,app/src/app.ts:1088).['./', '/__pangular/', '/__devframes/pangular/']and the popup tries['/__pangular/', '/__devframes/pangular/', '/__devframe/', '/'], so one can find a server the other misses. Keep one exported list inpanel-frame.tsand use it in both (packages/devtools/src/overlay.ts:177,packages/devtools/src/popup.ts:61).hub:docks:activatecall: the fallback dock switch casts the RPC clientas unknownand callshub:docks:activate. If the client has no top-levelcall, the panel quietly switches to the fallback tab instead of the dock. Use the typed hub client API and add a test that the hub path activates the dock (app/src/app.ts:1073-1094) (to confirm).Math.random, and theBroadcastChannelclaim only catches collisions inside one browser. Pages from other devices (NativeScript, Angular Native, a phone) reporting to one hub can collide. Usecrypto.randomUUID().slice(0, 8)or longer, staying under the server's 50-character limit (packages/devtools/src/overlay.ts:96,packages/devtools/src/devframe.ts:272).Improvements
limits,redaction,refreshMsand the action toggles from config-file-only toctx.settings, with a Settings view in the panel (packages/devtools/src/config.ts,packages/devtools/src/devframe.ts,app/src/app.ts).buildReport()next to the CLIbuild, so CI and Analog/Vite builds can produce the static report without the CLI wrapper (packages/devtools/src/cli.ts,packages/devtools/package.json).sessionStoragekeyed bypageId, so a popup or extension reload returns to the same item (app/src/tab-memory.ts,app/src/app.ts).packages/devtools/src/popup.ts).Already tracked elsewhere
40vh/40vwtoday) and pop-out window: phase 3 of 4: a shell you live in #342#tab=&id=&page=) and anopen-in-panelagent tool: phase 3 of 4: a shell you live in #342Panel UX and accessibility
Problems
Medium
refresh()or load function, aspipes-inspector.ts:188-192already does. In the shell, retry the devframe connection with backoff and show a "Reconnect" button (app/src/pages/network-inspector.ts:296-303,app/src/pages/dashboard.ts:234-241,app/src/app.ts:372-375).loadedflag on the first shared state value and show arole="status""Loading…" state until then (app/src/pages/store-inspector.ts:130-143).onListKeyinpipes-inspector.ts:1341. A sharedappRovingListdirective would cover all of them (app/src/pages/network-inspector.ts:595-616,app/src/pages/store-inspector.ts:362,499).Low
signal-inspector.ts:159,store-inspector.ts:100,forms-inspector.ts:358,route-tree.ts:115,route-timeline.ts:37androute-inspector.ts:55. Placeholders mix "Filter" and "Find", and only some end with "…". Extract an<app-search-field>inapp/src/uiwith the icon, Escape,autocomplete="off",spellcheck="false"and an optional count, and pick one verb.padding-left, with noaria-level,aria-expandedor treegrid role, so screen readers lose the hierarchy. Userole="treegrid"witharia-levelandaria-expanded, and support Left/Right to collapse and expand (app/src/pages/route-tree.ts:176-177).$index: when the filter changes, rows are reused by position, so focus and selection jump. Track by a stable key such asroute.file + route.fullPath, a finding's code and location, or a component id (app/src/pages/route-inspector.ts:118,route-current.ts:95,167,route-lint.ts:65,forms-report.ts:162,cd-recording.ts:89).app/src/styles/_palette.scssorapp/src/view-accents.tswith dark and light pairs, and add them toapp/src/__tests__/tint-contrast.test.ts(app/src/pages/analog-inspector.ts:1133-1138,1693-1725).inspectorskey but has no docs link and no snippet. Add a link (asLimitNotedoes withLIMITS_DOCS_URL) and the exact option to turn it on, for exampleinspectors: { store: true }(app/src/app.ts:380-384)./to focus the current page's search and no key to switch tabs. Add ahostlistener inapp.tsfor/(expose each page's search through a small service) and Alt+1..9 for tabs, skipped while focus is in an input. The Cmd/Ctrl+K palette is tracked in phase 3 of 4: a shell you live in #342 (app/src/app.ts:149-153).Improvements
<app-state kind="loading|error|empty|no-match">component with Retry and "Clear filters" slots, plus a spec that checks every page renders all four states (app/src/ui,app/src/pages/*.ts).<app-search-field>and the roving-list directive (app/src/ui,app/src/pages/*.ts).role="separator",aria-valuenow, arrow keys), width saved per view (app/src/pages/network-inspector.ts,store-inspector.ts,di-inspector.ts,app/src/tab-memory.ts).app/src/pages/network-inspector.ts,store-inspector.ts,route-timeline.ts).aria-sorton the network table (time, status, duration) and the route config table (app/src/pages/network-inspector.ts:580-592,app/src/pages/route-inspector.ts).localStorageinside try/catch (app/src/pages/network-inspector.ts,app/src/tab-memory.ts).pnpm test:axeon the loading, error and no-match states too, not only the populated static report.Already tracked elsewhere
Generated by Claude Code