Update IA and status docs to reflect the landed M7d MPA conversion.
- docs/operator-ia.md: switch framing to implemented routes and add
/operator/persons/{personID} (lookup-only)
- openspec/changes/operator-mpa-conversion/tasks.md: mark verification
and documentation tasks as completed with notes on checks
- status/milestones.md: flip 7d to Done and summarize what landed
- status/operator-ux.md: add "7d Landed Decisions" section
- docs/design-system.md: small wording tweak about hx-boost navigations
7.6 KiB
Design System
This is a Bootstrap usage guide plus a custom-primitives reference — not a standalone design system. The member-console UI is Bootstrap-first: it uses Bootstrap 5.3.6 components and utilities directly, and supplements them only where Bootstrap genuinely falls short.
Status: M7c deliverable. Reused downstream by M8/M9 member-side UX.
1. Foundation
Bootstrap-first
There is no custom token layer and no parallel utility system. Spacing, typography, colors, breakpoints, and layout all come from Bootstrap. Reach for a Bootstrap utility class or component before writing CSS.
Role of app.css
internal/embeds/static/app.css is supplemental only. A rule belongs there
only when Bootstrap genuinely cannot express it. Today that is:
- the HTMX loading-indicator hook (
.htmx-indicator/.htmx-request) - fixed
.toast-containerpositioning - cross-browser
<meter>styling (Bootstrap does not style<meter>) - the operator nav-tab active-state weight
The file carries a contract comment block at the top stating this. Keep it short; if it starts to grow a token system, that is a signal to stop.
Bootstrap CSS variable convention
Do not hardcode color values in app.css or templates. Reference Bootstrap's
CSS variables so the UI stays in sync if Bootstrap's defaults change:
/* no */ background: #0d6efd;
/* yes */ background: var(--bs-primary);
Common ones: --bs-primary, --bs-secondary, --bs-body-color,
--bs-border-color, --bs-success, --bs-danger.
Future theming hook
Bootstrap 5's CSS-variable architecture is the intended seam for per-deployment
theming (see the "Custom theming (two-tier)" backlog item in
status/milestones.md). Tier 1 = operator overrides a handful of --bs-*
variables; Tier 2 = unrestricted custom CSS. Nothing is built yet — but because
app.css references --bs-* instead of hex values, that feature stays a small
server-side addition rather than a refactor.
2. Approved component palette
Buttons
| Variant | Use for | Notes |
|---|---|---|
btn-primary |
The single primary action on a screen/form | |
btn-outline-primary |
Emphasis secondary actions | |
btn-outline-secondary |
Neutral secondary actions (navigate, view, sub-views) | Default for "show me more" buttons |
btn-danger / btn-outline-danger |
Destructive actions | Pair with the confirm modal (§3) |
btn-link |
Tertiary / inline actions |
Retired — do not use:
| Variant | Color | Contrast on white | Why |
|---|---|---|---|
btn-outline-info |
#0dcaf0 cyan |
~1.8:1 | Fails WCAG 2.1 AA (needs 4.5:1) |
btn-outline-warning |
#ffc107 yellow |
~1.28:1 | Fails WCAG 2.1 AA (needs 4.5:1) |
Both outline variants render unreadable text against the app's white
background. Replace neutral uses with btn-outline-secondary and emphasis uses
with btn-outline-primary. (The solid btn-info / btn-warning and the
text-bg-* badge variants are unaffected — only the outline button variants are
retired.)
Badges
Use text-bg-* badge variants (text-bg-secondary, text-bg-success, etc.)
rather than bare bg-* where contrast matters — text-bg-* pairs the
background with a contrast-checked foreground.
Alerts
alert-success / alert-danger / alert-warning are fine for in-page,
persistent messaging (e.g. validation summaries rendered into a swap target).
For transient success/error feedback that must survive an HTMX swap or
navigation, use the toast primitives (§3) instead.
3. Custom primitives
Confirm modal
A single shared, accessible confirmation dialog replaces htmx's native
hx-confirm. Implemented by static/confirm-action-modal.js; the modal markup
lives in the operator layout (#confirmActionModal).
Trigger contract — data-* attributes on any trigger element:
| Attribute | Required | Meaning |
|---|---|---|
data-bs-toggle="modal" data-bs-target="#confirmActionModal" |
yes | Opens the modal |
data-action-url |
yes | Endpoint to call |
data-action-method |
yes | post or delete |
data-action-target |
yes | CSS selector for the HTMX swap target |
data-action-title |
no | Modal title (default Confirm) |
data-action-body |
no | Modal body text/HTML |
data-action-confirm-label |
no | Submit button label (default Confirm) |
data-action-style |
no | danger | primary | warning (default danger) |
data-action-fields |
no | JSON object of hidden form fields |
Behavior: a successful (2xx/3xx) response closes the modal; a failed request leaves it open so the operator can read the error in the swap target and retry.
Error toast
Transient error feedback. Markup (#errorToast) lives in each layout's
.toast-container; behavior in static/error-handler.js. It is shown
automatically by the htmx:beforeSwap handler for 4xx/5xx/network failures, and
can be shown manually via showErrorToast(message).
role="alert",aria-live="assertive"— interrupts the screen reader, because an error needs immediate attention.
Success toast
Transient success feedback. Markup (#successToast) lives in each layout's
.toast-container; behavior in static/success-toast.js.
Triggered from the server via an HX-Trigger response header:
// Works for both HTMX partial swaps and hx-boost navigations.
w.Header().Set("HX-Trigger", `{"showSuccessToast": "Product created"}`)
role="status",aria-live="polite"— announced without interrupting, because success is not urgent.- The
.toast-containerlives outside any HTMX swap target, so the toast survives partial swaps andhx-boostfull-page navigations. An inlinealert-successin a POST response body would be discarded by the redirect target and never seen — that is why this primitive exists.
Existing templates are not migrated to this primitive in M7c; M7e owns the convention and timing for adopting it across flows.
Loading states
HTMX request indicators use the .htmx-indicator hook (styled in app.css).
Place a .htmx-indicator element inside the triggering control and let HTMX
toggle it via .htmx-request. For destructive forms, also use
hx-disabled-elt to disable the submit button during the request (see the
confirm modal form).
Empty states
Empty collections render a centered, muted message rather than a bare blank area:
<div class="text-center py-4">
<p class="text-muted mb-0">No plan ladders found.</p>
</div>
Keep the copy specific ("No plan ladders found") rather than generic ("No data").
4. WCAG 2.1 AA baseline
- Contrast. Text and meaningful UI must meet 4.5:1 (3:1 for large text / components). The two retired outline button variants (§2) failed this; check any new color use against it.
aria-livefor async regions. Content that appears without a navigation — toasts, and HTMX swap targets that surface validation or results — must be in anaria-liveregion so screen readers announce the change. Useassertiveonly for errors;politefor everything else.- Focus management. Modals trap and restore focus (Bootstrap handles this for
.modal). Do not remove focus outlines. Interactive controls must be reachable and operable by keyboard. - Semantic markup. Use real
<button>/<a>elements (not clickable<div>s), associate every form control with a<label>, and give icon-only controls anaria-label. - Audit. M7f runs the formal axe + Lighthouse a11y pass and keyboard-nav review across all operator screens; this section is the standing checklist that pass measures against.