From 4e9741b01389e21575cf6a2f9fb1c6ce0d3fc497 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 29 Jul 2026 11:33:25 -0400 Subject: [PATCH] docs(phase-1): UI design contract for FED-05/FED-06 --- .../01-UI-SPEC.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 .planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md diff --git a/.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md b/.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md new file mode 100644 index 00000000..8c5ff54f --- /dev/null +++ b/.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md @@ -0,0 +1,163 @@ +--- +phase: 1 +slug: federation-mesh-hardening +status: draft +shadcn_initialized: false +preset: none +created: 2026-07-29 +--- + +# Phase 1 — UI Design Contract + +> Visual and interaction contract for the two UI-facing requirements in this phase: +> **FED-05** (inter-node Lightning channel-opening UX) and **FED-06** (on-brand paid-tick +> animation). The rest of Phase 1 (FED-01–04) is backend/parity work with no new UI surface. +> Generated by gsd-ui-researcher, verified by gsd-ui-checker. + +--- + +## Design System + +| Property | Value | +|----------|-------| +| Tool | none — no `components.json` found; project is **Vue 3**, and shadcn/ui does not support Vue (React-only), so the shadcn init gate does not apply here. Registry safety gate: not applicable. | +| Preset | not applicable | +| Component library | none — hand-authored Tailwind utilities + a custom "glass" CSS system (`glass-card`, `glass-button`, `glass-button-warning/danger/success`, `input-glass`, `alert-error/warning/info`, `BaseModal.vue`) defined in `neode-ui/src/style.css` and reused project-wide | +| Icon library | none — inline hand-authored SVG, 24×24 viewBox, `stroke-width="2"` outline style (heroicons-esque but not the package). The bolt path `M13 10V3L4 14h7v7l9-11h-7z` is already the house Lightning icon (used in `Server.vue`, `HomeWalletCard.vue`, `Web5Wallet.vue`) — reuse it verbatim for any new Lightning iconography in FED-05, do not source a new icon. | +| Font | Avenir Next (`font-sans`, body/UI text), Montserrat 700/800 (`font-archipelago`, headers only — not used in modals) | + +**Modal contract (hard rule, repeated user complaint):** Every new modal in this phase MUST use `BaseModal.vue` (already wraps `Teleport to="body"` + full-screen `bg-black/60 backdrop-blur-md` backdrop + column layout with pinned header/footer and scrolling body) or, if a bespoke modal is unavoidable, MUST replicate that exact `` + `fixed inset-0` + `@click.self="close"` pattern. Never nest a modal inside a `transform`-affected ancestor (glass-panel `translateZ` layers trap `position:fixed`). + +--- + +## Spacing Scale + +Declared values (must be multiples of 4) — matches `tailwind.config.js`'s existing 4px-grid `spacing` tokens (`1`=4px … `8`=32px) plus standard Tailwind rem multiples used throughout the codebase for larger gaps: + +| Token | Value | Usage | +|-------|-------|-------| +| xs | 4px | Icon-to-label gaps, badge padding | +| sm | 8px | Compact row spacing, `gap-2` | +| md | 16px | Default element spacing, `p-4` card padding | +| lg | 24px | Section padding, `mb-6` between panel sections | +| xl | 32px | Layout gaps between major picker columns | +| 2xl | 48px | `py-12` empty-state vertical padding | +| 3xl | 64px | Not used by this phase's new elements | + +Exceptions: 44px minimum touch target on all new interactive buttons (global rule already enforced in `style.css` for mobile — the "Copy URI" / "Open Channel" / "Request Channel" buttons inherit `min-height: 44px` from `.glass-button` automatically, no override needed). + +--- + +## Typography + +Scoped to this phase's new elements only (existing typography elsewhere is unchanged): + +| Role | Size | Weight | Line Height | +|------|------|--------|-------------| +| Label | 12px (`text-xs`) | 500 (medium) | 1.4 | +| Body | 14px (`text-sm`) | 400 (regular) | 1.5 | +| Heading | 18–20px (`text-lg`/`text-xl`) | 600 (semibold) | 1.3 | +| Display (inherited, FED-06 only) | 48px (`text-5xl`) | 800 (`font-black`) | 1.1 | + +The Display row is **inherited unchanged** from the existing `SendBitcoinModal.vue`/`WalletScanModal.vue` success-amount numerals (e.g. `12,345 sats`) — FED-06 only replaces the ring graphic behind/around that text, never the text itself. Modal titles ("Open Lightning Channel", "Request Channel") use Heading; node names, URI strings, and helper/meta text use Body or Label per the existing `LightningChannelsPanel.vue`/`NodeList.vue` convention (node name = Body, badges/hints = Label). + +--- + +## Color + +| Role | Value | Usage | +|------|-------|-------| +| Dominant (60%) | `#000000` + `rgba(0,0,0,.35–.65)` | Page background, `.glass`/`.glass-card` surfaces | +| Secondary (30%) | `rgba(0,0,0,.65)` blur(18px) card, `rgba(255,255,255,.05–.08)` nested rows | Modal cards, picker list rows (`bg-black/20` per-node rows, `bg-white/5` nested detail blocks) | +| Accent (10%) | Archipelago orange `#fb923c` / `rgba(251,146,60,*)` | **Reserved for:** the "Open Channel" / "Request Channel" / "Copy Lightning URI" primary CTA buttons (`.glass-button-warning`), the Lightning bolt icon fill, focus-visible glow rings, the active picker-tab underline (mirrors existing `.mode-switcher-btn-active` treatment) | +| Destructive | `#ef4444` family (`.glass-button-danger`) | Not used by FED-05 v1 (no destructive action ships this phase — channel *close* is existing, out-of-scope UI in `LightningChannelsPanel.vue`); declared for consistency if a future "revoke URI sharing" action is added | + +**Inherited semantic colors (pre-existing house convention, unchanged by this phase, NOT part of the 10% accent budget):** +- Success/paid emerald `#4ade80` text / `rgba(16,185,129,*)` fills — the paid-tick's center badge and "SENT"/amount numerals (FED-06 keeps this palette; only the surrounding ring geometry changes). +- Info blue `#60a5fa` — FIPS/Tor transport badges already shown next to trusted-node rows (`NodeList.vue`'s `transportBadge`); reused as-is in the FED-05 trusted-node picker rows, not introduced by this phase. + +Accent reserved for: **primary Lightning-channel action buttons, the Lightning bolt icon, focus rings, and the active picker-tab indicator only** — never for body text, card backgrounds, or informational badges. + +--- + +## Copywriting Contract + +| Element | Copy | +|---------|------| +| Primary CTA — own URI | **"Copy Lightning URI"** (copy-to-clipboard button; on success the label flips to **"Copied!"** for ~2s, mirroring `SendBitcoinModal.vue`'s existing `copyDetail`/`Copied!` pattern — do not invent a new copy-feedback idiom) | +| Primary CTA — trusted federated node | **"Open Channel"** (one-click; matches the verb already used in `LightningChannelsPanel.vue`'s existing Open Channel button/modal) | +| Primary CTA — meshed Lightning peer | **"Request Channel"** (opens the request flow reusing `PeerRequestModal.vue`'s pattern — optional message field, "Send Request" submit button, `sending` → **"Sending…"** busy label — do not build a new request-modal component from scratch) | +| Manual fallback entry point | **"Paste URI Manually"** (reveals a `Peer URI` input, placeholder `pubkey@host:port`, helper text `Format: pubkey@host:port` — verbatim reuse of `LightningChannelsPanel.vue`'s existing field copy) | +| Empty state heading | **"No Lightning peers yet"** | +| Empty state body | **"Add a federated node or connect with a meshed peer running Lightning to open a channel directly — or paste a peer's URI manually below."** | +| Error state | **"Couldn't reach that peer — check they're online and try again."** (tone/placement mirrors the existing `openError`/`alert-error` treatment in `LightningChannelsPanel.vue`; LND "still starting up" transient errors reuse that same component's amber `isStartupNotice` treatment rather than the red error style) | +| Destructive confirmation | Not applicable — FED-05 v1 ships open/request flows only, no destructive action | +| FED-06 copy | Not applicable — pure visual swap. Existing "SENT" / success-amount / "Done" button copy in `SendBitcoinModal.vue` and `WalletScanModal.vue` is unchanged; only the ring graphic behind the checkmark changes. | + +--- + +## UI Considerations + +Applicable state considerations resolved: 13 covered, 3 backstop, 0 unresolved. + +| Category | Element(s) | Status | Resolution / Reason | +|----------|------------|--------|---------------------| +| long-text | own-node URI display | ✅ covered | The displayed `pubkey@host:port` string truncates (CSS `truncate` + `title` tooltip, the existing house pattern) to fit its container; the full untruncated value is what gets copied to clipboard regardless of visual truncation | +| empty | trusted-nodes picker list | ✅ covered | Empty state copy row above renders once when both the trusted and meshed-peer lists are empty (shared empty state, not duplicated per column) | +| empty | meshed-LN-peers picker list | 🧪 backstop | Same shared empty-state copy as above; no wired test yet asserting the "shared, not duplicated" rendering rule — flag for planner/executor to add a component test | +| loading | trusted-nodes picker list | ✅ covered | Mirrors `NodeList.vue`'s existing "Loading nodes..." spinner row treatment | +| loading | meshed-LN-peers picker list | ✅ covered | Same spinner treatment as trusted-nodes list | +| error | trusted-nodes / meshed-peer picker lists | ✅ covered | Ties to the Copywriting Contract error row; styled with `.alert-error`/`openError` convention already in `LightningChannelsPanel.vue` | +| populated | trusted-nodes picker list | ✅ covered | Row layout mirrors `NodeList.vue`'s trusted-node row: name, trust badge, transport badge (FIPS/Tor), one-click "Open Channel" button | +| populated | meshed-LN-peers picker list | ✅ covered | Same row layout, "Request Channel" button in place of "Open Channel" (peers are not bilaterally trusted, so the action is a request, never a direct open) | +| zero-one-many | trusted-nodes / meshed-peer lists | ✅ covered (dismissed) | No item-count copy is planned for either list (unlike e.g. the channel-status tabs' count badges) — singular/plural phrasing is not applicable | +| overflow | picker list rows (long node names) | ✅ covered | `truncate` class + `:title` tooltip on the node-name span, identical to the existing `NodeList.vue` convention | +| partial | manual-URI-paste form | ✅ covered | A pasted pubkey without a host falls back to `lnd.openchannel`'s existing address-less-pubkey handling (`address = parts[1] \|\| undefined`), already proven in `LightningChannelsPanel.vue` | +| error | manual-URI-paste form | 🧪 backstop | Invalid-format message ("Peer URI must be `pubkey@host:port`") is specified but no explicit format-validation test is scoped yet — planner should add one, do not silently skip client-side validation before calling `lnd.openchannel` | +| long-text | manual-URI-paste form | ✅ covered | Same truncation/tooltip treatment as the own-node URI display | +| unclassified | request-to-open-channel flow | ✅ covered (dismissed) | Reuses `PeerRequestModal.vue` verbatim (message field, Send Request/Sending states) — its own state coverage predates this phase and is not re-specified here | +| overflow | paid-tick ring (`SendBitcoinModal.vue` + `WalletScanModal.vue`) | ✅ covered | New `badge` `ScreensaverRing` size variant (see below) is explicitly sized to fit inside the modal's `max-h-[90vh] overflow-y-auto` card without clipping — do not drop in the existing `compact` (240–320px) variant unscaled | +| static-content (motion) | paid-tick ring, all `ScreensaverRing` size variants | 🧪 backstop | `ScreensaverRing.vue`'s `segment-pulse` animation currently has **no** `prefers-reduced-motion` guard anywhere (a real gap — confirmed by reading the component; contrast with `SendBitcoinModal.vue`'s existing `.burst-ring`, which already has one). This phase must add `@media (prefers-reduced-motion: reduce) { .viz-segment { animation: none; opacity: 0.6; } }` inside `ScreensaverRing.vue` itself so the guard applies to every size variant (including the new `badge` one), matching the site-wide reduced-motion convention. No existing automated test covers this — flag for planner as a Wave 0 test gap. | + + + +--- + +## FED-06 Sizing Decision (resolves RESEARCH.md Pitfall 4 / Assumption A4) + +RESEARCH.md flagged the `ScreensaverRing` size mismatch (`compact` = 240–320px vs. the current 96–112px paid-tick badges) as needing a UI-spec decision before implementation. **Decision: add a new `badge` size variant to `ScreensaverRing.vue`**, not a CSS `transform: scale()` wrapper — cleaner, reusable across both call sites, and avoids reduced-motion/layout-box mismatches that a transform hack would introduce. + +| Variant | Diameter (mobile) | Diameter (≥768px) | `--viz-radius` | Used by | +|---------|-------------------|--------------------|-----------------|---------| +| `badge` (NEW) | 160px | 192px | 80px / 96px | `SendBitcoinModal.vue` `.send-success-burst` (replaces the 112px burst), `WalletScanModal.vue` `.success-ring` (replaces the 96px/`w-24` ring) | +| `compact` (existing, unchanged) | 240px | 320px | 120px / 160px | `SystemDangerZone.vue` and other existing overlay contexts — do not touch | +| `default` (existing, unchanged) | 280–400px (responsive) | — | 140–200px | Full-screen `Screensaver.vue` | + +Composition at both call sites: `` renders the radiating EQ segments; the existing `.burst-core` (green circle + checkmark, `SendBitcoinModal.vue`) or `.success-ring` inner content (`WalletScanModal.vue`) is layered centered on top via `position: absolute; inset: 0` within a shared `position: relative` wrapper sized to the `badge` diameter — same layering pattern `Screensaver.vue` already uses for `ScreensaverLogo` inside `ScreensaverRing`. Do not resize or restyle the checkmark/core itself; only its container changes from a bespoke 96–112px circle to the `badge`-sized wrapper. + +--- + +## Registry Safety + +| Registry | Blocks Used | Safety Gate | +|----------|-------------|--------------| +| shadcn official | none | not applicable — shadcn/ui is React-only; this is a Vue 3 project with an established hand-rolled design system (see Design System table) | +| third-party | none | not applicable | + +--- + +## 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