dreamstack/examples/dashboard.html
enzotar fcf2639d9b feat: Phase 2+3 — effects, streams, springs, search + dashboard
Phase 2 — Effect System & Streams:
- Algebraic effects (perform/handle/handleAsync) for composable,
  testable side-effects with swappable handlers
- Stream engine with map, filter, debounce, throttle, distinct,
  scan, flatMap, merge, fromEvent, fromSignal, fromPromise
- Search-with-autocomplete example: debounce + flatMap + effects

Phase 3 — Spring Physics:
- RK4 integrator with fixed substep and sleep-when-idle scheduler
- Springs are signals — anything that reads spring.value auto-updates
- Dashboard example: spring-animated sidebar, staggered card entrance,
  draggable spring ball with configurable stiffness/damping/mass

Runtime evolution: v0.1 (signals) → v0.2 (+ effects + streams) → v0.3 (+ springs)
2026-02-25 00:13:09 -08:00

746 lines
No EOL
26 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DreamStack Dashboard — Springs + Layout</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, system-ui, sans-serif;
background: #0a0a0f;
color: #e2e8f0;
min-height: 100vh;
overflow: hidden;
}
.dashboard {
display: grid;
grid-template-columns: auto 1fr;
height: 100vh;
}
.sidebar {
background: rgba(255, 255, 255, 0.02);
border-right: 1px solid rgba(255, 255, 255, 0.06);
overflow: hidden;
transition: none;
/* springs handle animation */
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
gap: 0.75rem;
}
.logo {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.9rem;
flex-shrink: 0;
}
.logo-text {
font-weight: 600;
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
}
.sidebar-nav {
flex: 1;
padding: 0.5rem;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 10px;
cursor: pointer;
color: rgba(255, 255, 255, 0.4);
transition: all 0.15s;
white-space: nowrap;
overflow: hidden;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.7);
}
.nav-item.active {
background: rgba(99, 102, 241, 0.12);
color: #a78bfa;
}
.nav-icon {
font-size: 1.1rem;
flex-shrink: 0;
}
.nav-label {
overflow: hidden;
}
.toggle-btn {
margin: 0.5rem;
padding: 0.75rem;
border: none;
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.4);
border-radius: 10px;
cursor: pointer;
font-size: 1.1rem;
transition: all 0.15s;
}
.toggle-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: white;
}
.main-content {
padding: 2rem;
overflow-y: auto;
}
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.page-title {
font-size: 1.5rem;
font-weight: 600;
}
.spring-info {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.03);
padding: 0.5rem 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16px;
padding: 1.5rem;
cursor: pointer;
transition: none;
/* springs handle this */
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.05), rgba(139, 92, 246, 0.05));
opacity: 0;
transition: opacity 0.3s;
}
.card:hover::before {
opacity: 1;
}
.card-icon {
font-size: 2rem;
margin-bottom: 0.75rem;
}
.card-title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.25rem;
}
.card-value {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #6366f1, #8b5cf6, #c084fc);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.card-change {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.35);
margin-top: 0.25rem;
}
.card-change.up {
color: #22c55e;
}
.card-change.down {
color: #ef4444;
}
.draggable-area {
margin-top: 2rem;
}
.drag-title {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.35);
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
.spring-playground {
position: relative;
height: 300px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16px;
overflow: hidden;
}
.spring-ball {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
position: absolute;
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.3);
user-select: none;
}
.spring-ball:active {
cursor: grabbing;
}
.spring-trail {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(99, 102, 241, 0.15);
pointer-events: none;
}
.spring-controls {
display: flex;
gap: 1rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.35);
min-width: 60px;
}
.control-slider {
-webkit-appearance: none;
width: 120px;
height: 4px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.1);
outline: none;
}
.control-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #8b5cf6;
cursor: pointer;
}
.control-value {
font-size: 0.75rem;
color: #8b5cf6;
min-width: 30px;
text-align: right;
font-family: 'JetBrains Mono', monospace;
}
.powered-by {
text-align: center;
margin-top: 2rem;
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.12);
}
.powered-by span {
color: rgba(139, 92, 246, 0.3);
}
</style>
</head>
<body>
<div id="app"></div>
<script>
// ═══════════════════════════════════════════════════════════
// DreamStack Runtime v0.3.0 — Signals + Effects + Springs
// ═══════════════════════════════════════════════════════════
const DS = (() => {
let currentEffect = null;
let batchDepth = 0;
let pendingEffects = new Set();
class Signal {
constructor(val) { this._value = val; this._subs = new Set(); }
get value() { if (currentEffect) this._subs.add(currentEffect); return this._value; }
set value(v) {
if (this._value === v) return;
this._value = v;
if (batchDepth > 0) for (const s of this._subs) pendingEffects.add(s);
else for (const s of [...this._subs]) s._run();
}
}
class Derived {
constructor(fn) {
this._fn = fn; this._value = undefined; this._subs = new Set();
this._eff = new Effect(() => {
const old = this._value; this._value = this._fn();
if (old !== this._value) {
if (batchDepth > 0) for (const s of this._subs) pendingEffects.add(s);
else for (const s of [...this._subs]) s._run();
}
});
this._eff._run();
}
get value() { if (currentEffect) this._subs.add(currentEffect); return this._value; }
}
class Effect {
constructor(fn) { this._fn = fn; this._disposed = false; }
_run() {
if (this._disposed) return;
const prev = currentEffect; currentEffect = this;
try { this._fn(); } finally { currentEffect = prev; }
}
dispose() { this._disposed = true; }
}
const signal = v => new Signal(v);
const derived = fn => new Derived(fn);
const effect = fn => { const e = new Effect(fn); e._run(); return e; };
const batch = fn => {
batchDepth++;
try { fn(); } finally { batchDepth--; if (batchDepth === 0) flush(); }
};
const flush = () => {
const effs = [...pendingEffects]; pendingEffects.clear();
for (const e of effs) e._run();
};
// ═══════════════════════════════════════════════════════
// Spring Physics Engine
// ═══════════════════════════════════════════════════════
// RK4 integrator, interruptible, integrates with signals
const activeSpringSet = new Set();
let rafId = null;
let lastTime = 0;
class Spring {
constructor({ value = 0, target = 0, stiffness = 170, damping = 26, mass = 1 } = {}) {
this._signal = signal(value);
this._velocity = 0;
this._target = target;
this.stiffness = stiffness;
this.damping = damping;
this.mass = mass;
this._settled = true;
}
get value() { return this._signal.value; }
set value(v) { this._signal.value = v; }
get target() { return this._target; }
set target(t) {
this._target = t;
this._settled = false;
activeSpringSet.add(this);
startLoop();
}
// Instantly set position (no animation)
set(v) {
this._signal.value = v;
this._target = v;
this._velocity = 0;
this._settled = true;
activeSpringSet.delete(this);
}
// RK4 step
_step(dt) {
const pos = this._signal._value;
const vel = this._velocity;
const k = this.stiffness;
const d = this.damping;
const m = this.mass;
// RK4 derivatives
const accel = (p, v) => (-k * (p - this._target) - d * v) / m;
const k1v = accel(pos, vel);
const k1p = vel;
const k2v = accel(pos + k1p * dt / 2, vel + k1v * dt / 2);
const k2p = vel + k1v * dt / 2;
const k3v = accel(pos + k2p * dt / 2, vel + k2v * dt / 2);
const k3p = vel + k2v * dt / 2;
const k4v = accel(pos + k3p * dt, vel + k3v * dt);
const k4p = vel + k3v * dt;
const newVel = vel + (dt / 6) * (k1v + 2 * k2v + 2 * k3v + k4v);
const newPos = pos + (dt / 6) * (k1p + 2 * k2p + 2 * k3p + k4p);
this._velocity = newVel;
this._signal.value = newPos;
// Check if settled
if (Math.abs(newVel) < 0.01 && Math.abs(newPos - this._target) < 0.01) {
this._signal.value = this._target;
this._velocity = 0;
this._settled = true;
activeSpringSet.delete(this);
}
}
}
function startLoop() {
if (rafId !== null) return;
lastTime = performance.now();
rafId = requestAnimationFrame(loop);
}
function loop(now) {
const dt = Math.min((now - lastTime) / 1000, 0.064); // clamp to ~15fps min
lastTime = now;
batch(() => {
for (const spring of activeSpringSet) {
// Fixed timestep substeps for stability
const steps = Math.ceil(dt / (1 / 120));
const subDt = dt / steps;
for (let i = 0; i < steps; i++) {
spring._step(subDt);
}
}
});
if (activeSpringSet.size > 0) {
rafId = requestAnimationFrame(loop);
} else {
rafId = null; // Sleep when no springs are active
}
}
function spring(opts) { return new Spring(opts); }
return { signal, derived, effect, batch, flush, spring, Spring };
})();
// ═══════════════════════════════════════════════════════════
// Dashboard App
// ═══════════════════════════════════════════════════════════
(() => {
const app = document.getElementById('app');
// ── Signals ──
const sidebarExpanded = DS.signal(true);
const activePage = DS.signal('dashboard');
// ── Springs ──
const sidebarWidth = DS.spring({ value: 240, target: 240, stiffness: 200, damping: 24 });
const sidebarOpacity = DS.spring({ value: 1, target: 1, stiffness: 200, damping: 24 });
// Card animations (staggered)
const cardScales = [
DS.spring({ value: 0, target: 1, stiffness: 300, damping: 22 }),
DS.spring({ value: 0, target: 1, stiffness: 300, damping: 22 }),
DS.spring({ value: 0, target: 1, stiffness: 300, damping: 22 }),
DS.spring({ value: 0, target: 1, stiffness: 300, damping: 22 }),
];
// Draggable ball
const ballX = DS.spring({ value: 150, target: 150, stiffness: 170, damping: 26 });
const ballY = DS.spring({ value: 120, target: 120, stiffness: 170, damping: 26 });
// Stagger card entrance
cardScales.forEach((s, i) => {
setTimeout(() => { s.target = 1; }, i * 100);
});
// ── Dashboard layout ──
const dashboard = document.createElement('div');
dashboard.className = 'dashboard';
app.appendChild(dashboard);
// Sidebar
const sidebar = document.createElement('div');
sidebar.className = 'sidebar';
dashboard.appendChild(sidebar);
DS.effect(() => {
sidebar.style.width = sidebarWidth.value + 'px';
});
// Sidebar header
const sidebarHeader = document.createElement('div');
sidebarHeader.className = 'sidebar-header';
sidebarHeader.innerHTML = '<div class="logo">DS</div><div class="logo-text">DreamStack</div>';
sidebar.appendChild(sidebarHeader);
DS.effect(() => {
const logoText = sidebarHeader.querySelector('.logo-text');
logoText.style.opacity = sidebarOpacity.value;
});
// Nav items
const nav = document.createElement('div');
nav.className = 'sidebar-nav';
sidebar.appendChild(nav);
const navItems = [
{ icon: '📊', label: 'Dashboard', id: 'dashboard' },
{ icon: '📈', label: 'Analytics', id: 'analytics' },
{ icon: '⚡', label: 'Signals', id: 'signals' },
{ icon: '🎯', label: 'Effects', id: 'effects' },
{ icon: '🌊', label: 'Streams', id: 'streams' },
{ icon: '🎨', label: 'Springs', id: 'springs' },
];
navItems.forEach(item => {
const el = document.createElement('div');
el.className = 'nav-item';
el.innerHTML = `<span class="nav-icon">${item.icon}</span><span class="nav-label">${item.label}</span>`;
el.addEventListener('click', () => { activePage.value = item.id; });
DS.effect(() => {
el.classList.toggle('active', activePage.value === item.id);
});
DS.effect(() => {
el.querySelector('.nav-label').style.opacity = sidebarOpacity.value;
});
nav.appendChild(el);
});
// Toggle button
const toggleBtn = document.createElement('button');
toggleBtn.className = 'toggle-btn';
toggleBtn.textContent = '☰';
toggleBtn.addEventListener('click', () => {
const expanded = !sidebarExpanded.value;
sidebarExpanded.value = expanded;
sidebarWidth.target = expanded ? 240 : 64;
sidebarOpacity.target = expanded ? 1 : 0;
});
sidebar.appendChild(toggleBtn);
// Main content
const main = document.createElement('div');
main.className = 'main-content';
dashboard.appendChild(main);
// Header
const mainHeader = document.createElement('div');
mainHeader.className = 'main-header';
const pageTitle = document.createElement('h2');
pageTitle.className = 'page-title';
const springInfo = document.createElement('div');
springInfo.className = 'spring-info';
mainHeader.appendChild(pageTitle);
mainHeader.appendChild(springInfo);
main.appendChild(mainHeader);
DS.effect(() => { pageTitle.textContent = activePage.value.charAt(0).toUpperCase() + activePage.value.slice(1); });
DS.effect(() => {
springInfo.textContent = `sidebar: ${sidebarWidth.value.toFixed(0)}px | ball: (${ballX.value.toFixed(0)}, ${ballY.value.toFixed(0)})`;
});
// Stats cards
const cardsGrid = document.createElement('div');
cardsGrid.className = 'cards-grid';
main.appendChild(cardsGrid);
const stats = [
{ icon: '⚡', title: 'Signals', value: '1,247', change: '+12%', up: true },
{ icon: '🔄', title: 'Updates/s', value: '60', change: '0ms glitch', up: true },
{ icon: '📦', title: 'Bundle', value: '6.8KB', change: 'No VDOM', up: true },
{ icon: '🎯', title: 'Effects', value: '3', change: 'All handled', up: true },
];
stats.forEach((stat, i) => {
const card = document.createElement('div');
card.className = 'card';
DS.effect(() => {
const s = cardScales[i].value;
card.style.transform = `scale(${s})`;
card.style.opacity = s;
});
card.innerHTML = `
<div class="card-icon">${stat.icon}</div>
<div class="card-title">${stat.title}</div>
<div class="card-value">${stat.value}</div>
<div class="card-change ${stat.up ? 'up' : 'down'}">${stat.change}</div>
`;
cardsGrid.appendChild(card);
});
// Draggable spring area
const dragArea = document.createElement('div');
dragArea.className = 'draggable-area';
const dragTitle = document.createElement('div');
dragTitle.className = 'drag-title';
dragTitle.textContent = '🎾 Spring Physics — Drag the ball';
dragArea.appendChild(dragTitle);
main.appendChild(dragArea);
const playground = document.createElement('div');
playground.className = 'spring-playground';
dragArea.appendChild(playground);
// Ball
const ball = document.createElement('div');
ball.className = 'spring-ball';
ball.textContent = '🎾';
playground.appendChild(ball);
DS.effect(() => {
ball.style.left = (ballX.value - 30) + 'px';
ball.style.top = (ballY.value - 30) + 'px';
});
// Drag handling
let isDragging = false;
ball.addEventListener('mousedown', (e) => {
isDragging = true;
e.preventDefault();
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const rect = playground.getBoundingClientRect();
const x = Math.max(30, Math.min(rect.width - 30, e.clientX - rect.left));
const y = Math.max(30, Math.min(rect.height - 30, e.clientY - rect.top));
// Direct set while dragging (no spring)
ballX.set(x);
ballY.set(y);
});
window.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
// Spring back to center
const rect = playground.getBoundingClientRect();
ballX.target = rect.width / 2;
ballY.target = rect.height / 2;
}
});
// Click anywhere in playground to send ball there
playground.addEventListener('click', (e) => {
if (isDragging) return;
const rect = playground.getBoundingClientRect();
ballX.target = e.clientX - rect.left;
ballY.target = e.clientY - rect.top;
});
// Spring controls
const controls = document.createElement('div');
controls.className = 'spring-controls';
dragArea.appendChild(controls);
function makeControl(label, min, max, initial, onChange) {
const group = document.createElement('div');
group.className = 'control-group';
const lbl = document.createElement('span');
lbl.className = 'control-label';
lbl.textContent = label;
const slider = document.createElement('input');
slider.type = 'range';
slider.className = 'control-slider';
slider.min = min;
slider.max = max;
slider.value = initial;
const val = document.createElement('span');
val.className = 'control-value';
val.textContent = initial;
slider.addEventListener('input', () => {
val.textContent = slider.value;
onChange(Number(slider.value));
});
group.appendChild(lbl);
group.appendChild(slider);
group.appendChild(val);
controls.appendChild(group);
}
makeControl('Stiffness', 10, 500, 170, (v) => { ballX.stiffness = v; ballY.stiffness = v; });
makeControl('Damping', 1, 60, 26, (v) => { ballX.damping = v; ballY.damping = v; });
makeControl('Mass', 0.5, 10, 1, (v) => { ballX.mass = v; ballY.mass = v; });
// Powered by
const powered = document.createElement('div');
powered.className = 'powered-by';
powered.innerHTML = 'Built with <span>DreamStack</span> — springs + signals + zero re-renders';
main.appendChild(powered);
})();
</script>
</body>
</html>