Files

172 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-0104) 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 `<Teleport to="body">` + `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). Exactly two weights govern this phase's new elements — 400 and 600; Label and Body are differentiated from each other by size and color (not weight), matching how `NodeList.vue` already distinguishes node-name text from badge/hint text:
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Label | 12px (`text-xs`) | 400 (regular), `text-white/60` | 1.4 |
| Body | 14px (`text-sm`) | 400 (regular), `text-white` | 1.5 |
| Heading | 20px (`text-xl`) | 600 (semibold) | 1.3 |
Heading is pinned to `text-xl` (20px), not a range — this matches `BaseModal.vue`'s own `<h3 class="text-xl font-semibold">` title (the component every new modal in this phase must use per the Modal contract above) and `WalletScanModal.vue`'s pane title, i.e. the size the existing house modals actually use most for their titles. Modal titles ("Open Lightning Channel", "Request Channel") use Heading; node names use Body (`text-white`); URI strings, badges, and helper/meta text use Label (`text-white/60`) per the existing `LightningChannelsPanel.vue`/`NodeList.vue` convention.
**Inherited — not governed by this contract:** The `SendBitcoinModal.vue`/`WalletScanModal.vue` success-amount numerals (e.g. `12,345 sats`, `text-5xl font-black` — 48px / weight 800) are pre-existing, unchanged display text. FED-06 only replaces the ring graphic behind/around that text, never the text itself, so this weight/size falls outside the phase's new-elements typography contract above and is not counted toward its weight budget.
---
## 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 |
| long-text | paid-tick ring (`SendBitcoinModal.vue` + `WalletScanModal.vue`) | ✅ covered (dismissed) | The ring itself renders no text content (pure SVG/CSS segments); the 48px sats amount inside it is inherited text explicitly out of this contract (see Typography inherited note) |
| 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` (240320px) 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. |
<!-- Status vocabulary (locked by probe-core projectTruths):
✅ covered → a plain truth string lifted into must_haves.truths
🧪 backstop → a flat scalar { statement, verification: backstop }; at verify time, no explicit
evidence → insufficient_spec → human_needed (never a silent pass, #1154)
⚠ unresolved → an explicit planner assumption (surfaced, never silently dropped)
Rows are REPLACED (not appended) on a probe re-run — idempotent. -->
---
## FED-05 Visual Anchor
Primary visual anchor: the trusted-nodes list (federation trust is the primary path); meshed-lightning-peers list second; manual-paste fallback visually de-emphasized below both (collapsed behind the "Paste URI Manually" entry point per the Copywriting Contract above, not rendered as a third equal-weight column).
---
## FED-06 Sizing Decision (resolves RESEARCH.md Pitfall 4 / Assumption A4)
RESEARCH.md flagged the `ScreensaverRing` size mismatch (`compact` = 240320px vs. the current 96112px 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) | 280400px (responsive) | — | 140200px | Full-screen `Screensaver.vue` |
Composition at both call sites: `<ScreensaverRing size="badge" />` 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 96112px 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