mirror of
https://github.com/himanshudongre/smriti.git
synced 2026-08-28 05:14:59 +00:00
Some checks failed
Deploy website to GitHub Pages / deploy (push) Has been cancelled
This pass replaces the v2 visual language with a more handcrafted, signature-feeling system. Structure and copy are preserved; only the visual + interaction layer is reworked. Visual metaphor --------------- The dominant motif is a braid of two agent strands (claude-code in ember, codex in indigo) weaving over and under each other through hexagonal "checkpoint stones." The stones carry short hashes; the rightmost is "wet" — currently being written — and gets a soft ember pulse. Braiding has cultural weight (DNA, rope, river confluence) and reads as "structured intertwining," which is exactly what shared reasoning state is. The same motif scales down to a taut, stretched braid in the drift section. Palette ------- Refined to feel like a signature, not a template: - Deeper cool-black `#070710` background (warm accents pop harder against blue-black than against grey-black). - Ember `#ffb663` — warmer and more saturated than v2's `#f5c773`; campfire ember, not corporate gold. This is the Smriti color. - Indigo `#7c8cff` — saturated, fresh; not v2's washed `#8ba0c7`. - Aurora `#7be3b0` — third pole used sparingly for matches / done states, so the page isn't just ember + indigo. - Warm off-white paper `#e9e7e2` for body text. - Subtle SVG-noise grain overlay (5% opacity, overlay blend) gives the dark surface tooth. - Three hand-placed ember-glow anchor regions (hero, centerpiece, CTA) signal hierarchy — not decorative gradient blobs. Section redesigns ----------------- - Hero — braid + 5 hexagonal stones replacing v2's circle-and-line timeline. Subtle 14s sway on the strands. The rightmost (wet) stone glows and pulses. - Missing primitive — replaced the rigid 3-column-grid + connector gutter with a typographic glossary. Mapped rows get an ember dot; unmapped rows get an indigo dot and the "no equivalent" label. Reads like a hand-typeset comparison sheet, not a spreadsheet. - HANDOFF.md vs structured — both panels now have a lined-paper background. The HANDOFF panel has a `COLLISION` (red) badge; the checkpoint panel has a `RESOLVED` (aurora) badge. The conflict line types in, gets struck through, and the resolution types in underneath on scroll-in. - What Smriti is — checkpoint render becomes a parsed structured tree with labeled field/value pairs and intent/agent tag pills. - Multi-agent coordination (centerpiece) — completely rebuilt. Three columns: claude-code panel (left) | task pool (center) | codex panel (right). Tasks are "packets" that fly from the central pool into the side panels via JS-animated transforms, with springy easing. Each agent has a working stack that populates as packets land. Plays once on scroll-in, replayable. Bonus: clicking any unclaimed packet fires it manually to whichever agent has fewer claims. - Drift — repo timeline replaced with a tensile braid stretched between the checkpoint stone and the HEAD stone. On scroll-in, the HEAD stone nudges +14px right, gets the warning color, and the "+2 commits ahead" badge fades in. - Built with Smriti — terminal panel now has traffic-light dots, a blinking caret, and ember underline animation on "67 cross-agent continuations" on scroll-in. Numbers are semantically colored (ember = main, indigo = codex, aurora = success rates). - Try it — two terminals now sit on a very subtle skew axis (like an open laptop spread); hover snaps them straight. - Brand mark — replaced the dot-grid with a hexagonal stone and two woven strand-threads. Soft ember pulse every ~6s. Motion ------ Tasteful, per-section motion treatments rather than uniform fade-up. Slow braid sway on hero. Live-typing markdown collision. Springy packet flight. Drift stone nudge. CLI caret blink. Brand mark pulse. SVG path draw-in. Honors `prefers-reduced-motion: reduce` — all animations skip, every element lands in its terminal state, carets go static. Implementation -------------- Still pure HTML + CSS + ~330 lines of vanilla JS. No framework, no build step, no webfont, no third-party requests. IntersectionObserver thresholds tuned to 0.18 so animations fire reliably on mobile (tall stacked sections were below the previous 0.35–0.45 thresholds). Existing GitHub Pages workflow uploads ./website unchanged. Mobile responsive at 980 / 720 / 420 breakpoints. Theatre stacks vertically on mobile, packets still fly to the correct positions via computed bounding rects. Nav collapses to the GitHub CTA on small screens.
425 lines
14 KiB
JavaScript
425 lines
14 KiB
JavaScript
/* =============================================================
|
|
Smriti landing page — interactions (v2.1)
|
|
No dependencies. Five responsibilities:
|
|
1. Reveal-on-scroll for .reveal sections.
|
|
2. Drift canvas — toggles .is-drifted on scroll-in (CSS handles the rest).
|
|
3. Terminal panel — toggles .is-revealed for the highlight underline.
|
|
4. Markdown surface — types the conflict line, strikes it through,
|
|
types the resolution. On scroll-in. Replayable.
|
|
5. Protocol theatre — 3-column live coordination demo. Tasks fly from
|
|
the central pool into the side agent panels as packets, settle into
|
|
work-stacks. Replay button. Click-to-fire bonus interaction.
|
|
Honors prefers-reduced-motion: skips animations and lands every element
|
|
in its terminal state.
|
|
============================================================= */
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
|
|
// ── 1. Reveal-on-scroll ────────────────────────────────────────
|
|
const revealEls = document.querySelectorAll(".reveal");
|
|
if (reduced || !("IntersectionObserver" in window)) {
|
|
revealEls.forEach((el) => el.classList.add("in"));
|
|
} else {
|
|
const revealObs = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add("in");
|
|
revealObs.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{ threshold: 0.1, rootMargin: "0px 0px -8% 0px" }
|
|
);
|
|
revealEls.forEach((el) => revealObs.observe(el));
|
|
}
|
|
|
|
// ── 2. Drift canvas toggle ────────────────────────────────────
|
|
const drift = document.querySelector("[data-drift]");
|
|
if (drift) {
|
|
if (reduced || !("IntersectionObserver" in window)) {
|
|
drift.classList.add("is-drifted");
|
|
} else {
|
|
const driftObs = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
setTimeout(() => drift.classList.add("is-drifted"), 400);
|
|
driftObs.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{ threshold: 0.18 }
|
|
);
|
|
driftObs.observe(drift);
|
|
}
|
|
}
|
|
|
|
// ── 3. Terminal reveal toggle ──────────────────────────────────
|
|
const terminal = document.querySelector("[data-terminal]");
|
|
if (terminal) {
|
|
if (reduced || !("IntersectionObserver" in window)) {
|
|
terminal.classList.add("is-revealed");
|
|
} else {
|
|
const termObs = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
terminal.classList.add("is-revealed");
|
|
termObs.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{ threshold: 0.18 }
|
|
);
|
|
termObs.observe(terminal);
|
|
}
|
|
}
|
|
|
|
// ── 4. Markdown surface — type the collision, strike, resolve ───
|
|
const mdSurface = document.querySelector("[data-md-surface]");
|
|
if (mdSurface) {
|
|
const conflict = mdSurface.querySelector("[data-md-conflict]");
|
|
const resolution = mdSurface.querySelector("[data-md-resolution]");
|
|
const conflictText = conflict ? conflict.textContent : "";
|
|
const resolutionText = resolution
|
|
? resolution.textContent.replace(/\s*$/, "")
|
|
: "";
|
|
|
|
function resetMd() {
|
|
if (conflict) {
|
|
conflict.textContent = conflictText;
|
|
conflict.classList.remove("struck");
|
|
conflict.style.opacity = "0";
|
|
}
|
|
if (resolution) {
|
|
resolution.innerHTML = '<span class="typing-caret"></span>';
|
|
resolution.style.opacity = "1";
|
|
}
|
|
}
|
|
|
|
function playMd() {
|
|
if (reduced) {
|
|
// Land in resolved state directly.
|
|
if (conflict) {
|
|
conflict.textContent = conflictText;
|
|
conflict.classList.add("struck");
|
|
conflict.style.opacity = "0.5";
|
|
}
|
|
if (resolution) {
|
|
resolution.innerHTML =
|
|
resolutionText + '<span class="typing-caret"></span>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
resetMd();
|
|
// Phase 1: fade conflict in.
|
|
setTimeout(() => {
|
|
if (conflict) {
|
|
conflict.style.transition = "opacity 350ms";
|
|
conflict.style.opacity = "1";
|
|
}
|
|
}, 250);
|
|
|
|
// Phase 2: strike it.
|
|
setTimeout(() => {
|
|
if (conflict) conflict.classList.add("struck");
|
|
}, 1400);
|
|
|
|
// Phase 3: type the resolution.
|
|
setTimeout(() => {
|
|
if (!resolution) return;
|
|
let i = 0;
|
|
const caretHtml = '<span class="typing-caret"></span>';
|
|
const tick = () => {
|
|
if (i <= resolutionText.length) {
|
|
resolution.innerHTML = resolutionText.slice(0, i) + caretHtml;
|
|
i++;
|
|
setTimeout(tick, 28);
|
|
}
|
|
};
|
|
tick();
|
|
}, 2050);
|
|
}
|
|
|
|
if (reduced || !("IntersectionObserver" in window)) {
|
|
playMd();
|
|
} else {
|
|
const mdObs = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
playMd();
|
|
mdObs.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{ threshold: 0.18 }
|
|
);
|
|
mdObs.observe(mdSurface);
|
|
}
|
|
}
|
|
|
|
// ── 5. Protocol theatre ────────────────────────────────────────
|
|
const theatre = document.querySelector("[data-theatre]");
|
|
if (!theatre) return;
|
|
|
|
const pool = theatre.querySelector("[data-pool]");
|
|
const claudePanel = theatre.querySelector('[data-agent="claude"]');
|
|
const codexPanel = theatre.querySelector('[data-agent="codex"]');
|
|
const claudeStack = theatre.querySelector('[data-work-stack="claude"]');
|
|
const codexStack = theatre.querySelector('[data-work-stack="codex"]');
|
|
const claudeStatus = claudePanel.querySelector("[data-agent-status]");
|
|
const codexStatus = codexPanel.querySelector("[data-agent-status]");
|
|
const stepText = theatre.querySelector("[data-step-text]");
|
|
const stepCounter = theatre.querySelector("[data-step-counter]");
|
|
const replayBtn = theatre.querySelector("[data-theatre-replay]");
|
|
|
|
function getPackets() {
|
|
return Array.from(pool.querySelectorAll(".packet"));
|
|
}
|
|
|
|
function setStatus(agent, text) {
|
|
const el = agent === "claude" ? claudeStatus : codexStatus;
|
|
if (el) el.textContent = text;
|
|
}
|
|
|
|
function clearStacks() {
|
|
claudeStack.innerHTML = "";
|
|
codexStack.innerHTML = "";
|
|
}
|
|
|
|
function resetTheatre() {
|
|
[claudePanel, codexPanel].forEach((p) => p.classList.remove("is-active"));
|
|
setStatus("claude", "idle");
|
|
setStatus("codex", "idle");
|
|
clearStacks();
|
|
getPackets().forEach((p) =>
|
|
p.classList.remove("claimed-claude", "claimed-codex", "dimmed")
|
|
);
|
|
if (stepCounter) stepCounter.textContent = "0 / 5";
|
|
}
|
|
|
|
function makeWorkItem(intent, name, pid, agent) {
|
|
const li = document.createElement("li");
|
|
li.className = "packet claimed-" + agent;
|
|
li.innerHTML =
|
|
'<span class="intent ' + intent + '">' + intent + "</span>" +
|
|
'<span class="name">' + name + "</span>" +
|
|
'<span class="pid">' + pid + "</span>";
|
|
li.style.opacity = "0";
|
|
li.style.transform = "translateY(6px)";
|
|
li.style.transition = "opacity 300ms, transform 300ms";
|
|
return li;
|
|
}
|
|
|
|
// Animate a packet flying from the pool into an agent's work-stack.
|
|
// Returns a Promise that resolves when the packet has settled.
|
|
function flyPacket(packet, agent) {
|
|
return new Promise((resolve) => {
|
|
const targetStack = agent === "claude" ? claudeStack : codexStack;
|
|
const intent = packet.dataset.intent;
|
|
const name = packet.querySelector(".name").textContent;
|
|
const pid = packet.querySelector(".pid").textContent;
|
|
|
|
// Mark the pool packet as claimed (color tint + still in pool).
|
|
const claimClass = "claimed-" + agent;
|
|
packet.classList.add(claimClass);
|
|
|
|
if (reduced) {
|
|
const li = makeWorkItem(intent, name, pid, agent);
|
|
targetStack.appendChild(li);
|
|
requestAnimationFrame(() => {
|
|
li.style.opacity = "1";
|
|
li.style.transform = "translateY(0)";
|
|
});
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const theatreRect = theatre.getBoundingClientRect();
|
|
const srcRect = packet.getBoundingClientRect();
|
|
|
|
// Compute destination: top of the target stack, offset by current
|
|
// child count to stack vertically.
|
|
const stackRect = targetStack.getBoundingClientRect();
|
|
const dstX = stackRect.left - theatreRect.left;
|
|
const dstY = stackRect.top - theatreRect.top
|
|
+ targetStack.childElementCount * 40;
|
|
|
|
// Build the flying packet — a clone of the original styled identically.
|
|
const fly = document.createElement("div");
|
|
fly.className = "packet-flying packet " + claimClass;
|
|
fly.style.left = (srcRect.left - theatreRect.left) + "px";
|
|
fly.style.top = (srcRect.top - theatreRect.top) + "px";
|
|
fly.style.width = srcRect.width + "px";
|
|
fly.innerHTML =
|
|
'<span class="intent ' + intent + '">' + intent + "</span>" +
|
|
'<span class="name">' + name + "</span>" +
|
|
'<span class="pid">' + pid + "</span>";
|
|
theatre.appendChild(fly);
|
|
|
|
// Force layout flush, then transition to destination.
|
|
// eslint-disable-next-line no-unused-expressions
|
|
fly.offsetWidth;
|
|
|
|
const deltaX = dstX - (srcRect.left - theatreRect.left);
|
|
const deltaY = dstY - (srcRect.top - theatreRect.top);
|
|
|
|
requestAnimationFrame(() => {
|
|
fly.style.transform =
|
|
"translate(" + deltaX + "px, " + deltaY + "px) scale(0.98)";
|
|
});
|
|
|
|
let resolved = false;
|
|
const finalize = () => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
const li = makeWorkItem(intent, name, pid, agent);
|
|
targetStack.appendChild(li);
|
|
requestAnimationFrame(() => {
|
|
li.style.opacity = "1";
|
|
li.style.transform = "translateY(0)";
|
|
});
|
|
fly.remove();
|
|
resolve();
|
|
};
|
|
|
|
fly.addEventListener("transitionend", finalize, { once: true });
|
|
// Fallback in case transitionend doesn't fire (e.g. tab hidden).
|
|
setTimeout(finalize, 1700);
|
|
});
|
|
}
|
|
|
|
function setCaption(html, n) {
|
|
if (stepText) stepText.innerHTML = html;
|
|
if (stepCounter) stepCounter.textContent = n + " / 5";
|
|
}
|
|
|
|
// ── Scripted 5-step play ───────────────────────────────────────
|
|
let playing = false;
|
|
let playToken = 0;
|
|
|
|
function wait(ms) {
|
|
return new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
async function play() {
|
|
if (playing) return;
|
|
playing = true;
|
|
const myToken = ++playToken;
|
|
resetTheatre();
|
|
|
|
const stepDelay = reduced ? 0 : 1400;
|
|
const aborted = () => myToken !== playToken;
|
|
|
|
// Step 1
|
|
setCaption('<span class="muted">claude-code reads the pool.</span>', 1);
|
|
claudePanel.classList.add("is-active");
|
|
setStatus("claude", "reading");
|
|
await wait(stepDelay);
|
|
if (aborted()) { playing = false; return; }
|
|
|
|
// Step 2 — fly t-2 to claude
|
|
const t2 = pool.querySelector('[data-task="t-2"]');
|
|
setCaption(
|
|
'claude-code claims <strong>[implement] deletion-safety-guard</strong>.',
|
|
2
|
|
);
|
|
if (t2) await flyPacket(t2, "claude");
|
|
setStatus("claude", "working");
|
|
await wait(reduced ? 0 : 600);
|
|
if (aborted()) { playing = false; return; }
|
|
|
|
// Step 3
|
|
setCaption(
|
|
'<span class="muted">codex reads the pool — sees claude-code\'s claim.</span>',
|
|
3
|
|
);
|
|
claudePanel.classList.remove("is-active");
|
|
codexPanel.classList.add("is-active");
|
|
setStatus("codex", "reading");
|
|
await wait(stepDelay);
|
|
if (aborted()) { playing = false; return; }
|
|
|
|
// Step 4 — fly t-1 to codex
|
|
const t1 = pool.querySelector('[data-task="t-1"]');
|
|
setCaption(
|
|
'codex picks complementary work — <strong>[test] deletion-safety-tests</strong>.',
|
|
4
|
|
);
|
|
if (t1) await flyPacket(t1, "codex");
|
|
setStatus("codex", "working");
|
|
await wait(reduced ? 0 : 600);
|
|
if (aborted()) { playing = false; return; }
|
|
|
|
// Step 5 — settle, dim remaining
|
|
setCaption(
|
|
'Different agents. Different work. <span class="muted">Coordination through shared state.</span>',
|
|
5
|
|
);
|
|
codexPanel.classList.remove("is-active");
|
|
const t3 = pool.querySelector('[data-task="t-3"]');
|
|
const t4 = pool.querySelector('[data-task="t-4"]');
|
|
if (t3) t3.classList.add("dimmed");
|
|
if (t4) t4.classList.add("dimmed");
|
|
|
|
playing = false;
|
|
}
|
|
|
|
// ── Click-to-fire bonus interaction ────────────────────────────
|
|
// Dimmed (passed-over) packets stay interactive — clicking re-activates
|
|
// them. Already-claimed packets are inert. Mid-play clicks are ignored.
|
|
pool.addEventListener("click", (e) => {
|
|
const packet = e.target.closest(".packet");
|
|
if (!packet) return;
|
|
if (
|
|
packet.classList.contains("claimed-claude") ||
|
|
packet.classList.contains("claimed-codex")
|
|
) {
|
|
return;
|
|
}
|
|
if (playing) return;
|
|
|
|
// Re-activate if dimmed.
|
|
packet.classList.remove("dimmed");
|
|
|
|
const claudeCount = claudeStack.childElementCount;
|
|
const codexCount = codexStack.childElementCount;
|
|
const agent = codexCount < claudeCount ? "codex" : "claude";
|
|
flyPacket(packet, agent);
|
|
});
|
|
|
|
// ── Replay button ──────────────────────────────────────────────
|
|
if (replayBtn) {
|
|
replayBtn.addEventListener("click", () => {
|
|
playing = false;
|
|
playToken++;
|
|
requestAnimationFrame(() => play());
|
|
});
|
|
}
|
|
|
|
// ── Auto-play once on scroll-into-view ─────────────────────────
|
|
if (reduced || !("IntersectionObserver" in window)) {
|
|
play();
|
|
} else {
|
|
let hasPlayed = false;
|
|
const playObs = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting && !hasPlayed) {
|
|
hasPlayed = true;
|
|
play();
|
|
playObs.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{ threshold: 0.18 }
|
|
);
|
|
playObs.observe(theatre);
|
|
}
|
|
})();
|