docs(14): UI design contract for /pax8 surface

Pre-populated entirely from existing DESIGN.md tokens plus Phase 14
CONTEXT.md/RESEARCH.md decisions — no new visual language introduced.
Flags the required additive DetailModal.tsx extension for the cost
breakdown drill-down.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
This commit is contained in:
lorentz 2026-07-11 12:30:39 -04:00
parent cd33955417
commit a71ca10f9b

View file

@ -0,0 +1,189 @@
---
phase: 14
slug: pax8-ui-surface
status: draft
shadcn_initialized: true
preset: "style=new-york, baseColor=neutral, cssVariables=true, iconLibrary=lucide, rsc=true"
created: 2026-07-11
---
# Phase 14 — UI Design Contract
> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
This phase adds a new top-level page (`/pax8`) inside an already-mature,
already-tokenized design system (see `DESIGN.md`, `components.json`). No new
visual language is being introduced — this contract pins down which existing
tokens/components apply and resolves the phase-specific gaps CONTEXT.md left
to discretion (tab labels, column set, copy). Nearly every field below is
pre-populated from `DESIGN.md`, `14-CONTEXT.md` (D-01…D-09), and
`14-RESEARCH.md` — no user questions were required for this phase.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | shadcn (already initialized — `components.json` present) |
| Preset | `style: new-york`, `baseColor: neutral`, `cssVariables: true`, `rsc: true`, `tsx: true`, prefix: none (source: `components.json`) |
| Component library | Radix UI primitives (via shadcn), `components/ui/` |
| Icon library | `lucide-react`, imported per-icon (source: `DESIGN.md` §6) |
| Font | IBM Plex Sans (`--font-plex-sans`, weights 300/400/500/600/700) for UI text; IBM Plex Mono for numerics/IDs/timestamps only (source: `DESIGN.md` §2 Type) |
No shadcn init gate was needed — the project already has a locked preset.
No new `npx shadcn add` components are required for this phase (see
Component Inventory below) so the registry vetting gate does not apply.
---
## Spacing Scale
Declared values (must be multiples of 4) — matches the sitewide scale in
`DESIGN.md` §3, applied as-is to `/pax8`:
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon-to-text gaps (`gap-1`), inline badge padding |
| sm | 8px | Compact row spacing (`gap-2`), candidate-card internal gaps |
| md | 16px | Default element spacing (`gap-4`), Card body padding |
| lg | 24px | Page container padding (`container mx-auto px-6 py-6`), section gaps (`space-y-6`) |
| xl | 32px | Layout gaps between major page regions (Companies tab ↔ Needs Review tab content) |
| 2xl | 48px | Not used this phase |
| 3xl | 64px | Not used this phase |
Exceptions: none. `/pax8` follows the standard page shell exactly
(`PageHeader` + `container mx-auto px-6 py-6` + `space-y-6` between the tab
bar, table/card sections) — same shell `device-link-conflicts` uses.
---
## Typography
Matches the sitewide scale documented in `DESIGN.md` §2 (Type) and used
throughout `DetailModal.tsx` / `device-link-conflicts/page.tsx`. This is a
pre-existing, sitewide scale — not introduced for this phase — so it
carries 3 weights (400/600/700) rather than the generic 2-weight target;
noted as an accepted exception since it matches every other page in the app.
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 14px (`text-sm`) | 400 (regular) | 1.5 |
| Label | 12px (`text-xs`, uppercase, `tracking-wider`) | 600 (semibold) | 1.4 |
| Heading | 18px (`text-lg`, Card/section titles) | 600 (semibold) | 1.3 |
| Display | 24px (`text-2xl`, page H1 via `PageHeader`) | 700 (bold) | 1.2 |
Numerics (subscription counts, cost totals, seat counts) render in
`font-mono` (IBM Plex Mono) with `tabular-nums`, matching `DetailModal`'s
existing `.num`/hours-entry convention — never in the sans body font.
---
## Color
Matches `DESIGN.md` §2 (Colors, OKLch) exactly — no new hex values, no new
tokens for this phase.
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `--background` / `--card` (white light / near-black dark) | Page surface, table/card backgrounds |
| Secondary (30%) | `--secondary` / `--muted` (`oklch(0.97 0 0)` light) | Table header row, DetailModal field-label column (`bg-muted/40`), "Details" inline badge row background |
| Accent (10%) | `--primary` (Wulf blue `oklch(0.55 0.16 220)`) | Reserved for: the "Link company" resolve button, active `Tabs` indicator (Companies / Needs Review), input/select focus rings, and hyperlinks (matched-company name link, `ExternalLink` affordances) |
| Destructive | `--destructive` (`oklch(0.577 0.245 27.325)`) | No destructive actions exist in this phase (read-only view + a non-destructive resolve mutation) — token reserved but unused; do not repurpose it for the "Needs Review" flag itself (that's an informational/warning state, not destructive) |
Accent reserved for (explicit list — nothing else may use `--primary`):
1. "Link company" / "Resolve" action buttons in the Needs Review tab
2. Active state of the Companies / Needs Review `Tabs` trigger
3. Focus rings on `Input`/`Select`/`Command` (manual company search)
4. Text links (matched Autotask company name, any external-link affordance)
Warning/attention color for the Needs Review section itself: amber
(`border-amber-200` card border + `text-amber-500` `AlertTriangle` icon),
matching `device-link-conflicts`'s existing convention exactly — this is a
semantic status hue per `DESIGN.md`'s "status hues outside this set" rule,
not the 10% accent.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Primary CTA | **"Link company"** — button on each top-3 candidate row in the Needs Review tab (mirrors `device-link-conflicts`'s per-candidate resolve button); the manual-search fallback path's action button reads **"Link to selected company"** once a search result is chosen |
| Empty state heading (Companies tab, zero synced companies) | "No PAX8 companies synced yet" |
| Empty state body (Companies tab) | "Run the PAX8 sync from `/admin/integrations`, then refresh this page." |
| Empty state heading (Needs Review tab, zero open reviews) | "No companies need review" |
| Empty state body (Needs Review tab, zero open reviews) | "Every synced PAX8 company is matched to an Autotask company." (mirrors `device-link-conflicts`'s "No conflicts" `Alert` copy pattern exactly) |
| Empty state — per-row, zero-candidate case (D-09) | Heading: **"No suggested matches"** · Body: **"Search manually to link this company to its Autotask counterpart."** — rendered inline within that review card, not a separate page/flow |
| Error state | Heading: "Couldn't load PAX8 data" · Body: "Check your connection and try again, or visit `/admin/integrations` if PAX8 sync is disabled." (mirrors the existing `Alert variant="destructive"` / "Failed to load" pattern in `device-link-conflicts`) |
| Destructive confirmation | None in this phase. There is no delete/write-back action — the resolve mutation only repoints `pax8_companies.autotask_company_id` and is correctable by re-resolving. Mirrors `device-link-conflicts`'s precedent of no confirmation dialog on resolve; a success toast (`sonner`, `toast.success`) reading **"Linked to {companyName}"** is sufficient feedback, matching the existing `"Linked to CI {ciId}"` pattern verbatim in shape |
| Resolve failure toast | `toast.error(err.message ?? 'Resolve failed')` — matches `device-link-conflicts` exactly |
| Tab labels | **"Companies"** and **"Needs Review"** (per D-04's own example labels); "Needs Review" carries a count badge showing the open-review total when > 0 (Claude's Discretion item, resolved: yes, show count — consistent with `device-link-conflicts`'s "N unresolved conflicts" line and with the existing count-badge pattern already used on `DetailModal`'s Time/Notes tab triggers) |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|--------------|
| shadcn official | `Tabs`, `Dialog`/`DetailModal` (extended in place), `Card`, `Badge`, `Select`, `Alert`, `Skeleton`, `Button`, `Separator`, `Input` — all already vendored in `components/ui/`, zero new `npx shadcn add` installs required | not required |
No third-party registries declared or used. Registry vetting gate does not
apply to this phase.
---
## Component Inventory & Implementation Notes
Everything needed is either already vendored (`components/ui/`) or already
built (`components/admin/DataTable.tsx`) — the only build-vs-reuse decision
this phase makes is an **additive extension to `DetailModal.tsx`**, flagged
as Pitfall 1 in `14-RESEARCH.md` and repeated here because it directly
governs whether D-02's "used as-is" framing produces a usable UI:
- **Companies tab**`components/admin/DataTable.tsx` (D-01), columns:
company name, matched Autotask company (or an "Unmatched" `Badge`), active
subscription count, city/country (sortable). Row click opens the
extended `DetailModal`.
- **Cost-breakdown drill-down**`components/admin/DetailModal.tsx`,
**extended additively**, not passed data "as-is":
1. Add an optional `kind?: 'ticket' | 'company' | 'pax8_company'` prop so
`detectGroups()` stops guessing from ambiguous field-name sniffing
(a PAX8 company object has `name`, not `company_name`).
2. Add a `PAX8_COMPANY_GROUPS` field-group set (Identity: name, status,
city/state/country; System: id, synced_at, is_deleted) reusing existing
`FieldType`s (`bool`, `date`, `id` — no new scalar types needed).
3. Add one new, unconditional formatted-tab section (not a `FieldGroup`)
that renders the subscriptions/cost-breakdown array as a compact table:
product name, quantity, billing term, latest billed amount
(`line_total`, never recomputed from `unit_price × quantity` — Pitfall
3), styled with the same rounded-border card look as every other
formatted-tab group, plus a summed total row.
4. Do not touch `TICKET_GROUPS`/`COMPANY_GROUPS` or their detection
branches — this must be a pure addition, verified by reading the file
in full (`components/admin/DetailModal.tsx` lines 41132, 256260).
5. Raw tab: unchanged mechanism, fed the raw company + subscriptions +
order-items payload.
- **Needs Review tab** — new cards mirroring
`app/admin/device-link-conflicts/page.tsx`'s existing list-with-resolve
layout exactly (amber-bordered `Card`, candidate rows with confidence
`Badge`, per-candidate "Link company" button) plus a manual-search
fallback using shadcn `Command`/`Popover` over the existing
`/api/data/companies-list` endpoint (D-05) — new UI, not vendored, but a
direct structural copy of an existing page.
- **Nav entry** — one new object in `components/navigation/app-navigation.tsx`'s
`navigationItems` array (both desktop `NavigationMenu` and mobile `Sheet`
consume the same array).
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending