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>
476 lines
26 KiB
HTML
476 lines
26 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 extract | Impeccable</title>
|
|
<meta name="description" content="Pull reusable components, tokens, and patterns into the design system.">
|
|
<meta name="theme-color" content="#fafafa">
|
|
<link rel="canonical" href="https://impeccable.style/docs/extract">
|
|
<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">/extract</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" aria-current="page">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> / System</p>
|
|
<h1 class="skill-detail-title"><span class="skill-detail-title-namespace"><span class="skill-detail-title-slash">/</span>impeccable</span>extract</h1>
|
|
<p class="skill-detail-tagline">Pull reusable components, tokens, and patterns into the design system.</p>
|
|
|
|
<div class="skill-meta-strip">
|
|
<span class="skill-meta-chip skill-meta-category" data-category="system">System</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-extract">
|
|
<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: 280px; display: flex; flex-direction: column; gap: 6px; font-family: monospace; font-size: 11px;">
|
|
<div style="padding: 8px; background: #1e1e1e; color: #d4d4d4; border-radius: 4px; overflow: hidden;">
|
|
<span style="color: #9cdcfe;">padding</span><span style="color: #d4d4d4;">: </span><span style="color: #ce9178;">12px 16px</span><span style="color: #d4d4d4;">;</span>
|
|
</div>
|
|
<div style="padding: 8px; background: #1e1e1e; color: #d4d4d4; border-radius: 4px; overflow: hidden;">
|
|
<span style="color: #9cdcfe;">padding</span><span style="color: #d4d4d4;">: </span><span style="color: #ce9178;">18px 24px</span><span style="color: #d4d4d4;">;</span>
|
|
</div>
|
|
<div style="padding: 8px; background: #1e1e1e; color: #d4d4d4; border-radius: 4px; overflow: hidden;">
|
|
<span style="color: #9cdcfe;">color</span><span style="color: #d4d4d4;">: </span><span style="color: #ce9178;">#3b82f6</span><span style="color: #d4d4d4;">;</span>
|
|
</div>
|
|
<div style="padding: 8px; background: #1e1e1e; color: #d4d4d4; border-radius: 4px; overflow: hidden;">
|
|
<span style="color: #9cdcfe;">color</span><span style="color: #d4d4d4;">: </span><span style="color: #ce9178;">#3a80f5</span><span style="color: #d4d4d4;">;</span>
|
|
</div>
|
|
<div style="padding: 8px; background: #1e1e1e; color: #d4d4d4; border-radius: 4px; overflow: hidden;">
|
|
<span style="color: #9cdcfe;">font-size</span><span style="color: #d4d4d4;">: </span><span style="color: #ce9178;">14px</span><span style="color: #d4d4d4;">;</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="split-after">
|
|
<div class="split-content">
|
|
<div style="width: 100%; max-width: 280px; display: flex; flex-direction: column; gap: 8px;">
|
|
<div style="font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--color-ash); margin-bottom: 4px;">Design Tokens</div>
|
|
<div style="padding: 10px; background: var(--color-paper); border: 1px solid var(--color-mist); border-radius: 6px;">
|
|
<div style="font-size: 10px; color: var(--color-ash); margin-bottom: 6px;">SPACING</div>
|
|
<div style="display: flex; gap: 8px;">
|
|
<div style="text-align: center;">
|
|
<div style="width: 24px; height: 8px; background: var(--color-accent); border-radius: 2px; margin-bottom: 4px;"></div>
|
|
<span style="font-family: monospace; font-size: 9px; color: var(--color-charcoal);">sm</span>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<div style="width: 32px; height: 8px; background: var(--color-accent); border-radius: 2px; margin-bottom: 4px;"></div>
|
|
<span style="font-family: monospace; font-size: 9px; color: var(--color-charcoal);">md</span>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<div style="width: 48px; height: 8px; background: var(--color-accent); border-radius: 2px; margin-bottom: 4px;"></div>
|
|
<span style="font-family: monospace; font-size: 9px; color: var(--color-charcoal);">lg</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="padding: 10px; background: var(--color-paper); border: 1px solid var(--color-mist); border-radius: 6px;">
|
|
<div style="font-size: 10px; color: var(--color-ash); margin-bottom: 6px;">COLORS</div>
|
|
<div style="display: flex; gap: 6px;">
|
|
<div style="width: 24px; height: 24px; background: var(--color-ink); border-radius: 4px;" title="ink"></div>
|
|
<div style="width: 24px; height: 24px; background: var(--color-charcoal); border-radius: 4px;" title="charcoal"></div>
|
|
<div style="width: 24px; height: 24px; background: var(--color-accent); border-radius: 4px;" title="accent"></div>
|
|
<div style="width: 24px; height: 24px; background: var(--color-mist); border-radius: 4px; border: 1px solid var(--color-ash);" title="mist"></div>
|
|
</div>
|
|
</div>
|
|
<div style="padding: 10px; background: var(--color-paper); border: 1px solid var(--color-mist); border-radius: 6px;">
|
|
<div style="font-size: 10px; color: var(--color-ash); margin-bottom: 6px;">TYPOGRAPHY</div>
|
|
<div style="font-family: var(--font-display); font-size: 16px; font-style: italic; color: var(--color-ink);">Display</div>
|
|
<div style="font-size: 12px; color: var(--color-charcoal);">Body text</div>
|
|
</div>
|
|
</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">Scattered styles → Documented design tokens</p>
|
|
<span class="split-label-item" data-point="after">After</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<section class="skill-detail-editorial prose">
|
|
<div class="docs-viz-hero">
|
|
<div class="docs-viz-flow">
|
|
<div class="docs-viz-flow-step">
|
|
<span class="docs-viz-flow-num">01</span>
|
|
<span class="docs-viz-flow-name">Discover drift</span>
|
|
<span class="docs-viz-flow-hint">Repeated hex values, button variants, spacing scales, text styles.</span>
|
|
</div>
|
|
<div class="docs-viz-flow-step">
|
|
<span class="docs-viz-flow-num">02</span>
|
|
<span class="docs-viz-flow-name">Propose primitives</span>
|
|
<span class="docs-viz-flow-hint">Token names, component APIs with variant + size, text styles.</span>
|
|
</div>
|
|
<div class="docs-viz-flow-step docs-viz-flow-step--accent">
|
|
<span class="docs-viz-flow-num">03</span>
|
|
<span class="docs-viz-flow-name">Migrate call sites</span>
|
|
<span class="docs-viz-flow-hint">Replace duplicated CSS with the new primitives. No orphan code left behind.</span>
|
|
</div>
|
|
</div>
|
|
<p class="docs-viz-caption">The skill only extracts what's used three or more times with the same intent. Two usages are not a pattern, and migration always happens in the same pass.</p>
|
|
</div>
|
|
|
|
<h2 id="when-to-use-it">When to use it</h2>
|
|
<p><code>/impeccable extract</code> is for the moment your codebase has accidentally become a design system. Repeated button styles in 12 places. Three variants of the same card. Hex colors scattered throughout. Hand-rolled spacing that accidentally matches a scale. Reach for it when you want to consolidate this drift into reusable primitives.</p>
|
|
<p>Use it after a product has shipped enough features to reveal the patterns. Premature extraction creates abstractions that do not match reality.</p>
|
|
<h2 id="how-it-works">How it works</h2>
|
|
<p>The skill discovers the design system structure first, then identifies extraction opportunities:</p>
|
|
<ol>
|
|
<li><strong>Tokens</strong>: find repeated literal values (colors, spacing, radii, shadows, font sizes). Propose token names, add to the token system, replace usages.</li>
|
|
<li><strong>Components</strong>: find UI patterns that repeat with minor variation (buttons, cards, inputs, modals). Extract into a single component with variants, migrate callers.</li>
|
|
<li><strong>Composition patterns</strong>: find layout or interaction patterns that repeat (form rows, toolbar groups, empty states). Extract into composition primitives.</li>
|
|
<li><strong>Type styles</strong>: find repeated font-size + weight + line-height combinations. Extract into text styles.</li>
|
|
<li><strong>Animation patterns</strong>: find repeated easing, duration, or keyframe combinations. Extract into motion tokens.</li>
|
|
</ol>
|
|
<p>The skill is cautious. It only extracts things used three or more times, with the same intent. It never extracts "because it might be reused later". Premature abstraction is worse than duplication.</p>
|
|
<h2 id="try-it">Try it</h2>
|
|
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable extract the button styles</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable extract the button styles" aria-label="Copy to clipboard"></button></div>
|
|
<p>Expected output:</p>
|
|
<ul>
|
|
<li>Found 14 button instances across 8 files</li>
|
|
<li>4 distinct variants: primary (filled accent), secondary (bordered), ghost (text-only), destructive (filled red)</li>
|
|
<li>All 4 variants use the same size scale (small, default, large)</li>
|
|
<li>Extracted into <code><Button variant="primary" size="default"></code> with token-driven styles</li>
|
|
<li>Migrated 14 call sites, removed ~180 lines of duplicated CSS</li>
|
|
<li>Added 3 missing tokens: <code>--button-radius</code>, <code>--button-padding-y</code>, <code>--button-padding-x</code></li>
|
|
</ul>
|
|
<h2 id="pitfalls">Pitfalls</h2>
|
|
<ul>
|
|
<li><strong>Extracting too early.</strong> Two usages are not a pattern. Three might be. Wait until the pattern is obvious.</li>
|
|
<li><strong>Over-generalizing.</strong> The extracted component should match the current use cases closely, not anticipate every possible future one. You can always add variants later.</li>
|
|
<li><strong>Forgetting the migration.</strong> Extraction without migration leaves the old duplicated code around and creates a third way of doing the same thing. Always migrate in the same pass.</li>
|
|
<li><strong>Extracting things that differ in intent.</strong> Two buttons that look similar but serve different purposes (primary action vs link styled as button) should probably stay separate.</li>
|
|
</ul>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
<section class="skill-source-card">
|
|
<header class="skill-source-card-header">
|
|
<span class="skill-source-card-label">reference/extract.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">
|
|
<h1 id="extract-flow">Extract Flow</h1>
|
|
<p>Identify reusable patterns, components, and design tokens, then extract and consolidate them into the design system for systematic reuse.</p>
|
|
<h2 id="step-1-discover-the-design-system">Step 1: Discover the Design System</h2>
|
|
<p>Find the design system, component library, or shared UI directory. Understand its structure: component organization, naming conventions, design token structure, import/export conventions.</p>
|
|
<p><strong>CRITICAL</strong>: If no design system exists, STOP and call the AskUserQuestion tool to clarify. before creating one. Understand the preferred location and structure first.</p>
|
|
<h2 id="step-2-identify-patterns">Step 2: Identify Patterns</h2>
|
|
<p>Look for extraction opportunities in the target area:</p>
|
|
<ul>
|
|
<li><strong>Repeated components</strong>: Similar UI patterns used 3+ times (buttons, cards, inputs)</li>
|
|
<li><strong>Hard-coded values</strong>: Colors, spacing, typography, shadows that should be tokens</li>
|
|
<li><strong>Inconsistent variations</strong>: Multiple implementations of the same concept</li>
|
|
<li><strong>Composition patterns</strong>: Layout or interaction patterns that repeat (form rows, toolbar groups, empty states)</li>
|
|
<li><strong>Type styles</strong>: Repeated font-size + weight + line-height combinations</li>
|
|
<li><strong>Animation patterns</strong>: Repeated easing, duration, or keyframe combinations</li>
|
|
</ul>
|
|
<p>Assess value: only extract things used 3+ times with the same intent. Premature abstraction is worse than duplication.</p>
|
|
<h2 id="step-3-plan-extraction">Step 3: Plan Extraction</h2>
|
|
<p>Create a systematic plan:</p>
|
|
<ul>
|
|
<li><strong>Components to extract</strong>: Which UI elements become reusable components?</li>
|
|
<li><strong>Tokens to create</strong>: Which hard-coded values become design tokens?</li>
|
|
<li><strong>Variants to support</strong>: What variations does each component need?</li>
|
|
<li><strong>Naming conventions</strong>: Component names, token names, prop names that match existing patterns</li>
|
|
<li><strong>Migration path</strong>: How to refactor existing uses to consume the new shared versions</li>
|
|
</ul>
|
|
<p><strong>IMPORTANT</strong>: Design systems grow incrementally. Extract what is clearly reusable now, not everything that might someday be reusable.</p>
|
|
<h2 id="step-4-extract-enrich">Step 4: Extract & Enrich</h2>
|
|
<p>Build improved, reusable versions:</p>
|
|
<ul>
|
|
<li><strong>Components</strong>: Clear props API with sensible defaults, proper variants for different use cases, accessibility built in (ARIA, keyboard navigation, focus management), documentation and usage examples</li>
|
|
<li><strong>Design tokens</strong>: Clear naming (primitive vs semantic), proper hierarchy and organization, documentation of when to use each token</li>
|
|
<li><strong>Patterns</strong>: When to use this pattern, code examples, variations and combinations</li>
|
|
</ul>
|
|
<h2 id="step-5-migrate">Step 5: Migrate</h2>
|
|
<p>Replace existing uses with the new shared versions:</p>
|
|
<ul>
|
|
<li><strong>Find all instances</strong>: Search for the patterns you extracted</li>
|
|
<li><strong>Replace systematically</strong>: Update each use to consume the shared version</li>
|
|
<li><strong>Test thoroughly</strong>: Ensure visual and functional parity</li>
|
|
<li><strong>Delete dead code</strong>: Remove the old implementations</li>
|
|
</ul>
|
|
<h2 id="step-6-document">Step 6: Document</h2>
|
|
<p>Update design system documentation:</p>
|
|
<ul>
|
|
<li>Add new components to the component library</li>
|
|
<li>Document token usage and values</li>
|
|
<li>Add examples and guidelines</li>
|
|
<li>Update any Storybook or component catalog</li>
|
|
</ul>
|
|
<p><strong>NEVER</strong>:</p>
|
|
<ul>
|
|
<li>Extract one-off, context-specific implementations without generalization</li>
|
|
<li>Create components so generic they are useless</li>
|
|
<li>Extract without considering existing design system conventions</li>
|
|
<li>Skip proper TypeScript types or prop documentation</li>
|
|
<li>Create tokens for every single value (tokens should have semantic meaning)</li>
|
|
<li>Extract things that differ in intent (two buttons that look similar but serve different purposes should stay separate)</li>
|
|
</ul>
|
|
<p>Remember: A good design system is a living system. Extract patterns as they emerge, enrich them thoughtfully, and maintain them consistently.</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>
|