Implementation guide for maintaining and extending PostNot's application UI. This document describes the system that exists in the product today, the conventions new work must follow, and the known areas where the system should become more consistent.
This is an internal product and engineering reference. Customer-facing visual language belongs in README.md and the public site.
PostNot should feel like a focused desktop workbench: warm rather than clinical, dense enough for technical work, and calm even when the request or response data is complex.
The interface follows these principles:
- Clarity before decoration. Request state, hierarchy, and the next action must be obvious without relying on ornament.
- Consistency over novelty. Reuse existing tokens, controls, panels, and interaction patterns before adding a new variant.
- Compact, not cramped. Preserve the current desktop density while keeping labels, focus states, and click targets usable.
- Progressive disclosure. Keep common request work visible and move advanced settings, scripts, raw details, and destructive choices behind explicit controls.
- Local-first confidence. Explain persistence, secret handling, imports, exports, and irreversible actions at the point where they matter.
- State must not depend on color alone. Pair color with text, icons, shape, or position for status and selection.
- Light and dark are one system. New UI is incomplete until both themes have been considered.
Use this order when implementation and documentation disagree:
src/lib/styles/tokens.cssfor semantic visual values and theme overrides.src/lib/styles/app.cssfor shared components, layouts, states, and responsive behavior.- Shared Svelte components under
src/lib/componentsfor structure and behavior. - This guide for intent, usage rules, and how to extend the system.
Feature-local <style> blocks are allowed for genuinely feature-specific composition. They must use shared tokens and should not recreate a shared control, panel, status, modal, or feedback pattern.
Always consume semantic tokens. Do not choose a literal color based only on its appearance in one theme.
| Intent | Token | Use |
|---|---|---|
| Application canvas | --bg-app |
Root window background |
| Panel | --bg-panel |
Standard panels and notification surfaces |
| Strong panel | --bg-panel-strong |
Inputs and surfaces needing separation |
| Sidebar | --bg-sidebar |
Primary navigation background |
| Primary action | --bg-accent |
Send, save, and other dominant actions |
| Primary action hover | --bg-accent-strong |
Hover and stronger accent emphasis |
| Primary button | --button-primary-bg, --button-primary-bg-hover |
Accessible filled-button backgrounds per theme |
| Primary button text | --button-primary-text |
Theme-specific text with at least 4.5:1 contrast on primary buttons |
| Accent tint | --bg-accent-soft |
Selected or highlighted surfaces |
| Subtle/quiet surface | --surface-subtle, --surface-muted |
Nested cards and low-emphasis grouping |
| Control surface | --control-bg, --control-hover-bg |
Secondary controls and interactive rows |
| Selected control | --control-selected-bg, --control-selected-text |
Active tabs and selected toggles |
| Primary text | --text-primary |
Titles and main content |
| Secondary text | --text-secondary |
Labels, descriptions, and metadata |
| Muted text | --text-muted |
Tertiary details; never essential information alone |
| Inverse text | --text-inverse |
Text on the sidebar and other dark surfaces |
| Borders | --border-soft, --border-strong |
Default and emphasized boundaries |
| Success | --success |
Completed or valid state |
| Warning | --warning |
Caution and recoverable risk |
| Danger | --danger |
Failures, destructive actions, and invalid state |
| Destructive action | --danger-bg, --danger-bg-hover, --danger-text |
Filled destructive buttons that need theme-specific contrast |
| Code surface | --bg-code |
JSON, scripts, raw bodies, paths, and logs |
| Overlay | --overlay-backdrop, --shadow-overlay |
Dialog backdrops and floating-layer elevation |
| HTTP methods | --method-*, --method-*-inverse |
Method labels on normal and dark/sidebar surfaces |
| Syntax | --syntax-* |
JSON, scripts, and variable highlighting |
| Realtime status | --success, --warning, --danger, --text-muted |
Connected, transitional, failed, and disconnected indicators paired with text |
The light palette uses cream surfaces, deep teal text/navigation, and burnt orange action color. The dark palette preserves those relationships with higher-luminance text and accent values. The optional Forest theme uses the same semantic roles with a deeper green workbench palette and a cooler green accent. Never copy a resolved light-theme value into component CSS.
HTTP method colors are a special categorical palette. Use the existing .method-get, .method-query, .method-post, .method-put, .method-patch, .method-delete, .method-head, and .method-options classes. Do not use those colors for semantic success or failure.
Realtime protocol labels are compact categorical identifiers, not health states. Use the shared .protocol-badge with the exact short labels WS and S.IO; use .realtime-status-* only for connection state. A status dot must always be paired with visible or screen-reader text.
| Role | Token | Current size |
|---|---|---|
| Page title | --font-page-title |
1.45rem |
| Panel title | --font-panel-title |
1.05rem |
| Section title | --font-section-title |
0.95rem |
| Label | --font-label |
0.8rem |
| Body/control | --font-body |
0.9rem |
| Metadata | --font-meta |
0.8rem |
| Code/data | --font-code |
0.86rem |
- Use
--font-sansfor interface copy and--font-monofor code, URLs when scanning benefits, paths, JSON, scripts, and raw payloads. - Use sentence case for headings, labels, actions, tabs, and menu items.
- Buttons should begin with a verb: “Save request”, “Create environment”, “Clear history”.
- Keep help text direct and explain consequences, not the control label again.
- Use an ellipsis only when an action opens a flow requiring more input.
Use the compact spacing scale for shared primitives and new layouts:
| Token | Value | Typical use |
|---|---|---|
--space-1 |
4px |
Icon/text gaps and micro-adjustments |
--space-2 |
8px |
Tightly related content |
--space-3 |
12px |
Controls in a row and compact lists |
--space-4 |
16px |
Cards, form groups, and dialog gaps |
--space-5 |
20px |
Panel and dialog padding |
--space-6 |
24px |
Major internal separation |
Existing intermediate values may remain where density or geometry requires them. Do not introduce another spacing value without a concrete layout constraint.
The application supports a UI scale from 0.6 to 1.5 through --ui-scale. Validate fixed, sticky, and overlay UI at the default and at both extremes.
| Role | Token |
|---|---|
| Large container | --radius-lg (14px) |
| Standard group | --radius-md (10px) |
| Input/small surface | --radius-sm (8px) |
| Panel | --radius-panel (12px) |
| Card/control | --radius-card, --radius-control (8px) |
| Compact icon control | --radius-compact (6px) |
| Chip/pill | --radius-pill |
Use --border-soft for structure and --border-strong for selection or emphasis. Prefer borders and subtle surface shifts to shadows for hierarchy. Reserve --shadow-soft and overlay shadows for floating layers such as dialogs and notifications.
Use --motion-fast (140ms) for small state changes and --motion-base (180ms) for controls and overlays, with --ease-standard. Motion should explain state change, not delay work. Do not animate large data regions or introduce looping animation except for active progress/loading feedback.
The shared reduced-motion rule removes decorative transitions and pulse/slide animations while preserving static loading state and notification expiry. NotificationHost.svelte also disables its JavaScript fly/fade movement when reduced motion is requested; future JavaScript transitions must make the same explicit check because CSS alone does not control them.
AppShell.svelte owns the two-column desktop frame: a 320px sidebar and a flexible workspace. At widths below 980px, it becomes a single flowing column. The sidebar has three stable zones: compact workspace switching for Requests, WebSockets, and Playbooks; a Collections section whose heading opens the full Collections workspace and whose tree owns the remaining height; and anchored utilities for Environments, MCP integration, and Settings. Every destination requires an explicit non-color-only active state and an accessible name, including icon-only utility controls.
Use this hierarchy:
- Page heading and optional short description.
- Primary actions aligned with the heading where space allows.
- Panels for major work areas.
- Cards or field groups only when they clarify a relationship.
Avoid nesting decorative panels more than two levels deep. A border, section title, or spacing change is often sufficient.
The application is desktop-first but must remain operable in a narrow window. Existing shared breakpoints are 1720, 1500, 1220, 980, 900, and 720px; feature-local layouts may use a nearby constraint only when their content requires it.
- Collapse multi-column detail and form layouts to one column before controls become compressed.
- Allow toolbars to wrap.
- Keep primary actions visible and avoid horizontal page scrolling.
- Give code/data regions their own bounded scrolling rather than expanding the entire page without limit.
- Dialogs must use
dvhfallbacks and retain reachable headers/actions.
| Class/variant | Use when |
|---|---|
.button-primary |
The single dominant action in a section or dialog |
.button-secondary |
A supporting action with visible affordance |
.button-ghost |
Low-emphasis or tertiary action |
.button-danger |
An action is destructive or difficult to reverse |
.button-compact |
Small button size for dense lists, rows, and compact toolbars |
.button-large |
Large button size for high-emphasis request or dialog actions |
.icon-button / .row-action-button |
Familiar action where text would add clutter |
.tab-button |
Switches a local view; active state uses .active |
Button sizing is separate from action intent. The standard control scale is:
| Size | Classes | Height | Use when |
|---|---|---|---|
| Small | .button-compact or .icon-button.button-compact |
--control-height-sm (32px) |
Dense action groups, table/list rows, panel headers with several controls |
| Medium | no size class | --control-height-md (36px) |
Default forms, dialogs, and page actions |
| Large | .button-large or .icon-button.button-large |
--control-height-lg (40px) |
Primary request/send/save surfaces and rare high-emphasis actions |
Rules:
- Prefer one primary action per visible action group.
- Pair destructive styling with a clear verb; use confirmation for deletion of durable user data.
- Icon-only buttons require an
aria-labeland usually atitle. - Keep controls in the same action group on the same size. For example, pair
.button-secondary.button-compactwith.icon-button.button-compact, not a default medium icon button. - Do not introduce feature-local button heights or widths when one of the three shared sizes fits.
- Disabled controls must remain understandable from nearby context. Use a loading label when the action is in progress.
- Use shared heights:
--control-height-sm,--control-height-md, and--control-height-lg. - Preserve the global
:focus-visiblering. Custom compound controls must provide an equivalent inset or outer ring.
Use .text-input, .method-select, .body-mode-select, and .body-textarea rather than creating new base input styling.
- Every input needs a visible label or an accessible name.
- Put
.field-helpafter the label and before or after the control consistently within a section. - Mark optional fields in supporting text; do not mark every required field if most of the form is required.
- Show validation near the affected field when recovery is local. Use a page-level
.feedback.feedback-errorblock only for submission or loading failures affecting the whole surface. - Do not erase user input after a failed action.
- Secret fields must support masking and must not expose their resolved values in previews, history, notifications, or default exports.
Key/value editing uses KeyValueEditor.svelte, which owns .editor-block, .editor-header, .row-list, .kv-row, .row-toggle, the variable-aware value field, and row action controls. Requests and realtime connections use this same component for query parameters and headers. Header consumers also use the shared header-suggestions.ts catalog so names and context-sensitive values remain consistent. The title and Add row action stay in the header, rows stay in the list, and delete uses the standard icon action. Do not create feature-local key/value editors or add a full-width creation row below the data.
Authentication editors use AuthEditor.svelte, which owns the .editor-block composition: Auth and the .body-mode-select belong in .editor-header; the selected method's fields use .auth-grid; and None renders the standard empty state. HTTP requests provide the optional client-credentials token-fetch helper, while realtime connections use the same OAuth2 layout for a manually supplied access token. Protocol-specific workspaces must not introduce a second auth layout.
All interactive checkboxes use the styled .row-toggle control. Settings-like checkbox rows also add .settings-checkbox so the control aligns with its title and supporting copy. Never rely on the browser-default checkbox in an application workspace.
Use JsonEditor.svelte for editable JSON in requests, realtime messages, Socket.IO auth payloads, and Socket.IO argument arrays. It owns syntax tokenization, environment-variable highlighting and suggestions, caret-safe overlay scrolling, and Enter/Tab indentation. Feature code owns schema-specific validation and formatting actions; it must pass error state through ariaInvalid.
Use .panel for a major workspace region. .panel is intentionally only the visual shell: it provides the background, border, radius, and shadow, but no content padding. Every panel must declare one inset strategy so a new surface cannot silently render against its border.
| Inset strategy | Class | Spacing | Use |
|---|---|---|---|
| Standard | .panel-inset |
--space-5, reduced to --space-4 at 720px |
Normal editors, results, settings, collection pages, and feature workspaces |
| Compact | .panel-inset-compact |
--space-3 |
Tab strips and similarly dense single-row panels |
| Flush | .panel-flush |
0 |
Deliberately edge-to-edge content whose children own every inset |
| Custom | .panel-custom-inset plus a purpose-specific class |
Defined by that component | Dialogs or constrained layouts that cannot use a standard density |
Do not put feature-local padding on a standard panel. Add the appropriate modifier in markup and let the shared modifier own its responsive behavior. A custom inset is an exception that must be documented with the component, not a substitute for choosing a density.
Use .panel-title for a panel's page-level h1. Use .panel-heading around stacked eyebrow/title/supporting-copy groups; it removes browser-default child margins and supplies one --space-1 gap. When a parent already supplies gap, keep its child heading margins at zero. Do not combine a parent gap with heading margin-top or margin-bottom to create the same separation twice.
Use a subtle surface plus a border for nested cards, as demonstrated by .request-script-card and .multipart-file-card.
A panel should have:
- a concise title;
- an action area only when the action applies to the whole panel;
- a clear loading, error, empty, or content state;
- no duplicate page-level title.
Panel review checklist:
- Does the
.paneldeclare standard, compact, flush, or documented custom inset behavior? - Does a stacked heading use
.panel-heading, with.panel-titlefor a page-level title? - Is vertical separation owned by exactly one mechanism: parent
gap, section margin, or component inset? - At the compact breakpoint, does the content retain
--space-4from the panel edge without horizontal overflow?
Use tabs only when views are peers and switching does not submit or navigate through a workflow. Implement role="tablist", role="tab", and aria-selected. The request workspace uses RequestTabs.svelte; local panel tabs use .panel-tabs and .tab-button.
Selection must remain visible without hover and should not rely on text color alone.
The WebSockets workspace reuses the request-tab chip geometry through RealtimeTabs.svelte and the shared horizontalWheelScroll attachment for overflowing strips. Each tab must expose the definition name, protocol badge, connection status text, unsaved marker, and a visible close affordance inside the chip; click that affordance or press Delete on the focused tab to close it. The new-tab action immediately follows the chips but remains outside the semantic tablist, because ARIA tablists may own only tabs. Do not move close/new actions into a detached far-edge toolbar. Restored tabs begin disconnected; do not visually imply that a saved/open tab is a live connection.
Realtime workspace tabs, connection-setting tabs, and transcript-filter tabs use roving focus. Left/Right moves to the previous/next peer, Home/End moves to the first/last peer, and focus follows selection. Delete closes the focused workspace tab through the normal dirty/live confirmation flow. Keep the selected tab at tabindex="0", peers at -1, and connect each tab to its panel with aria-controls/aria-labelledby.
Use DialogShell.svelte with the standard save-dialog size or a purpose-specific size class. Its public properties are ariaLabelledby, onDismiss, optional sizeClass, optional dismissible, and the content snippet. It owns the backdrop, dialog role, focus trap/restore, Escape handling, and backdrop dismissal. Every dialog's content must include:
- a heading whose id matches
ariaLabelledby; - Escape and backdrop dismissal unless a critical operation sets
dismissible={false}; - a visible close/cancel action;
.modal-scroll-bodywhen content can exceed the viewport.
Primary confirmation belongs last in the action row. A destructive confirmation should name the affected object and explain whether recovery is possible.
Saving HTTP and realtime definitions into collection trees uses CollectionSaveDialog.svelte. It owns the collection/folder picker geometry, request-count copy, selected state, and action order; callers provide only labels, current targets, and persistence callbacks. New saved-request types must extend this dialog rather than copying its markup into a route.
Use the notifications store for transient confirmation or failure following a user action. Supported tones are info, success, warning, and error. Notifications expose polite live updates and alert semantics for errors. Their progress bar controls expiry, pauses while the notification is hovered, and becomes visually static under reduced motion without changing the expiry duration.
Use inline feedback when the message:
- blocks progress in the current form;
- needs to remain until the user fixes something;
- explains an empty or partial state;
- is tied to a specific field or result.
Avoid showing the same message both inline and as a notification unless one communicates global impact and the other gives local recovery guidance.
Block feedback uses .feedback plus exactly one tone class: .feedback-info, .feedback-success, .feedback-warning, or .feedback-error. Add role="alert" only when an error appears dynamically and requires immediate attention. Keep field-level validation text local to its control.
Every async or data-dependent surface must design all four states: initial/loading, populated, empty, and error.
.empty-stateshould explain what is absent and, when useful, provide the next action.- Loading copy should identify what is loading. Use progress indicators for operations with measurable progress.
.feedback.feedback-erroris the shared prominent error treatment. Put technical details in a disclosure when the user-facing explanation can be clearer.- Preserve partial or stale content when it remains useful, and label its state rather than blanking the screen.
Primary navigation is route-based and uses .sidebar-link plus .sidebar-link-active. Collections use a shared folder glyph/path implementation across the sidebar and Collections page.
The desktop collection navigator uses flat compact rows rather than collection cards. Collection rows keep the name and request count; folder rows keep the shared glyph, name, and direct-child count; saved-definition rows keep the HTTP method or realtime protocol, name, and URL. Last-updated metadata belongs in the Collections workspace, not in primary navigation. Logical indentation increases for the first three folder levels and then caps at 24px, so arbitrary nesting cannot consume the row's content width. The complete hierarchy remains available in each row's tooltip. Do not place a passive sticky breadcrumb over the scrolling tree: path chrome must provide a real navigation action or be omitted. Saved-definition rows may grow to two name lines plus one truncated URL line and must never crop text vertically with a fixed height.
Collection search is a compact filter rail, not a full-size form field. Keep the keyboard shortcut visible while idle, use the same accent rail and flat-row geometry for results, and retain the matched entity's type, name, URL, ancestry, and collection count. Updated timestamps remain in the Collections workspace rather than search results.
- Tree rows need a clear selected, expanded, hover, and drop-target state.
- Expand/collapse controls require
aria-expandedand an accessible name. - Drag-and-drop must have a non-drag alternative for essential movement or ordering work. Collection folders and saved requests use the Collections-page Move… dialog, which exposes destination collection, folder/root, and first/after-sibling position to keyboard users. Destination contents load only when that collection is selected; keep the folder, position, and confirmation controls disabled while loading and guard stale completions when users change destinations quickly.
- Do not invent a second folder icon or tree-guide geometry.
Use monospace type and --bg-code. Long values must wrap or scroll within their region. JSON should use JsonViewer.svelte; request/response displays should reuse existing key/value and detail patterns.
- Do not truncate the only available copy of a value.
- Provide copy actions for high-value identifiers, URLs, paths, tokens, and payload fragments where appropriate.
- Keep secrets masked by default.
- Syntax color is categorical decoration and may not be the only way to identify invalid content.
RealtimeEditor.svelte and RealtimeTranscript.svelte establish the shared realtime workbench pattern:
- editor and transcript panels use
.panel-inset; the connection tab strip uses.panel-inset-compact; - one large Connect or Disconnect action in the connection header;
- a visible status row with text plus
.realtime-status-dot; - peer connection settings in semantic local tabs;
- an advanced reconnect section disabled until opt-in;
- a composer grouped separately from handshake settings;
- a bounded transcript using
role="log"with live announcements disabled so high-volume traffic does not interrupt assistive technology; - transcript direction written as “Sent”, “Received”, or “Event” and reinforced by a border accent;
- explicit empty, filtered-empty, trim, error, disconnected, reconnect-required, and large-payload states;
- bounded file-backed payload inspection through deliberate Read or Copy actions, plus complete Save, instead of inserting large data into the DOM automatically.
Keep connection definition and session state distinct in copy and hierarchy. “Save” persists a reusable definition; “Connect” starts an ephemeral native session; “Clear” removes only the current session transcript; closing a dirty or live tab explains both consequences.
Realtime failures in a background route use the notification action to return to the affected tab. Do not announce every incoming message globally. When follow mode is paused because the user scrolls away from the bottom, provide an explicit “Follow new messages” action rather than moving their reading position.
New UI must meet these minimum requirements:
- All actions and fields are reachable and operable by keyboard.
- Focus is always visible.
- DOM order matches visual and interaction order.
- Icon-only controls have accessible names; decorative icons use
aria-hidden="true". - Dynamic status uses an appropriate live region without repeatedly interrupting the user.
- Dialog focus is trapped and restored.
- Tabs, listboxes, trees, disclosures, and progress controls expose their state semantically.
- Realtime tab status and protocol are exposed as text; status dots, direction borders, and badges are supplementary.
- High-volume transcripts use a named
role="log"witharia-live="off"; connection-state changes use a concise polite live region. - Information is not communicated by color alone.
- Text and interactive elements retain sufficient contrast in light and dark themes.
- Click targets should normally be at least
32px; use36–40pxfor common actions. - Zoom/UI scale and a narrow window do not hide essential controls or content.
For a complex custom interaction, document keyboard behavior alongside its implementation. Native elements are preferred when they provide the needed semantics.
- Name objects consistently: request, saved request, realtime definition, connection, session transcript, collection, folder, environment, variable, playbook, step, run, and history entry.
- Use “request” for the editable/sendable HTTP object. Use “realtime definition” when distinguishing a saved WebSocket/Socket.IO configuration from its live connection; concise local labels may use “realtime request” where the shared collection model is already clear.
- Use “connection” for a live or connectable WebSocket/Socket.IO tab and “session transcript” for its ephemeral message log. Do not call it durable history.
- Confirm successful persistence with the object name when helpful.
- Error messages should say what failed and what the user can do next. Preserve raw backend details in an expandable technical section when needed.
- State when a value is saved locally, stored in the OS credential store, redacted, unresolved, or exported in full.
- Never imply that previewing a request sends traffic. Preview surfaces must clearly remain read-only.
- For long-running operations, show current activity and expose cancellation when cancellation is safe.
Before implementation, write down:
- The user's goal and the primary action.
- Where the feature fits in the existing page/navigation hierarchy.
- Which existing panels, controls, form rows, dialogs, notifications, and data views it reuses.
- Its loading, empty, error, success, disabled, and cancellation states.
- Keyboard and screen-reader behavior.
- Light/dark, UI-scale, narrow-window, long-content, and secret-data behavior.
Implementation sequence:
- Compose existing shared classes and components.
- Add a semantic token only when an intent is reused or needs theme-specific values.
- Add a shared class/component when the pattern will recur or already appears more than once.
- Keep truly local layout rules beside the feature, using tokens for all visual decisions.
- Validate the state matrix below and run
npm testandnpm run check. - Update this guide when a new reusable pattern or rule is introduced.
| Area | Questions |
|---|---|
| Default | Is the main task and primary action obvious? |
| Hover/focus/active | Are mouse and keyboard states equally clear? |
| Disabled/loading | Is the reason or current activity understandable? |
| Empty | Does the surface explain what to do next? |
| Error | Is the failure actionable and user input preserved? |
| Success | Is confirmation proportional and non-disruptive? |
| Long content | Do names, URLs, code, and errors wrap or scroll safely? |
| Themes | Does it work in light, dark, and system preference? |
| Scale/layout | Does it work at 0.6, 1, 1.5, and a narrow window? |
| Accessibility | Are semantics, names, focus, keyboard, and announcements correct? |
| Privacy | Are secrets masked and exports explicit? |
Audit date: 2026-06-22. Scope: 17 shared frontend components, shared tokens/styles, application shell, and route-level UI patterns after the 0.20.13 remediation.
Components reviewed: 17 | Remaining issue groups: 2 | Score: 94/100
The system now has semantic method/syntax/overlay/sidebar tokens, a compact spacing scale, canonical text-button names, a shared dialog shell, reduced-motion behavior, standardized block feedback, and a keyboard alternative for collection drag-and-drop. Playbooks now consume the shared spacing and typography scale, sidebar collection disclosures support Arrow/Home/End keyboard traversal, and loading/error updates in the sidebar, Playbooks, and history surfaces use consistent status or alert semantics. Remaining work is mostly opportunistic migration of untouched feature-local values and continued live-region review as async surfaces change.
| Issue | Current examples | Direction |
|---|---|---|
| Multiple radius names resolve to the same value | --radius-sm, --radius-card, --radius-control |
Keep semantic names; do not replace them with literal 8px |
| Legacy feature-specific validation names | .auth-error-text, .error-text, .run-error-text |
Keep local text errors near fields; use .feedback-* for message blocks |
| Shared and route-local component styling coexist | global app.css plus Playbooks route styles |
Promote a pattern once it is used by a second feature |
| Category | Coverage | Known gap |
|---|---|---|
| Theme colors | Strong | app.css has no hardcoded hex colors; 21 rgb/rgba occurrences remain in isolated legacy state surfaces |
| Typography | Strong | Playbooks' repeated text roles use the shared typography tokens; isolated component-specific sizes remain where geometry requires them |
| Radius | Strong | Several legacy literal pill radii remain |
| Control height | Good | Specialized controls use fixed heights where composition demands it |
| Spacing | Good | Shared scale exists and Playbooks' repeated gaps and padding now consume it; isolated geometry-specific values remain |
| Elevation | Good | Standard and overlay elevation are tokenized |
| Motion | Good | Shared durations, static reduced-motion expiry, hover pause, and JS notification transition handling are implemented |
| Pattern | Variants/states | Accessibility | Documentation | Score |
|---|---|---|---|---|
| Buttons and icon actions | Good | Visible focus; labels depend on call site | Complete | 9/10 |
| Inputs and key/value rows | Good | Mostly native controls and visible labels | Complete | 8/10 |
| Tabs | Good | ARIA implemented in main tab surfaces | Complete | 8/10 |
| Dialogs | Good | Shared DialogShell, focus trap, Escape, backdrop, focus restore |
Complete | 9/10 |
| Notifications | Good | Live regions, tone roles, reduced motion, hover pause | Complete | 9/10 |
| Panels/cards | Good visually | Semantic structure depends on call site | Complete | 8/10 |
| Empty/loading/error states | Block feedback standardized | Core sidebar, Playbooks, and history updates announce consistently | Partial | 8/10 |
| Trees and drag-and-drop | Pointer drag plus lazy keyboard move dialog | Sidebar disclosures expose state and Arrow/Home/End traversal | Complete | 9/10 |
- Continue standardizing live-region behavior when untouched async loading, empty, and error surfaces are changed.
- Migrate repeated feature-local visual values to shared tokens when a second use establishes reusable intent.
These are improvement directions, not permission for an unrelated sweeping refactor. Feature changes should improve the code they touch while preserving recognizable behavior.
Before merging a UI change:
- Existing tokens and patterns were reused before new ones were added.
- The UI has intentional loading, empty, error, success, and disabled states.
- Light, dark, and system themes were checked.
- Default, minimum, and maximum UI scale were considered.
- Narrow-window and long-content behavior were checked.
- Keyboard navigation and visible focus were checked.
- Accessible names, roles, states, and live announcements are correct.
- Secrets and credential-looking values remain masked by default.
- Destructive actions explain impact and require appropriate confirmation.
- New reusable behavior is documented here.
-
npm testpasses when shared behavior or interaction helpers change. -
npm run checkpasses.