mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 16:46:31 +03:00
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>
364 lines
20 KiB
HTML
364 lines
20 KiB
HTML
<!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'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'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'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'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><script></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 === "development"</code> and you can revert any time.</p>
|
||
<p>Open your dev server URL (not port 8400, that's the helper server, not the app). You'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> "More playful." "Less SaaS." "Feel like a newsletter from a magazine."</li>
|
||
<li><strong>Drop a comment pin</strong> by clicking anywhere on the picked element. The pin'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 = "this part matters." Arrow = direction. Cross = "delete this." 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'll see a spinner ("Generating variants...") 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's "squint test" failure mode. You can tell the picker "try again, all three felt too similar" 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'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>"stop live mode"</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><script></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>"element lives in a generated file"</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't force-accept into the generated file.</li>
|
||
<li><strong>Variants don'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>
|