Files
pbakaus_impeccable/site/public/tutorials/iterate-live.html
T
Paul BakausandClaude Fable 5 90f9eeb99b Split service layer into private impeccable-site repo
The public repo keeps the OSS promise surface: skill, CLI, extension,
tests, and the provider build. The site, labs, concept/composition
catalogs, image pipeline, Cloudflare functions, and authoring guide move
to pbakaus/impeccable-site.

concept-seed tests run against a synthetic fixture catalog; the plugin
icon and skill categories moved in-repo; build validation narrows to
README prose and non-site counts; release notes read from a sibling
impeccable-site checkout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 22:41:53 -07:00

364 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Iterate on UI with Live Mode | Tutorials | Impeccable</title>
<meta name="description" content="Use /impeccable live to visually iterate on a real element in your dev server: pick, annotate, generate three variants, accept the one you want, and have it written back to source.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style/tutorials/iterate-live">
<link rel="icon" type="image/svg+xml" href="../favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../css/sub-pages.css">
</head>
<body class="sub-page skills-layout-page tutorials-page">
<a href="#main" class="skip-link">Skip to content</a>
<!-- site-header v1 -->
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
<span class="site-header-brand-name">Impeccable</span>
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home">Home</a>
<a href="/designing" data-nav="designing">Designing</a>
<a href="/docs" data-nav="docs" aria-current="page">Docs</a>
<a href="/slop" data-nav="slop">Slop</a>
<a href="/live-mode" data-nav="live">Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">21k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
</header>
<main id="main">
<div class="skills-layout">
<aside class="skills-sidebar" aria-label="Documentation">
<button class="skills-sidebar-toggle" type="button" aria-expanded="false" aria-controls="skills-sidebar-inner">
<span class="skills-sidebar-toggle-label">Iterate on UI with Live Mode</span>
<svg class="skills-sidebar-toggle-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="skills-sidebar-inner" id="skills-sidebar-inner">
<p class="skills-sidebar-label">Docs</p>
<div class="skills-sidebar-group" data-category="tutorials">
<p class="skills-sidebar-group-title">Tutorials</p>
<ul class="skills-sidebar-list">
<li><a href="/tutorials/getting-started">Getting started</a></li>
<li><a href="/tutorials/iterate-live" aria-current="page">Iterate on UI with Live Mode</a></li>
<li><a href="/tutorials/brand-vs-product">Brand vs product, pick a register</a></li>
<li><a href="/tutorials/critique-with-overlay">Critique with the visual overlay</a></li>
</ul>
</div>
<hr class="skills-sidebar-divider">
<div class="skills-sidebar-group" data-category="create">
<p class="skills-sidebar-group-title">Create</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/impeccable">/impeccable</a></li>
<li><a href="/docs/craft">craft</a></li>
<li><a href="/docs/shape">shape</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="evaluate">
<p class="skills-sidebar-group-title">Evaluate</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/audit">audit</a></li>
<li><a href="/docs/critique">critique</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="refine">
<p class="skills-sidebar-group-title">Refine</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/animate">animate</a></li>
<li><a href="/docs/bolder">bolder</a></li>
<li><a href="/docs/colorize">colorize</a></li>
<li><a href="/docs/delight">delight</a></li>
<li><a href="/docs/layout">layout</a></li>
<li><a href="/docs/overdrive">overdrive</a></li>
<li><a href="/docs/quieter">quieter</a></li>
<li><a href="/docs/typeset">typeset</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="simplify">
<p class="skills-sidebar-group-title">Simplify</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/adapt">adapt</a></li>
<li><a href="/docs/clarify">clarify</a></li>
<li><a href="/docs/distill">distill</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="harden">
<p class="skills-sidebar-group-title">Harden</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/harden">harden</a></li>
<li><a href="/docs/onboard">onboard</a></li>
<li><a href="/docs/optimize">optimize</a></li>
<li><a href="/docs/polish">polish</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="system">
<p class="skills-sidebar-group-title">System</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/document">document</a></li>
<li><a href="/docs/extract">extract</a></li>
<li><a href="/docs/live">live</a></li>
<li><a href="/docs/teach">teach</a></li>
</ul>
</div>
</div>
</aside>
<div class="skills-main">
<article class="tutorial-detail">
<header class="tutorial-detail-header">
<p class="skill-detail-eyebrow"><a href="/tutorials">Tutorials</a> / 02</p>
<h1 class="tutorial-detail-title">Iterate on UI with Live Mode</h1>
<p class="tutorial-detail-tagline">Pick an element, generate three variants, accept one. Canvas-like iteration without leaving your code.</p>
</header>
<section class="tutorial-detail-body prose">
<h2 id="what-youll-build">What you&#39;ll build</h2>
<p>You will use <code>/impeccable live</code> on your dev server to iterate on a single piece of UI (a hero, a card, a section) and end with one of three AI-generated variants written back to source as real code. You&#39;ll see the canvas-style picking, annotation, and three-up cycling flow.</p>
<p>Total time: about ten minutes. Most of that is picking what to iterate on.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Impeccable installed (see <a href="/tutorials/getting-started">getting started</a> if you have not). Run <code>/impeccable teach</code> first if you haven&#39;t yet: variants lean on <code>PRODUCT.md</code> and <code>DESIGN.md</code> for brand fit.</li>
<li>A running dev server with HMR (Vite, Next.js, SvelteKit, Astro, Nuxt, Bun) OR a static HTML file open in a browser.</li>
<li>A page with at least one piece of UI you&#39;d like to iterate on. A newsletter card, a hero, a pricing tier, something small enough to hold in your head.</li>
</ul>
<h2 id="step-1-start-live-mode">Step 1. Start live mode</h2>
<p>From your harness, run:</p>
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable live</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable live" aria-label="Copy to clipboard"></button></div>
<p>The skill starts a small local helper server on port 8400 and injects a <code>&lt;script&gt;</code> tag into your dev entry file that loads the picker. If your project has a strict Content Security Policy, the first run detects it and offers a one-time, dev-only patch for <code>script-src</code> and <code>connect-src</code>. Accept the patch: it is guarded by <code>NODE_ENV === &quot;development&quot;</code> and you can revert any time.</p>
<p>Open your dev server URL (not port 8400, that&#39;s the helper server, not the app). You&#39;ll see a dark pill at the bottom of the page with <strong>Pick</strong> highlighted.</p>
<h2 id="step-2-pick-an-element">Step 2. Pick an element</h2>
<div class="docs-viz-step">
<div class="docs-viz-picker-row">
<div class="docs-viz-picker-target">
<span class="docs-viz-picker-pin">1</span>
Newsletter signup
<span class="docs-viz-picker-note">more playful</span>
</div>
</div>
</div>
<p>Click the element you want to iterate on. A picker outline appears around it, and a light context bar pops up next to the selection with a command chip on the left and a freeform text field.</p>
<p>A few things you can do before pressing Go:</p>
<ul>
<li><strong>Click the command chip</strong> (default is <code>impeccable</code>, the freeform action). Pick a specific action like <code>bolder</code>, <code>delight</code>, <code>layout</code>, or <code>typeset</code> to constrain the variants along one dimension.</li>
<li><strong>Type in the freeform field.</strong> &quot;More playful.&quot; &quot;Less SaaS.&quot; &quot;Feel like a newsletter from a magazine.&quot;</li>
<li><strong>Drop a comment pin</strong> by clicking anywhere on the picked element. The pin&#39;s position is load-bearing: a comment near the title is about the title, not the whole element.</li>
<li><strong>Draw a stroke</strong> by dragging across the element. Closed loop = &quot;this part matters.&quot; Arrow = direction. Cross = &quot;delete this.&quot; The skill reads strokes by shape, not by pixel content.</li>
</ul>
<p>When the brief feels clear, hit <strong>Go</strong>.</p>
<h2 id="step-3-cycle-through-the-three-variants">Step 3. Cycle through the three variants</h2>
<div class="docs-viz-step">
<div class="docs-viz-variants">
<div class="docs-viz-variant docs-viz-variant--v1">
<span class="docs-viz-variant-badge">1 / 3</span>
<span class="docs-viz-variant-kicker">No. 04</span>
<p class="docs-viz-variant-title">Letters, <em>occasionally</em>.</p>
<span class="docs-viz-variant-btn">Send me one</span>
</div>
<div class="docs-viz-variant docs-viz-variant--v2 is-active">
<span class="docs-viz-variant-badge">2 / 3</span>
<span class="docs-viz-variant-kicker">Dispatch</span>
<p class="docs-viz-variant-title">Design notes, <br>every other<br>Thursday.</p>
<span class="docs-viz-variant-btn">Join →</span>
</div>
<div class="docs-viz-variant docs-viz-variant--v3">
<span class="docs-viz-variant-badge">3 / 3</span>
<span class="docs-viz-variant-kicker">Field Notes</span>
<p class="docs-viz-variant-title">A monthly letter, for people who still read email.</p>
<span class="docs-viz-variant-btn">Receive ✺</span>
</div>
</div>
</div>
<p>You&#39;ll see a spinner (&quot;Generating variants...&quot;) and within a few seconds, three variants hot-swap into the page in place. Not a preview, the actual rendered DOM on your actual dev server with your actual context.</p>
<p>Use the arrow keys (or the prev / next buttons on the context bar) to cycle through them. A counter at the top right shows <code>1 / 3</code>, <code>2 / 3</code>, <code>3 / 3</code>.</p>
<p>The three variants are designed to be <strong>genuinely different</strong>, not three riffs on one idea. Freeform variants anchor to three different design archetypes (broadsheet masthead, oversized-glyph poster, catalog-style spec rows, and so on). Action-specific variants vary along the dimension the action names: <code>colorize</code> gives you three hue families, <code>animate</code> gives you three motion vocabularies, <code>layout</code> gives you three structural arrangements.</p>
<p>If two variants feel like they rhyme, that is the skill&#39;s &quot;squint test&quot; failure mode. You can tell the picker &quot;try again, all three felt too similar&quot; and get a fresh set.</p>
<h2 id="step-4-accept-one">Step 4. Accept one</h2>
<div class="docs-viz-step" style="text-align:center">
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
</div>
<p>When you find the one you like, click <strong>Accept</strong> on the context bar (or press Enter). Three things happen:</p>
<ol>
<li>The picked element is replaced with the accepted variant on the page.</li>
<li>The variant is written back to source: the same file your picker was injected into, or the component source if live detected a generated file during step 1.</li>
<li>If the accept touched CSS, the relevant rules are consolidated into your project&#39;s real stylesheet, not left inline.</li>
</ol>
<p>Discard all three (press Escape) and the original stays. No trace, no commented-out leftovers.</p>
<h2 id="step-5-stop-live-mode">Step 5. Stop live mode</h2>
<p>When you are done iterating, stop the helper:</p>
<ul>
<li>Say <strong>&quot;stop live mode&quot;</strong> in your harness chat, or</li>
<li>Click the <strong>×</strong> on the picker pill, or</li>
<li>Close the browser tab: the helper detects the dropped connection after eight seconds and exits cleanly.</li>
</ul>
<p>The stop also strips the <code>&lt;script&gt;</code> tag from your dev entry and stops the helper server on port 8400.</p>
<h2 id="what-to-try-next">What to try next</h2>
<ul>
<li>Run <code>/impeccable live</code> on a different page after a <code>/impeccable polish</code> pass to A/B the polished version against two more directions.</li>
<li>Pair with <a href="/tutorials/critique-with-overlay">critique with the overlay</a>: run critique first, fix priority issues, then use live to explore redirections on the element critique flagged.</li>
<li>Reach for <code>/impeccable craft</code> when you want the shape-then-build flow (a new feature end-to-end, not a single element).</li>
</ul>
<h2 id="common-issues">Common issues</h2>
<ul>
<li><strong>The picker never appears on the page.</strong> Either the helper did not start (look for errors in the terminal) or CSP is blocking the inject. Re-run <code>/impeccable live</code> and let it re-check CSP. If you declined the patch on first run, delete the <code>cspChecked</code> line in <code>.impeccable/live/config.json</code> and re-run.</li>
<li><strong>&quot;element lives in a generated file&quot;</strong> on Go. Live detected that the picked element is in a compiled output, not a source file. It routes the accept through a fallback path so the variant still lands in true source. Follow the hint; don&#39;t force-accept into the generated file.</li>
<li><strong>Variants don&#39;t feel brand-aligned.</strong> Check that <code>PRODUCT.md</code> and <code>DESIGN.md</code> exist at the project root. Without them, live leans toward generic defaults. Run <code>/impeccable teach</code> and <code>/impeccable document</code> first.</li>
<li><strong>The helper port is in use.</strong> Another live session left its server running. <code>npx impeccable live stop</code> releases the port.</li>
</ul>
</section>
</article>
</div>
</div>
</main>
<script>
// Copy buttons on rendered code blocks
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
if (!btn) return;
const text = btn.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
// Mobile sidebar toggle (shown on narrow viewports, hidden on desktop).
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
// Before/after split-compare: drag on touch, hover OR drag on mouse.
// Pointer events attach to the padded .split-comparison wrapper so
// there is a ~20px invisible buffer around the visible box. The
// divider only snaps back when the pointer leaves that outer buffer.
(function initSplitCompare() {
const wrappers = document.querySelectorAll('.split-comparison');
if (wrappers.length === 0) return;
const hasHover = matchMedia('(hover: hover)').matches;
const DEFAULT_POSITION = 50;
for (const wrapper of wrappers) {
const container = wrapper.querySelector('.split-container');
const splitAfter = wrapper.querySelector('.split-after');
const splitDivider = wrapper.querySelector('.split-divider');
if (!container || !splitAfter || !splitDivider) continue;
const tanAngle = Math.tan(10 * Math.PI / 180);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
skewOffset = 50 * r.height * tanAngle / r.width;
}
};
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
let targetX = DEFAULT_POSITION;
let currentX = DEFAULT_POSITION;
let rafId = null;
const paint = (pct) => {
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
splitAfter.style.clipPath =
`polygon(${x + skewOffset}% 0%, 100% 0%, 100% 100%, ${x - skewOffset}% 100%)`;
splitDivider.style.left = `${x}%`;
};
const step = () => {
currentX += (targetX - currentX) * 0.2;
if (Math.abs(targetX - currentX) < 0.1) {
currentX = targetX;
rafId = null;
} else {
rafId = requestAnimationFrame(step);
}
paint(currentX);
};
const setTarget = (pct) => {
targetX = pct;
if (rafId === null) rafId = requestAnimationFrame(step);
};
paint(DEFAULT_POSITION);
// Percentage is always relative to the VISIBLE .split-container,
// not the padded .split-comparison wrapper. The pointer event
// target is the wrapper but the clip-path math uses the inner box.
const pctFromClientX = (clientX) => {
const rect = container.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
};
let hovering = false;
let dragging = false;
wrapper.addEventListener('pointerenter', (e) => {
if (hasHover && e.pointerType === 'mouse') {
hovering = true;
}
});
wrapper.addEventListener('pointerdown', (e) => {
dragging = true;
wrapper.setPointerCapture(e.pointerId);
setTarget(pctFromClientX(e.clientX));
});
wrapper.addEventListener('pointermove', (e) => {
if (dragging || hovering) {
setTarget(pctFromClientX(e.clientX));
}
});
const endDrag = (e) => {
if (dragging) {
dragging = false;
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
}
};
wrapper.addEventListener('pointerup', endDrag);
wrapper.addEventListener('pointercancel', endDrag);
wrapper.addEventListener('pointerleave', (e) => {
endDrag(e);
if (hovering) {
hovering = false;
setTarget(DEFAULT_POSITION);
}
});
}
})();
</script>
</body>
</html>