From 287af1042e53b1eb059c7b7f4cfb5a0c96dcbaeb Mon Sep 17 00:00:00 2001 From: zeapsu Date: Wed, 13 May 2026 14:54:36 -0700 Subject: [PATCH 1/7] docs: add Tome Dashboard proposal Add an interactive HTML artifact that proposes the Tome Dashboard as the default overview mode, with a Spotlight-style command bar and focused component views. --- designs/tome-dashboard-proposal.html | 617 +++++++++++++++++++++++++++ 1 file changed, 617 insertions(+) create mode 100644 designs/tome-dashboard-proposal.html diff --git a/designs/tome-dashboard-proposal.html b/designs/tome-dashboard-proposal.html new file mode 100644 index 0000000..4051756 --- /dev/null +++ b/designs/tome-dashboard-proposal.html @@ -0,0 +1,617 @@ + + + + + + Sage Tome Dashboard Proposal + + + +
+ + +
+
+ Tome active + Dashboard mode · command bar available +
+ +
+ + +
+
+
+
Tome Dashboard
+

Weak Measurement in BECs

+

A focused workspace for stochastic Gross-Pitaevskii dynamics, TSSP methods, and weak-measurement models. Dashboard is the default view. Natural language is optional, not mandatory.

+
+
+ + + + +
+
+ +
+
12sources
+
884chunks
+
4sessions
+
3stale artifacts
+
+ +
+ + + + + + + + + + + +
+
+ +
+
+

Focused View

