feat(frontend): Spectrum UX port — Tabs + AlertBanner + BackLink + compact tables (sprint 11) #13
26
CHANGELOG.md
26
CHANGELOG.md
@@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added — Sprint 11 (Spectrum UX port: 4 primitives + compact density global)
|
||||||
|
|
||||||
|
**Frontend only** (233 vitest passing — baseline 212 + 21 new tests across 4 new specs)
|
||||||
|
|
||||||
|
- `frontend/src/hooks/useHashTab.ts` — NEW. Pure-TS hook that reads `window.location.hash`, falls back to a default tab id, and listens to `hashchange` (cleanup on unmount). No style dependency.
|
||||||
|
- `frontend/src/components/Tabs.tsx` — NEW. `<Tabs items activeId onChange>` component. Underline variant: `tab-underline` / `tab-underline-active` CSS recipes. Count pill: `tab-count-pill` / `tab-count-pill-active` (`rounded-pill` exception per DESIGN.md). `role="tablist"` + `aria-selected` on tabs. Zero transitions.
|
||||||
|
- `frontend/src/components/AlertBanner.tsx` — NEW. `<AlertBanner variant="error|warn|success|info" title? children>`. Border-l-4 semantic strip, `bg-paper`, Lucide icon at `size={16}`. ARIA: `role="alert"` for error/warn, `role="status"` for success/info. No shadow, no radius, no transition.
|
||||||
|
- `frontend/src/components/BackLink.tsx` — NEW. `<BackLink to children>` renders `ArrowLeft` (Lucide, 14px) + text with `caption-md text-graphite hover:text-primary`. Replaces 3 hand-rolled back-link instances.
|
||||||
|
- `frontend/src/styles/index.css` — Added recipes: `.tab-underline`, `.tab-underline-active`, `.tab-count-pill`, `.tab-count-pill-active`, `.alert-error`, `.alert-warn`, `.alert-success`, `.alert-info`, `.table-compact` (32px row density, global).
|
||||||
|
- `frontend/src/pages/EngagementDetailPage.tsx` — Refactored to 3-tab layout (Schedule / Description / Simulations) wired via `useHashTab('schedule')`. Count pill on Simulations tab. BackLink replaces hand-rolled link.
|
||||||
|
- `frontend/src/pages/SimulationFormPage.tsx` — Done banner and SOC-blocked banner migrated to `<AlertBanner>`. Both back-link instances replaced with `<BackLink>`.
|
||||||
|
- `frontend/src/pages/TemplateFormPage.tsx` — Back-link replaced with `<BackLink>`.
|
||||||
|
- `frontend/src/pages/EngagementsListPage.tsx` — Table marked `table-compact`, inline `px-xl py-md` padding removed from th/td (recipe handles it).
|
||||||
|
- `frontend/src/pages/TemplatesListPage.tsx` — Same compact table treatment.
|
||||||
|
- `frontend/src/pages/UsersAdminPage.tsx` — Same compact table treatment.
|
||||||
|
- `frontend/src/components/SimulationList.tsx` — Same compact table treatment.
|
||||||
|
- `DESIGN.md` — Added `### Navigation › Sub-page tabs` subsection; updated `### Data Tables` row min-height 44px → 32px with WCAG SC 2.5.5 rationale; added `### Inline Banners` subsection under `### Toast Notifications`.
|
||||||
|
|
||||||
|
**Tests added** (21 new assertions across 4 new spec files):
|
||||||
|
- `tests/hooks/useHashTab.test.tsx` — 4 tests (default, hash read on mount, navigate(), empty-hash fallback)
|
||||||
|
- `tests/components/Tabs.test.tsx` — 7 tests (render, aria-selected, active class, count pill, count-pill-active, onChange, brutalism)
|
||||||
|
- `tests/components/AlertBanner.test.tsx` — 6 tests (4 variants × role + class, title prop, brutalism)
|
||||||
|
- `tests/components/BackLink.test.tsx` — 4 tests (render, href, brutalism, svg icon)
|
||||||
|
|
||||||
|
**No backend changes.** No DB schema change. No migration.
|
||||||
|
|
||||||
### Changed — Sprint 7 (Terminal-SOC design refresh)
|
### Changed — Sprint 7 (Terminal-SOC design refresh)
|
||||||
|
|
||||||
**Frontend** (136 vitest passing — unchanged count, 3 assertions updated for new token names)
|
**Frontend** (136 vitest passing — unchanged count, 3 assertions updated for new token names)
|
||||||
|
|||||||
31
DESIGN.md
31
DESIGN.md
@@ -216,11 +216,23 @@ Shadows (`box-shadow`) are not used anywhere.
|
|||||||
- Text `{colors.slab-text}`, caption-md, `rounded-none`
|
- Text `{colors.slab-text}`, caption-md, `rounded-none`
|
||||||
- Active state: 2px primary bottom border
|
- Active state: 2px primary bottom border
|
||||||
|
|
||||||
|
#### Sub-page tabs
|
||||||
|
|
||||||
|
Used inside pages that need content partitioning without a URL change (e.g. EngagementDetailPage: Schedule / Description / Simulations).
|
||||||
|
|
||||||
|
- **Container**: `flex items-end border-b border-hairline gap-xs` — sits on the `{colors.hairline}` bottom line.
|
||||||
|
- **`.tab-underline`**: `text-graphite caption-bold cursor-pointer border-b-2 border-transparent hover:text-ink px-xs`. Hover is **instant** — no `transition-*`.
|
||||||
|
- **`.tab-underline-active`**: `text-primary border-primary` — 2px primary underline, primary text.
|
||||||
|
- **Count pill** (optional, e.g. simulation count): `.tab-count-pill` — `rounded-pill bg-cloud text-graphite text-[11px] px-xs py-0 font-mono`. `rounded-pill` is the documented exception.
|
||||||
|
- **Active count pill**: `.tab-count-pill-active` — `bg-primary-soft text-primary`.
|
||||||
|
- ARIA: `role="tablist"` on container; `role="tab"` + `aria-selected` on each button.
|
||||||
|
|
||||||
### Data Tables
|
### Data Tables
|
||||||
|
|
||||||
- `table-layout: fixed`, `word-break: break-word`
|
- `table-layout: fixed`, `word-break: break-word`
|
||||||
- Header row: background `{colors.cloud}`, `body-emphasis` text, 1px `{colors.hairline}` bottom border
|
- Header row: background `{colors.cloud}`, `body-emphasis` text, 1px `{colors.hairline}` bottom border
|
||||||
- Data cells: 44px min-height, 1px `{colors.hairline}` bottom border
|
- Data cells: **32px min-height** (compact density, all surfaces). Class: `.table-compact`.
|
||||||
|
- **WCAG SC 2.5.5 tradeoff**: the recommended 44×44px touch target is intentionally not met on data rows. Mimic is a BAS operator console used at desktop on mouse/keyboard — density and scanability take priority over touch target size. Action buttons within rows retain icon ≥ 24px with adequate padding to preserve a usable hit area.
|
||||||
- ID / date / technique columns: `font-mono`
|
- ID / date / technique columns: `font-mono`
|
||||||
- Zebra striping optional — use `{colors.cloud}` for odd rows if table is wide
|
- Zebra striping optional — use `{colors.cloud}` for odd rows if table is wide
|
||||||
|
|
||||||
@@ -229,6 +241,23 @@ Shadows (`box-shadow`) are not used anywhere.
|
|||||||
- Angular (`rounded-none`), 4px left border strip in semantic color (success/warn/bloom-deep/primary)
|
- Angular (`rounded-none`), 4px left border strip in semantic color (success/warn/bloom-deep/primary)
|
||||||
- Background `{colors.paper}`, 1px `{colors.hairline}` border
|
- Background `{colors.paper}`, 1px `{colors.hairline}` border
|
||||||
|
|
||||||
|
#### Inline Banners
|
||||||
|
|
||||||
|
For persistent in-page status messages (not ephemeral toasts). Use `<AlertBanner variant="...">` component.
|
||||||
|
|
||||||
|
Four variants — all share: `bg-paper border border-hairline border-l-4 rounded-none px-md py-sm flex items-start gap-sm`.
|
||||||
|
|
||||||
|
| Variant | Left strip color | ARIA role | Lucide icon |
|
||||||
|
|---------|-----------------|-----------|-------------|
|
||||||
|
| `error` | `border-l-bloom-deep` | `role="alert"` | `AlertCircle` |
|
||||||
|
| `warn` | `border-l-warn` | `role="alert"` | `AlertTriangle` |
|
||||||
|
| `success` | `border-l-success` | `role="status"` | `CheckCircle` |
|
||||||
|
| `info` | `border-l-primary` | `role="status"` | `Info` |
|
||||||
|
|
||||||
|
- Icon: Lucide at `size={16}`, `aria-hidden`, `mt-[2px] shrink-0` for baseline alignment.
|
||||||
|
- No shadow, no rounded corners, no transition.
|
||||||
|
- Optional `title` prop renders as `font-medium text-ink` above the message body.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Do's and Don'ts
|
## Do's and Don'ts
|
||||||
|
|||||||
Reference in New Issue
Block a user