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>
595 lines
34 KiB
HTML
595 lines
34 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>/impeccable polish | Impeccable</title>
|
|
<meta name="description" content="The meticulous final pass between good and great.">
|
|
<meta name="theme-color" content="#fafafa">
|
|
<link rel="canonical" href="https://impeccable.style/docs/polish">
|
|
<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">
|
|
<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">/polish</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" aria-current="page">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="skill-detail">
|
|
<div class="skill-detail-hero skill-detail-hero--has-demo">
|
|
<header class="skill-detail-header">
|
|
<p class="skill-detail-eyebrow"><a href="/docs">Docs</a> / Harden</p>
|
|
<h1 class="skill-detail-title"><span class="skill-detail-title-namespace"><span class="skill-detail-title-slash">/</span>impeccable</span>polish</h1>
|
|
<p class="skill-detail-tagline">The meticulous final pass between good and great.</p>
|
|
|
|
<div class="skill-meta-strip">
|
|
<span class="skill-meta-chip skill-meta-category" data-category="harden">Harden</span>
|
|
<span class="skill-meta-chip">User-invocable</span>
|
|
<span class="skill-meta-chip skill-meta-args">[target]</span>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="skill-demo" aria-label="Before and after demo">
|
|
<div class="split-comparison" data-demo="skill-polish">
|
|
<p class="skill-demo-eyebrow">Drag or hover to compare</p>
|
|
<div class="split-container">
|
|
<div class="split-before">
|
|
<div class="split-content">
|
|
<div style="width: 100%; max-width: 240px; padding: 16px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px;">
|
|
<div style="font-size: 16px; font-weight: bold; margin-bottom: 8px;">User Profile</div>
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
|
|
<div style="width: 40px; height: 40px; background: #ccc; border-radius: 50%;"></div>
|
|
<div>
|
|
<div style="font-size: 14px;">John Doe</div>
|
|
<div style="font-size: 12px; color: #888;">Developer</div>
|
|
</div>
|
|
</div>
|
|
<button style="width: 100%; padding: 8px; background: #333; color: white; border: none; border-radius: 4px; font-size: 13px;">Edit Profile</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="split-after">
|
|
<div class="split-content">
|
|
<div style="width: 100%; max-width: 240px; padding: 20px; background: var(--color-paper); border: 1px solid var(--color-mist); border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.06);">
|
|
<div style="font-family: var(--font-display); font-size: 1.125rem; font-weight: 400; margin-bottom: 16px; color: var(--color-ink);">User Profile</div>
|
|
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
|
<div style="width: 48px; height: 48px; background: var(--color-ink); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--color-paper); font-weight: 600; font-size: 1.125rem;">JD</div>
|
|
<div>
|
|
<div style="font-size: 0.9375rem; font-weight: 500; color: var(--color-ink);">John Doe</div>
|
|
<div style="font-size: 0.75rem; color: var(--color-ash); letter-spacing: 0.02em;">Developer</div>
|
|
</div>
|
|
</div>
|
|
<button style="width: 100%; padding: 10px; background: var(--color-ink); color: var(--color-paper); border: none; border-radius: 6px; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.01em; cursor: pointer;">Edit Profile</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="split-divider"></div>
|
|
</div>
|
|
<div class="split-labels">
|
|
<span class="split-label-item" data-point="before">Before</span>
|
|
<p class="skill-demo-caption">Rough edges → Refined, pixel-perfect details</p>
|
|
<span class="split-label-item" data-point="after">After</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<section class="skill-detail-editorial prose">
|
|
<h2 id="when-to-use-it">When to use it</h2>
|
|
<p><code>/impeccable polish</code> is the last thing you run before shipping. It hunts down the small details that separate a shipped feature from a polished one: half-pixel misalignments, inconsistent spacing, forgotten focus states, loading transitions that flash, copy that drifts in tone. It also aligns the feature with your design system, replacing hard-coded values with tokens, swapping custom components for shared ones, and fixing any drift from established patterns.</p>
|
|
<p>Reach for it when the feature is functionally complete, nothing is broken, and something still feels off. Also reach for it when a feature has drifted from the design system and needs to be pulled back in line.</p>
|
|
<h2 id="how-it-works">How it works</h2>
|
|
<p>Polish starts by discovering the design system (tokens, spacing scale, shared components), then works methodically across six dimensions:</p>
|
|
<ol>
|
|
<li><strong>Visual alignment and spacing</strong>: pixel-perfect grid adherence, consistent spacing scale, optical alignment on icons.</li>
|
|
<li><strong>Typography</strong>: hierarchy consistency, line length, widows and orphans, kerning on headlines.</li>
|
|
<li><strong>Color and contrast</strong>: token usage, theme parity, WCAG ratios, focus indicators.</li>
|
|
<li><strong>Interaction states</strong>: hover, focus, active, disabled, loading, error, success. Every state accounted for.</li>
|
|
<li><strong>Transitions and motion</strong>: smooth easing, no layout jank, respect for <code>prefers-reduced-motion</code>.</li>
|
|
<li><strong>Copy</strong>: consistent voice, correct tense, no placeholder strings, no stray TODOs.</li>
|
|
</ol>
|
|
<p>The skill is explicit about one thing: polish is the last step, not the first. If the feature is not functionally complete, polishing it is wasted work.</p>
|
|
<h2 id="try-it">Try it</h2>
|
|
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable polish the pricing page</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable polish the pricing page" aria-label="Copy to clipboard"></button></div>
|
|
<p>A healthy run looks like:</p>
|
|
<div class="code-block-wrap"><pre class="code-block"><code>Visual alignment: fixed 3 off-grid elements (8px baseline)
|
|
Typography: tightened h1 kerning, fixed widow on testimonial
|
|
Interaction: added hover state on FAQ items, focus ring on email input
|
|
Motion: softened modal entrance, added reduced-motion fallback
|
|
Copy: removed one "Lorem ipsum" stray, aligned button voice</code></pre><button class="code-block-copy" type="button" data-copy="Visual alignment: fixed 3 off-grid elements (8px baseline)
|
|
Typography: tightened h1 kerning, fixed widow on testimonial
|
|
Interaction: added hover state on FAQ items, focus ring on email input
|
|
Motion: softened modal entrance, added reduced-motion fallback
|
|
Copy: removed one "Lorem ipsum" stray, aligned button voice" aria-label="Copy to clipboard"></button></div>
|
|
<p>Five small fixes, no rewrites. That is the shape of a good polish pass.</p>
|
|
<h2 id="pitfalls">Pitfalls</h2>
|
|
<ul>
|
|
<li><strong>Polishing work that is not done.</strong> If there are TODOs in the code, you are not ready. Run <code>/impeccable polish</code> on finished features only.</li>
|
|
<li><strong>Treating polish as redesign.</strong> Polish refines what exists. If you find yourself rearchitecting a layout, you needed <code>/impeccable critique</code> or <code>/impeccable layout</code> instead.</li>
|
|
<li><strong>Running <code>/impeccable polish</code> without <code>/impeccable audit</code> first.</strong> Polish catches feel-based issues. Audit catches measurable ones. Use both.</li>
|
|
</ul>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
<section class="skill-source-card">
|
|
<header class="skill-source-card-header">
|
|
<span class="skill-source-card-label">reference/polish.md</span>
|
|
<span class="skill-source-card-subtitle">Loaded when the impeccable skill routes to this command.</span>
|
|
</header>
|
|
<div class="skill-source-card-body prose">
|
|
<blockquote>
|
|
<p><strong>Additional context needed</strong>: quality bar (MVP vs flagship).</p>
|
|
</blockquote>
|
|
<p>Perform a meticulous final pass to catch all the small details that separate good work from great work. The difference between shipped and polished.</p>
|
|
<h2 id="design-system-discovery">Design System Discovery</h2>
|
|
<p>Aligning the feature to the design system is <strong>not optional</strong>. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.</p>
|
|
<ol>
|
|
<li><strong>Find the design system</strong>: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.</li>
|
|
<li><strong>Note the conventions</strong>: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?</li>
|
|
<li><strong>Identify drift, then name the root cause</strong>: For every deviation, classify it as a <strong>missing token</strong> (the value should exist in the system but doesn't), a <strong>one-off implementation</strong> (a shared component already exists but wasn't used), or a <strong>conceptual misalignment</strong> (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category — patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.</li>
|
|
</ol>
|
|
<p>If a design system exists, polish <strong>must</strong> align the feature with it. If none exists, polish against the conventions visible in the codebase. <strong>If anything about the system is ambiguous, ask — never guess at design system principles.</strong></p>
|
|
<h2 id="pre-polish-assessment">Pre-Polish Assessment</h2>
|
|
<p>Understand the current state and goals before touching anything:</p>
|
|
<ol>
|
|
<li><p><strong>Review completeness</strong>:</p>
|
|
<ul>
|
|
<li>Is it functionally complete?</li>
|
|
<li>Are there known issues to preserve (mark with TODOs)?</li>
|
|
<li>What's the quality bar? (MVP vs flagship feature?)</li>
|
|
<li>When does it ship? (How much time for polish?)</li>
|
|
</ul>
|
|
</li>
|
|
<li><p><strong>Think experience-first</strong>: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish — a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.</p>
|
|
</li>
|
|
<li><p><strong>Identify polish areas</strong>:</p>
|
|
<ul>
|
|
<li>Visual inconsistencies</li>
|
|
<li>Spacing and alignment issues</li>
|
|
<li>Interaction state gaps</li>
|
|
<li>Copy inconsistencies</li>
|
|
<li>Edge cases and error states</li>
|
|
<li>Loading and transition smoothness</li>
|
|
<li>Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)</li>
|
|
</ul>
|
|
</li>
|
|
<li><p><strong>Triage cosmetic vs functional</strong>: Classify each issue as <strong>cosmetic</strong> (looks off, doesn't impede the user) or <strong>functional</strong> (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent — never perfect one corner while leaving another rough.</p>
|
|
</li>
|
|
</ol>
|
|
<p><strong>CRITICAL</strong>: Polish is the last step, not the first. Don't polish work that's not functionally complete.</p>
|
|
<h2 id="polish-systematically">Polish Systematically</h2>
|
|
<p>Work through these dimensions methodically:</p>
|
|
<h3 id="visual-alignment-spacing">Visual Alignment & Spacing</h3>
|
|
<ul>
|
|
<li><strong>Pixel-perfect alignment</strong>: Everything lines up to grid</li>
|
|
<li><strong>Consistent spacing</strong>: All gaps use spacing scale (no random 13px gaps)</li>
|
|
<li><strong>Optical alignment</strong>: Adjust for visual weight (icons may need offset for optical centering)</li>
|
|
<li><strong>Responsive consistency</strong>: Spacing and alignment work at all breakpoints</li>
|
|
<li><strong>Grid adherence</strong>: Elements snap to baseline grid</li>
|
|
</ul>
|
|
<p><strong>Check</strong>:</p>
|
|
<ul>
|
|
<li>Enable grid overlay and verify alignment</li>
|
|
<li>Check spacing with browser inspector</li>
|
|
<li>Test at multiple viewport sizes</li>
|
|
<li>Look for elements that "feel" off</li>
|
|
</ul>
|
|
<h3 id="information-architecture-flow">Information Architecture & Flow</h3>
|
|
<p>Visual polish on a misshapen flow is wasted work. Match the <em>shape</em> of the experience to the system, not just the surface.</p>
|
|
<ul>
|
|
<li><strong>Progressive disclosure</strong>: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.</li>
|
|
<li><strong>Established user flows</strong>: Multi-step actions follow the same shape as comparable flows elsewhere — modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.</li>
|
|
<li><strong>Hierarchy & complexity</strong>: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.</li>
|
|
<li><strong>Empty, loading, and arrival transitions</strong>: How content arrives, updates, and leaves matches how it does in adjacent features.</li>
|
|
<li><strong>Naming and mental model</strong>: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.</li>
|
|
</ul>
|
|
<h3 id="typography-refinement">Typography Refinement</h3>
|
|
<ul>
|
|
<li><strong>Hierarchy consistency</strong>: Same elements use same sizes/weights throughout</li>
|
|
<li><strong>Line length</strong>: 45-75 characters for body text</li>
|
|
<li><strong>Line height</strong>: Appropriate for font size and context</li>
|
|
<li><strong>Widows & orphans</strong>: No single words on last line</li>
|
|
<li><strong>Hyphenation</strong>: Appropriate for language and column width</li>
|
|
<li><strong>Kerning</strong>: Adjust letter spacing where needed (especially headlines)</li>
|
|
<li><strong>Font loading</strong>: No FOUT/FOIT flashes</li>
|
|
</ul>
|
|
<h3 id="color-contrast">Color & Contrast</h3>
|
|
<ul>
|
|
<li><strong>Contrast ratios</strong>: All text meets WCAG standards</li>
|
|
<li><strong>Consistent token usage</strong>: No hard-coded colors, all use design tokens</li>
|
|
<li><strong>Theme consistency</strong>: Works in all theme variants</li>
|
|
<li><strong>Color meaning</strong>: Same colors mean same things throughout</li>
|
|
<li><strong>Accessible focus</strong>: Focus indicators visible with sufficient contrast</li>
|
|
<li><strong>Tinted neutrals</strong>: No pure gray or pure black—add subtle color tint (0.01 chroma)</li>
|
|
<li><strong>Gray on color</strong>: Never put gray text on colored backgrounds—use a shade of that color or transparency</li>
|
|
</ul>
|
|
<h3 id="interaction-states">Interaction States</h3>
|
|
<p>Every interactive element needs all states:</p>
|
|
<ul>
|
|
<li><strong>Default</strong>: Resting state</li>
|
|
<li><strong>Hover</strong>: Subtle feedback (color, scale, shadow)</li>
|
|
<li><strong>Focus</strong>: Keyboard focus indicator (never remove without replacement)</li>
|
|
<li><strong>Active</strong>: Click/tap feedback</li>
|
|
<li><strong>Disabled</strong>: Clearly non-interactive</li>
|
|
<li><strong>Loading</strong>: Async action feedback</li>
|
|
<li><strong>Error</strong>: Validation or error state</li>
|
|
<li><strong>Success</strong>: Successful completion</li>
|
|
</ul>
|
|
<p><strong>Missing states create confusion and broken experiences</strong>.</p>
|
|
<h3 id="micro-interactions-transitions">Micro-interactions & Transitions</h3>
|
|
<ul>
|
|
<li><strong>Smooth transitions</strong>: All state changes animated appropriately (150-300ms)</li>
|
|
<li><strong>Consistent easing</strong>: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.</li>
|
|
<li><strong>No jank</strong>: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation</li>
|
|
<li><strong>Appropriate motion</strong>: Motion serves purpose, not decoration</li>
|
|
<li><strong>Reduced motion</strong>: Respects <code>prefers-reduced-motion</code></li>
|
|
</ul>
|
|
<h3 id="content-copy">Content & Copy</h3>
|
|
<ul>
|
|
<li><strong>Consistent terminology</strong>: Same things called same names throughout</li>
|
|
<li><strong>Consistent capitalization</strong>: Title Case vs Sentence case applied consistently</li>
|
|
<li><strong>Grammar & spelling</strong>: No typos</li>
|
|
<li><strong>Appropriate length</strong>: Not too wordy, not too terse</li>
|
|
<li><strong>Punctuation consistency</strong>: Periods on sentences, not on labels (unless all labels have them)</li>
|
|
</ul>
|
|
<h3 id="icons-images">Icons & Images</h3>
|
|
<ul>
|
|
<li><strong>Consistent style</strong>: All icons from same family or matching style</li>
|
|
<li><strong>Appropriate sizing</strong>: Icons sized consistently for context</li>
|
|
<li><strong>Proper alignment</strong>: Icons align with adjacent text optically</li>
|
|
<li><strong>Alt text</strong>: All images have descriptive alt text</li>
|
|
<li><strong>Loading states</strong>: Images don't cause layout shift, proper aspect ratios</li>
|
|
<li><strong>Retina support</strong>: 2x assets for high-DPI screens</li>
|
|
</ul>
|
|
<h3 id="forms-inputs">Forms & Inputs</h3>
|
|
<ul>
|
|
<li><strong>Label consistency</strong>: All inputs properly labeled</li>
|
|
<li><strong>Required indicators</strong>: Clear and consistent</li>
|
|
<li><strong>Error messages</strong>: Helpful and consistent</li>
|
|
<li><strong>Tab order</strong>: Logical keyboard navigation</li>
|
|
<li><strong>Auto-focus</strong>: Appropriate (don't overuse)</li>
|
|
<li><strong>Validation timing</strong>: Consistent (on blur vs on submit)</li>
|
|
</ul>
|
|
<h3 id="edge-cases-error-states">Edge Cases & Error States</h3>
|
|
<ul>
|
|
<li><strong>Loading states</strong>: All async actions have loading feedback</li>
|
|
<li><strong>Empty states</strong>: Helpful empty states, not just blank space</li>
|
|
<li><strong>Error states</strong>: Clear error messages with recovery paths</li>
|
|
<li><strong>Success states</strong>: Confirmation of successful actions</li>
|
|
<li><strong>Long content</strong>: Handles very long names, descriptions, etc.</li>
|
|
<li><strong>No content</strong>: Handles missing data gracefully</li>
|
|
<li><strong>Offline</strong>: Appropriate offline handling (if applicable)</li>
|
|
</ul>
|
|
<h3 id="responsiveness">Responsiveness</h3>
|
|
<ul>
|
|
<li><strong>All breakpoints</strong>: Test mobile, tablet, desktop</li>
|
|
<li><strong>Touch targets</strong>: 44x44px minimum on touch devices</li>
|
|
<li><strong>Readable text</strong>: No text smaller than 14px on mobile</li>
|
|
<li><strong>No horizontal scroll</strong>: Content fits viewport</li>
|
|
<li><strong>Appropriate reflow</strong>: Content adapts logically</li>
|
|
</ul>
|
|
<h3 id="performance">Performance</h3>
|
|
<ul>
|
|
<li><strong>Fast initial load</strong>: Optimize critical path</li>
|
|
<li><strong>No layout shift</strong>: Elements don't jump after load (CLS)</li>
|
|
<li><strong>Smooth interactions</strong>: No lag or jank</li>
|
|
<li><strong>Optimized images</strong>: Appropriate formats and sizes</li>
|
|
<li><strong>Lazy loading</strong>: Off-screen content loads lazily</li>
|
|
</ul>
|
|
<h3 id="code-quality">Code Quality</h3>
|
|
<ul>
|
|
<li><strong>Remove console logs</strong>: No debug logging in production</li>
|
|
<li><strong>Remove commented code</strong>: Clean up dead code</li>
|
|
<li><strong>Remove unused imports</strong>: Clean up unused dependencies</li>
|
|
<li><strong>Consistent naming</strong>: Variables and functions follow conventions</li>
|
|
<li><strong>Type safety</strong>: No TypeScript <code>any</code> or ignored errors</li>
|
|
<li><strong>Accessibility</strong>: Proper ARIA labels and semantic HTML</li>
|
|
</ul>
|
|
<h2 id="polish-checklist">Polish Checklist</h2>
|
|
<p>Go through systematically:</p>
|
|
<ul>
|
|
<li><input disabled="" type="checkbox"> Aligned to the design system (drift named and resolved by root cause)</li>
|
|
<li><input disabled="" type="checkbox"> Information architecture and flow shape match neighboring features</li>
|
|
<li><input disabled="" type="checkbox"> Visual alignment perfect at all breakpoints</li>
|
|
<li><input disabled="" type="checkbox"> Spacing uses design tokens consistently</li>
|
|
<li><input disabled="" type="checkbox"> Typography hierarchy consistent</li>
|
|
<li><input disabled="" type="checkbox"> All interactive states implemented</li>
|
|
<li><input disabled="" type="checkbox"> All transitions smooth (60fps)</li>
|
|
<li><input disabled="" type="checkbox"> Copy is consistent and polished</li>
|
|
<li><input disabled="" type="checkbox"> Icons are consistent and properly sized</li>
|
|
<li><input disabled="" type="checkbox"> All forms properly labeled and validated</li>
|
|
<li><input disabled="" type="checkbox"> Error states are helpful</li>
|
|
<li><input disabled="" type="checkbox"> Loading states are clear</li>
|
|
<li><input disabled="" type="checkbox"> Empty states are welcoming</li>
|
|
<li><input disabled="" type="checkbox"> Touch targets are 44x44px minimum</li>
|
|
<li><input disabled="" type="checkbox"> Contrast ratios meet WCAG AA</li>
|
|
<li><input disabled="" type="checkbox"> Keyboard navigation works</li>
|
|
<li><input disabled="" type="checkbox"> Focus indicators visible</li>
|
|
<li><input disabled="" type="checkbox"> No console errors or warnings</li>
|
|
<li><input disabled="" type="checkbox"> No layout shift on load</li>
|
|
<li><input disabled="" type="checkbox"> Works in all supported browsers</li>
|
|
<li><input disabled="" type="checkbox"> Respects reduced motion preference</li>
|
|
<li><input disabled="" type="checkbox"> Code is clean (no TODOs, console.logs, commented code)</li>
|
|
</ul>
|
|
<p><strong>IMPORTANT</strong>: Polish is about details. Zoom in. Squint at it. Use it yourself. The little things add up.</p>
|
|
<p><strong>NEVER</strong>:</p>
|
|
<ul>
|
|
<li>Polish before it's functionally complete</li>
|
|
<li>Polish without aligning to the design system — that's decoration on drift</li>
|
|
<li>Guess at design system principles instead of asking when something is ambiguous</li>
|
|
<li>Spend hours on polish if it ships in 30 minutes (triage)</li>
|
|
<li>Introduce bugs while polishing (test thoroughly)</li>
|
|
<li>Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)</li>
|
|
<li>Perfect one thing while leaving others rough (consistent quality level)</li>
|
|
<li>Create new one-off components when design system equivalents exist</li>
|
|
<li>Hard-code values that should use design tokens</li>
|
|
<li>Introduce new patterns or flows that diverge from established ones</li>
|
|
</ul>
|
|
<h2 id="final-verification">Final Verification</h2>
|
|
<p>Before marking as done:</p>
|
|
<ul>
|
|
<li><strong>Use it yourself</strong>: Actually interact with the feature</li>
|
|
<li><strong>Test on real devices</strong>: Not just browser DevTools</li>
|
|
<li><strong>Ask someone else to review</strong>: Fresh eyes catch things</li>
|
|
<li><strong>Compare to design</strong>: Match intended design</li>
|
|
<li><strong>Check all states</strong>: Don't just test happy path</li>
|
|
</ul>
|
|
<h2 id="clean-up">Clean Up</h2>
|
|
<p>After polishing, ensure code quality:</p>
|
|
<ul>
|
|
<li><strong>Replace custom implementations</strong>: If the design system provides a component you reimplemented, switch to the shared version.</li>
|
|
<li><strong>Remove orphaned code</strong>: Delete unused styles, components, or files made obsolete by polish.</li>
|
|
<li><strong>Consolidate tokens</strong>: If you introduced new values, check whether they should be tokens.</li>
|
|
<li><strong>Verify DRYness</strong>: Look for duplication introduced during polishing and consolidate.</li>
|
|
</ul>
|
|
<p>Remember: You have impeccable attention to detail and exquisite taste. Polish until it feels effortless, looks intentional, and works flawlessly. Sweat the details - they matter.</p>
|
|
|
|
</div>
|
|
</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>
|