+ +
+
+
+
+
+
+ + + + + + From 2d9e81979961e50c79ad84064671959530f142f4 Mon Sep 17 00:00:00 2001 From: zeapsu Date: Thu, 14 May 2026 01:18:29 -0700 Subject: [PATCH 2/7] feat: add customizable Tome Dashboard layout Make the HTML dashboard artifact support drag-to-reorder cards, resize handles, persisted layout state, and a reset control. --- designs/tome-dashboard-proposal.html | 261 ++++++++++++++++++++++++--- 1 file changed, 240 insertions(+), 21 deletions(-) diff --git a/designs/tome-dashboard-proposal.html b/designs/tome-dashboard-proposal.html index 4051756..20cc0c8 100644 --- a/designs/tome-dashboard-proposal.html +++ b/designs/tome-dashboard-proposal.html @@ -244,14 +244,28 @@ .metric strong { display: block; font-size: 20px; letter-spacing: -.02em; } .metric span { color: rgba(194,198,214,.58); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; } + .layout-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 24px 16px; + color: rgba(194,198,214,.62); + font-size: 12px; + } + .layout-toolbar strong { color: var(--on-surface); font-weight: 600; } .card-grid { + --cell: 92px; display: grid; - grid-template-columns: 1.15fr .85fr; + grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-auto-rows: var(--cell); + grid-auto-flow: dense; gap: 12px; padding: 0 18px 18px; + min-height: 520px; } .card { - min-height: 154px; + min-height: 0; padding: 17px; border: 0; border-radius: 22px; @@ -259,12 +273,58 @@ box-shadow: inset 0 0 0 1px rgba(66,71,84,.12); color: var(--on-surface); text-align: left; - transition: transform .16s, background .16s, box-shadow .16s; + transition: transform .16s, background .16s, box-shadow .16s, opacity .16s; position: relative; overflow: hidden; + grid-column: span var(--col-span, 2); + grid-row: span var(--row-span, 2); + user-select: none; } .card:hover { transform: translateY(-2px); background: color-mix(in srgb, var(--surface-container-high) 76%, transparent); box-shadow: inset 0 0 0 1px rgba(173,198,255,.18), 0 8px 24px rgba(0,0,0,.22); } - .wide { grid-column: span 1; min-height: 202px; } + .card.is-dragging { opacity: .38; transform: scale(.985); } + .card.drag-over { box-shadow: inset 0 0 0 1px rgba(173,198,255,.38), 0 0 0 4px rgba(173,198,255,.08); } + .card.is-resizing { box-shadow: inset 0 0 0 1px rgba(255,183,134,.42), 0 0 0 4px rgba(255,183,134,.08); } + .card.compact .source-list, .card.compact .quote, .card.compact .study-split { display: none; } + .card.compact .action-row { margin-top: 10px; } + .card.wide { --col-span: 3; --row-span: 2; } + .drag-handle { + position: absolute; + right: 44px; + top: 12px; + width: 26px; + height: 26px; + display: grid; + place-items: center; + border-radius: 9px; + color: rgba(194,198,214,.42); + background: rgba(14,14,14,.18); + cursor: grab; + z-index: 2; + } + .drag-handle:active { cursor: grabbing; } + .resize-grip { + position: absolute; + right: 10px; + bottom: 10px; + width: 22px; + height: 22px; + border: 0; + border-radius: 8px; + background: linear-gradient(135deg, transparent 46%, rgba(173,198,255,.45) 48%, rgba(173,198,255,.45) 56%, transparent 58%), + linear-gradient(135deg, transparent 62%, rgba(173,198,255,.28) 64%, rgba(173,198,255,.28) 72%, transparent 74%); + cursor: nwse-resize; + opacity: .72; + z-index: 2; + } + .resize-readout { + position: absolute; + right: 40px; + bottom: 12px; + color: rgba(194,198,214,.44); + font-size: 10px; + letter-spacing: .08em; + text-transform: uppercase; + } .card-head { display: flex; justify-content: space-between; align-items: start; gap: 10px; margin-bottom: 12px; } .card h3 { margin: 0; font-size: 16px; letter-spacing: -.015em; } .card small { color: rgba(194,198,214,.55); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; } @@ -373,7 +433,9 @@ } @media (max-width: 720px) { .metrics { grid-template-columns: repeat(2, 1fr); } - .card-grid { grid-template-columns: 1fr; } + .card-grid { grid-template-columns: 1fr; grid-auto-rows: minmax(120px, auto); } + .card { grid-column: span 1 !important; grid-row: span 2; } + .drag-handle, .resize-grip, .resize-readout, .layout-toolbar { display: none; } .dash-hero { grid-template-columns: 1fr; } .hero-actions { justify-content: start; } .provider { display: none; } @@ -409,9 +471,11 @@

Command bar job

Try the prototype

@@ -451,8 +515,16 @@

Weak Measurement in BECs

3stale artifacts
-
- +
+ +
+
+ ⋮⋮ + 3×3 +
Grounding

Sources

Current

12 documents are active in this Tome. New source additions should mark report, quiz, and audio as stale until regenerated.

@@ -461,42 +533,57 @@

Weak Measurement in BECs

url Numerical methods comparison
Manage sourcesUpload
- +
- + - + - + - + - +
@@ -598,12 +685,144 @@

Focused View

} function closeCommand() { overlay.classList.remove('visible'); } - document.querySelectorAll('[data-focus]').forEach(el => { + const grid = document.getElementById('dashboardGrid'); + const resetLayout = document.getElementById('resetLayout'); + const layoutKey = 'sage-dashboard-layout-v2'; + let draggedCard = null; + let resizing = null; + + const defaults = Array.from(grid.querySelectorAll('.card')).map(card => ({ + id: card.dataset.widget, + col: Number.parseInt(card.style.getPropertyValue('--col-span')) || 2, + row: Number.parseInt(card.style.getPropertyValue('--row-span')) || 2, + })); + + function updateCardChrome(card) { + const col = Number.parseInt(card.style.getPropertyValue('--col-span')) || 2; + const row = Number.parseInt(card.style.getPropertyValue('--row-span')) || 2; + card.querySelector('.resize-readout').textContent = `${col}×${row}`; + card.classList.toggle('compact', col <= 1 || row <= 1); + } + + function saveLayout() { + const layout = Array.from(grid.querySelectorAll('.card')).map(card => ({ + id: card.dataset.widget, + col: Number.parseInt(card.style.getPropertyValue('--col-span')) || 2, + row: Number.parseInt(card.style.getPropertyValue('--row-span')) || 2, + })); + localStorage.setItem(layoutKey, JSON.stringify(layout)); + } + + function loadLayout() { + const saved = JSON.parse(localStorage.getItem(layoutKey) || 'null'); + const layout = Array.isArray(saved) ? saved : defaults; + layout.forEach(item => { + const card = grid.querySelector(`[data-widget="${item.id}"]`); + if (!card) return; + card.style.setProperty('--col-span', Math.max(1, Math.min(4, item.col || 2))); + card.style.setProperty('--row-span', Math.max(1, Math.min(4, item.row || 2))); + grid.appendChild(card); + updateCardChrome(card); + }); + } + + function resetLayoutToDefault() { + localStorage.removeItem(layoutKey); + defaults.forEach(item => { + const card = grid.querySelector(`[data-widget="${item.id}"]`); + card.style.setProperty('--col-span', item.col); + card.style.setProperty('--row-span', item.row); + grid.appendChild(card); + updateCardChrome(card); + }); + } + + grid.querySelectorAll('.card').forEach(card => { + const handle = card.querySelector('.drag-handle'); + const grip = card.querySelector('.resize-grip'); + + card.addEventListener('click', (e) => { + if (e.target.closest('.drag-handle, .resize-grip, .ghost-action')) return; + showFocus(card.dataset.focus); + }); + card.addEventListener('keydown', (e) => { + if (e.key === 'Enter') showFocus(card.dataset.focus); + }); + + handle.addEventListener('dragstart', (e) => { + draggedCard = card; + card.classList.add('is-dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', card.dataset.widget); + }); + handle.addEventListener('dragend', () => { + card.classList.remove('is-dragging'); + grid.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); + draggedCard = null; + saveLayout(); + }); + card.addEventListener('dragover', (e) => { + if (!draggedCard || draggedCard === card) return; + e.preventDefault(); + card.classList.add('drag-over'); + }); + card.addEventListener('dragleave', () => card.classList.remove('drag-over')); + card.addEventListener('drop', (e) => { + e.preventDefault(); + card.classList.remove('drag-over'); + if (!draggedCard || draggedCard === card) return; + const cards = Array.from(grid.querySelectorAll('.card')); + const from = cards.indexOf(draggedCard); + const to = cards.indexOf(card); + grid.insertBefore(draggedCard, from < to ? card.nextSibling : card); + saveLayout(); + }); + + grip.addEventListener('pointerdown', (e) => { + e.preventDefault(); + e.stopPropagation(); + const rect = card.getBoundingClientRect(); + resizing = { + card, + startX: e.clientX, + startY: e.clientY, + startCol: Number.parseInt(card.style.getPropertyValue('--col-span')) || 2, + startRow: Number.parseInt(card.style.getPropertyValue('--row-span')) || 2, + cellW: rect.width / (Number.parseInt(card.style.getPropertyValue('--col-span')) || 2), + cellH: rect.height / (Number.parseInt(card.style.getPropertyValue('--row-span')) || 2), + }; + card.classList.add('is-resizing'); + grip.setPointerCapture(e.pointerId); + }); + }); + + document.addEventListener('pointermove', (e) => { + if (!resizing) return; + const dx = Math.round((e.clientX - resizing.startX) / Math.max(resizing.cellW, 80)); + const dy = Math.round((e.clientY - resizing.startY) / Math.max(resizing.cellH, 80)); + const nextCol = Math.max(1, Math.min(4, resizing.startCol + dx)); + const nextRow = Math.max(1, Math.min(4, resizing.startRow + dy)); + resizing.card.style.setProperty('--col-span', nextCol); + resizing.card.style.setProperty('--row-span', nextRow); + updateCardChrome(resizing.card); + }); + + document.addEventListener('pointerup', () => { + if (!resizing) return; + resizing.card.classList.remove('is-resizing'); + saveLayout(); + resizing = null; + }); + + document.querySelectorAll('.hero-actions [data-focus], .spot-row[data-focus]').forEach(el => { el.addEventListener('click', (e) => { const focus = e.currentTarget.getAttribute('data-focus'); showFocus(focus); }); }); + resetLayout.addEventListener('click', resetLayoutToDefault); + loadLayout(); + document.getElementById('backDashboard').addEventListener('click', showDashboard); document.getElementById('openCommand').addEventListener('click', openCommand); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeCommand(); }); From 1daf4a2a3ba59e0ca2a88d5a5fcb18725bc70fc1 Mon Sep 17 00:00:00 2001 From: zeapsu Date: Thu, 14 May 2026 01:24:17 -0700 Subject: [PATCH 3/7] feat: animate Tome Dashboard card reordering Replace the basic drag-and-drop behavior with pointer-driven card movement, a floating drag ghost, nearest-slot reordering, and FLIP-style slide animations for surrounding cards. --- designs/tome-dashboard-proposal.html | 224 +++++++++++++++++++++------ 1 file changed, 175 insertions(+), 49 deletions(-) diff --git a/designs/tome-dashboard-proposal.html b/designs/tome-dashboard-proposal.html index 20cc0c8..be0927d 100644 --- a/designs/tome-dashboard-proposal.html +++ b/designs/tome-dashboard-proposal.html @@ -281,9 +281,20 @@ user-select: none; } .card:hover { transform: translateY(-2px); background: color-mix(in srgb, var(--surface-container-high) 76%, transparent); box-shadow: inset 0 0 0 1px rgba(173,198,255,.18), 0 8px 24px rgba(0,0,0,.22); } - .card.is-dragging { opacity: .38; transform: scale(.985); } - .card.drag-over { box-shadow: inset 0 0 0 1px rgba(173,198,255,.38), 0 0 0 4px rgba(173,198,255,.08); } + .card.is-dragging { opacity: .32; transform: scale(.985); } + .card.drop-target { box-shadow: inset 0 0 0 1px rgba(173,198,255,.38), 0 0 0 4px rgba(173,198,255,.08); } .card.is-resizing { box-shadow: inset 0 0 0 1px rgba(255,183,134,.42), 0 0 0 4px rgba(255,183,134,.08); } + .drag-ghost { + position: fixed; + z-index: 50; + pointer-events: none; + opacity: .94; + transform: translate3d(0,0,0) scale(1.02); + box-shadow: 0 22px 70px rgba(0,0,0,.52), 0 0 0 1px rgba(173,198,255,.26), 0 0 80px rgba(173,198,255,.10); + filter: saturate(1.08); + transition: box-shadow .16s ease, opacity .16s ease; + } + .grid-settling .card:not(.is-dragging) { will-change: transform; } .card.compact .source-list, .card.compact .quote, .card.compact .study-split { display: none; } .card.compact .action-row { margin-top: 10px; } .card.wide { --col-span: 3; --row-span: 2; } @@ -471,7 +482,7 @@

