wulf-pulse/DESIGN.md
lorentz 9bfb57553d feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch.  Drops 2013-era
inline styles and consolidates patterns behind shared primitives.

Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
  the standards-guide blue (#0075AD) with utility classes for numerics
  (.num / .num-lg / .num-xl), metric labels, surface tints, and the
  wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
  Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
  "Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page

Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
  health table, worker pulse cards (analyzer / RMM / sync scheduler),
  token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
  to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
  integrations (e.g. SentinelOne) — no failure noise from broken-on-
  purpose entries.  Aliases supported (sentinelone → s1, etc.)

Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
  total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
  area chart, 30-day mean resolution time line chart, today's active
  engineers leaderboard

Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
  status, classification, source, company type, publish, active /
  yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)

Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
  PageHeader rule (consistent across flat links and submenu triggers);
  active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config

Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs

DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
  unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow

Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
  collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below

Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
  workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
  rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
  INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00

396 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Pulse — Design System
This file documents how the UI is put together today: the tokens, the
component vocabulary, the navigation IA, and the patterns each page is
expected to follow. It also calls out the rough edges that the
`nav-design-improvements` branch exists to address — treat the "Open
improvements" section as the working backlog for visual/UX cleanup.
System overview lives in `ARCHITECTURE.md`. Per-feature notes live in `docs/`.
## 1. Design principles
- **Information density over decoration.** Pulse is an internal operations
console. Tables, dashboards, and admin tools win over hero spacing.
- **One source of truth per pattern.** If shadcn/ui has a primitive, use it —
don't re-implement (`<table>` raw, custom skeletons, ad-hoc dropdowns).
- **Tokens, not hex.** Colors come from CSS variables in `app/globals.css`,
not Tailwind's full color palette. The same goes for radius and spacing.
- **Roles shape the nav, not the layout.** Hide items the user can't access;
don't restructure the page shell per role.
- **Read-first, write-confirmed.** Destructive or external-effect actions
(RMM execute, IT Glue write) live behind explicit dialogs with confirmation,
not inline buttons.
## 2. Tokens
App tokens are defined in `app/globals.css` (shadcn defaults). The Wulf
brand layer in `app/styles/brand.css` is imported last and repoints
`--primary` / `--accent` / `--ring` / `--chart-2` to the standards-guide
blue (`#0075AD`) and provides Wulf gray ramps (`--wulf-gray-100/300/700`).
Tailwind 4 reads them via `@theme inline`; never hard-code colors or
radii in components.
### Colors (OKLch)
| Token | Light | Dark | Use |
|---|---|---|---|
| `--background` / `--foreground` | white / near-black | near-black / near-white | Page surface and primary text |
| `--card` / `--card-foreground` | white | `oklch(0.205 0 0)` | Card surfaces |
| `--primary` / `--primary-foreground` | logo blue `oklch(0.55 0.16 220)` | brighter blue `oklch(0.62 0.17 220)` | Primary actions, active nav, focus ring |
| `--secondary` / `--muted` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | Subtle surfaces, table headers |
| `--muted-foreground` | `oklch(0.556 0 0)` | `oklch(0.708 0 0)` | Secondary text, descriptions |
| `--accent` | same as primary | same as primary | Hover/selection accents |
| `--destructive` | `oklch(0.577 0.245 27.325)` | `oklch(0.704 0.191 22.216)` | Errors, destructive buttons |
| `--border` / `--input` | `oklch(0.922 0 0)` | `oklch(1 0 0 / 10%)` | Borders, input outlines |
| `--ring` | logo blue | brighter blue | Focus ring |
| `--chart-1``--chart-5` | distinct hues | distinct hues | Recharts series |
| `--sidebar*` | mirrors page tokens | mirrors page tokens | Reserved for a future sidebar nav |
Status hues outside this set (success greens, warning ambers, info blues)
should still go through Tailwind's named palette at `-500` or `-600` and use
the muted background pair `bg-{hue}-500/15 text-{hue}-600` for badges so the
contrast stays acceptable in both modes.
### Radius
`--radius: 0.625rem`. Derived: `radius-sm` (`-4px`), `radius-md` (`-2px`),
`radius-lg` (=), `radius-xl` (`+4px`). Use `rounded-md` / `rounded-lg`
don't introduce custom radii.
### Type
Brand mandate (per `docs/StandardsGuide (1).pdf`, 2013): Helvetica /
Arial, Bold for headers and Light for the tagline. **As of 2026-05 the
app uses IBM Plex Sans instead** — same spirit (engineered sans, Light
weight available), reliably hosted, and pairs with Plex Mono for
numerics. Helvetica / Arial remain in the fallback chain.
- Sans: **IBM Plex Sans** via `next/font/google`, weights 300/400/500/600/700.
Wired via `--font-plex-sans` in `app/layout.tsx`.
- Mono: **IBM Plex Mono** via `next/font/google`, weights 400/500/600.
Used for numerics, IDs, timestamps — not for body text.
Type scale: stick to Tailwind defaults. Reserve `text-2xl font-bold
tracking-tight` for page titles (matches `PageHeader`); use `text-lg
font-semibold` for card titles and `text-sm text-muted-foreground` for
descriptions. Numerics go through the `.num` / `.num-lg` / `.num-xl`
utilities defined in `app/styles/brand.css`.
### Dark mode
CSS-class strategy (`.dark` on `<html>`). Toggled by `ThemeToggle` in the
top bar; persisted via `next-themes`. Component-level dark variants should be
unnecessary if you stick to tokens.
## 3. Layout
### Page shell
```
┌─ <AppNavigation /> (sticky h-16, backdrop blur, z-50) ─┐
├─ <PageHeader /> (optional, bordered, container-aligned)
└─ <main> (container mx-auto px-6 py-6)
├─ Cards / sections, separated by space-y-6
└─ …
```
`PageHeader` lives at the bottom of `components/navigation/app-navigation.tsx`.
It owns the title (`h1.text-2xl.font-bold.tracking-tight`), description
(`text-muted-foreground`), optional breadcrumbs (`/`-separated), and an
actions slot.
### Container & spacing rules
| Rule | Value |
|---|---|
| Horizontal container | `container mx-auto px-6` |
| Vertical page padding | `py-6` (default), `py-8` only on auth/landing |
| Section gap | `space-y-6` for stacked sections, `gap-6` for grid layouts |
| Card body padding | shadcn default — don't override unless wrapping a table (`p-0`) |
| Item-level gap | `gap-2` for inline rows, `gap-4` for form rows |
These are the targets. Today's pages don't all conform — see "Open
improvements" below.
## 4. Navigation IA
The top bar (`components/navigation/app-navigation.tsx`) is the only nav. It
is sticky, full-width, and structured left-to-right as:
1. **Brand** — logo + "Pulse" wordmark (`hidden sm:block` for the wordmark)
2. **Primary menu** (`<NavigationMenu>`)
3. **Right-side controls**`<ThemeToggle />`
### Items today
| Label | Route | Visibility |
|---|---|---|
| Dashboard | `/` | All |
| Configuration Items | `/configuration-items` | All |
| Backup Status ▾ | submenu | All |
| → Backup Status | `/backup-status` | |
| → RPO Comparison | `/veeam-comparison` | |
| → Ticket Analysis | `/veeam-analysis` | |
| Engagement ▾ | submenu | super-admin |
| → Overview | `/engagement` | |
| → Employee Profile | `/engagement/profile` | |
| Analyzer ▾ | submenu | All |
| → Browse Tickets | `/analyzer/tickets` | |
| → Aggregate Reports | `/analyzer/reports` | |
| → Needs Review | `/analyzer/queue` | |
| → IT Glue — Applications | `/analyzer/itglue/applications` | |
| → IT Glue — Configurations | `/analyzer/itglue/configurations` | |
| Admin | `/admin` | super-admin |
Role gating is hardcoded against `session.user.role`. Hidden items are not
rendered (no greyed-out variants).
`/admin` is the entry tile for everything admin-only — sync schedules, rules,
mappings, RMM Overshell, IT Glue write log, device-link conflicts. Sub-pages
don't appear in the top nav; they're reached from the admin landing grid.
### Mobile
`AppNavigation` returns `null` for any path under `/mobile` — kiosk and field
flows have their own shell. The desktop nav doesn't currently collapse to a
hamburger; under `sm` the brand wordmark hides and the menu items wrap.
Mobile-friendly behavior on small viewports is on the improvements list.
## 5. Components
### shadcn/ui primitives (in `components/ui/`)
| Available | Used heavily | Rare / underused |
|---|---|---|
| `accordion` | | yes (collapsible filters could use it) |
| `alert`, `alert-dialog` | yes (errors, confirmations) | |
| `badge` | yes (status, counts) | |
| `button` | yes | |
| `calendar`, `popover` | | engagement profile |
| `card`, `card-header/title/content` | yes (page sections) | |
| `checkbox`, `switch` | yes | |
| `collapsible` | yes (CI page) | |
| `dialog`, `alert-dialog` | yes (modals) | |
| `dropdown-menu` | yes (actions) | |
| `form` (react-hook-form bridge) | admin/auth only | |
| `input`, `label`, `textarea` | yes | |
| `multi-select` | analyzer filters | |
| `navigation-menu` | top bar only | |
| `progress` | | yes (could replace ad-hoc bars) |
| `scroll-area` | | yes |
| `select` | yes | |
| `separator` | yes | |
| `skeleton` | yes (loading states) | |
| `slider` | rare | |
| `sonner` (toast) | yes — `richColors`, top-right | |
| `table` | **inconsistent** — many pages use raw `<table>` | |
| `tabs` | yes (Engagement, CI, DetailModal) | |
### Feature components
| Path | Purpose |
|---|---|
| `components/navigation/app-navigation.tsx` | Top nav + `PageHeader` |
| `components/admin/DataTable.tsx` | Custom paginated/sortable table (not @tanstack) |
| `components/admin/DetailModal.tsx` | Ticket detail shell, tabs (status/priority maps now in `lib/status-registry.ts`) |
| `components/branding/wulf-mark.tsx` | `<WulfMark />` — W glyph or full wordmark |
| `components/branding/tagline-footer.tsx` | "Don't be afraid to cry · Wulf Consulting" footer line |
| `components/dashboard/kpi-card.tsx` | KpiCard with delta indicator and tonal left border |
| `components/navigation/page-header.tsx` | `<PageHeader />` — title, breadcrumbs, actions; supports `accent` and `watermark` |
| `components/navigation/status-indicator.tsx` | Top-bar StatusLight that links to `/status` |
| `components/ui/empty-state.tsx` | Shared zero-data placeholder |
| `components/ui/status-badge.tsx` | Small rounded pill driven by `lib/status-registry.ts` |
| `components/ui/status-light.tsx` | 8 px square indicator (5 states, 3 sizes) |
| `components/admin/SyncScheduler.tsx` | Schedule editor on `/admin` |
| `components/analyzer/analyze-button.tsx` | Trigger analysis from a ticket |
| `components/analyzer/share-modal.tsx` | Email share dialog |
| `components/analyzer/provider-toggle.tsx` | Anthropic/OpenRouter switch |
| `components/analyzer/related-tickets-panel.tsx` | Bundle members on analysis page |
| `components/analyzer/itglue-suggestions-panel.tsx` | Inline IT Glue cross-refs |
| `components/rmm/rmm-dispatch-dialog.tsx` | Pick + execute RMM script |
| `components/rmm/rmm-script-picker.tsx` | Script registry browser |
| `components/rmm/rmm-execution-stream.tsx` | Live tail of an execution |
| `components/configuration-items/config-item-modal.tsx` | CI detail shell |
Anything not in those two directories — and not a one-off page-local
subcomponent — should probably move there.
## 6. Icons
`lucide-react`, imported per-icon. Conventions:
- Inline with text: `h-4 w-4` (16 px), with `mr-2` if leading.
- Standalone tile/card icons: `h-6 w-6`.
- Admin landing tile icons: `h-8 w-8` plus a muted color (`text-muted-foreground`).
- Color: inherit from text; status icons (`text-orange-600`, `text-green-600`)
only when they carry semantic meaning, not for decoration.
Mixed `size-4` vs `h-4 w-4` exists today. Prefer `h-4 w-4` to match shadcn.
## 7. Tables, modals, forms
### Tables
There are two patterns in the codebase. Pick the leftmost that fits:
1. **`components/admin/DataTable.tsx`** — paginated, sortable, searchable.
Backed by `@tanstack/react-table` v8 in manual mode. Use this for any
list ≥ 25 rows or where users need to filter / sort. Pass
`getRowCanExpand` + `renderSubRow` for drill-down rows.
2. **shadcn `Table`** primitive in `components/ui/table.tsx` — for short
static lists wrapped in a `<Card>` with `<CardContent className="p-0">`.
Raw `<table>` markup is no longer in use anywhere under `app/`. If you find
yourself reaching for it, lift the layout into one of the two primitives.
### Modals & dialogs
- **`Dialog`** — most cases (forms, confirmations, RMM dispatch).
- **`AlertDialog`** — destructive confirmations only.
- **`DetailModal`** — tabbed read-only deep-dive on a ticket. Hardcoded
status/priority color map; if you need that map elsewhere, lift it out
rather than copy.
- A "panel" (e.g., `related-tickets-panel`) is a card laid out like a side
panel — it is not a modal and shouldn't trap focus.
### Forms
- **react-hook-form + Zod resolver** with the shadcn `Form` primitive — used
on auth and admin forms (invites, role edits). Use this for any new form
with ≥ 3 fields or any field that needs validation.
- **Controlled inputs** (`useState` + `onChange`) — acceptable for one-off
filter bars. Don't introduce a third pattern.
Error display today is inconsistent (toast vs inline). New forms should
display field errors inline via `<FormMessage>` and use toasts only for
submit-time outcomes.
## 8. Feedback
- **Toasts** — `sonner` configured top-right with `richColors` (`app/layout.tsx`).
Use `toast.success` / `toast.error` / `toast.info`. Don't render error
banners inside the page when a toast fits.
- **Loading** — `<Skeleton>` for content placeholders, `<Loader2 />` from
lucide with `animate-spin` for inline button spinners. Don't roll a custom
spinner div.
- **Empty states** — today: a centered `text-muted-foreground` line. Aim:
a small icon (lucide), a one-line headline, an optional CTA. There is no
shared `EmptyState` component yet — adding one is on the improvements list.
## 9. Charts
`recharts`, themed via the `--chart-1``--chart-5` CSS variables (Tailwind
exposes them as `text-chart-1` etc., so colors flip with light/dark
automatically). Common margins `{ top: 5, right: 8, bottom: 24, left: 0 }`.
Examples: `app/dashboard/page.tsx`, `app/engagement/page.tsx`.
## 10. Open improvements (this branch)
The `nav-design-improvements` branch tracks visual/UX cleanup. The list
below is the working backlog; expand as we go.
### Navigation
- [x] ~~Top-nav "Admin" item is a flat link~~ — now a dropdown with the
seven most-used admin pages (Sync, Workflow, RMM Overshell, IT Glue
Writes, Device Conflicts, Users & roles).
- [x] ~~No user menu / avatar / sign-out in the top bar~~`<UserMenu />`
now sits next to `ThemeToggle`. Shows initials, name/email, role
badge, links to `/settings` + `/settings/security`, and sign-out.
- [x] ~~No mobile-collapsed (hamburger) version~~`<MobileNav />` in a
Sheet. Reuses the desktop nav config so IA stays in sync. Desktop nav
hides under `md`.
- [x] ~~Submenu width is fixed~~ — now content-driven via `min-w-[320px]
max-w-[440px]`, single column with denser rows, so 7-item Admin and
5-item Analyzer menus fit without forced two-column awkwardness.
- [x] ~~Active-route highlighting on submenu items~~ — every top-bar item
(flat or trigger) marks active state with a 2 px Wulf-blue underline
that echoes the PageHeader rule. Active children inside dropdowns get
a tinted `bg-primary/10` background.
### Page shell
- [x] ~~Adopt `PageHeader` everywhere~~ — `/dashboard`, `/status`, all
`/admin/*` sub-pages, `/addigy-devices`, `/analyzer/ticket/[…]`,
`/analyzer/analysis/[…]` now use it. A handful of pages still need
it (kiosk, settings, sentinelone/* — low priority).
- [ ] Standardize container + padding (`container mx-auto px-6 py-6`). Pages
using `px-4`, `py-8`, `max-w-2xl` etc. should justify the deviation.
- [ ] Standardize section spacing (`space-y-6` between cards; pick `gap-6`
for grids).
- [x] ~~Add breadcrumbs to ticket / analysis detail routes~~. Still open
for `/configuration-items/[id]` and `/analyzer/itglue/{applications,
configurations}/[id]`.
### Components
- [x] ~~Build a shared `<EmptyState />`~~ — `components/ui/empty-state.tsx`.
- [x] ~~Build a shared `<StatusBadge />` that consumes the priority/status maps
from `DetailModal`~~ — registry in `lib/status-registry.ts`,
badge in `components/ui/status-badge.tsx`, DetailModal migrated.
- [x] ~~Extract `PageHeader` and add `accent` + `watermark` props~~ —
`components/navigation/page-header.tsx`. Adopted on `/dashboard`
and `/status`.
- [x] ~~Replace raw `<table>` markup on admin and Addigy pages with shadcn
`Table`~~ — Addigy, /status, all admin/sync/* pages, backup-status,
engagement/profile, veeam-comparison migrated. Only `/veeam-analysis`
remains (DataTable territory; filter + expand + paginate).
- [x] ~~Replace raw `<input type="checkbox">` on `/addigy-devices`~~ —
done; also caught one in `/admin/sync/mimecast`.
- [x] ~~/veeam-analysis raw `<table>`~~ — migrated to shadcn `Table`
primitive. The page keeps its own pagination + category filter + row
expansion (DataTable doesn't support row expansion yet, so a deeper
DataTable migration is folded into the long-term decision below).
- [x] ~~Decide on `DataTable` long-term~~ — **migrated to
`@tanstack/react-table` v8** in manual mode. The external API stays
stable (existing data-browser pages keep working without changes);
internally TanStack drives sort + expansion. New optional props for
consumers: `getRowCanExpand` and `renderSubRow` enable
expandable-row patterns (useful for `/veeam-analysis`-style
drill-downs).
### Status & dashboard split (2026-05-03)
- [x] Move integration health + sync health off `/dashboard` onto a
dedicated `/status` route. Top-bar `<StatusIndicator />` links there.
- [x] Rebuild `/dashboard` around KPI cards (today snapshot + needs
attention) and recent activity. Drop the integration / sync
cards.
- [x] ~~Add queue × priority heatmap to `/dashboard`~~ — `QueueHeatmap`
backed by `/api/dashboard/trends`.
- [x] ~~Add 30-day volume + mean resolution charts~~ — `VolumeTrend` and
`ResolutionTrend` components, recharts, brand-blue series.
- [x] ~~Add active engineers panel~~ — `ActiveEngineers` (today's hours
logged, ticket touch count).
- [x] ~~Worker pulse section on `/status`~~ — analyzer / RMM / sync
scheduler heartbeats via `/api/status/workers` and `WorkerPulse`.
### Tokens & theming
- [ ] Audit places that hard-code Tailwind palette colors (`text-orange-600`,
`bg-blue-500/15`) and either keep them as semantic status colors or
move them behind a token.
- [ ] Verify dark-mode contrast on status badges and chart legends; the 10%-
opacity borders in dark mode are subtle and may need lifting.
### Loading & empty
- [x] ~~Standardize Skeleton heights~~ — helpers in
`components/ui/skeleton-helpers.tsx`: `SkeletonRow`, `SkeletonRows`,
`SkeletonCard`, `SkeletonChart`, `SkeletonHeader`, `SkeletonTable`.
- [ ] Adopt the helpers across pages (still scattering `h-12` / `h-24` in
pages built before the helpers landed).
- [ ] Loading shells should match the post-load layout — skeletons inside
Cards, not a single full-width bar.
### Mobile
- [x] ~~CI filter bar overflows on small viewports~~ — company selector
now wraps and shrinks; the stat pill flows below.
- [x] ~~Analyzer multi-select dropdowns clip on narrow widths~~ —
Popover gets `max-w-[calc(100vw-1rem)]` and `collisionPadding={8}`.
- [ ] Tables horizontally scroll without a sticky first column; consider
responsive card-list fallbacks for narrow screens.
## 11. When in doubt
- Use a token, not a hex value.
- Use the shadcn primitive, not a custom one.
- Match the surrounding page's spacing scale rather than introducing a new
one.
- If the same thing exists in two shapes (e.g., raw vs DataTable), pick the
shape this doc documents and migrate the other.