# 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 (`` 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 ``). 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 ``` ┌─ (sticky h-16, backdrop blur, z-50) ─┐ ├─ (optional, bordered, container-aligned) └─
(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** (``) 3. **Right-side controls** — `` ### 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 `
` | | | `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` | `` — 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` | `` — 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 `` with ``. Raw `
` 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 `` 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** — `` for content placeholders, `` 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~~ — `` 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~~ — `` 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 ``~~ — `components/ui/empty-state.tsx`. - [x] ~~Build a shared `` 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 `
` 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 `` on `/addigy-devices`~~ — done; also caught one in `/admin/sync/mimecast`. - [x] ~~/veeam-analysis raw `
`~~ — 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 `` 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.