Command bar job

Try the prototype

    -
  • Drag a card by its handle to rearrange the Dashboard.
  • +
  • Drag a card by its handle to rearrange the Dashboard. Nearby cards slide into the available slot as you move.
  • Drag the corner grip to resize a card across the responsive grid.
  • Click card content to switch focus views.
  • Click the compact command bar or press /Ctrl + K.
  • @@ -522,7 +533,7 @@

    Weak Measurement in BECs

    - ⋮⋮ + ⋮⋮ 3×3
    Grounding

    Sources

    Current
    @@ -536,7 +547,7 @@

    Weak Measurement in BECs

    - ⋮⋮ + ⋮⋮ 1×2
    Recent thread

    Chat

    Resume
    @@ -546,7 +557,7 @@

    Weak Measurement in BECs

    - ⋮⋮ + ⋮⋮ 2×2
    Synthesis artifact

    Report

    Stale
    @@ -555,7 +566,7 @@

    Weak Measurement in BECs

    - ⋮⋮ + ⋮⋮ 2×2
    Study loop

    Quiz

    82% last score
    @@ -565,7 +576,7 @@

    Weak Measurement in BECs

    - ⋮⋮ + ⋮⋮ 2×2
    Review deck

    Flashcards

    38 cards
    @@ -577,7 +588,7 @@

    Weak Measurement in BECs

    - ⋮⋮ + ⋮⋮ 2×2
    Listen mode

    Audio Review

    Missing
    @@ -687,9 +698,11 @@

    Focused View

    const grid = document.getElementById('dashboardGrid'); const resetLayout = document.getElementById('resetLayout'); - const layoutKey = 'sage-dashboard-layout-v2'; - let draggedCard = null; + const layoutKey = 'sage-dashboard-layout-v3'; + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; let resizing = null; + let dragState = null; + let suppressClick = false; const defaults = Array.from(grid.querySelectorAll('.card')).map(card => ({ id: card.dataset.widget, @@ -726,57 +739,166 @@

    Focused View

    }); } + function animateLayout(mutator) { + if (reduceMotion) { + mutator(); + return; + } + const cards = Array.from(grid.querySelectorAll('.card')); + const first = new Map(cards.map(card => [card, card.getBoundingClientRect()])); + mutator(); + grid.classList.add('grid-settling'); + cards.forEach(card => { + if (dragState && card === dragState.card) return; + const before = first.get(card); + const after = card.getBoundingClientRect(); + const dx = before.left - after.left; + const dy = before.top - after.top; + if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return; + card.animate([ + { transform: `translate(${dx}px, ${dy}px)` }, + { transform: 'translate(0, 0)' } + ], { + duration: 260, + easing: 'cubic-bezier(.2,.8,.2,1)', + }); + }); + window.setTimeout(() => grid.classList.remove('grid-settling'), 280); + } + function resetLayoutToDefault() { localStorage.removeItem(layoutKey); - defaults.forEach(item => { - const card = grid.querySelector(`[data-widget="${item.id}"]`); - card.style.setProperty('--col-span', item.col); - card.style.setProperty('--row-span', item.row); - grid.appendChild(card); - updateCardChrome(card); + animateLayout(() => { + defaults.forEach(item => { + const card = grid.querySelector(`[data-widget="${item.id}"]`); + card.style.setProperty('--col-span', item.col); + card.style.setProperty('--row-span', item.row); + grid.appendChild(card); + updateCardChrome(card); + }); }); } + function createGhost(card, rect) { + const ghost = card.cloneNode(true); + ghost.classList.add('drag-ghost'); + ghost.classList.remove('is-dragging', 'drop-target'); + ghost.removeAttribute('id'); + ghost.style.width = `${rect.width}px`; + ghost.style.height = `${rect.height}px`; + ghost.style.left = `${rect.left}px`; + ghost.style.top = `${rect.top}px`; + ghost.style.gridColumn = 'auto'; + ghost.style.gridRow = 'auto'; + document.body.appendChild(ghost); + return ghost; + } + + function moveGhost(x, y) { + if (!dragState) return; + dragState.ghost.style.left = `${x - dragState.offsetX}px`; + dragState.ghost.style.top = `${y - dragState.offsetY}px`; + } + + function nearestCard(x, y) { + const candidates = Array.from(grid.querySelectorAll('.card')).filter(card => card !== dragState.card); + let best = null; + let bestScore = Infinity; + candidates.forEach(card => { + const rect = card.getBoundingClientRect(); + const inside = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + const score = Math.hypot(x - cx, y - cy) - (inside ? 1000 : 0); + if (score < bestScore) { + bestScore = score; + best = card; + } + }); + return best; + } + + function reorderAround(target, x) { + if (!target || !dragState || target === dragState.card || target === dragState.lastTarget) return; + const cards = Array.from(grid.querySelectorAll('.card')); + const from = cards.indexOf(dragState.card); + const to = cards.indexOf(target); + const targetRect = target.getBoundingClientRect(); + const afterTarget = x > targetRect.left + targetRect.width / 2; + dragState.lastTarget = target; + grid.querySelectorAll('.drop-target').forEach(el => el.classList.remove('drop-target')); + target.classList.add('drop-target'); + animateLayout(() => { + if (afterTarget) { + grid.insertBefore(dragState.card, target.nextSibling); + } else { + grid.insertBefore(dragState.card, target); + } + }); + } + + function startCardDrag(card, handle, e) { + if (resizing || window.innerWidth <= 720) return; + e.preventDefault(); + e.stopPropagation(); + const rect = card.getBoundingClientRect(); + dragState = { + card, + handle, + ghost: createGhost(card, rect), + offsetX: e.clientX - rect.left, + offsetY: e.clientY - rect.top, + startX: e.clientX, + startY: e.clientY, + lastTarget: null, + moved: false, + }; + card.classList.add('is-dragging'); + handle.setPointerCapture(e.pointerId); + moveGhost(e.clientX, e.clientY); + } + + function moveCardDrag(e) { + if (!dragState) return; + const distance = Math.hypot(e.clientX - dragState.startX, e.clientY - dragState.startY); + dragState.moved = dragState.moved || distance > 4; + moveGhost(e.clientX, e.clientY); + const target = nearestCard(e.clientX, e.clientY); + reorderAround(target, e.clientX); + } + + function endCardDrag() { + if (!dragState) return; + const { card, ghost, moved } = dragState; + grid.querySelectorAll('.drop-target').forEach(el => el.classList.remove('drop-target')); + const finalRect = card.getBoundingClientRect(); + ghost.animate([ + { left: ghost.style.left, top: ghost.style.top, opacity: .94, transform: 'scale(1.02)' }, + { left: `${finalRect.left}px`, top: `${finalRect.top}px`, opacity: .18, transform: 'scale(.99)' } + ], { + duration: reduceMotion ? 1 : 180, + easing: 'cubic-bezier(.2,.8,.2,1)', + }).finished.finally(() => ghost.remove()); + card.classList.remove('is-dragging'); + saveLayout(); + suppressClick = moved; + window.setTimeout(() => { suppressClick = false; }, 60); + dragState = null; + } + grid.querySelectorAll('.card').forEach(card => { const handle = card.querySelector('.drag-handle'); const grip = card.querySelector('.resize-grip'); card.addEventListener('click', (e) => { - if (e.target.closest('.drag-handle, .resize-grip, .ghost-action')) return; + if (suppressClick || e.target.closest('.drag-handle, .resize-grip, .ghost-action')) return; showFocus(card.dataset.focus); }); card.addEventListener('keydown', (e) => { if (e.key === 'Enter') showFocus(card.dataset.focus); }); - handle.addEventListener('dragstart', (e) => { - draggedCard = card; - card.classList.add('is-dragging'); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', card.dataset.widget); - }); - handle.addEventListener('dragend', () => { - card.classList.remove('is-dragging'); - grid.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); - draggedCard = null; - saveLayout(); - }); - card.addEventListener('dragover', (e) => { - if (!draggedCard || draggedCard === card) return; - e.preventDefault(); - card.classList.add('drag-over'); - }); - card.addEventListener('dragleave', () => card.classList.remove('drag-over')); - card.addEventListener('drop', (e) => { - e.preventDefault(); - card.classList.remove('drag-over'); - if (!draggedCard || draggedCard === card) return; - const cards = Array.from(grid.querySelectorAll('.card')); - const from = cards.indexOf(draggedCard); - const to = cards.indexOf(card); - grid.insertBefore(draggedCard, from < to ? card.nextSibling : card); - saveLayout(); - }); + handle.addEventListener('pointerdown', (e) => startCardDrag(card, handle, e)); grip.addEventListener('pointerdown', (e) => { e.preventDefault(); @@ -797,17 +919,21 @@

    Focused View

    }); document.addEventListener('pointermove', (e) => { + if (dragState) moveCardDrag(e); if (!resizing) return; const dx = Math.round((e.clientX - resizing.startX) / Math.max(resizing.cellW, 80)); const dy = Math.round((e.clientY - resizing.startY) / Math.max(resizing.cellH, 80)); const nextCol = Math.max(1, Math.min(4, resizing.startCol + dx)); const nextRow = Math.max(1, Math.min(4, resizing.startRow + dy)); - resizing.card.style.setProperty('--col-span', nextCol); - resizing.card.style.setProperty('--row-span', nextRow); - updateCardChrome(resizing.card); + animateLayout(() => { + resizing.card.style.setProperty('--col-span', nextCol); + resizing.card.style.setProperty('--row-span', nextRow); + updateCardChrome(resizing.card); + }); }); document.addEventListener('pointerup', () => { + if (dragState) endCardDrag(); if (!resizing) return; resizing.card.classList.remove('is-resizing'); saveLayout(); From e71a9c928d069165ca58c334b5d59ad6f3413744 Mon Sep 17 00:00:00 2001 From: zeapsu Date: Mon, 25 May 2026 10:27:16 -0700 Subject: [PATCH 4/7] feat: add redesigned Tome Home surface --- frontend/src/app/page.tsx | 414 ++++++++++++++++++++++++++++++-------- 1 file changed, 334 insertions(+), 80 deletions(-) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 5bda21a..bd099dc 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,8 +1,9 @@ "use client"; import { useState } from "react"; +import type { FormEvent, KeyboardEvent, ReactNode } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import CommandBar from "@/components/CommandBar"; +import type { Transition, Variants } from "framer-motion"; import QuizWidget, { SAMPLE_QUESTIONS } from "@/components/QuizWidget"; import FlashcardWidget, { SAMPLE_FLASHCARDS } from "@/components/FlashcardWidget"; import AudioPlayerWidget, { SAMPLE_TRACK } from "@/components/AudioPlayerWidget"; @@ -11,7 +12,25 @@ import HistoryPanel from "@/components/HistoryPanel"; import TomeSelector from "@/components/TomeSelector"; import ChatWidget from "@/components/ChatWidget"; -type ViewState = "idle" | "quiz" | "flashcards" | "audio" | "report" | "history" | "tomes" | "chat"; +type ViewState = + | "idle" + | "dashboard" + | "quiz" + | "flashcards" + | "audio" + | "report" + | "history" + | "tomes" + | "chat"; + +const capabilities: Array<{ label: string; icon: string; view: ViewState }> = [ + { label: "Sources", icon: "▤", view: "tomes" }, + { label: "Report", icon: "✎", view: "report" }, + { label: "Quiz", icon: "?", view: "quiz" }, + { label: "Flashcards", icon: "◇", view: "flashcards" }, + { label: "Audio", icon: "♪", view: "audio" }, + { label: "Chat", icon: "⌁", view: "chat" }, +]; export default function Home() { const [viewState, setViewState] = useState("idle"); @@ -29,7 +48,9 @@ export default function Home() { setViewState("report"); } else if (lower.includes("history")) { setViewState("history"); - } else if (lower.includes("tome") || lower.includes("collection") || lower.includes("library")) { + } else if (lower.includes("dashboard") || lower.includes("status")) { + setViewState("dashboard"); + } else if (lower.includes("tome") || lower.includes("collection") || lower.includes("library") || lower.includes("source")) { setViewState("tomes"); } else { setViewState("chat"); @@ -40,101 +61,334 @@ export default function Home() { setViewState("idle"); }; - const cardVariants = { + const cardVariants: Variants = { initial: { opacity: 0, y: 16, scale: 0.97 }, animate: { opacity: 1, y: 0, scale: 1 }, exit: { opacity: 0, y: -10, scale: 0.97 }, }; - const cardTransition = { duration: 0.28, ease: [0.25, 0.46, 0.45, 0.94] }; + const cardTransition: Transition = { duration: 0.28, ease: [0.25, 0.46, 0.45, 0.94] }; return ( -
    - {/* Ambient glow */} -
    - - {/* Command bar — always visible, shifts up when content appears */} -
    +
    +
    +
    + + + + {viewState === "idle" && ( + + + + )} + + {viewState === "dashboard" && ( + + + + )} + + {viewState === "quiz" && ( + + + + )} + + {viewState === "flashcards" && ( + + + + )} + + {viewState === "audio" && ( + + + + )} + + {viewState === "report" && ( + + + + )} + + {viewState === "history" && ( + + + + )} + + {viewState === "tomes" && ( + + + + )} + + {viewState === "chat" && ( + + + + )} + +
    +
    + ); +} + +function TopBar({ viewState, onNavigate }: { viewState: ViewState; onNavigate: (view: ViewState) => void }) { + return ( +
    + + + +
    + ); +} + +function TomeHome({ onSubmit, onNavigate }: { onSubmit: (text: string) => void; onNavigate: (view: ViewState) => void }) { + const [prompt, setPrompt] = useState(""); + + const submitPrompt = () => { + const trimmed = prompt.trim(); + if (!trimmed) return; + onSubmit(trimmed); + setPrompt(""); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + submitPrompt(); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + submitPrompt(); + } + }; + + return ( +
    +
    +
    + + Default after Tome selection +
    + +

    + Good afternoon, zeap. +

    +

    + A calm Tome-aware command center: ask Sage, reveal common capabilities, and jump into focused work without opening on a heavy dashboard. +

    + + + +
    + + +
    + +
    + + + + + + +
    + + +
    + + + +
    +
    +
    +

    Expanded Tome Dashboard

    +

    This still exists, but it should become a secondary state for inspecting artifact freshness, output status, and generated work. The dashboard is useful. It just should not be the first impression.

    + +
    +
    Sources12 PDFs and notes indexedCurrent
    +
    ReportGenerated before 3 new notes were addedStale
    +
    Quiz5-question test ready from selected TomeReady
    +
    AudioNo current audio review for this TomeMissing
    +
    + +
    +
    Focused component viewReport → Deep Work
    +
    +
    +
    +
    +

    Clicking a capability chip routes into the full component: report reader, quiz flow, flashcard review, audio player, source manager, or chat.

    +
    +
    +
    + + +
    +
    + +
    +
    +
    +

    Recommendation

    +

    The dashboard direction is conceptually right, but visually too state-heavy as the default. Sage should open like a calm AI research companion, not like an admin dashboard.

    + +
    + Great Sage verdict: + Default to Tome Home. Preserve Tome Dashboard as a secondary expanded state for artifact status and management. +
    + +
    +
    WhyThe screenshot reference lowers cognitive load while still teaching the user what the product can do.
    +
    RiskIf too minimal, users may miss artifact status. Solve that with subtle freshness badges or an explicit “View dashboard” affordance.
    +
    MVPImplement Tome Home first: Tome summary, composer, capability chips, focus routing, and a dashboard link.
    +
    +
    + + +
    +
    + + + + + diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index bd099dc..abf1f24 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -38,20 +38,22 @@ export default function Home() { const handleSubmit = (text: string) => { const lower = text.toLowerCase().trim(); - if (lower.includes("quiz")) { + if (lower === "/quiz") { setViewState("quiz"); - } else if (lower.includes("flashcard") || lower.includes("flash card")) { + } else if (lower === "/flashcards" || lower === "/flashcard") { setViewState("flashcards"); - } else if (lower.includes("audio") || lower.includes("listen") || lower.includes("podcast")) { + } else if (lower === "/audio") { setViewState("audio"); - } else if (lower.includes("report") || lower.includes("study guide") || lower.includes("summary")) { + } else if (lower === "/report") { setViewState("report"); - } else if (lower.includes("history")) { + } else if (lower === "/history") { setViewState("history"); - } else if (lower.includes("dashboard") || lower.includes("status")) { + } else if (lower === "/dashboard") { setViewState("dashboard"); - } else if (lower.includes("tome") || lower.includes("collection") || lower.includes("library") || lower.includes("source")) { + } else if (lower === "/sources" || lower === "/tomes") { setViewState("tomes"); + } else if (lower === "/chat") { + setViewState("chat"); } else { setViewState("chat"); } @@ -90,25 +92,25 @@ export default function Home() { )} {viewState === "quiz" && ( - + )} {viewState === "flashcards" && ( - + )} {viewState === "audio" && ( - + )} {viewState === "report" && ( - + )} @@ -186,23 +188,23 @@ function TomeHome({ onSubmit, onNavigate }: { onSubmit: (text: string) => void;
    - Default after Tome selection + Tome ready

    Good afternoon, zeap.

    - A calm Tome-aware command center: ask Sage, reveal common capabilities, and jump into focused work without opening on a heavy dashboard. + Ask questions, generate study materials, manage sources, or jump into focused work for this Tome.

    void }
    -

    Secondary overview

    +

    Tome overview

    Expanded Tome Dashboard

    - This dashboard stays available for artifact freshness, output status, and generated work management. It is useful context, just not the first impression. + Review sources, generated artifacts, freshness, and recent work for this Tome.

    @@ -292,12 +294,12 @@ function TomeDashboard({ onNavigate }: { onNavigate: (view: ViewState) => void }
    @@ -309,17 +311,20 @@ function FocusShell({ variants, transition, onBack, + previewNote, children, }: { keyName: string; variants: Variants; transition: Transition; onBack: () => void; + previewNote?: string; children: ReactNode; }) { return (
    + {previewNote && {previewNote}} {children}
    @@ -327,6 +332,14 @@ function FocusShell({ ); } +function PreviewNote({ children }: { children: ReactNode }) { + return ( +
    + {children} +
    + ); +} + function NavPill({ active, onClick, children }: { active: boolean; onClick: () => void; children: ReactNode }) { return ( void }

    Review sources, generated artifacts, freshness, and recent work for this Tome.

    +

    + Preview statuses shown until Tome selection and generated artifact state are wired. +

    - onNavigate("tomes")} /> - onNavigate("report")} /> - onNavigate("quiz")} /> - onNavigate("audio")} /> + onNavigate("tomes")} /> + onNavigate("report")} /> + onNavigate("quiz")} /> + onNavigate("audio")} />