From a71ca10f9bf11ad2edd82bdd3dbcd9f972313db3 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 11 Jul 2026 12:30:39 -0400 Subject: [PATCH] docs(14): UI design contract for /pax8 surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR --- .../phases/14-pax8-ui-surface/14-UI-SPEC.md | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 .planning/phases/14-pax8-ui-surface/14-UI-SPEC.md diff --git a/.planning/phases/14-pax8-ui-surface/14-UI-SPEC.md b/.planning/phases/14-pax8-ui-surface/14-UI-SPEC.md new file mode 100644 index 0000000..f45db9e --- /dev/null +++ b/.planning/phases/14-pax8-ui-surface/14-UI-SPEC.md @@ -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 41–132, 256–260). + 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