--- phase: 19 slug: template-editor-ui status: draft shadcn_initialized: false preset: none created: 2026-04-06 --- # Phase 19 — UI Design Contract > Visual and interaction contract for the template editor UI phase. Generated by gsd-ui-researcher, verified by gsd-ui-checker. --- ## Design System | Property | Value | |----------|-------| | Tool | none — manual inline styles + Tailwind utilities | | Preset | not applicable | | Component library | none (Radix not installed; components are custom) | | Icon library | none (text/Unicode symbols used inline, e.g. `+`, `✓`, `×`) | | Font (sans) | Geist Sans via `var(--font-sans)` / `var(--font-geist-sans)` | | Font (serif/display) | Cormorant Garamond via `var(--font-serif)` — wordmark only | Source: `globals.css`, `PortalNav.tsx`, `layout.tsx` **Note:** `components.json` was not found. The project does not use shadcn. All new components follow the existing inline-style + selective Tailwind pattern established in PreparePanel, ClientsPageClient, and PortalNav. --- ## Spacing Scale Declared values (multiples of 4 only): | Token | Value | Usage | |-------|-------|-------| | xs | 4px | Icon gaps, dot indicator margin | | sm | 8px | Inline element spacing, button padding vertical | | md | 16px | Panel internal padding, form field gaps | | lg | 24px | Section spacing within panels, list gaps | | xl | 32px | Page-level top margin | | 2xl | 48px | Empty-state vertical padding | | 3xl | 64px | Nav height (established: PortalNav is 64px) | Exceptions: - Role color dot: 8px diameter (not a layout token — cosmetic) - Signer/role pill height: 32px (matches PreparePanel signer pill pattern) - Right panel (TemplatePanel) width: 280px fixed — matches PreparePanel column width in DocumentPageClient split layout - AI Auto-place button and Save button minimum height: 36px (matches existing PreparePanel button height) Source: `PreparePanel.tsx`, `PortalNav.tsx`, `layout.tsx` (2rem = 32px page padding) --- ## Typography | Role | Size | Weight | Line Height | |------|------|--------|-------------| | Body | 14px (0.875rem) | 400 | 1.5 | | Label / caption | 12px (0.75rem) | 400 | 1.4 | | Section heading | 14px (0.875rem) | 600 | 1.3 | | Page heading | 24px (1.5rem) | 700 | 1.2 | Source: `ClientsPageClient.tsx` (h1: 1.5rem/700), `PreparePanel.tsx` (h2: font-semibold/text-gray-900, body: 0.875rem/400), `PortalNav.tsx` (links: 0.8125rem/500 uppercase) Weights used: regular (400) and semibold/bold (600/700). No third weight. Letter-spacing exceptions: - Nav links: `letter-spacing: 0.08em; text-transform: uppercase` — preserved exactly, no change - Wordmark: `letter-spacing: 0.02em` — no change --- ## Color | Role | Value | Usage | |------|-------|-------| | Dominant (60%) | `#FAF9F7` (cream) | Page background, panel backgrounds, modal backgrounds | | Secondary (30%) | `#F9FAFB` / `#F0EDE8` (cream-mid) | Panel interior fill, list row backgrounds, right panel | | Accent (10%) | `#C9A84C` (gold) | Primary CTA buttons only, active nav underline | | Navy | `#1B2B4B` | Page headings, nav background, secondary button fill | | Destructive | `#DC2626` (red-600) | Delete role action button only | Accent (`#C9A84C`) reserved for: - "Save Template" button (primary CTA) - "Add Role" button (secondary CTA within TemplatePanel) - Active nav link underline (existing pattern, unchanged) Navy (`#1B2B4B`) used for: - "AI Auto-place" button fill (matches PreparePanel AI button pattern) - Page heading text - Wordmark Semantic colors: - Success inline: `#059669` (green — "Saved" confirmation inline text, same as PreparePanel "Sent ✓") - Error inline: `#DC2626` (red — error messages) - Muted text: `#6B7280` (gray-500 — counts, timestamps, placeholder copy) - Border default: `#E5E7EB` (gray-200) Source: `globals.css`, `PreparePanel.tsx`, `ClientsPageClient.tsx`, `ConfirmDialog.tsx` --- ## Component Inventory Components this phase must produce (from CONTEXT.md D-12/D-13): ### 1. TemplatePanel (new) Right panel, 280px wide, sticky within the editor view. Sections (top to bottom): 1. **Template name** — editable inline `` (14px, border-bottom-only style when focused, gray-200 border at rest). Displays current name from DB. 2. **Roles section** — heading "Signers / Roles" (12px uppercase label), list of role pills, "Add role" button. 3. **Role pill** — colored dot (8px, `background: role.color`) + role label string + rename icon (pencil, text "Edit") + remove button (`×`). Free-form rename via inline edit on click. 4. **Add role** — text input + "Add" button. Preset suggestions: "Buyer", "Co-Buyer", "Seller", "Co-Seller" as clickable chips below the input. 5. **AI Auto-place button** — full-width, navy fill (`#1B2B4B`), white text, 36px height. Label: "AI Auto-place Fields". Shows spinner + "Placing…" during loading. 6. **Save button** — full-width, gold fill (`#C9A84C`), white text, 36px height. Label: "Save Template". Shows "Saving…" during loading. Shows inline "Saved" (green) or error message after. ### 2. TemplatePageClient (new) State owner. Layout: CSS flexbox row — left: `PdfViewerWrapper` + `FieldPlacer` (flex: 1, min-width: 0), right: `TemplatePanel` (280px, flex-shrink: 0). Gap: 24px. Pattern mirrors `DocumentPageClient`. ### 3. Templates list page (new) - Page heading: "Templates" (24px/700, navy) - Subtitle: "{N} template{s}" (14px/400, gray-500) - "New Template" button: top-right, gold fill (primary CTA). Label: "+ New Template" - Table/list rows: each row shows template name (14px/600, navy), form name (14px/400, gray-500), field count ("N fields", 12px/400, gray-500), last updated (12px/400, gray-500). Click row → navigate to editor. - Row hover: `background: #F0EDE8` (cream-mid), cursor pointer. ### 4. PortalNav update Add "Templates" link between "Clients" and "Profile" in `navLinks` array. No visual change — same style as existing links. ### 5. FieldPlacer update (non-breaking) Add optional `onPersist?: (fields: SignatureFieldData[]) => Promise | void` prop. When provided, replaces the 4 internal `persistFields(docId, fields)` call sites. Existing callers unaffected. --- ## Interaction States ### AI Auto-place button | State | Visual | |-------|--------| | Idle | Navy fill, "AI Auto-place Fields", full opacity | | Loading | Navy fill, "Placing…" + spinner (CSS border-radius spin), disabled | | Success | Button returns to idle; FieldPlacer reloads via key increment | | Error | Inline error message below button in red-600 text (14px), button returns to idle | ### Save button | State | Visual | |-------|--------| | Idle | Gold fill, "Save Template", full opacity | | Loading | Gold fill at 0.7 opacity, "Saving…", disabled | | Success | Inline "Saved" text in green (#059669) below button, fades after 3s | | Error | Inline error in red below button | ### Role pill inline rename - Click role label → label becomes a text input (same font, border-bottom style) - Enter or blur → commits rename, calls PATCH /api/templates/[id] with updated `signatureFields` role strings - Escape → cancels rename, reverts to original label ### Remove role - Click `×` on role pill - If no fields are assigned to that role: remove immediately - If fields are assigned to that role: show ConfirmDialog — "Remove role?" / "Removing 'Buyer' will unassign {N} field(s). This cannot be undone." / Confirm: "Remove Role" (red-600), Cancel: "Cancel" ### New Template modal (from list page) - Reuses AddDocumentModal pattern: overlay, white rounded card, form fields. - Fields: "Template name" (text input), "Select form" (PDF form picker from library). - CTA: "Create Template" (gold fill). On success: navigate to `/portal/templates/[id]`. --- ## Layout ### Template editor page (`/portal/templates/[id]`) ``` [PortalNav — 64px sticky top] [main — max-width 1200px, margin: 0 auto, padding: 32px] [page header — flex row] [h1 "Edit Template: {name}"] [field count badge "{N} fields placed" — optional, muted] [editor body — flex row, gap: 24px] [left: PdfViewerWrapper + FieldPlacer — flex:1] [right: TemplatePanel — width: 280px, flex-shrink: 0] ``` ### Templates list page (`/portal/templates`) ``` [PortalNav — 64px sticky top] [main — max-width 1200px, margin: 0 auto, padding: 32px] [page header — flex row space-between] [h1 "Templates"] [subtitle "{N} templates"] [CTA "+ New Template"] [list — flex column, gap: 8px] [template row × N] ``` --- ## Copywriting Contract | Element | Copy | |---------|------| | Primary CTA (list page) | "+ New Template" | | Primary CTA (editor) | "Save Template" | | Secondary CTA (editor) | "AI Auto-place Fields" | | Nav link | "Templates" | | Page heading (list) | "Templates" | | Page heading (editor) | "Edit Template: {name}" | | Empty state heading (list) | "No templates yet" | | Empty state body (list) | "Create a template to reuse field placements across documents." | | Empty state CTA (list) | "+ Create your first template" | | Error — AI auto-place | "AI placement failed. Check that the form PDF is accessible and try again." | | Error — save | "Save failed. Please try again." | | Loading — AI | "Placing…" | | Loading — save | "Saving…" | | Success — save | "Saved" | | Panel section label — roles | "Signers / Roles" | | Role add placeholder | "Role label (e.g. Buyer)" | | Role remove confirm title | "Remove role?" | | Role remove confirm body | "Removing '{role}' will unassign {N} field(s). This cannot be undone." | | Role remove confirm CTA | "Remove Role" | | Role remove cancel | "Cancel" | | Field count badge | "{N} field{s} placed" | Source: CONTEXT.md decisions D-04, D-05, D-12, D-13; modeled on PreparePanel copy patterns. --- ## Registry Safety | Registry | Blocks Used | Safety Gate | |----------|-------------|-------------| | shadcn official | none — shadcn not installed | not applicable | | Third-party | none declared | not applicable | No third-party registry blocks in scope for this phase. --- ## 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