wulf-pulse/DESIGN.md
lorentz c97e5fc45c feat: status popover + CSV export
Two follow-ons after the ⌘K palette:

StatusIndicator → Popover
- The top-bar status light is no longer a direct link to /status.
  Clicking it opens a popover with grouped issues (failing
  integrations, expired tokens, expiring tokens) so a quick glance
  answers "what's broken" without leaving the current page.  A "View
  full status" link at the bottom routes to /status when needed.
- The trigger keeps the same color rollup so the visual hint is
  visible without opening the popover.

DataTable → CSV export
- Optional `exportable` + `exportFilename` props add an "Export CSV"
  button next to the search bar.  Default behavior exports the current
  page; pass `onExportAll` for server-side full-result downloads.
- Built client-side from column defs (label → header, raw value →
  cell).  BOM-prefixed UTF-8 so Excel decodes correctly.  Quoting +
  escape handled.
- Enabled on /admin/data-browser/{companies,tickets} as initial demos.
  Other data-browser pages opt in by adding two props.

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

434 lines
21 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/[…]`, `/settings`, `/settings/security`,
`/sentinelone/coverage`, `/sentinelone/mappings` now use it. Kiosk
uses its own shell; the only stragglers are very small/internal
pages.
- [ ] 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 indicator popover (2026-05-03)
- [x] Top-bar StatusIndicator now opens a Popover instead of routing
directly to `/status`. The popover groups inline issues by tone
(failing integrations, expired tokens, expiring tokens) so a quick
glance answers "what's broken." A "View full status" link at the
bottom routes to `/status`.
### CSV export on DataTable (2026-05-03)
- [x] Optional `exportable` + `exportFilename` props add an "Export CSV"
button next to the search bar. Defaults to current-page export;
provide `onExportAll` for server-side full-result downloads.
Enabled on `/admin/data-browser/companies` and
`/admin/data-browser/tickets`.
### Command palette (2026-05-03)
- [x] **Cmd+K / Ctrl+K** opens a global launcher
(`components/navigation/command-palette.tsx`). Three sections:
Navigation (every primary route, role-gated), Recent activity
(last 5 audits + 5 observations from `/api/dashboard/overview`),
Companies (filtered client-side from `/api/companies`). The `/`
key also opens it when no input is focused. Top-bar shows a small
"Search · ⌘K" button (md+) for discoverability.
### 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`.
- [x] ~~Worker activity sparklines~~ — `ActivitySparkline` shows 24
hourly buckets per worker (success bottom-up in primary, failure
top-down in destructive). Backed by zero-filled hour series
generated in the `/api/status/workers` query.
### Tokens & theming
- [-] Hard-coded Tailwind palette colors are extensive (~770 references)
but most are **semantic** (status reds, info blues, warning ambers
via the `bg-{hue}-500/15 text-{hue}-700` recipe documented above).
Surveyed and deprioritized — case-by-case cleanup as new work
touches a page.
- [x] ~~Verify dark-mode contrast on status badges and chart legends~~ —
bumped dark `--border` from 10% to 14%, `--input` from 15% to 18%,
`--sidebar-border` to 14%. `<StatusLight>` outline lifted to
`ring-foreground/15 dark:ring-foreground/20`. DetailModal empty-cell
em-dash lifted from `/40` to `/70` so missing-value placeholders are
legible on dark surfaces.
### Loading & empty
- [x] ~~Standardize Skeleton heights~~ — helpers in
`components/ui/skeleton-helpers.tsx`: `SkeletonRow`, `SkeletonRows`,
`SkeletonCard`, `SkeletonChart`, `SkeletonHeader`, `SkeletonTable`.
Adopted on `/dashboard` and `/status`.
- [-] Helpers are **preferred for new code**. Existing ad-hoc
`<Skeleton h-NN>` patterns aren't broken (they render the same
shape just with arbitrary heights); leave them in place and
migrate opportunistically when touching the surrounding code.
### 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}`.
- [x] ~~Tables horizontally scroll without a sticky first column~~ —
`Table` primitive accepts `stickyFirstColumn` (also exposed on
`DataTable` and on by default for paginated tables). Hover and
selected row backgrounds carry through.
## 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.