mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 00:56:30 +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>
304 lines
13 KiB
HTML
304 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Tutorials | Impeccable</title>
|
|
<meta name="description" content="4 short, opinionated walk-throughs of the highest-leverage Impeccable workflows.">
|
|
<meta name="theme-color" content="#fafafa">
|
|
<link rel="canonical" href="https://impeccable.style/tutorials">
|
|
<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">Docs menu</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">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">
|
|
|
|
<div class="tutorials-content">
|
|
<header class="sub-page-header">
|
|
<p class="sub-page-eyebrow">4 walk-throughs</p>
|
|
<h1 class="sub-page-title">Tutorials</h1>
|
|
<p class="sub-page-lede">Short, opinionated walk-throughs of the highest-leverage workflows. Each one takes around ten minutes and ends with something working in your project.</p>
|
|
</header>
|
|
|
|
<div class="tutorial-cards">
|
|
|
|
<a class="tutorial-card" href="/tutorials/getting-started">
|
|
<span class="tutorial-card-number">01</span>
|
|
<div class="tutorial-card-body">
|
|
<h2 class="tutorial-card-title">Getting started</h2>
|
|
<p class="tutorial-card-tagline">From zero to your first polish pass in five minutes.</p>
|
|
</div>
|
|
<span class="tutorial-card-arrow">→</span>
|
|
</a>
|
|
|
|
<a class="tutorial-card" href="/tutorials/iterate-live">
|
|
<span class="tutorial-card-number">02</span>
|
|
<div class="tutorial-card-body">
|
|
<h2 class="tutorial-card-title">Iterate on UI with Live Mode</h2>
|
|
<p class="tutorial-card-tagline">Pick an element, generate three variants, accept one. Canvas-like iteration without leaving your code.</p>
|
|
</div>
|
|
<span class="tutorial-card-arrow">→</span>
|
|
</a>
|
|
|
|
<a class="tutorial-card" href="/tutorials/brand-vs-product">
|
|
<span class="tutorial-card-number">03</span>
|
|
<div class="tutorial-card-body">
|
|
<h2 class="tutorial-card-title">Brand vs product, pick a register</h2>
|
|
<p class="tutorial-card-tagline">Two worlds, two sets of defaults. Pick the right one and every command downstream benefits.</p>
|
|
</div>
|
|
<span class="tutorial-card-arrow">→</span>
|
|
</a>
|
|
|
|
<a class="tutorial-card" href="/tutorials/critique-with-overlay">
|
|
<span class="tutorial-card-number">04</span>
|
|
<div class="tutorial-card-body">
|
|
<h2 class="tutorial-card-title">Critique with the visual overlay</h2>
|
|
<p class="tutorial-card-tagline">Use /impeccable critique plus the browser overlay to review a live page with ground truth.</p>
|
|
</div>
|
|
<span class="tutorial-card-arrow">→</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</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>
|