Admin Dashboard: the final design โ
- Source: the operator's Claude Design project
4598a0dd-667b-42f3-8271-e6ddf05ddcee, pulled 2026-08-30. - Status: approved, wired and routed (2026-08-30).
/admin/dashboardrenders it with real counts and a real changelog, and it replaced the Dashboard that shipped on 2026-08-28. See What is wired, and what is deliberately not.
Why is this committed rather than linked? โ
Because the last set of mockups was lost. Three merchant design directions were rendered on 2026-08-28, served from a dead session's localhost:8099, and never committed. She asked to see them on 2026-08-30 and they no longer existed. Anything a design produces lives in the project folder from now on.
What is here? โ
| File | What it is |
|---|---|
Admin Dashboard Final.dc.html | The design to implement. 52 KB, zero em dashes |
Admin Dashboard Mockups.dc.html | The earlier iteration, 174 KB. Read the delta between the two: it shows what she rejected |
- Not vendored:
support.js, Claude Design's own generated runtime. It is theirs, it is marked do-not-edit, and it carries em dashes our lint bans. The design files do not need it to be read. - Not vendored:
_ds/.../colors_and_type.css. The design linkspackages/ui/theme.css, which is OUR token file, so the design system copy is reference rather than a dependency.
What does the design already tell us? โ
It was synced from dev, not from this branch. โ
github.mdin the project recordsbranch: dev,commit: 130153ca, dated 2026-08-28.- So a diff of her uploaded style files against this branch shows OUR 209 commits, not design intent. Diff against
origin/devinstead.
It already uses blue links. โ
- The design sets
a{color:#60a5fa}with#93c5fdon hover, Tailwind blue-400 and blue-300. - She reached the same conclusion independently on 2026-08-30 from the token side:
--accent-500and--warningare both#f59e0b, so the link colour and the caution colour are literally the same value. The design is ahead of the codebase here.
It contains the announcements authoring surface. โ
github.mdrecords a "Post Announcement modal mockup (5a)", grounded inNewIssueModal.jsx.- That answers her question from 2026-08-28, "if I want to submit an announcement, where exactly would it go?", which became
#992: feat(admin): an authoring surface for announcements someone WRITES, behind the Dashboard reading column.
What is the standing decision? โ
- This REPLACES the Dashboard that shipped on 2026-08-28, her words, asked and answered directly. It is not a patch on
DashboardHome.jsx. - Scaffold first, wire second. It gets built against fixture data in the admin Storybook on port 6008, she approves the visual, and only then is anything wired.
What changed between Mockups and Final? โ
Nothing was redrawn. The Final file is a SELECTION out of the Mockups file. โ
- Screen
4ais byte-identical in both files, and so is screen5b. A line-by-line diff of each pair returns only the surrounding viewer chrome. - So the delta is entirely about what she kept and what she dropped, which is the more useful reading anyway.
What she kept: 4a and 5b. โ
| Kept | What it is |
|---|---|
4a Editorial + wick | The page. 1c's editorial layout with 3a's wick timeline dropped into the hero |
5b Modal in context | The Post Announcement modal, drawn over 4a rather than on its own |
What she dropped, and what each one was. โ
| Dropped | What it was | What replaced it |
|---|---|---|
1a By the book | Role-card grammar, vertical timeline, stacked metric cards | 1c, which is the editorial hero and the collapsed icon rail |
1b Mission control | Horizontal phase stepper, condensed metrics panel, grouped changelog | Same |
2a The wick | The burn-line timeline, first pass | 3a, which keeps the burn line and takes 2b's label stack |
2b Night arc | Lanterns along a curved street at night | Same |
2c Standing lanterns | One beam of light per phase | Same |
3a on its own | The chosen timeline, as a standalone riff | 4a, which is 3a inside 1c |
4b Flat headers | The same page with NO title band on the two cards | 4a, so the title band survived a direct A/B against its own removal |
5a Post announcement | The modal on its own, without the page behind it | 5b, the same modal in context |
Two things the dropped screens settle. โ
- The title band is deliberate.
4bexisted only to show the page without it, and she picked the version that has it. That is worth knowing before anyone "simplifies" the band away later. 5a's label mentions "type pills" and its markup has none. The label is stale;5aand5bcontain the same modal, character for character. There is no type or category control in the design.
Do her uploaded token copies differ from origin/dev? โ
No. Every value she redeclared matches origin/dev exactly. โ
- The design does not vendor a token file. What it carries is an inline
:root{}block at the top of each.dc.html, and that block is the thing to compare. - Checked against
origin/dev'sapps/admin/src/shared/styles/styles.css:
| Token | Design | origin/dev |
|---|---|---|
--bg | #000000 | #000000 |
--surface | #18181b | #18181b |
--surface-elevated | #1e1e1e | #1e1e1e |
--text | #e6eef8 | #e6eef8 |
--muted | #9aa6b2 | #9aa6b2 |
--border | rgba(255,255,255,0.08) | rgba(255,255,255,0.08) |
--border-soft | rgba(255,255,255,0.05) | rgba(255,255,255,0.05) |
--accent-*,--info,--successand--dangerare not redeclared at all: the design links our realpackages/ui/theme.cssand resolves them from it.--font-sansis the one value that reads differently, and it is not a difference in intent: the design lists'Inter Variable', Inter, system-ui, -apple-system, sans-serifwherepackages/ui/fonts.cssalso carries'Segoe UI', Roboto, 'Helvetica Neue', Arialin the middle. A truncated fallback tail in the viewer's own chrome. The components set nofont-familyand inherit the real stack.
The one thing the comparison DOES explain: why the design draws title bands as rgba(0,0,0,.35). โ
--surface-dark(#131316) exists onorigin/devand is absent from the subset she pasted into the design. The design could not have used the token, because in that document the token did not exist.- So the raw rgba is an artifact of the paste, not a decision against
--surface-dark. The build uses the token.
What was built, and what is not wired? โ
Where it lives. โ
apps/admin/src/admin/dashboard/, beside the Dashboard that shipped on 2026-08-28. Nothing inDashboardHome.jsxor its parts was touched, so the shipped page cannot move under her while she reviews its replacement. (It arrived in alaunch/subfolder; that folder is gone as of 2026-08-30, when the page stopped being called a launch dashboard. It is the admin Dashboard.)- Storybook: port 6008 (
npm run storybook:admin). The page isScreens/Dashboard/AdminDashboard; its parts are underComponents/Dashboard/.
| File | What it is |
|---|---|
AdminDashboard.jsx | The page. Hero, wick, reading column, rail, modal |
LaunchTimeline.jsx | The wick, positioned from each phase's at percentage |
DashboardCard.jsx | The card shell: title band, full-width rule, body |
AnnouncementsPanel.jsx | The reading half of #992 |
ReleaseChangelogPanel.jsx | What shipped, one row per change |
CountsRail.jsx | The counts, including the "Not available" tile |
PostAnnouncementModal.jsx | The writing half of #992 |
dashboardFixtures.js | Every number and string on the page. This file is the whole data layer today |
AdminDashboard.css | All of the above |
It was a scaffold until 2026-08-30. That paragraph now lives below. โ
One accessibility follow-up, deliberately not taken in a scaffold pass. โ
- The modal's "Message *" is a
<span class="form-label">, not a<label for>, becauseMarkdownEditorexposes no id oraria-labelfor its textarea. axe passes it (the textarea has a placeholder, which axe accepts as a name), so this is a quality gap rather than a violation. - Closing it properly means adding a
textareaId/textareaAriaLabelprop toMarkdownEditor, which is shared withNewIssueModalandSelfHostedDocsEditor. Additive and small, but it is a change to a shipped component, so it belongs with the wiring rather than inside a scaffold.
Five places the build deliberately does not copy the design. โ
The first four are a codified rule beating a detail of the mock; the fifth is her own later call. Each is commented at the point of use.
| The design drew | The build does | Why |
|---|---|---|
Title bands as rgba(0,0,0,.35) | --surface-dark | Her rule, and the token was simply missing from the design's paste (above) |
| Rail tiles as a near-transparent white wash | --surface | Every card background in the admin portal is --surface. Depth comes from the border and the shadow |
| Dividers inset by the card's body padding | Dividers spanning the whole card | Her rule, stated 2026-08-28 |
rgba(154,166,178,.7) for quiet meta text, and opacity: .65 on the unavailable tile | --text-secondary, and the tile at full strength saying "Not available" in words | --muted at 70% composites to 4.15:1 on --surface, under the 4.5:1 bar. A quieter token, never an opacity |
| The pin control as a full-width row at the top of the modal BODY | The pin control in the modal HEADER, sized to its content | Her call on the second look. See What changed after her second look |
And one place the build deviates from the PORTAL, on her explicit instruction. โ
- The title input is not capped at 560px in this modal.
input.form-input[type="text"] { max-width: 560px }in the shared stylesheet governs every text input in the portal,NewIssueModalincluded, and this modal shipped capped because of it. - Operator, 2026-08-30: "we didn't need to shorten the title input." An announcement title is the headline people read in the list, so seeing it at the width it will occupy matters more than the tidiness of a short field.
- Implemented as
.launch-modal input.form-input[type='text'] { max-width: none }, scoped to this modal and specific enough to win (the global is(0,2,1), this is(0,3,1)). Do not "restore" the house rule here. Nothing else in the portal changes.
Where the design was followed against the current codebase: blue links. โ
--launch-link: #60a5fa/--launch-link-hover: #93c5fd, scoped to this page, exactly as the design sets them.- That also frees the card titles to be amber.
DashPanelon the shipped page uses--textfor its titles precisely because links there are amber and an amber heading made them stop reading as links. With blue links the conflict is gone, so the amber the design draws is correct rather than a regression. - The system-wide blue-links pilot is a separate queued task. When it lands, the two locals above point at whatever token it introduces.
What changed after her first look? โ
She approved everything except the Post Announcement modal, 2026-08-30: "everything looks beautiful except the post announcement popup. I think we need a bit more height than what it currently has. And we don't need to full width on the 'pin to top' toggle. Make it fit more comfortable please."
| What | From | To |
|---|---|---|
| Modal height | 75% of the overlay, the design's value | 88%, plus max-height: 100% so it stays inside the gutters at any window size. On an 820px viewport that is 579px to 679px |
| Pin-to-top row | Full width, justify-content: space-between | width: fit-content, max-width: 100%, and a --space-4 gap. The switch now sits beside its label instead of hundreds of pixels away |
| Modal body padding and field gap | --space-3 (16px) | --space-4 (24px), which is what every card body in the portal already uses |
| Pin row padding | --space-2 --space-3 (8/16px) | --space-3 (16px) |
One thing the height change exposed, which is why the two had to ship together. โ
MarkdownEditorships a fixed box:.md-textareaisheight: 240pxand.md-previewcaps at400px, because its other two callers put it inside a scrolling form.- Inside a fixed-height modal that meant raising the modal MOVED the empty space rather than removing it: the editor stayed 240px and a 240px hole opened between it and the footer. Visible in the first capture.
- Fixed with a block scoped to
.launch-modalthat lets the editor fill its field, plusresize: none(a drag handle that pushes the footer out of a panel which cannot grow is not a feature).NewIssueModalandSelfHostedDocsEditorare untouched and keep the drag-resizable box they were built around.
Unchanged at that point: the width. โ
- She named height, not width. The modal is still
70.5%capped at60rem.
What changed after her second look? โ
Her words, 2026-08-30: "We didn't need to shorten the title input...... Perhaps the 'pin to top' option should be in the header in an elegant fashion."
The title input is full width now. Covered above, as a deliberate override of a house rule. โ
The pin control moved into the header. โ
- Decided by her: the header. How it looks there was mine, so here is what was chosen and why.
| Choice | Why |
|---|---|
Label, rule and switch in one group, --space-2 apart, with --space-4 before the close button | The 3:1 spacing ratio is what makes them read as one control. No divider needed to say it. This is the direct inverse of the body version's mistake, where space-between threw the switch away from the words it belongs to |
| Text right-aligned above the switch, two lines | Both lines end flush against the switch, so label, rule and control share one edge and form a block rather than three things that happen to be near each other |
justify-content: flex-start on the header with margin-right: auto on the title | The shared .modal-header is space-between, which with three children would spread title, pin and close evenly across the bar. That is the same failure again, one level up |
| A hairline border on the OFF track | In the body the switch sat on --surface-dark and a bare --surface-2 track read against it. In the header it sits on --surface, where --surface-2 is barely a shade away and the control would read as a loose dot. The knob still carries the affordance at --muted, 7.15:1; the border is what makes it a switch |
| The rule shortened to "Only one pin at a time" | A header cannot hold the design's full sentence. The dropped half ("this replaces the current pin; the old one stays in the list") is a statement about STORAGE and belongs in #992's data model. What is left is the half that stays true whether the switch is on or off, which is what a permanently visible line has to be |
- The header did not get taller: 65px before, 65px after. The pin group is 31px inside a bar whose height was already set by the 32px close button.
The height stays at 88%, and that is a decision rather than an oversight. โ
- The body did get space back: the pin row and its gap were about 102px.
- All of it went to the message editor, which is the only
flex: 1child, so the writing area grew from 249px to 351px on an 820px viewport rather than the modal carrying dead space. Measured, not assumed. - Lowering it would partly undo what she asked for on the first look. The pin row was never what made the editor small; a fixed 240px
MarkdownEditorwas, and that was fixed separately. Dropping back toward 75% now would shrink the editor again for a reason that no longer applies. - If she wants it shorter anyway it is one value in
.launch-modal.
One thing for the wiring, not for now: what gets focus on open. โ
- Tab order follows the DOM, so the first focusable in the dialog is now the pin switch, then Close, then Title. Nothing is wrong with it, but on a writing surface focus should land on the Title input when the modal opens, and that is an autofocus and focus-trap concern that belongs with #992.
What did axe say? โ
Run: headless chromium against the admin Storybook on 6008, axe-core over #storybook-root, nine stories. Harness: ../harness/admin-dash-capture.mjs. Full report: ../harness/admin-dash-axe.json.
| Story | Violations | Incomplete | Passes | Page errors |
|---|---|---|---|---|
admin-dash-page | 0 | 1 rule / 19 nodes | 23 | 0 |
admin-dash-modal | 0 | 1 rule / 20 nodes | 27 | 0 |
admin-dash-narrow | 0 | 1 rule / 19 nodes | 23 | 0 |
admin-dash-empty | 0 | 1 rule / 19 nodes | 23 | 0 |
admin-dash-timeline | 0 | 1 rule / 8 nodes | 17 | 0 |
admin-dash-announcements | 0 | 0 | 14 | 0 |
admin-dash-changelog | 0 | 0 | 15 | 0 |
admin-dash-rail | 0 | 0 | 13 | 0 |
admin-dash-modal-alone | 0 | 1 rule / 1 node | 21 | 0 |
Zero violations. The incompletes are all color-contrast, and incomplete is not a pass. โ
- The hero (19 to 20 nodes). axe cannot resolve a gradient ground, and the hero carries the design's amber wash. Moving the wash to a
::beforeor a sibling layer does not help: axe then reports the same nodes incomplete because of the pseudo element instead. Measured by hand against the wash at its darkest point (--accent-900at 22% over--bg, which composites to#1b0f04):
| Pair | Ratio | Bar |
|---|---|---|
--text on the wash (blurb) | 16.08:1 | 4.5 |
--accent-500 on the wash (December 2026, section labels) | 8.75:1 | 4.5 |
--muted on the wash (eyebrow, phase dates) | 7.59:1 | 4.5 |
--text-secondary on the wash (phases ahead of us) | 6.11:1 | 4.5 |
--accent-500 on the amber step pill over the wash | 6.89:1 | 4.5 |
- The timeline story (8 nodes). Same cause, the radial glow under the current phase. Same measurements.
- The modal (1 node).
Element content contains only non-text charactersonMarkdownEditor's Quote toolbar button, whose entire label is the glyph. Pre-existing, in a shared component this work reuses rather than wrote, and it also fires whereverNewIssueModalis rendered.
The modal revision moved this number, and an unexplained improvement gets the same check as an unexplained failure. โ
- It was 3 nodes before the revision and is 1 after. The two that went away are the footer's Cancel and Post Announcement buttons, previously reported as
Element's background color could not be determined because it is overlapped by another element. They are now measured, and they pass. - Reproduced both ways to be sure it was the change and not a flaky run: the committed CSS gives 3 nodes on 6 of 6 runs and the revised CSS gives 1 on 6 of 6, same server, same story, same 1340x820 viewport, with and without waiting on
document.fonts.ready. - The cause is geometric. The revision moved the footer from y 625..699 to y 675..749 and removed the dead gap that sat above it. Nothing overlaps either button's centre in either state (
elementsFromPointreturns the button, then.modal-footer, then.modal-contentin both), so axe's overlap verdict was coming off the text rect's edges in the cramped layout rather than off a real covering element. The exact axe internal is not pinned; the direction is, and it is toward more coverage, not less. - It held through the header move: still 1 node after the pin control moved into the header and the title input was uncapped.
The modal header is a fully measured context, unlike the hero. โ
The header has no gradient and no wash, so axe resolved every pair in it against a solid --surface and reported real numbers rather than an incomplete:
| Element | Ratio | Foreground on background | Size |
|---|---|---|---|
#launch-post-announcement-title | 15.14:1 | #e6eef8 on #18181b | 18px |
#launch-pin-label | 15.14:1 | #e6eef8 on #18181b | 13px |
#launch-pin-note | 7.14:1 | #9aa6b2 on #18181b | 11px |
- The one thing axe does not check here is the OFF switch track, because non-text contrast is not in its default rule set. The knob carries the affordance at
--muted, 7.15:1 on--surface, and the track has a--borderhairline. If the off state wants to be louder that is a token change on.launch-switch, not a structural one.
How do I look at it? โ
npm run storybook:admin # port 6008, NOT 6006Then Screens/Dashboard/AdminDashboard. Four stories: Default, PostAnnouncementOpen, Narrow, Empty.
Captures, committed beside the other project screens:
| Capture | Story |
|---|---|
../screens/admin-dash-page.png | The page |
../screens/admin-dash-modal.png | Post Announcement over the page |
../screens/admin-dash-narrow.png | 900px, rail under the reading column |
../screens/admin-dash-empty.png | No announcements, nothing shipped |
../screens/admin-dash-timeline.png | The wick on its own |
../screens/admin-dash-announcements.png | The announcements card |
../screens/admin-dash-changelog.png | The changelog card |
../screens/admin-dash-rail.png | The counts rail |
../screens/admin-dash-modal-alone.png | The modal on its own |
What is wired, and what is deliberately not? โ
Routing: /admin/dashboard renders it, and /admin redirects there. โ
AdminShellimportsAdminDashboardPage. The index route now REDIRECTS rather than rendering the page a second time:isActive()matches on the pathname, so/adminlit no nav item at all and you were looking at the Dashboard with nothing marked in the sidebar. Same idiomdocsandvenuesalready use.- Verified in a browser: landing on
/adminends at/admin/dashboardwith the Dashboard nav item active, no.page-header-barand no leftover.dash-splitmarkup from the page it replaced.
The two real sources were REUSED, not rewritten. โ
| Source | What feeds it | Where it came from |
|---|---|---|
| The counts rail | useDashboardRail(), the four calls the screens each count names already make | The Dashboard this replaced |
| The changelog | loadRecentReleases() through the docs API, reading docs/changelogs/dev/ | The same |
- What is NEW is two pure adapters beside the reader, because the card's row shape is not the reader's day-and-group shape:
flattenReleaseEntries()andchangelogKind(), with tests. AdminDashboard.jsxstays a props-in view andAdminDashboardPage.jsxis the only file that knows where the numbers come from. That boundary is load-bearing:useAdminDashboardreaches../../firebase, which initialises the Firebase app on import, so a view that imported it would make every Storybook story boot Firebase to draw a card.
Three things are deliberately NOT wired, each answered by her. โ
| What | Her answer, 2026-08-30 | What shipped |
|---|---|---|
| "View all" on both cards | "we might actually need to build this in somewhere" | A <span>, not a link. No href, a title naming #1009, and --text-secondary rather than the page's blue: a blue link that does nothing on click is a defect report waiting to be filed |
| Alpha's six steps, and the hero blurb | "I think these are honestly copy at the moment" | Hardcoded, moved OUT of dashboardFixtures.js into dashboardCopy.js so the file the routed page imports is not named "fixtures". Its header says where a real source would come from (loadLaunchStages()) and why wiring it would be worse: the plan gives stage names and dates, and gives neither the at spacing nor the six steps |
| The Post Announcement modal | #992 owns the storage model and the firestore.rules gate, and neither exists | Untouched. It opens over the routed page, and onSubmit goes to a caller that passes nothing |
- Announcements have no READ path either, which is the same issue. The card now says "Announcements are not stored yet, so nothing can be posted or read here. Tracked in #992" rather than "No announcements yet", which would claim a working feature nobody had posted to.
What the live data showed that the fixtures hid. โ
- The flame went missing.
useDashboardRail's rows carry a label, a value and a destination; the fixtures carrylit. Nothing failed. The rail simply drew seven identical tiles with a widow in the last row while every story showed the design. Mapped inuseAdminDashboard(which tile is the flame is a fact about this PAGE, not about the reader) and re-captured. - Refresh was invisible.
useDashboardRailsetsloading: falsewhen a read settles and never sets it back, so a re-read left the numbers sitting at their old values with no feedback. The button now tracks its own round trip. Flipping the reader's flag instead would blank every number on each refresh, which is worse than no feedback. - A changelog heading is free text. The fixtures had three kinds;
docs/changelogs/dev/has Fixed 111, Added 85, Changed 33, Security 29, Removed 7 and a dozen one-offs a person typed once. So the dot vocabulary is a CLOSED set with a neutralother, matched on the heading's first word (the repo contains "Fixed (round 2, re-review of the fix diff)"), and the legend lists only the kinds on screen. - A count in flight is not an absent one, and a failed changelog read is not an empty window. Both were single states in the design and are two states each against real services.
- The card reads
dev, not this branch. The docs API serves the repo'sdevbranch, whose newest changelog on 2026-08-30 isdev-08.21.2026. The08.24/08.27/08.30files exist only onfeat/admin-and-merchant-portals, so the live card correctly shows Aug 21 and Aug 20. It will move forward when this branch merges.
The one thing worth reconsidering, flagged rather than acted on. โ
- The changelog's "View all" HAS a real destination and it was deleted with the old page:
docsUrl('/changelogs/README')on the docs site, which the retiredReleaseStreamlinked as "All changelogs". The decision relayed for this build was that both View alls ship inert, so both do. If she wants the changelog one live it is one line, anddocsSite.jscomes back out of git history.
What the browser run measured. โ
- Target: http://localhost:3001 (the operator's own admin dev server, rooted in this checkout on
feat/admin-and-merchant-portals, so it already serves the committed change). Read only: nothing was restarted or killed. - Harnesses, committed beside the others:
../harness/admin-dash-routed.mjs,../harness/admin-dash-routed-detail.mjs,../harness/admin-dash-routed-interact.mjs. - Three runs, each after a change to the rendering code rather than once at the end:
routed-01.jsonfound the missing flame,routed-02.jsonconfirmed it, androuted-03.jsonis the live re-check after the a11y fix below.
| Capture | What it shows |
|---|---|
../screens/routed-02-1440-viewport.png | The routed page, real counts, real changelog, Dashboard lit in the sidebar |
../screens/routed-modal-1440.png | Post Announcement open over the routed page |
../screens/routed-01-1440-full.png | The first run, kept as evidence: seven identical tiles, no flame, a widow in the last row. That is what a prop the fixtures carry and the loader forgets looks like |
| Live value | Read |
|---|---|
| Venues | 22,636 |
| Users | 35 |
| Offers to review | 3 (the fixture had this one as "Not available") |
| Lanterns lit now / Waves pending / New signups 24h / Merchant applications | 0 |
| Changelog | 8 rows across Aug 21 and Aug 20, every one carrying its PR link, v0.1.0 ยท dev |
- Refresh re-reads both sources and disables while it does. The modal opens over the page, holds its title and message fields, and closes. It was NOT submitted.
What did axe say about the ROUTED page? โ
Zero violations on the page, and the numbers match the scaffold's. โ
| Scope | Violations | Incomplete | Passes |
|---|---|---|---|
.launch-page, routed | 0 | 1 rule / 19 nodes (color-contrast) | 22 |
.launch-page, routed, modal open | 0 | 1 rule / 21 nodes | 26 |
The scaffold's admin-dash-page story, for comparison | 0 | 1 rule / 19 nodes | 23 |
- Same cause as the scaffold run: axe cannot resolve the hero's gradient wash, and every pair over it was measured by hand at 6.11:1 or better against a 4.5 bar. Routing did not change it.
The stories were re-swept after the wiring, and that is what found the one real a11y regression. โ
- 19 stories, 0 violations and 0 page errors on every one. Harness:
../harness/admin-dash-stories-after-wiring.mjs. - The sweep was not a formality. The new Loading state put an
aria-labelon a plain<div>, which has no role to accept a name, and axe failed it asaria-prohibited-attron all seven pending tiles. The routed page scored 0 the whole time, because by the time anything settles there are no pending tiles left to measure. The same shape was one incomplete node on the truncated tile. - Fixed by saying it in a
.launch-srspan instead, which is what the file already did for "No value". The lesson generalises: a state that exists only DURING a load cannot be measured on a settled page, so it needs a story or it ships unlooked at.
The whole DOCUMENT reports two violations, and neither is this page's. โ
- Running axe over the routed document rather than the page region finds
color-contrast(4 nodes) andscrollable-region-focusable(1 node). A count on the whole document is not automatically the page's, so the run measured a CONTROL route with no Dashboard on it. /admin/usersreports the identical two, node for node: the four sidebar section labels (#666d76on#18181b, 3.38:1 against a 4.5 bar) and the sidebar<nav>as a scrollable region with no keyboard access. Both are shell-wide and pre-date this work. The<nav>one is the same surface as #922.
What needed a decision before anything was wired? โ
1. Does the sidebar collapse on this route? ANSWERED 2026-08-30: no. โ
- Her answer was one word. No route-level collapse behaviour was added.
- The design draws a 68px icon rail; the portal's sidebar is 310px expanded and already collapses to 72px by the user's own control, which is the same idiom and is enough. The page sits in the main column either way.
2. Where do the "View all" links go? ANSWERED 2026-08-30: nowhere yet. โ
- "we might actually need to build this in somewhere." Filed as #1009; both ship INERT and say so at the point of use. See Three things are deliberately NOT wired and the note above it about the changelog's one real candidate destination.
3. Who may post, and what "Pin to top" means. ANSWERED 2026-08-30, no longer open. โ
- Only the operator may post, edit, delete or withdraw an announcement. Hardcoded as an explicit stopgap, because the admin role is flat and there is no mechanism to hang finer permissions on yet. Withdrawing hides the announcement and the record stays. Full detail on
#992: feat(admin): an authoring surface for announcements someone WRITES, behind the Dashboard reading column. - This does not license wiring any of it. Two things it changes about the scaffold, both already in:
| What the ruling means | What the scaffold does |
|---|---|
| The pin is a SINGLE-USER affordance, not a shared one | "Only one pin at a time" is a statement about her own pin, so it reads correctly as a permanent line under the control. It is not a race between authors |
| Provenance exists on every record and cannot be backfilled | Every announcement in the fixtures carries an author, and AnnouncementsPanel shows it on every row rather than on the rows that happen to have one |
- Still open inside this, and it is a wiring question rather than a design one: withdraw is a THIRD state beside posted and deleted, so the reading panel will eventually need to know not to render a withdrawn record while the record still exists.
4. Alpha's six steps: real state or copy? ANSWERED 2026-08-30: copy. โ
- "I think these are honestly copy at the moment." Build, Prove and Clear stay marked done and the timeline needs no source at all. They moved to
dashboardCopy.jswith a header saying not to wire it, and why.
5. The title input cap. ANSWERED 2026-08-30, no longer open. โ
- "We didn't need to shorten the title input." The cap is overridden for this modal only, and the override is documented above so nobody restores it by tidying.
6. Blue links here, versus the system-wide pilot. โ
- This page ships blue links now because the design does. If the pilot picks a different blue, or a token name, this page follows it.