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

19 KiB
Raw Blame History

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

  • Toastssonner 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

  • 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).
  • 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.
  • No mobile-collapsed (hamburger) version<MobileNav /> in a Sheet. Reuses the desktop nav config so IA stays in sync. Desktop nav hides under md.
  • 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.
  • 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

  • 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).
  • Add breadcrumbs to ticket / analysis detail routes. Still open for /configuration-items/[id] and /analyzer/itglue/{applications, configurations}/[id].

Components

  • Build a shared <EmptyState />components/ui/empty-state.tsx.
  • 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.
  • Extract PageHeader and add accent + watermark propscomponents/navigation/page-header.tsx. Adopted on /dashboard and /status.
  • 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).
  • Replace raw <input type="checkbox"> on /addigy-devices — done; also caught one in /admin/sync/mimecast.
  • /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).
  • Decide on DataTable long-termmigrated 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)

  • Move integration health + sync health off /dashboard onto a dedicated /status route. Top-bar <StatusIndicator /> links there.
  • Rebuild /dashboard around KPI cards (today snapshot + needs attention) and recent activity. Drop the integration / sync cards.
  • Add queue × priority heatmap to /dashboardQueueHeatmap backed by /api/dashboard/trends.
  • Add 30-day volume + mean resolution chartsVolumeTrend and ResolutionTrend components, recharts, brand-blue series.
  • Add active engineers panelActiveEngineers (today's hours logged, ticket touch count).
  • 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

  • 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

  • CI filter bar overflows on small viewports — company selector now wraps and shrinks; the stat pill flows below.
  • 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.