Files
member-console/docs/design-system.md
Christian Galo 2aa58d555a Mark 7d SPA to MPA conversion as done
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
2026-05-16 02:33:03 -05:00

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-container positioning
  • 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-container lives outside any HTMX swap target, so the toast survives partial swaps and hx-boost full-page navigations. An inline alert-success in 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-live for async regions. Content that appears without a navigation — toasts, and HTMX swap targets that surface validation or results — must be in an aria-live region so screen readers announce the change. Use assertive only for errors; polite for 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 an aria-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.