diff --git a/public/css/main.css b/public/css/main.css index 43dfa0fc0..bbe651246 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -2274,6 +2274,7 @@ code { display: flex; gap: var(--spacing-xl); align-items: stretch; + justify-content: center; margin-bottom: var(--spacing-xl); } diff --git a/public/css/workflow.css b/public/css/workflow.css index 99992e6e2..d9ee5767e 100644 --- a/public/css/workflow.css +++ b/public/css/workflow.css @@ -2932,8 +2932,8 @@ 40% { transform: scale(0.78); } } -/* Picker bar at the bottom */ -.live-demo-bar { +/* Global bar — the persistent dark pill at the bottom of every session. */ +.live-demo-gbar { position: absolute; bottom: 14px; left: 50%; @@ -2942,52 +2942,39 @@ padding: 0 4px; background: oklch(14% 0 0); color: oklch(92% 0 0); - border-radius: 999px; + border: 1px solid oklch(22% 0 0); + border-radius: 10px; display: flex; align-items: center; gap: 2px; font-family: var(--font-body); font-size: 12px; box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2); - overflow: hidden; - transition: width 320ms var(--ease-out); + z-index: 4; } -.live-demo-bar > div { - display: none; - align-items: center; - gap: 2px; - padding: 0 6px; -} -.live-demo-bar[data-phase="idle"] .live-demo-bar-idle, -.live-demo-bar[data-phase="configuring"] .live-demo-bar-config, -.live-demo-bar[data-phase="generating"] .live-demo-bar-generating, -.live-demo-bar[data-phase="cycling"] .live-demo-bar-cycle, -.live-demo-bar[data-phase="accepted"] .live-demo-bar-accepted { - display: flex; -} -.live-demo-bar-slash { +.live-demo-gbar-brand { font-family: var(--font-display); font-size: 16px; color: var(--color-accent); - padding: 0 8px 0 10px; + padding: 0 10px; } -.live-demo-bar-btn { +.live-demo-gbar-btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 10px; background: transparent; border: 0; - border-radius: 8px; + border-radius: 7px; color: oklch(75% 0 0); font: inherit; cursor: pointer; } -.live-demo-bar-btn.is-active { +.live-demo-gbar-btn.is-active { background: var(--color-accent-dim); color: var(--color-accent); } -.live-demo-bar-dmd { +.live-demo-gbar-dmd { display: inline-grid; grid-template: repeat(2, 1fr) / repeat(2, 1fr); width: 12px; @@ -2995,129 +2982,200 @@ border-radius: 3px; overflow: hidden; } -.live-demo-bar-dmd span:nth-child(1) { background: oklch(60% 0.25 350); } -.live-demo-bar-dmd span:nth-child(2) { background: oklch(60% 0.15 45); } -.live-demo-bar-dmd span:nth-child(3) { background: oklch(55% 0.12 250); } -.live-demo-bar-dmd span:nth-child(4) { background: oklch(30% 0 0); } - -.live-demo-bar-cmd { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 12px; - background: var(--color-accent-dim); - color: var(--color-accent); - border-radius: 6px; - font-family: var(--font-mono); - font-size: 11px; -} -.live-demo-bar-cmd-label { - opacity: 0.7; - text-transform: uppercase; - font-size: 9px; - letter-spacing: 0.2em; -} -.live-demo-bar-cmd-name { - font-weight: 500; - letter-spacing: 0.05em; -} -.live-demo-bar-divider { +.live-demo-gbar-dmd span:nth-child(1) { background: oklch(60% 0.25 350); } +.live-demo-gbar-dmd span:nth-child(2) { background: oklch(60% 0.15 45); } +.live-demo-gbar-dmd span:nth-child(3) { background: oklch(55% 0.12 250); } +.live-demo-gbar-dmd span:nth-child(4) { background: oklch(30% 0 0); } +.live-demo-gbar-divider { width: 1px; height: 18px; background: oklch(28% 0 0); margin: 0 4px; } -.live-demo-bar-input { +.live-demo-gbar-x { + padding: 7px 10px; + background: transparent; + border: 0; + border-radius: 7px; + color: oklch(60% 0 0); + font-size: 13px; + cursor: pointer; +} + +/* Contextual bar — LIGHT paper-backed pill that floats near the picked + element during a session. Matches live-browser.js's buildConfigureRow + look (dark command pill, transparent input, ×N count, magenta Go). */ +.live-demo-ctx { + position: absolute; + left: 50%; + transform: translate(-50%, 6px); + padding: 6px; + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 10px; + box-shadow: 0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06); + font-family: var(--font-body); + font-size: 12px; + color: var(--color-ink); + display: none; + opacity: 0; + transition: opacity 260ms var(--ease-out), transform 260ms var(--ease-out); + z-index: 3; + max-width: 90%; +} +.live-demo-ctx[data-phase="configuring"], +.live-demo-ctx[data-phase="generating"], +.live-demo-ctx[data-phase="cycling"], +.live-demo-ctx[data-phase="accepted"] { + display: block; + opacity: 1; + transform: translate(-50%, 0); +} +.live-demo-ctx-row { + display: none; + align-items: center; + gap: 4px; +} +.live-demo-ctx[data-phase="configuring"] .live-demo-ctx-row--configure, +.live-demo-ctx[data-phase="generating"] .live-demo-ctx-row--generating, +.live-demo-ctx[data-phase="cycling"] .live-demo-ctx-row--cycling, +.live-demo-ctx[data-phase="accepted"] .live-demo-ctx-row--accepted { + display: flex; +} + +/* Configure row: [delight ▾] [input] [×3] [Go →] */ +.live-demo-ctx-pill { display: inline-flex; align-items: center; - min-width: 140px; - font-family: var(--font-mono); - font-size: 11px; - color: oklch(85% 0 0); - padding: 0 4px; + gap: 4px; + padding: 5px 10px; + background: var(--color-ink); + color: var(--color-paper); + border: 0; + border-radius: 6px; + font-family: var(--font-body); + font-size: 12px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; } -.live-demo-bar-caret { +.live-demo-ctx-pill-caret { font-size: 9px; opacity: 0.7; margin-left: 2px; } +.live-demo-ctx-input { + display: inline-flex; + align-items: center; + flex: 1; + min-width: 180px; + padding: 5px 8px; + font-family: var(--font-body); + font-size: 12px; + color: var(--color-ink); +} +.live-demo-ctx-caret { display: inline-block; - width: 6px; - height: 12px; - background: var(--color-accent); + width: 1px; + height: 13px; + background: var(--color-ink); margin-left: 2px; animation: liveDemoCaret 1s steps(1) infinite; } @keyframes liveDemoCaret { 50% { opacity: 0; } } -.live-demo-bar-go { +.live-demo-ctx-count { + padding: 4px 6px; + background: transparent; + border: 1px solid var(--color-mist); + border-radius: 5px; font-family: var(--font-mono); font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; - padding: 7px 12px; + font-weight: 600; + color: var(--color-ash); + cursor: pointer; +} +.live-demo-ctx-go { + padding: 5px 12px; background: var(--color-accent); - color: white; + color: var(--color-paper); border: 0; border-radius: 6px; + font-family: var(--font-body); + font-size: 12px; + font-weight: 600; cursor: pointer; - margin-left: auto; + white-space: nowrap; } -.live-demo-bar-generating { - padding: 0 14px !important; - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.08em; - color: oklch(85% 0 0); +/* Generating row */ +.live-demo-ctx-row--generating { + gap: 10px; + padding: 4px 12px 4px 6px; + font-family: var(--font-body); + font-size: 12px; + color: var(--color-charcoal); } -.live-demo-bar-shader { +.live-demo-ctx-spinner { width: 14px; height: 14px; border-radius: 50%; - border: 1.5px solid oklch(35% 0 0); + border: 1.5px solid var(--color-mist); border-top-color: var(--color-accent); - margin-right: 10px; animation: liveDemoSpin 700ms linear infinite; } -@keyframes liveDemoSpin { - to { transform: rotate(360deg); } -} +@keyframes liveDemoSpin { to { transform: rotate(360deg); } } -.live-demo-bar-counter { - font-family: var(--font-mono); - font-size: 11px; - color: oklch(92% 0 0); - padding: 0 8px; - min-width: 38px; - text-align: center; -} -.live-demo-bar-discard { - padding: 7px 10px; +/* Cycling row */ +.live-demo-ctx-row--cycling { gap: 2px; padding: 2px; } +.live-demo-ctx-nav { + padding: 4px 10px; background: transparent; border: 0; - border-radius: 8px; - color: oklch(72% 0 0); + border-radius: 5px; + color: var(--color-charcoal); + font-size: 14px; + cursor: pointer; +} +.live-demo-ctx-counter { + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-ink); + padding: 0 6px; + min-width: 40px; + text-align: center; +} +.live-demo-ctx-divider { + width: 1px; + height: 18px; + background: var(--color-mist); + margin: 0 4px; +} +.live-demo-ctx-discard { + padding: 4px 10px; + background: transparent; + border: 0; + border-radius: 5px; + color: var(--color-ash); font-size: 13px; cursor: pointer; } -.live-demo-bar-accept { - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; - padding: 7px 14px; - background: var(--color-accent); - color: white; +.live-demo-ctx-accept { + padding: 5px 14px; + background: var(--color-ink); + color: var(--color-paper); border: 0; border-radius: 6px; - cursor: pointer; - margin-left: 2px; -} - -.live-demo-bar-accepted { - padding: 0 14px !important; - color: oklch(80% 0.15 145); font-family: var(--font-body); font-size: 12px; - gap: 8px !important; + font-weight: 600; + cursor: pointer; +} + +/* Accepted confirmation row */ +.live-demo-ctx-row--accepted { + gap: 8px; + padding: 6px 14px; + font-family: var(--font-body); + font-size: 12px; + color: oklch(45% 0.18 145); } /* Phase caption under the frame */ diff --git a/public/index.html b/public/index.html index 434118a28..552db49fa 100644 --- a/public/index.html +++ b/public/index.html @@ -249,12 +249,8 @@

Every AI model learned from the same templates. Without intervention, they all produce the same predictable mistakes. Impeccable names them, detects them, and teaches the AI to avoid them.

- +
-
- -
-
@@ -597,7 +593,7 @@ - + + +
+
+ + + + + + +
+
+ + Generating variants… +
+
+ + 1 / 3 + + + + +
+
+ + Variant 3 written to source +
+
+
- -
- -
- / - - - -
- - -
- - command - delight - - - - - - -
- - -
- - Generating variants… -
- - -
- - 1 / 3 - - - - -
- - -
- - Variant 3 written to source -
+ +
+ / + + + + +
diff --git a/public/js/components/live-demo.js b/public/js/components/live-demo.js index b7b93d87a..0a352d72b 100644 --- a/public/js/components/live-demo.js +++ b/public/js/components/live-demo.js @@ -1,32 +1,28 @@ -// Interactive Live Mode demo loop. Plays when the section is in view, -// pauses when out. Respects prefers-reduced-motion (freezes on a -// representative frame). +// Interactive Live Mode demo loop. Matches the real picker flow: +// - a persistent dark global bar stays at the bottom of the frame the whole time +// - a light contextual bar floats above the picked element during a session, +// morphing between configure → generating → cycling → accepted // -// Phases map to the `data-phase` attribute on the picker bar: -// idle → configuring → generating → cycling → accepted → (reset to idle) +// Plays only while the section is in view. Respects prefers-reduced-motion. const PHASE = { - IDLE: 'idle', + HIDDEN: 'hidden', CONFIGURING: 'configuring', GENERATING: 'generating', CYCLING: 'cycling', ACCEPTED: 'accepted', }; -// Each step describes one action + how long to hold before the next step. -// dt = delay BEFORE this step runs (ms). Total cycle ≈ sum of all dt. const TIMELINE = [ { dt: 400, action: 'cursor-show' }, { dt: 400, action: 'cursor-to-target' }, { dt: 900, action: 'outline-show', caption: 'Hover to pick.' }, { dt: 500, action: 'cursor-click' }, - { dt: 200, action: 'open-config', caption: 'Picked. Choose a command.' }, - { dt: 700, action: 'cursor-to-cmd' }, - { dt: 400, action: 'set-command', cmd: 'delight', caption: 'delight — add personality.' }, - { dt: 500, action: 'cursor-to-input' }, - { dt: 300, action: 'type', text: 'more playful', caption: 'Annotate. Comment. Stroke.' }, - { dt: 1400, action: 'draw-stroke' }, - { dt: 700, action: 'cursor-to-go' }, + { dt: 200, action: 'open-ctx', caption: 'Picked. Contextual bar appears.' }, + { dt: 700, action: 'cursor-to-input' }, + { dt: 300, action: 'type', text: 'more playful', caption: 'Type a refinement, or skip.' }, + { dt: 1200, action: 'draw-stroke', caption: 'Annotate on the page, if you want.' }, + { dt: 900, action: 'cursor-to-go' }, { dt: 300, action: 'click-go', caption: 'Generating three variants…' }, { dt: 1600, action: 'show-variant', n: 1, caption: 'Variant 1 of 3.' }, { dt: 1400, action: 'show-variant', n: 2, caption: 'Variant 2 of 3.' }, @@ -45,18 +41,17 @@ export function initLiveDemo() { const outline = root.querySelector('[data-demo-outline]'); const annotations = root.querySelector('[data-demo-annotations]'); const cursor = root.querySelector('[data-demo-cursor]'); - const bar = root.querySelector('[data-demo-bar]'); - const cmdName = root.querySelector('[data-demo-cmd-name]'); + const ctx = root.querySelector('[data-demo-ctx]'); const inputText = root.querySelector('[data-demo-input-text]'); const counter = root.querySelector('[data-demo-counter]'); const captionLabel = root.querySelector('[data-demo-caption-label]'); const variants = Array.from(root.querySelectorAll('.live-demo-variant')); - if (!stage || !target || !bar) return; + if (!stage || !target || !ctx) return; const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - // Position outline over target whenever target moves. + // Position the outline around the target. const positionOutline = () => { const stageRect = stage.getBoundingClientRect(); const targetRect = target.getBoundingClientRect(); @@ -66,7 +61,27 @@ export function initLiveDemo() { outline.style.height = (targetRect.height + 8) + 'px'; }; - // Move cursor to coordinates relative to the stage. + // Position the contextual bar below the target (or above if below would + // collide with the global bar). Mirrors positionBar() in live-browser.js. + const positionCtx = () => { + const stageRect = stage.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + const ctxRect = ctx.getBoundingClientRect(); + const GAP = 10; + const BAR_RESERVE = 60; + const belowTop = targetRect.bottom - stageRect.top + GAP; + const aboveTop = targetRect.top - stageRect.top - ctxRect.height - GAP; + let top; + if (belowTop + ctxRect.height + GAP <= stage.clientHeight - BAR_RESERVE) { + top = belowTop; + } else if (aboveTop >= GAP) { + top = aboveTop; + } else { + top = stage.clientHeight - ctxRect.height - BAR_RESERVE; + } + ctx.style.top = top + 'px'; + }; + const moveCursor = (selector, offsetX = 0, offsetY = 0) => { const stageRect = stage.getBoundingClientRect(); const el = typeof selector === 'string' ? root.querySelector(selector) : selector; @@ -83,17 +98,23 @@ export function initLiveDemo() { v.classList.toggle('is-active', match); }); counter.textContent = n + ' / 3'; - // Re-measure outline after layout settles. - requestAnimationFrame(positionOutline); + requestAnimationFrame(() => { + positionOutline(); + positionCtx(); + }); + }; + + const setCtxPhase = (phase) => { + ctx.dataset.phase = phase; + if (phase !== PHASE.HIDDEN) requestAnimationFrame(positionCtx); }; const reset = () => { - bar.dataset.phase = PHASE.IDLE; + setCtxPhase(PHASE.HIDDEN); cursor.classList.remove('is-visible', 'is-click'); outline.classList.remove('is-visible'); annotations.classList.remove('is-visible', 'is-comment-visible'); inputText.textContent = ''; - cmdName.textContent = 'delight'; showVariant(0); }; @@ -130,14 +151,8 @@ export function initLiveDemo() { cursor.classList.add('is-click'); setTimeout(() => cursor.classList.remove('is-click'), 260); break; - case 'open-config': - bar.dataset.phase = PHASE.CONFIGURING; - break; - case 'cursor-to-cmd': - moveCursor(root.querySelector('[data-demo-cmd]')); - break; - case 'set-command': - cmdName.textContent = s.cmd; + case 'open-ctx': + setCtxPhase(PHASE.CONFIGURING); break; case 'cursor-to-input': moveCursor(root.querySelector('[data-demo-input]')); @@ -156,10 +171,10 @@ export function initLiveDemo() { cursor.classList.add('is-click'); setTimeout(() => cursor.classList.remove('is-click'), 260); annotations.classList.remove('is-visible', 'is-comment-visible'); - bar.dataset.phase = PHASE.GENERATING; + setCtxPhase(PHASE.GENERATING); break; case 'show-variant': - if (bar.dataset.phase !== PHASE.CYCLING) bar.dataset.phase = PHASE.CYCLING; + if (ctx.dataset.phase !== PHASE.CYCLING) setCtxPhase(PHASE.CYCLING); showVariant(s.n); break; case 'cursor-to-accept': @@ -168,7 +183,7 @@ export function initLiveDemo() { case 'click-accept': cursor.classList.add('is-click'); setTimeout(() => cursor.classList.remove('is-click'), 260); - bar.dataset.phase = PHASE.ACCEPTED; + setCtxPhase(PHASE.ACCEPTED); outline.classList.remove('is-visible'); break; case 'reset': @@ -201,14 +216,13 @@ export function initLiveDemo() { cancelToken++; }; - // Start/stop based on visibility. if (reduced) { - // Freeze on the "cycling, variant 3" frame for a representative still. - bar.dataset.phase = PHASE.CYCLING; + // Freeze on a representative still: cycling, variant 3. showVariant(3); counter.textContent = '3 / 3'; positionOutline(); outline.classList.add('is-visible'); + setCtxPhase(PHASE.CYCLING); setCaption('Three variants. Pick the one you want.'); return; } @@ -221,6 +235,8 @@ export function initLiveDemo() { }, { threshold: 0.35 }); io.observe(root); - // Also re-measure outline on window resize since positions depend on layout. - window.addEventListener('resize', () => requestAnimationFrame(positionOutline)); + window.addEventListener('resize', () => requestAnimationFrame(() => { + positionOutline(); + positionCtx(); + })); }