Hero: vocabulary-first copy + command-palette switcher in the demo

Rewrite the hero around the why (the missing design vocabulary) instead of the
live-mode how: "The missing design vocabulary for agents." The live demo now
opens the picker's command palette and picks a verb before generating, which is
the move that makes the live approach unique and was previously skipped.

- Demo palette mirrors the real action picker (live-browser.js): same 12 verbs,
  the same SVG icons, a 4-col icon-over-label grid, selected chip on a kinpaku
  wash with its icon recolored. Light + dark covered.
- Shared <LiveDemoPalette> component renders the grid from one list, so the hero
  and /live-mode no longer hand-copy the markup. /live-mode lands on "delight",
  the hero on "colorize" (via data-demo-pick); pages without a palette filter the
  switcher beats out of the shared timeline.
- Trim the opening beats so the cursor clicks the element at ~1.3s (was ~2s), and
  slow the palette browse so the vocabulary is readable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-06-05 14:05:41 -07:00
co-authored by Claude Opus 4.8
parent 0939528b95
commit 1f975a69e4
6 changed files with 198 additions and 11 deletions
+61 -6
View File
@@ -14,11 +14,17 @@ const PHASE = {
};
const TIMELINE = [
{ dt: 400, action: 'cursor-show' },
{ dt: 400, action: 'cursor-to-target' },
{ dt: 800, action: 'outline-show', caption: 'Pick any element on your live page.' },
{ dt: 450, action: 'cursor-click' },
{ dt: 200, action: 'open-ctx' },
{ dt: 220, action: 'cursor-show' },
{ dt: 260, action: 'cursor-to-target' },
{ dt: 560, action: 'outline-show', caption: 'Pick any element on your live page.' },
{ dt: 300, action: 'cursor-click' },
{ dt: 180, action: 'open-ctx' },
{ dt: 340, action: 'cursor-to-cmd' },
{ dt: 320, action: 'open-palette', caption: 'Open the command palette: the design vocabulary.' },
{ dt: 950, action: 'browse-palette', cmd: 'bolder' },
{ dt: 700, action: 'browse-palette', cmd: 'typeset' },
{ dt: 700, action: 'browse-target' },
{ dt: 1000, action: 'pick-target', caption: 'Pick a design verb, or just describe it.' },
{ dt: 560, action: 'cursor-to-button' },
{ dt: 250, action: 'draw-circle', caption: 'Circle whats off-brand…' },
{ dt: 700, action: 'drop-pin' },
@@ -34,6 +40,11 @@ const TIMELINE = [
{ dt: 1800, action: 'reset', caption: 'Pick any element on your live page.' },
];
// Steps that drive the command-palette switcher. Pages whose context bar has no
// [data-demo-palette] (e.g. /live-mode) get these filtered out of the timeline,
// so they don't sit through dead pauses for a UI they don't render.
const PALETTE_ACTIONS = new Set(['cursor-to-cmd', 'open-palette', 'browse-palette', 'browse-target', 'pick-target']);
export function initLiveDemo() {
const root = document.getElementById('live-demo');
if (!root) return;
@@ -46,6 +57,11 @@ export function initLiveDemo() {
const ctx = root.querySelector('[data-demo-ctx]');
const inputText = root.querySelector('[data-demo-input-text]');
const noteText = root.querySelector('[data-demo-note-text]');
const palette = root.querySelector('[data-demo-palette]');
const cmdName = root.querySelector('[data-demo-cmd-name]');
const defaultCmd = cmdName ? cmdName.textContent : '';
// Drop the palette-switcher beats on pages that don't render the palette.
const timeline = palette ? TIMELINE : TIMELINE.filter((s) => !PALETTE_ACTIONS.has(s.action));
const counter = root.querySelector('[data-demo-counter]');
const captionLabel = root.querySelector('[data-demo-caption-label]');
const variants = Array.from(root.querySelectorAll('.live-demo-variant'));
@@ -140,12 +156,28 @@ export function initLiveDemo() {
annotations.classList.remove('is-visible', 'is-pin-visible', 'is-note-visible');
if (noteText) noteText.textContent = '';
if (inputText) inputText.textContent = '';
if (palette) palette.classList.remove('is-open');
if (palette) palette.querySelectorAll('.live-demo-ctx-palette-chip').forEach((r) => r.classList.remove('is-highlight'));
if (cmdName) cmdName.textContent = defaultCmd;
showVariant(0);
};
const clearAnnotations = () =>
annotations.classList.remove('is-visible', 'is-pin-visible', 'is-note-visible');
const setPalette = (open) => {
if (!palette) return;
palette.classList.toggle('is-open', open);
if (!open) palette.querySelectorAll('.live-demo-ctx-palette-chip').forEach((r) => r.classList.remove('is-highlight'));
};
const highlightCmd = (cmd) => {
if (!palette) return null;
const chip = palette.querySelector(`[data-cmd="${cmd}"]`);
palette.querySelectorAll('.live-demo-ctx-palette-chip').forEach((r) => r.classList.toggle('is-highlight', r === chip));
return chip;
};
const setCaption = (text) => {
if (text && captionLabel) captionLabel.textContent = text;
};
@@ -183,6 +215,29 @@ export function initLiveDemo() {
case 'open-ctx':
setCtxPhase(PHASE.CONFIGURING);
break;
case 'cursor-to-cmd':
moveCursor(root.querySelector('[data-demo-ctx-pill]'));
break;
case 'open-palette':
setPalette(true);
break;
case 'browse-palette': {
const chip = highlightCmd(s.cmd);
if (chip) moveCursor(chip);
break;
}
case 'browse-target': {
// The verb each page lands on, declared via data-demo-pick on the palette.
const chip = highlightCmd(palette && palette.dataset.demoPick);
if (chip) moveCursor(chip);
break;
}
case 'pick-target':
cursor.classList.add('is-click');
setTimeout(() => cursor.classList.remove('is-click'), 220);
if (cmdName && palette && palette.dataset.demoPick) cmdName.textContent = palette.dataset.demoPick;
setPalette(false);
break;
case 'cursor-to-button':
// Over the purple "Book Now" button (lower-left).
moveCursor(target, -150, 26);
@@ -243,7 +298,7 @@ export function initLiveDemo() {
const myToken = ++cancelToken;
while (running && myToken === cancelToken) {
reset();
for (const s of TIMELINE) {
for (const s of timeline) {
const stillMe = await sleep(s.dt, myToken);
if (!stillMe || !running) return;
await step(s);