16 KiB
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-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 <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
#4ade80text /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'stransportBadge); 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 (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-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 = 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: <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 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