diff --git a/apps/dashboard/src/app.css b/apps/dashboard/src/app.css index d7c0f7c..841c2a8 100644 --- a/apps/dashboard/src/app.css +++ b/apps/dashboard/src/app.css @@ -301,3 +301,274 @@ body { to { opacity: 1; } } } + +/* ═══════════════════════════════════════════════════════════════════════ + THE "ALIVE" LAYER (bleeding-edge, progressively enhanced) + ─────────────────────────────────────────────────────────────────────── + Shared primitives that make every page breathe, respond, and feel seen. + Everything here degrades gracefully and is disabled under reduced-motion. + ═══════════════════════════════════════════════════════════════════════ */ + +/* ── Registered animatable custom props (@property) ────────────────────── + Registering a property as / lets the browser TWEEN it, + which plain CSS vars can't do. Powers the rotating conic borders below. */ +@property --angle { + syntax: ''; + initial-value: 0deg; + inherits: false; +} +@property --shine { + syntax: ''; + initial-value: 0%; + inherits: false; +} + +/* ── Scroll-reveal (paired with use:reveal) ───────────────────────────── + Elements start translated + transparent; .reveal-in lands them. The + action toggles the class via IntersectionObserver, with per-item --reveal-delay + for staggered list entrances. */ +.reveal { + opacity: 0; + transform: translateY(var(--reveal-y, 16px)); + transition: + opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.55s cubic-bezier(0.22, 1, 0.36, 1); + transition-delay: var(--reveal-delay, 0ms); + will-change: opacity, transform; +} +.reveal-in { + opacity: 1; + transform: none; +} +@media (prefers-reduced-motion: reduce) { + .reveal { + opacity: 1; + transform: none; + transition: none; + } +} + +/* ── @starting-style page/section entry ───────────────────────────────── + Native entry animation with zero JS: the element animates FROM the + starting-style the first time it's rendered. Used on route content. */ +@media not (prefers-reduced-motion: reduce) { + .enter { + transition: + opacity 0.4s ease, + transform 0.4s cubic-bezier(0.22, 1, 0.36, 1); + } + @starting-style { + .enter { + opacity: 0; + transform: translateY(10px); + } + } +} + +/* ── Conic-gradient animated border (premium "live" frame) ────────────── + A slowly rotating iridescent ring around a panel. The @property --angle + makes the conic gradient's start angle tweenable. Use class .live-border. */ +.live-border { + position: relative; + isolation: isolate; +} +.live-border::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: conic-gradient( + from var(--angle), + transparent 0%, + var(--color-synapse) 18%, + var(--color-dream) 33%, + transparent 50%, + transparent 100% + ); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0.6; + z-index: -1; +} +@media not (prefers-reduced-motion: reduce) { + .live-border::before { + animation: border-rotate 6s linear infinite; + } + @keyframes border-rotate { + to { + --angle: 360deg; + } + } +} + +/* ── Cursor spotlight surface (paired with use:spotlight) ──────────────── + A soft radial glow follows the pointer across a panel. --spot-x/y/o are + set by the action; default opacity 0 so it's invisible until hovered. */ +.spotlight-surface { + position: relative; + overflow: hidden; +} +.spotlight-surface::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: radial-gradient( + 340px circle at var(--spot-x, 50%) var(--spot-y, 50%), + rgba(129, 140, 248, 0.12), + transparent 60% + ); + opacity: var(--spot-o, 0); + transition: opacity 0.3s ease; + z-index: 0; +} + +/* ── Tilt glare (paired with use:tilt glare) ────────────────────────────── */ +.tilt-glare { + position: relative; + overflow: hidden; +} +.tilt-glare::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: radial-gradient( + circle at var(--glare-x, 50%) var(--glare-y, 50%), + rgba(255, 255, 255, 0.14), + transparent 45% + ); + opacity: var(--glare-o, 0); + transition: opacity 0.3s ease; +} + +/* ── Breathing live indicator (calmer than a hard blink) ──────────────── + A dot that gently inhales/exhales scale + glow — reads as "alive, + listening" rather than "error blinking". */ +.breathe { + animation: breathe 3.2s ease-in-out infinite; +} +@keyframes breathe { + 0%, 100% { + transform: scale(1); + filter: drop-shadow(0 0 2px currentColor); + opacity: 0.85; + } + 50% { + transform: scale(1.18); + filter: drop-shadow(0 0 7px currentColor); + opacity: 1; + } +} +@media (prefers-reduced-motion: reduce) { + .breathe { animation: none; } +} + +/* A live "radar ping" ring that expands and fades — wrap a dot in .ping-host. */ +.ping-host { + position: relative; +} +.ping-host::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + background: currentColor; + opacity: 0.6; + z-index: -1; +} +@media not (prefers-reduced-motion: reduce) { + .ping-host::before { + animation: ping 2.4s cubic-bezier(0, 0, 0.2, 1) infinite; + } + @keyframes ping { + 0% { transform: scale(1); opacity: 0.5; } + 80%, 100% { transform: scale(2.6); opacity: 0; } + } +} + +/* ── Shimmer skeleton (loading that feels intentional, not frozen) ─────── + A diagonal light sweep over skeleton blocks. Use .shimmer on the + placeholder; it replaces a plain pulse with a directional sheen. */ +.shimmer { + position: relative; + overflow: hidden; + background: rgba(255, 255, 255, 0.03); +} +.shimmer::after { + content: ''; + position: absolute; + inset: 0; + transform: translateX(-100%); + background: linear-gradient( + 90deg, + transparent, + rgba(129, 140, 248, 0.12), + transparent + ); +} +@media not (prefers-reduced-motion: reduce) { + .shimmer::after { + animation: shimmer 1.6s ease-in-out infinite; + } + @keyframes shimmer { + 100% { transform: translateX(100%); } + } +} + +/* ── Gradient text that slowly drifts (alive headings) ──────────────────── */ +.text-aurora { + background: linear-gradient( + 100deg, + var(--color-synapse-glow), + var(--color-dream-glow), + var(--color-recall), + var(--color-synapse-glow) + ); + background-size: 250% 100%; + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +@media not (prefers-reduced-motion: reduce) { + .text-aurora { + animation: aurora-drift 8s ease-in-out infinite; + } + @keyframes aurora-drift { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + } +} + +/* ── Hover lift utility — cards rise + glow when pointed at ───────────────*/ +.lift { + transition: + transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.28s ease, + border-color 0.28s ease; +} +.lift:hover { + transform: translateY(-3px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(99, 102, 241, 0.18); +} +@media (prefers-reduced-motion: reduce) { + .lift:hover { transform: none; } +} + +/* ── Tabular numerals helper (count-ups don't jitter) ──────────────────── */ +.tabular-nums { + font-variant-numeric: tabular-nums; + font-feature-settings: 'tnum'; +} + +/* ── Focus ring consistency (accessible + on-brand) ─────────────────────── */ +:where(button, a, input, select, [role='button'], [tabindex]):focus-visible { + outline: 2px solid var(--color-synapse-glow); + outline-offset: 2px; + border-radius: 0.4rem; +} diff --git a/apps/dashboard/src/lib/actions/interactions.ts b/apps/dashboard/src/lib/actions/interactions.ts new file mode 100644 index 0000000..c4e43b3 --- /dev/null +++ b/apps/dashboard/src/lib/actions/interactions.ts @@ -0,0 +1,136 @@ +// ═══════════════════════════════════════════════════════════════════════ +// Micro-interaction actions — the "alive" layer for pointer feel. +// ─────────────────────────────────────────────────────────────────────── +// All actions no-op under prefers-reduced-motion and clean up their own +// listeners on destroy. Pure pointer events, no dependency. +// ═══════════════════════════════════════════════════════════════════════ + +const prefersReducedMotion = () => + typeof window !== 'undefined' && + window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; + +// ── magnetic: element drifts toward the cursor, snaps back on leave ────── +export interface MagneticOptions { + /** How far the element is allowed to drift, in px. */ + strength?: number; +} +export function magnetic(node: HTMLElement, options: MagneticOptions = {}) { + if (prefersReducedMotion()) return {}; + const strength = options.strength ?? 8; + let raf = 0; + + function onMove(e: PointerEvent) { + const r = node.getBoundingClientRect(); + const mx = e.clientX - (r.left + r.width / 2); + const my = e.clientY - (r.top + r.height / 2); + const dx = Math.max(-1, Math.min(1, mx / (r.width / 2))) * strength; + const dy = Math.max(-1, Math.min(1, my / (r.height / 2))) * strength; + cancelAnimationFrame(raf); + raf = requestAnimationFrame(() => { + node.style.transform = `translate(${dx}px, ${dy}px)`; + }); + } + function onLeave() { + cancelAnimationFrame(raf); + node.style.transform = ''; + } + + node.style.transition = 'transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)'; + node.addEventListener('pointermove', onMove); + node.addEventListener('pointerleave', onLeave); + return { + destroy() { + node.removeEventListener('pointermove', onMove); + node.removeEventListener('pointerleave', onLeave); + cancelAnimationFrame(raf); + }, + }; +} + +// ── tilt: 3D parallax tilt toward the cursor (cards, hero panels) ──────── +export interface TiltOptions { + /** Max tilt in degrees. */ + max?: number; + /** Lift the card toward the viewer on hover, in px. */ + lift?: number; + /** Add a moving sheen highlight that follows the cursor. */ + glare?: boolean; +} +export function tilt(node: HTMLElement, options: TiltOptions = {}) { + if (prefersReducedMotion()) return {}; + const max = options.max ?? 6; + const lift = options.lift ?? 0; + const glare = options.glare ?? false; + let raf = 0; + + node.style.transformStyle = 'preserve-3d'; + node.style.transition = 'transform 0.3s cubic-bezier(0.23, 1, 0.32, 1)'; + + if (glare) { + node.style.setProperty('--glare-x', '50%'); + node.style.setProperty('--glare-y', '50%'); + node.style.setProperty('--glare-o', '0'); + } + + function onMove(e: PointerEvent) { + const r = node.getBoundingClientRect(); + const px = (e.clientX - r.left) / r.width; + const py = (e.clientY - r.top) / r.height; + const rx = (0.5 - py) * max * 2; + const ry = (px - 0.5) * max * 2; + cancelAnimationFrame(raf); + raf = requestAnimationFrame(() => { + node.style.transform = `perspective(900px) rotateX(${rx}deg) rotateY(${ry}deg)${lift ? ` translateZ(${lift}px)` : ''}`; + if (glare) { + node.style.setProperty('--glare-x', `${px * 100}%`); + node.style.setProperty('--glare-y', `${py * 100}%`); + node.style.setProperty('--glare-o', '1'); + } + }); + } + function onLeave() { + cancelAnimationFrame(raf); + node.style.transform = ''; + if (glare) node.style.setProperty('--glare-o', '0'); + } + + node.addEventListener('pointermove', onMove); + node.addEventListener('pointerleave', onLeave); + return { + destroy() { + node.removeEventListener('pointermove', onMove); + node.removeEventListener('pointerleave', onLeave); + cancelAnimationFrame(raf); + }, + }; +} + +// ── spotlight: a soft radial glow that tracks the cursor over a surface ── +// Sets --spot-x / --spot-y custom props the element's CSS can read to +// position a radial-gradient highlight. Makes large panels feel responsive +// to the pointer even when nothing is "hovered". +export function spotlight(node: HTMLElement) { + if (prefersReducedMotion()) return {}; + let raf = 0; + function onMove(e: PointerEvent) { + const r = node.getBoundingClientRect(); + cancelAnimationFrame(raf); + raf = requestAnimationFrame(() => { + node.style.setProperty('--spot-x', `${e.clientX - r.left}px`); + node.style.setProperty('--spot-y', `${e.clientY - r.top}px`); + node.style.setProperty('--spot-o', '1'); + }); + } + function onLeave() { + node.style.setProperty('--spot-o', '0'); + } + node.addEventListener('pointermove', onMove); + node.addEventListener('pointerleave', onLeave); + return { + destroy() { + node.removeEventListener('pointermove', onMove); + node.removeEventListener('pointerleave', onLeave); + cancelAnimationFrame(raf); + }, + }; +} diff --git a/apps/dashboard/src/lib/actions/reveal.ts b/apps/dashboard/src/lib/actions/reveal.ts new file mode 100644 index 0000000..43efd48 --- /dev/null +++ b/apps/dashboard/src/lib/actions/reveal.ts @@ -0,0 +1,68 @@ +// ═══════════════════════════════════════════════════════════════════════ +// reveal — scroll-into-view entrance animation as a Svelte action. +// ─────────────────────────────────────────────────────────────────────── +// Usage:
// default rise+fade +//
+// +// Adds an IntersectionObserver that flips the element to its "revealed" +// state the first time it scrolls into view. Pairs with the .reveal / +// .reveal-in CSS in app.css. Honors prefers-reduced-motion by revealing +// instantly (no transform), so motion-sensitive users still see content. +// +// Why an action and not CSS scroll-timeline alone: scroll-driven +// animation-timeline is great for continuous scrubbing, but for a simple +// "appear once when seen" we want a one-shot that also works as a staggered +// list entrance with per-item delay — an observer is the robust path with +// universal mid-2026 support. +// ═══════════════════════════════════════════════════════════════════════ + +export interface RevealOptions { + /** Pixels to translate up from on entry. */ + y?: number; + /** ms delay before the transition starts (use for stagger). */ + delay?: number; + /** Only reveal once, then stop observing (default true). */ + once?: boolean; + /** 0-1 visibility threshold to trigger. */ + threshold?: number; +} + +const prefersReducedMotion = () => + typeof window !== 'undefined' && + window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; + +export function reveal(node: HTMLElement, options: RevealOptions = {}) { + const { y = 16, delay = 0, once = true, threshold = 0.12 } = options; + + // Motion-off: show immediately, do nothing else. + if (prefersReducedMotion()) { + node.classList.add('reveal-in'); + return {}; + } + + node.classList.add('reveal'); + node.style.setProperty('--reveal-y', `${y}px`); + if (delay) node.style.setProperty('--reveal-delay', `${delay}ms`); + + const io = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + node.classList.add('reveal-in'); + if (once) io.unobserve(node); + } else if (!once) { + node.classList.remove('reveal-in'); + } + } + }, + { threshold, rootMargin: '0px 0px -8% 0px' } + ); + + io.observe(node); + + return { + destroy() { + io.disconnect(); + }, + }; +} diff --git a/apps/dashboard/src/lib/components/AnimatedNumber.svelte b/apps/dashboard/src/lib/components/AnimatedNumber.svelte new file mode 100644 index 0000000..0aab878 --- /dev/null +++ b/apps/dashboard/src/lib/components/AnimatedNumber.svelte @@ -0,0 +1,91 @@ + + +{prefix}{formatted}{suffix} diff --git a/apps/dashboard/src/lib/components/Dropdown.svelte b/apps/dashboard/src/lib/components/Dropdown.svelte new file mode 100644 index 0000000..9c12e69 --- /dev/null +++ b/apps/dashboard/src/lib/components/Dropdown.svelte @@ -0,0 +1,338 @@ + + + + +
+ {#if label} + {label} + {/if} + + + {#if open} +
+ {#each options as opt, i (opt.value)} + + {/each} +
+ {/if} +
+ + diff --git a/apps/dashboard/src/lib/components/Icon.svelte b/apps/dashboard/src/lib/components/Icon.svelte new file mode 100644 index 0000000..ed248a0 --- /dev/null +++ b/apps/dashboard/src/lib/components/Icon.svelte @@ -0,0 +1,143 @@ + + + + + + + diff --git a/apps/dashboard/src/lib/components/PageHeader.svelte b/apps/dashboard/src/lib/components/PageHeader.svelte new file mode 100644 index 0000000..00ad638 --- /dev/null +++ b/apps/dashboard/src/lib/components/PageHeader.svelte @@ -0,0 +1,69 @@ + + +
+
+
+ +
+
+

{title}

+ {#if subtitle} +

{subtitle}

+ {/if} +
+
+ {#if children} +
+ {@render children()} +
+ {/if} +
+ + diff --git a/apps/dashboard/src/routes/(app)/activation/+page.svelte b/apps/dashboard/src/routes/(app)/activation/+page.svelte index 2433e43..ed4bcc0 100644 --- a/apps/dashboard/src/routes/(app)/activation/+page.svelte +++ b/apps/dashboard/src/routes/(app)/activation/+page.svelte @@ -24,6 +24,11 @@ } from '$components/ActivationNetwork.svelte'; import { filterNewSpreadEvents } from '$components/activation-helpers'; import type { Memory, VestigeEvent } from '$types'; + import PageHeader from '$lib/components/PageHeader.svelte'; + import Icon from '$lib/components/Icon.svelte'; + import AnimatedNumber from '$lib/components/AnimatedNumber.svelte'; + import { reveal } from '$lib/actions/reveal'; + import { spotlight, magnetic } from '$lib/actions/interactions'; let searchQuery = $state(''); let loading = $state(false); @@ -180,19 +185,30 @@ }); -
-
-

Spreading Activation

-

- Collins & Loftus 1975 — activation spreads from a seed memory to - neighbours along semantic edges, decaying by 0.93 per animation frame - until it drops below 0.05. Search seeds a focused burst; live mode - overlays every spread event fired by the cognitive engine in real time. -

-
+
+ +
+ + {liveEnabled ? 'Live' : 'Paused'} + · + + bursts +
+
-
+
Seed Memory
@@ -236,8 +254,8 @@ {#if loading}
-
-

Computing activation...

+
+

Computing activation…

{:else if errorMessage} @@ -250,8 +268,8 @@
{:else if !focusedSource && searched}
-
-
+
+

No matching memory

Nothing in the graph matches @@ -263,8 +281,8 @@

{:else if !focusedSource}
-
-
+
+

Waiting for activation

Seed a burst with the search bar above, or enable live mode to diff --git a/apps/dashboard/src/routes/(app)/contradictions/+page.svelte b/apps/dashboard/src/routes/(app)/contradictions/+page.svelte index 13ee04e..4415f0e 100644 --- a/apps/dashboard/src/routes/(app)/contradictions/+page.svelte +++ b/apps/dashboard/src/routes/(app)/contradictions/+page.svelte @@ -1,5 +1,10 @@

-

Explore Connections

+
{#each (['associations', 'chains', 'bridges'] as const) as m} @@ -144,29 +154,51 @@ {#if sourceMemory} {#if loading} -
-
-

Exploring {mode}...

+
+
+ + Exploring {mode}… +
+
+ {#each Array(4) as _, i} +
+
+
+
+
+
+
+ {/each} +
{:else if associations.length > 0}
-

{associations.length} Connections Found

+

+ + Connections Found +

- {#each associations as assoc, i} -
-
- {i + 1} -
-
-

{assoc.content}

-
- {#if assoc.nodeType}{assoc.nodeType}{/if} - {#if assoc.score}Score: {Number(assoc.score).toFixed(3)}{/if} - {#if assoc.similarity}Similarity: {Number(assoc.similarity).toFixed(3)}{/if} - {#if assoc.retention}{(Number(assoc.retention) * 100).toFixed(0)}% retention{/if} - {#if assoc.connectionType}{assoc.connectionType}{/if} + {#each associations as assoc, i (i)} +
+
+
+ {i + 1} +
+
+

{assoc.content}

+
+ {#if assoc.nodeType}{assoc.nodeType}{/if} + {#if assoc.score}Score: {Number(assoc.score).toFixed(3)}{/if} + {#if assoc.similarity}Similarity: {Number(assoc.similarity).toFixed(3)}{/if} + {#if assoc.retention}{(Number(assoc.retention) * 100).toFixed(0)}% retention{/if} + {#if assoc.connectionType}{assoc.connectionType}{/if} +
@@ -174,16 +206,26 @@
{:else} -
-
-

No connections found for this query.

+
+ +

No connections surfaced yet

+

+ {#if mode === 'associations'} + This memory hasn't formed strong links here. Try a broader source query — the graph rewards more general seeds. + {:else} + No {mode} found between these two memories. Pick a different source or target and the path may light up. + {/if} +

{/if} {/if}
-

Importance Scorer

+

+ + Importance Scorer +

4-channel neuroscience scoring: novelty, arousal, reward, attention