feat(federation): GSAP-powered 3D orbital node map, fills viewport to bottom

- Add gsap 3.15 + design-system-aware motion module (src/utils/motion.ts):
  shared colour/duration/ease tokens mirrored from style.css, gsap.defaults,
  live prefers-reduced-motion check.
- Replace the d3 force NetworkMap with NetworkMap3D.vue: peers on projected
  3D orbital rings around the self node, cinematic intro (camera dolly +
  staggered fly-in + ring draw), idle rotation with drag-to-orbit inertia,
  depth-sorted painter's order, trust-colour palette, online/offline states,
  sonar pulse on self, tap-a-node opens the detail modal.
- Map view now fills the dashboard panel to the bottom edge on desktop,
  mobile and companion: .dashboard-scroll-panel:has(.node-map-stage) turns
  the panel into a column (tab-bar/safe-area/audio-player aware padding)
  instead of leaving the old dead bottom margin.
- Reduced motion: intro/idle skipped, scene renders static.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-09 17:09:09 -04:00
co-authored by Claude Fable 5
parent 7fbdabf136
commit fa14f6ebaa
9 changed files with 770 additions and 189 deletions
+69
View File
@@ -0,0 +1,69 @@
/**
* Design-system-aware GSAP setup — the single place animation code pulls
* timing, easing, and colour tokens from, so every GSAP-driven surface moves
* (and is coloured) like the rest of the glass UI instead of inventing its
* own physics per component.
*
* Usage: `import { gsap, motionTokens, prefersReducedMotion } from '@/utils/motion'`
* — never `import gsap from 'gsap'` directly, or the shared defaults are lost.
*/
import { gsap } from 'gsap'
/** Colour tokens mirrored from style.css / tailwind.config.js. The UI is
* dark-only (style.css pins `color-scheme: dark`), so these are constants,
* not theme-dependent lookups. */
export const motionTokens = {
color: {
/** Brand accent — the orange used for focus glows and highlights
* (tailwind orange-400, e.g. `.glass-button:focus-visible`). */
accent: '#fb923c',
/** Trust-level palette — matches NodeList / trust badges. */
trusted: '#4ade80',
observer: '#fb923c',
untrusted: '#ef4444',
neutral: '#9ca3af',
/** Text/line opacities on the dark glass ground. */
textPrimary: 'rgba(255, 255, 255, 0.95)',
textSecondary: 'rgba(255, 255, 255, 0.7)',
textFaint: 'rgba(255, 255, 255, 0.45)',
line: 'rgba(255, 255, 255, 0.18)',
lineFaint: 'rgba(255, 255, 255, 0.08)',
glassDark: 'rgba(0, 0, 0, 0.35)',
glassDarker: 'rgba(0, 0, 0, 0.6)',
},
/** Durations (seconds) — align with the CSS transitions already shipped
* (modal 0.3s, press feedback 0.1s). */
duration: {
fast: 0.18,
base: 0.3,
slow: 0.6,
/** Scene-setting intros (map fly-in, hero moments). */
cinematic: 1.4,
},
ease: {
/** Default UI ease — matches the snappy glass feel. */
out: 'power3.out',
inOut: 'power2.inOut',
/** Playful overshoot for elements "arriving" (node pop-ins). */
arrive: 'back.out(1.6)',
/** Springy attention pulse. */
pulse: 'sine.inOut',
},
} as const
// Shared defaults: any tween that doesn't say otherwise moves like the rest
// of the design system.
gsap.defaults({
ease: motionTokens.ease.out,
duration: motionTokens.duration.base,
})
/** Live reduced-motion check. Query at animation-build time (not module
* scope) so OS-level toggles apply without a reload. Callers should skip
* intros / idle loops and jump to end state when this is true. */
export function prefersReducedMotion(): boolean {
return typeof window !== 'undefined'
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true
}
export { gsap }