Files

16 KiB
Raw Permalink Blame History

phase, slug, status, shadcn_initialized, preset, created
phase slug status shadcn_initialized preset created
1 federation-mesh-hardening draft false none 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.

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