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>
515 lines
29 KiB
HTML
515 lines
29 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 layout | Impeccable</title>
|
|
<meta name="description" content="Fix layout, spacing, and visual rhythm.">
|
|
<meta name="theme-color" content="#fafafa">
|
|
<link rel="canonical" href="https://impeccable.style/docs/layout">
|
|
<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">/layout</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" aria-current="page">layout</a></li>
|
|
<li><a href="/docs/overdrive">overdrive</a></li>
|
|
<li><a href="/docs/quieter">quieter</a></li>
|
|
<li><a href="/docs/typeset">typeset</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="skills-sidebar-group" data-category="simplify">
|
|
<p class="skills-sidebar-group-title">Simplify</p>
|
|
<ul class="skills-sidebar-list">
|
|
<li><a href="/docs/adapt">adapt</a></li>
|
|
<li><a href="/docs/clarify">clarify</a></li>
|
|
<li><a href="/docs/distill">distill</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="skills-sidebar-group" data-category="harden">
|
|
<p class="skills-sidebar-group-title">Harden</p>
|
|
<ul class="skills-sidebar-list">
|
|
<li><a href="/docs/harden">harden</a></li>
|
|
<li><a href="/docs/onboard">onboard</a></li>
|
|
<li><a href="/docs/optimize">optimize</a></li>
|
|
<li><a href="/docs/polish">polish</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="skills-sidebar-group" data-category="system">
|
|
<p class="skills-sidebar-group-title">System</p>
|
|
<ul class="skills-sidebar-list">
|
|
<li><a href="/docs/document">document</a></li>
|
|
<li><a href="/docs/extract">extract</a></li>
|
|
<li><a href="/docs/live">live</a></li>
|
|
<li><a href="/docs/teach">teach</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
</aside>
|
|
<div class="skills-main">
|
|
|
|
<article class="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> / Refine</p>
|
|
<h1 class="skill-detail-title"><span class="skill-detail-title-namespace"><span class="skill-detail-title-slash">/</span>impeccable</span>layout</h1>
|
|
<p class="skill-detail-tagline">Fix layout, spacing, and visual rhythm.</p>
|
|
|
|
<div class="skill-meta-strip">
|
|
<span class="skill-meta-chip skill-meta-category" data-category="refine">Refine</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-layout">
|
|
<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;">
|
|
<div style="text-align: center; margin-bottom: 16px;">
|
|
<div style="font-size: 14px; font-weight: bold; color: #333;">Team Members</div>
|
|
</div>
|
|
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
<div style="padding: 16px; background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 8px; text-align: center;">
|
|
<div style="width: 32px; height: 32px; background: #ddd; border-radius: 50%; margin: 0 auto 8px;"></div>
|
|
<div style="font-size: 13px; color: #333;">Alice Chen</div>
|
|
<div style="font-size: 12px; color: #888;">Designer</div>
|
|
</div>
|
|
<div style="padding: 16px; background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 8px; text-align: center;">
|
|
<div style="width: 32px; height: 32px; background: #ddd; border-radius: 50%; margin: 0 auto 8px;"></div>
|
|
<div style="font-size: 13px; color: #333;">Bob Park</div>
|
|
<div style="font-size: 12px; color: #888;">Engineer</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="split-after">
|
|
<div class="split-content">
|
|
<div style="width: 100%; max-width: 240px; padding: 16px; font-family: 'Instrument Sans', sans-serif;">
|
|
<div style="font-size: 0.8125rem; font-weight: 600; color: var(--color-ink); margin-bottom: 16px;">Team Members</div>
|
|
<div style="display: flex; flex-direction: column; gap: 6px;">
|
|
<div style="display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--color-mist);">
|
|
<div style="width: 28px; height: 28px; background: var(--color-accent); border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 600;">AC</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-size: 0.8125rem; font-weight: 500; color: var(--color-ink);">Alice Chen</div>
|
|
<div style="font-size: 0.6875rem; color: var(--color-ash);">Designer</div>
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 12px; padding: 10px 0;">
|
|
<div style="width: 28px; height: 28px; background: color-mix(in oklch, var(--color-accent) 60%, var(--color-ink)); border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 600;">BP</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-size: 0.8125rem; font-weight: 500; color: var(--color-ink);">Bob Park</div>
|
|
<div style="font-size: 0.6875rem; color: var(--color-ash);">Engineer</div>
|
|
</div>
|
|
</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">Equal spacing everywhere → Intentional rhythm and hierarchy</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 layout</code> is for pages where nothing is technically wrong but nothing is breathing either. Equal padding everywhere, monotonous card grids, content that runs edge to edge, hierarchy that relies on size alone. Reach for it when a layout "feels off" and you cannot articulate why.</p>
|
|
<p>Good triggers: "everything feels crowded", "it reads like a wall", "I do not know where to look first".</p>
|
|
<h2 id="how-it-works">How it works</h2>
|
|
<p>The skill runs through five layout dimensions:</p>
|
|
<ol>
|
|
<li><strong>Spacing</strong>: is the spacing scale consistent or are there random 13px gaps, are related elements grouped tightly with generous space between groups, is there any rhythm at all.</li>
|
|
<li><strong>Visual hierarchy</strong>: does the eye land on the primary action within 2 seconds, is the hierarchy doing real work or is everything shouting.</li>
|
|
<li><strong>Grid and structure</strong>: is there an underlying grid or is the layout random, are elements aligned to baselines.</li>
|
|
<li><strong>Rhythm</strong>: does the page alternate between tight and generous spacing, or is everything uniform.</li>
|
|
<li><strong>Density</strong>: is the layout cramped or is it wasteful, does density match the content type.</li>
|
|
</ol>
|
|
<p>Fixes usually involve rebuilding the spacing scale, introducing asymmetry, collapsing monotonous grids into a mixed layout with hero and supporting elements, and giving the primary action real space.</p>
|
|
<h2 id="try-it">Try it</h2>
|
|
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable layout the settings page</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable layout the settings page" aria-label="Copy to clipboard"></button></div>
|
|
<p>Typical changes:</p>
|
|
<ul>
|
|
<li>Spacing scale unified to 8 / 16 / 24 / 48 / 96px</li>
|
|
<li>Section breaks at 48px, row gaps at 16px, form field groups at 8px</li>
|
|
<li>Primary actions pulled out of the form flow with 32px buffer</li>
|
|
<li>Decorative borders removed, replaced with spacing-driven grouping</li>
|
|
<li>Sidebar and main column proportions rebalanced (280 / flex vs 25 / 75)</li>
|
|
</ul>
|
|
<h2 id="pitfalls">Pitfalls</h2>
|
|
<ul>
|
|
<li><strong>Confusing arrange with distill.</strong> If the problem is too many things, run <code>/impeccable distill</code> first. Layout is for arranging what is already the right set.</li>
|
|
<li><strong>Expecting it to rescue a broken grid.</strong> If the page has no grid at all, arrange will build one. Just know that the diff is going to be larger than you expect.</li>
|
|
<li><strong>Ignoring the hierarchy verdict.</strong> If arrange says "nothing is primary", no amount of spacing work fixes that. You need a content decision, not a layout tweak.</li>
|
|
</ul>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
<section class="skill-source-card">
|
|
<header class="skill-source-card-header">
|
|
<span class="skill-source-card-label">reference/layout.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">
|
|
<p>Assess and improve layout and spacing that feels monotonous, crowded, or structurally weak — turning generic arrangements into intentional, rhythmic compositions.</p>
|
|
<hr>
|
|
<h2 id="register">Register</h2>
|
|
<p>Brand: asymmetric compositions, fluid spacing with <code>clamp()</code>, intentional grid-breaking for emphasis. Rhythm through contrast — tight groupings paired with generous separations.</p>
|
|
<p>Product: predictable grids, consistent densities, familiar navigation patterns. Responsive behavior is structural (collapse sidebar, responsive table), not fluid typography. Consistency IS an affordance.</p>
|
|
<hr>
|
|
<h2 id="assess-current-layout">Assess Current Layout</h2>
|
|
<p>Analyze what's weak about the current spatial design:</p>
|
|
<ol>
|
|
<li><p><strong>Spacing</strong>:</p>
|
|
<ul>
|
|
<li>Is spacing consistent or arbitrary? (Random padding/margin values)</li>
|
|
<li>Is all spacing the same? (Equal padding everywhere = no rhythm)</li>
|
|
<li>Are related elements grouped tightly, with generous space between groups?</li>
|
|
</ul>
|
|
</li>
|
|
<li><p><strong>Visual hierarchy</strong>:</p>
|
|
<ul>
|
|
<li>Apply the squint test: blur your (metaphorical) eyes — can you still identify the most important element, second most important, and clear groupings?</li>
|
|
<li>Is hierarchy achieved effectively? (Space and weight alone can be enough — but is the current approach working?)</li>
|
|
<li>Does whitespace guide the eye to what matters?</li>
|
|
</ul>
|
|
</li>
|
|
<li><p><strong>Grid & structure</strong>:</p>
|
|
<ul>
|
|
<li>Is there a clear underlying structure, or does the layout feel random?</li>
|
|
<li>Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly)</li>
|
|
<li>Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule)</li>
|
|
</ul>
|
|
</li>
|
|
<li><p><strong>Rhythm & variety</strong>:</p>
|
|
<ul>
|
|
<li>Does the layout have visual rhythm? (Alternating tight/generous spacing)</li>
|
|
<li>Is every section structured the same way? (Monotonous repetition)</li>
|
|
<li>Are there intentional moments of surprise or emphasis?</li>
|
|
</ul>
|
|
</li>
|
|
<li><p><strong>Density</strong>:</p>
|
|
<ul>
|
|
<li>Is the layout too cramped? (Not enough breathing room)</li>
|
|
<li>Is the layout too sparse? (Excessive whitespace without purpose)</li>
|
|
<li>Does density match the content type? (Data-dense UIs need tighter spacing; marketing pages need more air)</li>
|
|
</ul>
|
|
</li>
|
|
</ol>
|
|
<p><strong>CRITICAL</strong>: Layout problems are often the root cause of interfaces feeling "off" even when colors and fonts are fine. Space is a design material — use it with intention.</p>
|
|
<h2 id="plan-layout-improvements">Plan Layout Improvements</h2>
|
|
<p>Consult the <a href="spatial-design.md">spatial design reference</a> for detailed guidance on grids, rhythm, and container queries.</p>
|
|
<p>Create a systematic plan:</p>
|
|
<ul>
|
|
<li><strong>Spacing system</strong>: Use a consistent scale — whether that's a framework's built-in scale (e.g., Tailwind), rem-based tokens, or a custom system. The specific values matter less than consistency.</li>
|
|
<li><strong>Hierarchy strategy</strong>: How will space communicate importance?</li>
|
|
<li><strong>Layout approach</strong>: What structure fits the content? Flex for 1D, Grid for 2D, named areas for complex page layouts.</li>
|
|
<li><strong>Rhythm</strong>: Where should spacing be tight vs generous?</li>
|
|
</ul>
|
|
<h2 id="improve-layout-systematically">Improve Layout Systematically</h2>
|
|
<h3 id="establish-a-spacing-system">Establish a Spacing System</h3>
|
|
<ul>
|
|
<li>Use a consistent spacing scale — framework scales (Tailwind, etc.), rem-based tokens, or a custom scale all work. What matters is that values come from a defined set, not arbitrary numbers.</li>
|
|
<li>Name tokens semantically if using custom properties: <code>--space-xs</code> through <code>--space-xl</code>, not <code>--spacing-8</code></li>
|
|
<li>Use <code>gap</code> for sibling spacing instead of margins — eliminates margin collapse hacks</li>
|
|
<li>Apply <code>clamp()</code> for fluid spacing that breathes on larger screens</li>
|
|
</ul>
|
|
<h3 id="create-visual-rhythm">Create Visual Rhythm</h3>
|
|
<ul>
|
|
<li><strong>Tight grouping</strong> for related elements (8-12px between siblings)</li>
|
|
<li><strong>Generous separation</strong> between distinct sections (48-96px)</li>
|
|
<li><strong>Varied spacing</strong> within sections — not every row needs the same gap</li>
|
|
<li><strong>Asymmetric compositions</strong> — break the predictable centered-content pattern when it makes sense</li>
|
|
</ul>
|
|
<h3 id="choose-the-right-layout-tool">Choose the Right Layout Tool</h3>
|
|
<ul>
|
|
<li><strong>Use Flexbox for 1D layouts</strong>: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks.</li>
|
|
<li><strong>Use Grid for 2D layouts</strong>: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control.</li>
|
|
<li><strong>Don't default to Grid</strong> when Flexbox with <code>flex-wrap</code> would be simpler and more flexible.</li>
|
|
<li>Use <code>repeat(auto-fit, minmax(280px, 1fr))</code> for responsive grids without breakpoints.</li>
|
|
<li>Use named grid areas (<code>grid-template-areas</code>) for complex page layouts — redefine at breakpoints.</li>
|
|
</ul>
|
|
<h3 id="break-card-grid-monotony">Break Card Grid Monotony</h3>
|
|
<ul>
|
|
<li>Don't default to card grids for everything — spacing and alignment create visual grouping naturally</li>
|
|
<li>Use cards only when content is truly distinct and actionable — never nest cards inside cards</li>
|
|
<li>Vary card sizes, span columns, or mix cards with non-card content to break repetition</li>
|
|
</ul>
|
|
<h3 id="strengthen-visual-hierarchy">Strengthen Visual Hierarchy</h3>
|
|
<ul>
|
|
<li>Use the fewest dimensions needed for clear hierarchy. Space alone can be enough — generous whitespace around an element draws the eye. Some of the most sophisticated designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient.</li>
|
|
<li>Be aware of reading flow — in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation).</li>
|
|
<li>Create clear content groupings through proximity and separation.</li>
|
|
</ul>
|
|
<h3 id="manage-depth-elevation">Manage Depth & Elevation</h3>
|
|
<ul>
|
|
<li>Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip)</li>
|
|
<li>Build a consistent shadow scale (sm → md → lg → xl) — shadows should be subtle</li>
|
|
<li>Use elevation to reinforce hierarchy, not as decoration</li>
|
|
</ul>
|
|
<h3 id="optical-adjustments">Optical Adjustments</h3>
|
|
<ul>
|
|
<li>If an icon looks visually off-center despite being geometrically centered, nudge it — but only if you're confident it actually looks wrong. Don't adjust speculatively.</li>
|
|
</ul>
|
|
<p><strong>NEVER</strong>:</p>
|
|
<ul>
|
|
<li>Use arbitrary spacing values outside your scale</li>
|
|
<li>Make all spacing equal — variety creates hierarchy</li>
|
|
<li>Wrap everything in cards — not everything needs a container</li>
|
|
<li>Nest cards inside cards — use spacing and dividers for hierarchy within</li>
|
|
<li>Use identical card grids everywhere (icon + heading + text, repeated)</li>
|
|
<li>Center everything — left-aligned with asymmetry feels more designed</li>
|
|
<li>Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work — but it should display actual data, not decorative numbers.</li>
|
|
<li>Default to CSS Grid when Flexbox would be simpler — use the simplest tool for the job</li>
|
|
<li>Use arbitrary z-index values (999, 9999) — build a semantic scale</li>
|
|
</ul>
|
|
<h2 id="verify-layout-improvements">Verify Layout Improvements</h2>
|
|
<ul>
|
|
<li><strong>Squint test</strong>: Can you identify primary, secondary, and groupings with blurred vision?</li>
|
|
<li><strong>Rhythm</strong>: Does the page have a satisfying beat of tight and generous spacing?</li>
|
|
<li><strong>Hierarchy</strong>: Is the most important content obvious within 2 seconds?</li>
|
|
<li><strong>Breathing room</strong>: Does the layout feel comfortable, not cramped or wasteful?</li>
|
|
<li><strong>Consistency</strong>: Is the spacing system applied uniformly?</li>
|
|
<li><strong>Responsiveness</strong>: Does the layout adapt gracefully across screen sizes?</li>
|
|
</ul>
|
|
<p>Remember: Space is the most underused design tool. A layout with the right rhythm and hierarchy can make even simple content feel polished and intentional.</p>
|
|
<h2 id="live-mode-signature-params">Live-mode signature params</h2>
|
|
<p>Each variant MUST declare a <code>density</code> param. Drive all spacing tokens in the variant's scoped CSS through <code>calc(var(--p-density, 1) * <base>)</code> — paddings, gaps, column widths. Users slide from airy to packed and see layout re-breathe with no regeneration.</p>
|
|
<div class="code-block-wrap"><pre class="code-block code-block--json"><code>{"id":"density","kind":"range","min":0.6,"max":1.4,"step":0.05,"default":1,"label":"Density"}</code></pre><button class="code-block-copy" type="button" data-copy="{"id":"density","kind":"range","min":0.6,"max":1.4,"step":0.05,"default":1,"label":"Density"}" aria-label="Copy to clipboard"></button></div>
|
|
<p>For variants whose topology genuinely changes (stacked vs. side-by-side, grid vs. bento), use a <code>steps</code> param whose scoped CSS branches via <code>:scope[data-p-structure="X"]</code>. One structure param + one density param is a powerful combo; resist adding a third.</p>
|
|
<div class="code-block-wrap"><pre class="code-block code-block--json"><code>{"id":"structure","kind":"steps","default":"grid","label":"Structure","options":[
|
|
{"value":"stacked","label":"Stacked"},
|
|
{"value":"grid","label":"Grid"},
|
|
{"value":"bento","label":"Bento"}
|
|
]}</code></pre><button class="code-block-copy" type="button" data-copy="{"id":"structure","kind":"steps","default":"grid","label":"Structure","options":[
|
|
{"value":"stacked","label":"Stacked"},
|
|
{"value":"grid","label":"Grid"},
|
|
{"value":"bento","label":"Bento"}
|
|
]}" aria-label="Copy to clipboard"></button></div>
|
|
<p>See <code>reference/live.md</code> for the full params contract.</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>
|