Style guide and design system
How the console's pages look, and the rules behind the look. Every value on this page is copied from web/console.css, the console's one stylesheet. The components are drawn with this site's copy of it, plus the rules it lacks, copied into this page's own stylesheet.
The console builds its pages from the pieces in web/src/ui.ts and web/src/form.ts. A page shows what the API answers. It never decides a label, a state's colour, or which buttons to show: each button appears only when the API lists its action.
Light is the default. The switch in the top bar shows the dark theme, and every sample below follows it.
Colour tokens
31, each light and dark
Type sizes
6, from 12 to 22 px
Control height
36 px; 44 px on phones
Icons
28 line drawings
Colour
Each token has a light and a dark value. Dark comes from <html data-theme="dark">, the person's choice in the theme menu (web/src/theme.ts), or, before the script has run and with no choice made, from the system's prefers-color-scheme. The top bar is navy in both themes. One ultramarine accent; the status colours are kept for status only.
The swatch in each column shows that theme's value, whichever theme this page is in. A see-through value is shown over what it sits on.
Surfaces
| Token | Light | Dark | Use |
|---|---|---|---|
--canvas | #f4f5f8 | #0f1218 | The page behind everything |
--surface | #ffffff | #171b23 | Panels, cards, tables, menus, inputs, dialogs |
--raised | #f7f8fa | #1e232d | A raised or striped surface: table headers, code blocks, the row under the pointer |
--scrim | rgb(12 16 26 / 45%) | rgb(0 0 0 / 60%) | The shade behind an open dialog, and behind the navigation drawer |
--edge-shadow | rgb(0 0 0 / 22%) | rgb(255 255 255 / 16%) | The "more this way" shade at the edge of a strip that scrolls sideways (the tabs) |
Ink
| Token | Light | Dark | Use |
|---|---|---|---|
--ink | #1c2230 | #e6e9ef | Text |
--muted | #5a6272 | #9ba4b4 | Secondary text: hints, table headers, breadcrumbs, a summary card's label |
Lines
| Token | Light | Dark | Use |
|---|---|---|---|
--line | #e0e3e9 | #2a303c | Borders of panels, cards, tables and rows |
--line-strong | #c3c8d2 | #3c4452 | Borders of controls (buttons, inputs), the breadcrumbs' separator |
Accent
| Token | Light | Dark | Use |
|---|---|---|---|
--accent | #3a4ed6 | #9aa7ff | Links, the primary button, the current tab and navigation entry |
--accent-hover | #2f40b8 | #b6bfff | The primary button under the pointer |
--accent-ink | #ffffff | #121738 | Text on the accent |
--accent-soft | #eceffd | #232a4d | A plain button under the pointer, the current navigation entry, a ticked row, a field's focus halo |
--focus | #3a4ed6 | #9aa7ff | The focus ring |
Semantic
| Token | Light | Dark | Use |
|---|---|---|---|
--ok | #17784a | #63d394 | Success: its chips and the done notice |
--ok-soft | #e4f4ea | #16301f | The fill behind success |
--bad | #c0262d | #ff8f88 | Danger and failure: failed chips, refusals, field errors, the danger button |
--bad-soft | #fcebea | #3a1a1a | The fill behind a refusal, a failure notice, a danger button under the pointer |
--warn | #8d5a00 | #f1be5e | Warning: skipped runs, maintenance banners, a value shown once |
--warn-soft | #fcf1d9 | #33270f | The fill behind a warning |
--info | #0b67b0 | #72b7ff | Under way or for information: queued and running chips, the info banner |
--info-soft | #e5f0fa | #15283d | The fill behind information |
--idle | #5a6272 | #a3acbb | A neutral state: paused, deleted, disabled |
--idle-soft | #eef0f3 | #252b35 | The fill behind a neutral chip, inline code, the loading skeleton's bars, a hovered row menu |
The top bar
| Token | Light | Dark | Use |
|---|---|---|---|
--bar-bg | #1b2235 | #151a26 | The top bar's navy, and the band behind the sign-in card |
--bar-ink | #eef1f8 | #eef1f8 (as light) | Text on the bar |
--bar-muted | #a9b1c6 | #a9b1c6 (as light) | Quieter text on the bar, the project picker's chevron |
--bar-hover | rgb(255 255 255 / 10%) | rgb(255 255 255 / 10%) (as light) | The project picker's fill, a control on the bar under the pointer |
--bar-line | rgb(255 255 255 / 16%) | rgb(255 255 255 / 12%) | The bar's bottom edge, the picker's border, the ring round the avatar |
--bar-logo | #5b6cf0 | #5b6cf0 (as light) | The mark's tile on the bar |
--bar-logo-ink | #ffffff | #ffffff (as light) | The mark's bars on the bar |
Status: colour and shape
A state is never told by colour alone. Each tone of chip has its own shape before its words, so the chips read apart in grey too. The chip says the API's label; its tone follows the state the API answered (toneOf in web/src/ui.ts).
| Tone | Shape | The states it shows | Chips, as drawn |
|---|---|---|---|
ok | A filled dot | A running queue, a succeeded task, an enabled job, a successful run, a level within its limit, a backup that is fine, a running cron | Succeeded Enabled Active |
bad | A filled diamond (a square turned 45 degrees) | A failed task or run, a level over its limit, a backup that is stale, failing or off, a stale cron | Failed Failed (503) Stale |
warn | A filled triangle | A skipped run, a level near its limit, a backup that never ran, a cron with parts failing or not run yet, an expired invitation | Skipped (daily quota) Not run yet |
info | A ring open on one side | A queued or running task, a running job run, a backup under way | Queued Retrying (attempt 3) |
idle | An open ring | A paused queue or job, a deleted task, a disabled person or account, and any state not listed | Paused Deleted Disabled |
Contrast
The ratios below are worked out from the hex values above, with the WCAG formula. Text needs 4.5:1, large text and the parts of a control 3:1.
| Text or mark | On | Light | Dark | Note |
|---|---|---|---|---|
--ink | --surface | 15.90:1 | 14.18:1 | Body text |
--muted | --surface | 6.13:1 | 6.87:1 | Hints, table headers, labels |
--muted | --canvas | 5.62:1 | 7.47:1 | Breadcrumbs, a page's own hints |
--accent | --surface | 6.48:1 | 7.68:1 | Links and plain buttons; the focus ring is the same colour |
--accent-ink | --accent | 6.48:1 | 7.75:1 | The primary button |
--accent | --accent-soft | 5.66:1 | 6.19:1 | The current navigation entry, a hovered button |
--ok | --ok-soft | 4.82:1 | 7.64:1 | A success chip or notice |
--bad | --bad-soft | 5.12:1 | 7.10:1 | A failure chip, a refusal |
--bad | --surface | 5.90:1 | 7.83:1 | A field error, the danger button |
--warn | --warn-soft | 5.21:1 | 8.53:1 | A warning chip, the maintenance banner |
--info | --info-soft | 5.08:1 | 7.08:1 | A queued chip, the info banner |
--idle | --idle-soft | 5.37:1 | 6.22:1 | A paused chip |
--bar-ink | --bar-bg | 14.00:1 | 15.38:1 | The top bar's text |
--bar-muted | --bar-bg | 7.38:1 | 8.11:1 | The top bar's quieter text |
--bar-logo-ink | --bar-logo | 4.33:1 | 4.33:1 | The mark's bars: a picture, not text |
--line-strong | --surface | 1.68:1 | 1.76:1 | A control's border: under 3:1. Every field has a visible label above it, and the focus ring is the accent. |
Typography
One system font stack for everything, and a monospace stack for code and values. No web font is loaded: the CSP lets a page load only the console's own files.
--font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Mono", "Roboto Mono", Menlo, Consolas, monospace;
The type scale
| Token | Size | Used for | Sample |
|---|---|---|---|
--text-xs | 0.75rem, 12 px | Chips, the navigation's group titles | Queued tasks 1,234 |
--text-sm | 0.8125rem, 13 px | Hints, field labels, field errors, table headers, breadcrumbs, a summary card's label | Queued tasks 1,234 |
--text-md | 0.875rem, 14 px | Body text, controls, h3 | Queued tasks 1,234 |
--text-lg | 1rem, 16 px | h2, the name in the top bar, a phone card's title | Queued tasks 1,234 |
--text-xl | 1.25rem, 20 px | A summary card's value, a dialog's title, h1 on a phone | Queued tasks 1,234 |
--text-2xl | 1.375rem, 22 px | h1, the page's title | Queued tasks 1,234 |
Weights
- 400: body text.
- 500: buttons, navigation links, tabs, notices, field errors.
- 600: headings, field labels, table headers, chips, a summary card's value, the current tab and navigation entry.
Line heights and spacing of letters
- 1.5: body text, code blocks and text areas.
- 1.25:
h1, which is set 0.01em tighter. - 1.35:
h2. 1.3: a summary card's value. 1.2: buttons and chips. - The navigation's group titles are set 0.02em wider.
Numbers and code
- Numbers use tabular figures (
font-variant-numeric: tabular-nums) in summary cards, numeric columns, the next five runs of a job and the charts' axes, so columns of digits line up: 1,111 / 8,888. Numeric columns are right-aligned on wide screens and left-aligned in a phone's row card. - Code in a line: the mono stack at 0.857em of the text around it, on
--idle-soft, with--radius-smcorners and 0.1em by 0.4em of padding. A long value breaks anywhere rather than widening the page:https://example.com/api/worker. - A code block:
--raised, a 1 px--lineborder,--radiuscorners; its lines wrap, and past 320 px tall it scrolls. - A text area uses the mono stack at
--text-sm, for JSON and bodies. - This site keeps code at 12 px or more; the console's rule is 0.857em.
Spacing and shape
Space
| Token | Value | Shown | Where |
|---|---|---|---|
--space-1 | 4 px | Gaps inside a header's toolbar, between lines of a list | |
--space-2 | 8 px | Between buttons in a toolbar; under the breadcrumbs | |
--space-3 | 12 px | Between summary cards; the gaps of the top bar | |
--space-4 | 16 px | Inside a table cell's sides; the top bar's sides; a page's sides on a phone | |
--space-5 | 20 px | A page's top; a panel's top and bottom; between form fields | |
--space-6 | 24 px | A panel's sides; a dialog's padding | |
--space-8 | 32 px | A page's sides on a wide screen; above each h2 | |
--space-10 | 40 px | Below a page's content |
Corners
--radius-sm4 px: code, a file chooser's button--radius6 px: buttons, inputs, notices, menus, code blocks--radius-lg10 px: panels, cards, tables, dialogs, empty states999pxChips (not a token)50%Icon buttons, the avatar, a row's ⋮ button (not a token)
Shadows
--shadow-smThe primary button, notices, a phone's row card--shadowMenus and popovers; the skip link--shadow-lgDialogs, the sign-in card, the navigation drawer
| Token | Light | Dark |
|---|---|---|
--shadow-sm | 0 1px 2px rgb(20 26 40 / 8%) | 0 1px 2px rgb(0 0 0 / 40%) |
--shadow | 0 1px 2px rgb(20 26 40 / 8%), 0 8px 24px rgb(20 26 40 / 12%) | 0 1px 2px rgb(0 0 0 / 40%), 0 8px 24px rgb(0 0 0 / 45%) |
--shadow-lg | 0 2px 6px rgb(20 26 40 / 10%), 0 24px 48px rgb(20 26 40 / 22%) | 0 2px 6px rgb(0 0 0 / 40%), 0 24px 48px rgb(0 0 0 / 55%) |
Sizes
- 36 px
--controlButtons, inputs, selects - 44 px
--control, on phonesAt 640 px wide and narrower, and on any touch screen - 56 px
--bar-heightThe top bar
| Width | What changes |
|---|---|
| Wider than 960 px | The left navigation is a column of 248 px (--nav-width) beside the page. |
| 960 px and narrower | The navigation becomes a drawer behind the menu button, up to 280 px wide, over a scrim. |
| 640 px and narrower | One column. Table rows become cards, controls are 44 px, the console's name in the top bar is hidden (its mark stays), and a details page keeps its first action and Refresh as buttons, the rest in a ⋮ menu. |
| Any touch screen | Controls of 44 px whatever the width (pointer: coarse). |
Components
Each is drawn below with the console's classes. Buttons and fields are live: point at them, or reach them with Tab. The top bar, the navigation, the tabs and the page header are pictures, as this site draws the console's screens.
Buttons
| Kind | How it looks | Use it for | Never for |
|---|---|---|---|
| Plain | Accent words on the surface, a --line-strong border; --accent-soft under the pointer | Most actions: Edit, Pause, Rotate, Download a sample | The one action a form exists for |
| Primary | Accent fill, --accent-ink words, a small shadow; --accent-hover under the pointer | A form's submit (Create, Save, Sign in), Try again, Copy for a value shown once | An action that cannot be undone: that is a danger button behind a question |
| Danger | Red words; --bad-soft and a red border under the pointer | Delete, Revoke, and the answer in a confirm dialog | A safe action, to catch the eye |
| Quiet | No border, no fill | Refresh, Cancel, Later, Dismiss this message | The main action of a page or form |
- Disabled
- At 45% opacity, with a not-allowed pointer and no fill. In the bar of actions over a table, a button that does not apply yet is muted rather than faded.
- Busy
- While its request runs, the control that sent it is disabled and marked
aria-busy="true"(busy()inui.ts; a form's submit gets the classsending). It keeps full colour, shows a progress pointer and a turning ring after its words, and a second tap sends nothing. - Icons
- A toolbar button wears the icon its words name (Refresh, Create, Edit, Pause, Resume, Delete and the rest); the words stay the button's text.
Chips
- Use it for
- A state, in the API's own words: a task's, a job's last run, a key's, a usage level.
- Never for
- An action or a link, or a tone the page works out for itself.
Panels and summary cards
Tasks in queue
12
Tasks running
1
Completed in the last minute
3
Location
Global (Cloudflare)
Type
Push
Task
- Schedule time
- Oct 22, 2026, 7:33:33 PM
- Attempts
- 0
- Attempt deadline
3m
- Summary cards
- Use them for the few counts at the top of a details page or the dashboard (a queue's: Tasks in queue, Tasks running, Completed in the last minute, Location, Type). Never for navigation: the write-up's link cards do that. Two to a row on a phone.
- Panels
- Use one for a titled part of a page: a form, a list, label and value pairs. Never for a single line of text, or for a state: that is a chip or a notice.
Tables
| Name | State | Attempts | Last error | Actions |
|---|---|---|---|---|
order-1001 | Queued | 0 | — | |
order-1000 | Succeeded | 1 | — | |
order-0999 | Failed | 1 | 404 Not Found; stopped: 404 is not retried |
- Wide screens
- A table sits in a frame (
.table-frame) and scrolls inside it, never the page. Its header row stays in view while a long table scrolls, the row under the pointer is--raised, a ticked row is--accent-soft, and the last column, with the row's ⋮ menu or buttons, stays at the right edge however wide the table is. - Phones
- At 640 px and narrower the header row is hidden and each row is a card. Every cell carries its column's name in
data-label, shown beside its value. The Name (or Email) cell is the card's title, without a label; the tick box and the ⋮ menu sit in the card's top corners; cells marked extra wait behind Show details at the card's foot. (This site's copy puts each label above its value instead.) - Use it for
- Rows of the same kind: tasks, jobs, keys, people, attempts. With no rows, an empty state in its place.
- Never for
- Laying out a form, or one item's details: those are label and value pairs.
Forms
Optional. Letters, digits, _ and -. Left empty, the queue names it. A name is refused while the queue remembers it.
url is required: an http:// or https:// URL
- Label
- Every field has a visible label above it, tied to its control (
form.field()inweb/src/form.ts). - Hint
- Muted, at
--text-sm, under the control, and read with it (aria-describedby). - Field error
- The API's message for that field, in red under it; the control's border turns red and it is marked
aria-invalid. The page never checks what was typed: it sends it, and shows eachfieldsmessage at the input its path names (headers.X-Foogoes toheaders). The first field in error takes the focus. - Form message
- At the form's top, red with a thick left edge and
role="alert": a refusal with no field of its own, orCheck these:
and a list of the messages that have no input. A refusal of a chosen file lists each message with its path in the file, such astasks.3.url. - File chooser
- The text box's frame, its button drawn as the console's own in either theme (
::file-selector-button), not the browser's. - Never
- A placeholder in place of a label, or a page's own check of the input.
Notices and banners
Revoked shop-app.
The task is running or already done
- Notice
- One live line per page, kept in view under the top bar while the page scrolls: what was done (ok), or the API's
error(bad). A new one replaces the last. It arrives with a short fade and drop, and has a Dismiss button. - Banner
- Above the page's content. Warn: one for each maintenance switch that is on, with who switched it and when. Info: a newer build of the console, with Reload and Later; and on a queue's Logs tab,
Only failed attempts are logged. Turn on success sampling to keep a share of the successful ones too.
- Never for
- A field's own problem (that goes beside the field), or anything that must stay on screen after the next action.
Tabs
A picture of a queue page's tabs, Tasks chosen.
- How
- 44 px tall, muted words; the chosen tab in accent at 600 with a 3 px bar under it. A strip wider than the screen scrolls sideways, with a shade at an edge while more is hidden past it; on a phone it runs to the screen's edges, and the chosen tab is scrolled into view. The chosen tab is kept in the address (
?tab=). - Use it for
- Views of one thing: a queue's Metrics, Logs, Tasks, Configuration and Failed; a job's Logs, Results and Configuration.
- Never for
- Steps that must be done in order: the job form uses numbered steps with Continue and Back.
The top bar and the navigation
A picture of the console's frame: the top bar, the left navigation with Queues current, and a task page's breadcrumbs and header.
- Top bar
- Navy (
--bar-bg) in both themes, 56 px, kept at the top while the page scrolls. From the left: the menu button (960 px and narrower), the mark and name (a link to the dashboard), the project picker, then the theme menu (System, Light, Dark) and the person's menu: who they are, the version, Your account, Sign out, and Sign out on every device. - Navigation
- Grouped: Dashboard, Projects and Usage; then, with a project chosen, Cloud Tasks (Queues), Cloud Scheduler (Jobs) and Project settings (API keys, Secrets, Alerts, Export and import); then Admin (People, Audit log). An entry shows only with its permission: API keys with
keys, Alerts withupdate, People and Audit log for admins. The current one is accent on--accent-softwith a 3 px accent edge at its left. With no project chosen it saysChoose a project to see its queues, jobs and settings.
- This site
- The bar under this page's top bar (
.subnav) is the write-up's own navigation, not the console's.
Breadcrumbs and the page header
- Breadcrumbs
- Muted at
--text-sm, a/in--line-strongbetween the steps, the page itself in ink and not a link (crumbs()inui.ts). On a phone each link's target grows taller around its line. - Page header
- The title (
h1), a chip with its state when it has one, then the page's own buttons, flat on the page; a line under it all. On a phone the buttons go to rows of their own under the title. - Never for
- Breadcrumbs as the only way back: the navigation is always there.
Empty states and loading
No attempts yet.
Something went wrong on this page. Reload and try again.
- Empty
- The empty icon, then one line that says what would be here and, where it helps, why:
No failed tasks. Failed tasks are kept for 7 days.
A table with no rows is an empty state instead. - Could not load
- The error in place of what could not be loaded, a red left edge,
role="alert", and Try again. Never a blank page. - Loading
- Until a page's first answer: its words (
Loading the keys…
) over a pulsing skeleton of rows, markedaria-busy.
Dialogs and values shown once
Revoke shop-app?
Apps using this key are refused from now on. This cannot be undone.
The key shop-app
Copy it now and keep it in the app's settings. You won't see it again: the console keeps only a hash of it.
- Confirm dialog
- A modal over the scrim, before something that cannot be undone: the question, what it means, Cancel (quiet) and the action (danger). Escape cancels, and the focus goes back to the control that opened it (
confirmAction()). - Shown once
- A new API key or signing secret: warn colours, the warning, the value in a read-only field and Copy. When the clipboard is not there, the value is selected for the person to copy (
The text is selected: copy it from there
). - Never for
- A question before a safe action, or a value the console can show again.
Icons
Each is a line drawing on a 24-unit grid, in currentColor, 18 px in a button, with a 1.8 stroke and round ends (web/src/icons.ts). An icon is hidden from screen readers: the button or link it sits in carries the words. No icon font and no CDN.
dashboardDashboard, in the navigationprojectsProjects, in the navigationusageUsage, in the navigationqueuesQueues, in the navigationjobsJobs, in the navigationkeysAPI keys, in the navigationsecretsSecrets, in the navigationalertsAlerts, in the navigationusersPeople, and Your accountauditAudit log, in the navigationrefreshRefreshaddCreate queue, Create task, Create job, Create project, Invite, Create HTTP task, Create taskseditEditpausePauseplayResume, Run now, Force run, Run taskretryRetry, Retry alldeleteDelete, Delete all, Delete taskpurgePurgecopyCopy; Export and import, in the navigationmenuThe menu button that opens the navigation (960 px and narrower)moreA row's ⋮ menu, and a details page's More actionssystemThe theme menu, and its System choicelightThe theme menu's Light choicedarkThe theme menu's Dark choiceemptyAn empty state, and what could not be loadedcloseDismiss this message, on a noticecolumnsColumns, the column chooserchevronShow details, on a phone's row cardlogoThe mark: a queue of three calls leaving on a schedule. Accent tile; on the bar,--bar-logo
Motion and micro-interactions
| Token | Value | Use |
|---|---|---|
--ease | cubic-bezier(0.2, 0, 0, 1) | Every transition and the arrivals: quick to start, gentle to stop |
--fast | 120 ms | Hover and focus changes |
--slow | 200 ms | A notice or dialog arriving; the navigation drawer |
A backup has started; it finishes within a few minutes.
The ring turns and the skeleton pulses. Point at this box to see the notice arrive again.
| What moves | How | Timing |
|---|---|---|
| A notice arriving | Fades in and drops 4 px into place (arrive) | --slow, --ease |
| A dialog opening | The same arrival | --slow, --ease |
| The busy ring | A 14 px ring in the button's colour, open on one side, turning (spin) until the answer | 0.8 s a turn, linear |
| The loading skeleton | Its rows pulse between full and 55% opacity (pulse) | 1.4 s each way, --ease |
| Hover | Buttons, navigation links, icon buttons, table rows, a row's ⋮ button and tabs change their fill, border or words | --fast, --ease |
| Focus on a field | Its border turns accent, with a 3 px --accent-soft halo (--bad-soft for a field in error) | --fast, --ease |
| The navigation drawer | Slides in from the left, over a scrim (960 px and narrower) | --slow, --ease |
| Show details on a phone card | Its chevron turns half a turn | --fast, --ease |
| A sortable column | Its sort mark fades in on hover or focus | --fast, --ease |
Reduced motion. With prefers-reduced-motion: reduce every animation and transition is off, on elements and on their ::before and ::after: the ring and the skeleton stand still, and notices appear at once.
Accessibility rules the console follows
| Rule | How the console keeps it | Where |
|---|---|---|
| Visible focus | A 2 px --focus ring, 2 px out, on whatever has keyboard focus (:focus-visible); on the navy bar the ring is --bar-ink. A field gets an accent border and a 3 px halo. A tab draws its ring inside itself, since the strip clips its edges. A skip link, Skip to the page, shows when it takes the focus. | web/console.css, web/src/shell.ts |
| 44 px targets on phones | --control is 44 px at 640 px and narrower and on any touch screen. So are row menus, menu items, navigation links, tick boxes, the buttons in a table, the segmented buttons, icon buttons and the person's menu; a breadcrumb or a link in a table gets a taller target around its line. Check boxes are 22 px on a touch screen. | web/console.css |
| Text only | Pages set text only through textContent, with the small h() helper; ESLint refuses innerHTML, outerHTML, insertAdjacentHTML, srcdoc, document.write, eval and new Function in web/. The CSP allows no inline script or style. A name or URL from the API is always shown as text. | web/src/ui.ts, web/src/form.ts, the repo's CLAUDE.md |
| Labels on inputs | Each field has a label for its control, and its hint and error are read with it (aria-describedby). A field in error is marked aria-invalid, a group's message is tied to its fieldset, and the first field in error takes the focus. A value shown once is named with aria-label. | web/src/form.ts, web/src/ui.ts |
| Live regions | A page's notices are one role="status" region, aria-live="polite"; a failure notice is role="alert". The form message, a refused file and a part that could not load are role="alert"; the Copy button's status, the maintenance banners and the new-version banner are role="status". | web/src/ui.ts, web/src/form.ts, web/src/shell.ts |
| Not by colour alone | Every chip tone has a shape of its own, and the words are the API's label. | web/console.css, web/src/ui.ts |
| Busy, said | A control whose request runs is disabled and aria-busy, so a second tap sends nothing; a loading placeholder is aria-busy. | web/src/ui.ts, web/src/form.ts |
| Keyboard | A row's ⋮ menu is a group of plain buttons that Tab moves through (not role="menu", which promises arrow keys). Escape closes a menu and gives the focus back to its button; a menu opens inside the screen. Tabs move with the arrows, Home and End. A confirm dialog gives the focus back to what opened it, and after a row's action redraws the list, the focus goes back to that row's menu button, or the next one. | web/src/ui.ts |
Built from Cloud Console's main branch (CAP-162), 22 Oct 2026. Example values only: no real keys, emails or ids.