mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Star count was 21k on sub-pages and the header partial, 22k on the homepage. Updated all seven source files to 23k (actual: 23,692). Changelog section gains a curated "Highlights since v3.0" block above the full version history, which now collapses behind a disclosure toggle. Fixes the vertical bloat from 9 entries in three weeks while keeping v3.0's anchor content visible. Also fixes a nesting bug where an orphan </div> from the old changelog-list wrapper prematurely closed the changelog-section, breaking the two-column changelog+FAQ grid layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
397 lines
20 KiB
HTML
397 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>Live Mode | Impeccable</title>
|
||
<meta name="description" content="Iterate on UI in the browser. Pick an element, drop a comment, get three production-quality variants, accept one, and it writes back to source. /impeccable live.">
|
||
<meta name="theme-color" content="#fafafa">
|
||
<link rel="canonical" href="https://impeccable.style/live-mode">
|
||
<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">
|
||
|
||
<link rel="stylesheet" href="../css/live-mode.css">
|
||
<script type="module">
|
||
import { initLiveDemo } from "../js/components/live-demo.js";
|
||
document.addEventListener("DOMContentLoaded", initLiveDemo);
|
||
</script>
|
||
</head>
|
||
<body class="sub-page live-mode-page-body">
|
||
<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">Docs</a>
|
||
<a href="/slop" data-nav="slop">Slop</a>
|
||
<a href="/live-mode" data-nav="live" aria-current="page">Live</a>
|
||
</nav>
|
||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 23k 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">23k</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="live-mode-page">
|
||
<header class="live-mode-page-header">
|
||
<p class="live-mode-page-eyebrow">New in v3.0 <span class="live-mode-page-eyebrow-badge">Alpha</span></p>
|
||
<h1 class="live-mode-page-title">Live Mode</h1>
|
||
<p class="live-mode-page-lede">Pick any element in the browser. Drop a comment or a stroke. Three production-quality variants swap in via your framework's HMR. Accept the one you want and it writes back to source.</p>
|
||
<p class="live-mode-page-alpha-note"><strong>Why alpha:</strong> Live Mode works end-to-end and is ready to try, but it still needs more testing against real-world repos and framework configs. Expect rough edges on uncommon setups, and please report what breaks.</p>
|
||
<div class="live-mode-start" aria-label="Start live mode command">
|
||
<span class="live-mode-start-prompt">$</span>
|
||
<code class="live-mode-start-cmd">/impeccable live</code>
|
||
<button class="live-mode-start-copy" type="button" aria-label="Copy command" data-copy="/impeccable live">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="live-mode-demo-wrap" aria-label="Live Mode interactive demo">
|
||
|
||
<div class="live-demo" id="live-demo" aria-label="Live Mode interactive demo loop">
|
||
<div class="live-demo-frame-col"><div class="live-demo-frame">
|
||
<div class="live-demo-chrome">
|
||
<span class="live-demo-dot"></span>
|
||
<span class="live-demo-dot"></span>
|
||
<span class="live-demo-dot"></span>
|
||
<span class="live-demo-url">localhost:3000</span>
|
||
</div>
|
||
|
||
<div class="live-demo-stage">
|
||
<div class="live-demo-skeleton" aria-hidden="true">
|
||
<div class="live-demo-skel-nav">
|
||
<span class="live-demo-skel-logo"></span>
|
||
<span class="live-demo-skel-link"></span>
|
||
<span class="live-demo-skel-link"></span>
|
||
<span class="live-demo-skel-link"></span>
|
||
<span class="live-demo-skel-cta"></span>
|
||
</div>
|
||
<div class="live-demo-skel-heading"></div>
|
||
<div class="live-demo-skel-line"></div>
|
||
<div class="live-demo-skel-line live-demo-skel-line--short"></div>
|
||
</div>
|
||
|
||
<div class="live-demo-target" data-demo-target>
|
||
<div class="live-demo-variant is-active" data-variant="original">
|
||
<div class="live-demo-card live-demo-card--plain">
|
||
<span class="live-demo-card-kicker">Newsletter</span>
|
||
<h3>Subscribe for updates</h3>
|
||
<p>Monthly-ish design notes.</p>
|
||
<button type="button">Subscribe</button>
|
||
</div>
|
||
</div>
|
||
<div class="live-demo-variant" data-variant="1">
|
||
<div class="live-demo-card live-demo-card--v1">
|
||
<span class="live-demo-card-kicker">No. 04</span>
|
||
<h3>Letters, <em>occasionally</em>.</h3>
|
||
<p>A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
|
||
<button type="button">Send me one</button>
|
||
</div>
|
||
</div>
|
||
<div class="live-demo-variant" data-variant="2">
|
||
<div class="live-demo-card live-demo-card--v2">
|
||
<div class="live-demo-card-stamp">☞</div>
|
||
<span class="live-demo-card-kicker">Dispatch</span>
|
||
<h3>Design notes, <br>every other<br>Thursday.</h3>
|
||
<button type="button">Join the list →</button>
|
||
</div>
|
||
</div>
|
||
<div class="live-demo-variant" data-variant="3">
|
||
<div class="live-demo-card live-demo-card--v3">
|
||
<div class="live-demo-card-sticker"><span>☆</span><span>☆</span><span>☆</span></div>
|
||
<span class="live-demo-card-kicker">Field Notes</span>
|
||
<h3>A monthly letter, for people who still read email for pleasure.</h3>
|
||
<button type="button">Receive the letter <span aria-hidden="true">✺</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="live-demo-outline" data-demo-outline aria-hidden="true"></div>
|
||
|
||
<div class="live-demo-annotations" data-demo-annotations aria-hidden="true">
|
||
<svg class="live-demo-stroke" viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true">
|
||
<path d="M 10,40 Q 60,10 110,38 T 210,32 T 290,20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" pathLength="1"/>
|
||
</svg>
|
||
<div class="live-demo-comment">more playful</div>
|
||
</div>
|
||
|
||
<div class="live-demo-ctx" data-demo-ctx data-phase="hidden">
|
||
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
|
||
<button type="button" class="live-demo-ctx-pill" data-demo-ctx-pill>
|
||
<span data-demo-cmd-name>delight</span>
|
||
<span class="live-demo-ctx-pill-caret" aria-hidden="true">▾</span>
|
||
</button>
|
||
<span class="live-demo-ctx-input" data-demo-input>
|
||
<span data-demo-input-text></span><span class="live-demo-ctx-caret"></span>
|
||
</span>
|
||
<button type="button" class="live-demo-ctx-count">×3</button>
|
||
<button type="button" class="live-demo-ctx-go" data-demo-go>Go <span aria-hidden="true">→</span></button>
|
||
</div>
|
||
<div class="live-demo-ctx-row live-demo-ctx-row--generating">
|
||
<span class="live-demo-ctx-spinner" aria-hidden="true"></span>
|
||
<span>Generating variants…</span>
|
||
</div>
|
||
<div class="live-demo-ctx-row live-demo-ctx-row--cycling">
|
||
<button type="button" class="live-demo-ctx-nav" aria-label="Previous variant">‹</button>
|
||
<span class="live-demo-ctx-counter" data-demo-counter>1 / 3</span>
|
||
<button type="button" class="live-demo-ctx-nav" aria-label="Next variant">›</button>
|
||
<span class="live-demo-ctx-divider"></span>
|
||
<button type="button" class="live-demo-ctx-discard" aria-label="Discard">✕</button>
|
||
<button type="button" class="live-demo-ctx-accept" data-demo-accept>Accept</button>
|
||
</div>
|
||
<div class="live-demo-ctx-row live-demo-ctx-row--accepted">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||
<span>Variant 3 written to source</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="live-demo-cursor" data-demo-cursor aria-hidden="true">
|
||
<svg width="18" height="22" viewBox="0 0 18 22" fill="none">
|
||
<path d="M1 1 L1 17 L5 13 L8 20 L11 19 L7.5 12 L13 12 Z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="live-demo-gbar" data-demo-gbar>
|
||
<span class="live-demo-gbar-brand">/</span>
|
||
<button type="button" class="live-demo-gbar-btn is-active" data-demo-gbar-pick>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>
|
||
<span>Pick</span>
|
||
</button>
|
||
<button type="button" class="live-demo-gbar-btn">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||
</button>
|
||
<button type="button" class="live-demo-gbar-btn">
|
||
<span class="live-demo-gbar-dmd" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
|
||
</button>
|
||
<span class="live-demo-gbar-divider"></span>
|
||
<button type="button" class="live-demo-gbar-x" aria-label="Exit live mode">✕</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="live-mode-demo-caption">Click the frame or scroll it into view to start the loop. Respects <code>prefers-reduced-motion</code>.</p>
|
||
</section>
|
||
|
||
<section class="live-mode-stages" aria-label="What happens in a live mode session">
|
||
<h2 class="live-mode-stages-title">What happens, in three moves</h2>
|
||
<div class="live-mode-stages-grid">
|
||
<article class="live-mode-stage">
|
||
<span class="live-mode-stage-num">01 · Pick</span>
|
||
<h3 class="live-mode-stage-name">Point at what bugs you</h3>
|
||
<p class="live-mode-stage-desc">Click any element on your running dev server. Add a comment pin where the issue lives. Draw a stroke through the bit you want to change. Or just type "more playful".</p>
|
||
<div class="live-mode-stage-viz">
|
||
<div class="docs-viz-picker-row" style="min-height:72px;padding:10px">
|
||
<div class="docs-viz-picker-target" style="font-size:12px;padding:6px 12px">
|
||
Newsletter card
|
||
<span class="docs-viz-picker-pin" style="width:18px;height:18px;font-size:9px">1</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
<article class="live-mode-stage">
|
||
<span class="live-mode-stage-num">02 · Generate</span>
|
||
<h3 class="live-mode-stage-name">Three genuinely different takes</h3>
|
||
<p class="live-mode-stage-desc">Variants anchor to different archetypes, not three riffs on color. Each one explores a different primary axis: hierarchy, typography, density, layout, or palette strategy.</p>
|
||
<div class="live-mode-stage-viz">
|
||
<div class="docs-viz-variants" style="width:100%;gap:4px">
|
||
<div class="docs-viz-variant docs-viz-variant--v1" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">No.04</span></div>
|
||
<div class="docs-viz-variant docs-viz-variant--v2 is-active" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Dispatch</span></div>
|
||
<div class="docs-viz-variant docs-viz-variant--v3" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Field</span></div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
<article class="live-mode-stage">
|
||
<span class="live-mode-stage-num">03 · Accept</span>
|
||
<h3 class="live-mode-stage-name">Lands in real source</h3>
|
||
<p class="live-mode-stage-desc">The accepted variant replaces the picked element in your source file. CSS consolidates into your real stylesheet, not inline. Discard all three and the original stays.</p>
|
||
<div class="live-mode-stage-viz">
|
||
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="live-mode-pathways" aria-label="Where to go next">
|
||
<h2 class="live-mode-pathways-title">Where next</h2>
|
||
<div class="live-mode-pathways-grid">
|
||
<a class="live-mode-pathway" href="/tutorials/iterate-live">
|
||
<span class="live-mode-pathway-kind">Tutorial</span>
|
||
<h3 class="live-mode-pathway-title">Walk it step by step</h3>
|
||
<p class="live-mode-pathway-desc">A ten-minute walkthrough from first run to accepted variant. Covers CSP patching, the picker actions, and the fallback flow for generated files.</p>
|
||
<span class="live-mode-pathway-cta">Open the tutorial →</span>
|
||
</a>
|
||
<a class="live-mode-pathway" href="/docs/live">
|
||
<span class="live-mode-pathway-kind">Reference</span>
|
||
<h3 class="live-mode-pathway-title">Full command reference</h3>
|
||
<p class="live-mode-pathway-desc">Everything your AI harness reads when <code>/impeccable live</code> runs: the poll loop, the wrap/accept helpers, the CSP templates, and every event shape.</p>
|
||
<span class="live-mode-pathway-cta">Read the reference →</span>
|
||
</a>
|
||
<a class="live-mode-pathway" href="/#downloads">
|
||
<span class="live-mode-pathway-kind">Install</span>
|
||
<h3 class="live-mode-pathway-title">Get Impeccable set up</h3>
|
||
<p class="live-mode-pathway-desc">Install the skill and CLI once, then run <code>/impeccable live</code> from your AI harness. Works with Claude Code, Cursor, Codex, Gemini, and more.</p>
|
||
<span class="live-mode-pathway-cta">See the install steps →</span>
|
||
</a>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="live-mode-frameworks" aria-label="Supported frameworks">
|
||
<span class="live-mode-frameworks-label">Supported dev servers</span>
|
||
<ul class="live-mode-frameworks-list">
|
||
<li>Vite</li>
|
||
<li>Next.js (incl. monorepos)</li>
|
||
<li>SvelteKit</li>
|
||
<li>Astro</li>
|
||
<li>Nuxt</li>
|
||
<li>Bun</li>
|
||
<li>Plain static HTML</li>
|
||
</ul>
|
||
</section>
|
||
</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>
|