feat(dashboard): alive overhaul — unique icons, dropdowns, motion on every page

Make the dashboard feel alive every second, with clear controls, for the
July 14 HN relaunch. Memory Cinema is left fully untouched (zero changes to
MemoryCinema.svelte / graph/cinema/*; its tests still pass).

Foundation (lifts every page):
- Icon.svelte: inline-SVG icon system, zero runtime dep. A UNIQUE semantic
  silhouette per nav item — kills the old duplicated Unicode glyphs (◎◈◉◷
  were each reused across multiple items). Wired into sidebar, mobile nav,
  command palette, logo.
- Dropdown.svelte: accessible, keyboard-nav, type-ahead, animated select
  replacement with color dots / badges. Replaces dead native <select>s.
- AnimatedNumber.svelte: rAF count-up/tween, reduced-motion safe.
- PageHeader.svelte: shared masthead (drawn route icon + aurora title).
- actions/reveal.ts + actions/interactions.ts: scroll-reveal, magnetic,
  tilt(+glare), spotlight — all no-op under reduced-motion.
- app.css "alive layer": @property animatable gradients, conic live-border,
  breathe/ping, shimmer skeletons, @starting-style entry, aurora text, lift.

Per-page: every route (graph non-cinema controls, reasoning, memories,
timeline, feed, explore, activation, dreams, schedule, importance,
duplicates, contradictions, patterns, intentions, stats, settings) now uses
PageHeader, real Icons, count-ups, staggered reveals, shimmer loaders,
spotlight cards, and warm empty states. Native selects and button-row
filters became clear Dropdowns where it improves clarity.

Gates: svelte-check 0 errors/0 warnings, 937/937 tests pass, build green,
verified live in the browser preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-06-22 02:58:26 -05:00
parent bc81da46eb
commit 1fbbecb0b3
24 changed files with 2360 additions and 585 deletions

View file

@ -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 <angle>/<percentage> lets the browser TWEEN it,
which plain CSS vars can't do. Powers the rotating conic borders below. */
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
@property --shine {
syntax: '<percentage>';
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;
}