diff --git a/apps/dashboard/src/app.css b/apps/dashboard/src/app.css index 418467a..d7c0f7c 100644 --- a/apps/dashboard/src/app.css +++ b/apps/dashboard/src/app.css @@ -43,6 +43,43 @@ html { font-family: var(--font-mono); } +/* ═══════════════════════════════════════════ + OKLCH / DISPLAY-P3 ACCENT PALETTE (PROGRESSIVE ENHANCEMENT) + ═══════════════════════════════════════════ + The @theme block above keeps the original sRGB hex values, which + Tailwind reads at build time and which serve as the fallback for + sRGB monitors and browsers without OKLCH support. + + When the browser understands oklch(), we redefine the SAME vivid + accents + node-type colors using their OKLCH equivalents. These are + faithful conversions of the hex values (same hue/chroma identity); + on a wide-gamut display-p3 monitor they render more saturated while + reading as the same color. The void/abyss/surface neutrals are left + untouched — only the vivid accents benefit from the wider gamut. */ +@supports (color: oklch(0 0 0)) { + :root { + /* Accent colors */ + --color-synapse: oklch(0.585 0.222 277); + --color-synapse-glow: oklch(0.685 0.169 277); + --color-dream: oklch(0.627 0.265 304); + --color-dream-glow: oklch(0.714 0.203 305); + --color-memory: oklch(0.623 0.214 259); + --color-recall: oklch(0.696 0.17 162); + --color-decay: oklch(0.637 0.237 25); + --color-warning: oklch(0.769 0.188 70); + + /* Node type colors */ + --color-node-fact: oklch(0.623 0.214 259); + --color-node-concept: oklch(0.606 0.25 292); + --color-node-event: oklch(0.769 0.188 70); + --color-node-person: oklch(0.696 0.17 162); + --color-node-place: oklch(0.715 0.143 215); + --color-node-note: oklch(0.551 0.027 264); + --color-node-pattern: oklch(0.656 0.241 354); + --color-node-decision: oklch(0.637 0.237 25); + } +} + body { margin: 0; min-height: 100vh; @@ -234,3 +271,33 @@ body { .retention-low { color: var(--color-warning); } .retention-good { color: var(--color-recall); } .retention-strong { color: var(--color-synapse); } + +/* ═══════════════════════════════════════════ + VIEW TRANSITIONS (CROSSFADE) + ═══════════════════════════════════════════ + Native View Transitions API crossfade between routes. Pairs with the + onNavigate hook in +layout.svelte that calls document.startViewTransition. + Reduced-motion users get an instant cut (the @media guard disables the + animation entirely, so the default browser cross-fade does not run). */ +@media not (prefers-reduced-motion: reduce) { + ::view-transition-old(root), + ::view-transition-new(root) { + animation-duration: 180ms; + animation-timing-function: ease; + } + ::view-transition-old(root) { + animation-name: vt-fade-out; + } + ::view-transition-new(root) { + animation-name: vt-fade-in; + } + + @keyframes vt-fade-out { + from { opacity: 1; } + to { opacity: 0; } + } + @keyframes vt-fade-in { + from { opacity: 0; } + to { opacity: 1; } + } +} diff --git a/apps/dashboard/src/lib/stores/websocket.ts b/apps/dashboard/src/lib/stores/websocket.ts index 8554581..fda02f9 100644 --- a/apps/dashboard/src/lib/stores/websocket.ts +++ b/apps/dashboard/src/lib/stores/websocket.ts @@ -6,11 +6,13 @@ const MAX_EVENTS = 200; function createWebSocketStore() { const { subscribe, set, update } = writable<{ connected: boolean; + reconnecting: boolean; events: VestigeEvent[]; lastHeartbeat: VestigeEvent | null; error: string | null; }>({ connected: false, + reconnecting: false, events: [], lastHeartbeat: null, error: null @@ -32,7 +34,7 @@ function createWebSocketStore() { ws.onopen = () => { reconnectAttempts = 0; - update(s => ({ ...s, connected: true, error: null })); + update(s => ({ ...s, connected: true, reconnecting: false, error: null })); }; ws.onmessage = (event) => { @@ -65,6 +67,7 @@ function createWebSocketStore() { function scheduleReconnect(url: string) { if (reconnectTimer) clearTimeout(reconnectTimer); + update(s => ({ ...s, reconnecting: true })); const delay = Math.min(1000 * 2 ** reconnectAttempts, 30000); reconnectAttempts++; reconnectTimer = setTimeout(() => connect(url), delay); @@ -74,7 +77,7 @@ function createWebSocketStore() { if (reconnectTimer) clearTimeout(reconnectTimer); ws?.close(); ws = null; - set({ connected: false, events: [], lastHeartbeat: null, error: null }); + set({ connected: false, reconnecting: false, events: [], lastHeartbeat: null, error: null }); } function clearEvents() { @@ -108,6 +111,7 @@ export const websocket = createWebSocketStore(); // Derived stores for specific event types export const isConnected = derived(websocket, $ws => $ws.connected); +export const isReconnecting = derived(websocket, $ws => $ws.reconnecting); export const eventFeed = derived(websocket, $ws => $ws.events); export const heartbeat = derived(websocket, $ws => $ws.lastHeartbeat); export const memoryCount = derived(websocket, $ws => diff --git a/apps/dashboard/src/routes/(app)/graph/+page.svelte b/apps/dashboard/src/routes/(app)/graph/+page.svelte index 1df2683..a06dbe7 100644 --- a/apps/dashboard/src/routes/(app)/graph/+page.svelte +++ b/apps/dashboard/src/routes/(app)/graph/+page.svelte @@ -262,34 +262,38 @@ disown {/if} -
+
-
+
e.key === 'Enter' && searchGraph()} - class="flex-1 px-3 py-2 glass rounded-xl text-text text-sm + class="flex-1 min-w-0 px-3 py-2 glass rounded-xl text-text text-sm placeholder:text-muted focus:outline-none focus:!border-synapse/40 transition" />
-
+
-
+