mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
design-lint: mark hallmark PAGE_ONLY gates and craft page checks pageOnly so --scope component drops them
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a088fc3d8c
commit
54d415bd2d
1 file changed
+36
-36
@@ -57,44 +57,44 @@ export const RULES: Record<string, RuleMeta> = {
|
||||
'tiny-text': { tier: 'ask', gate: '75', title: 'running text under 12px, functional text under 11px, or prose leaded under 1.3', fix: 'body 16px at 1.5 to 1.65; labels one step smaller', render: true },
|
||||
'wide-tracking': { tier: 'blocks', gate: '67', title: 'the tracked-caps label: small text set uppercase or letter-spaced past 0.03em', fix: 'the body face, a step smaller, sentence case, untracked' },
|
||||
// ── Structure and layout ──
|
||||
'template-cards': { tier: 'ask', gate: '3', title: 'three equal icon-above-heading tiles, an icon in a rounded tile, or a thick coloured side stripe', fix: 'vary widths and heights, inline icons, hairline or no border' },
|
||||
'template-cards': { tier: 'ask', gate: '3', title: 'three equal icon-above-heading tiles, an icon in a rounded tile, or a thick coloured side stripe', fix: 'vary widths and heights, inline icons, hairline or no border', pageOnly: true },
|
||||
'nested-cards': { tier: 'blocks', gate: '4', title: 'a card inside a card', fix: 'one containment layer' },
|
||||
'vh-height': { tier: 'blocks', gate: '6a', title: 'a hero sized in vh', fix: 'dvh or svh' },
|
||||
'raw-spacing': { tier: 'blocks', gate: '24a', title: 'a raw padding, margin or gap length where a --space-* token exists', fix: 'the token' },
|
||||
'off-scale-spacing': { tier: 'ask', gate: '24b', title: 'spacing off the page\'s scale', fix: 'the scale; three optical nudges at most' },
|
||||
'overflow-x-unclipped': { tier: 'ask', gate: '34', title: 'overflow-x missing on html and body, or a 100vw width', fix: 'overflow-x: clip on both; width: 100%' },
|
||||
'horizontal-scroll': { tier: 'blocks', gate: '34', title: 'horizontal scroll at a width between 320 and 1920', fix: 'find the element wider than its viewport and cap it', render: true },
|
||||
'hero-balance': { tier: 'ask', gate: '44a/44c', title: 'the hero\'s content sits off centre for its screen, or its visual stands apart from the text', fix: 'centre the content in its screen; put the visual beside the text', render: true },
|
||||
'hero-fold': { tier: 'blocks', gate: '44b', title: 'headline, lede, primary action and visual not all visible at 1280x800', fix: 'pull the display clamp down, tighten leading, a two-line lede, trim padding', render: true },
|
||||
'wide-container': { tier: 'blocks', gate: '44d', title: 'at 1920x1080 the container has no max-width, gutters under 120px, or a grid gains columns', fix: 'cap 1360 to 1520px and centre; hold column counts; cap vw visuals' },
|
||||
'bare-fr-track': { tier: 'blocks', gate: '50', title: 'an image-bearing grid track on bare 1fr', fix: 'minmax(0, 1fr)' },
|
||||
'display-head-unwrapped': { tier: 'blocks', gate: '50', title: 'a display head without overflow-wrap: break-word and min-width: 0', fix: 'overflow-wrap: break-word; min-width: 0 (never anywhere)' },
|
||||
'overflow-x-unclipped': { tier: 'ask', gate: '34', title: 'overflow-x missing on html and body, or a 100vw width', fix: 'overflow-x: clip on both; width: 100%', pageOnly: true },
|
||||
'horizontal-scroll': { tier: 'blocks', gate: '34', title: 'horizontal scroll at a width between 320 and 1920', fix: 'find the element wider than its viewport and cap it', render: true, pageOnly: true },
|
||||
'hero-balance': { tier: 'ask', gate: '44a/44c', title: 'the hero\'s content sits off centre for its screen, or its visual stands apart from the text', fix: 'centre the content in its screen; put the visual beside the text', render: true, pageOnly: true },
|
||||
'hero-fold': { tier: 'blocks', gate: '44b', title: 'headline, lede, primary action and visual not all visible at 1280x800', fix: 'pull the display clamp down, tighten leading, a two-line lede, trim padding', render: true, pageOnly: true },
|
||||
'wide-container': { tier: 'blocks', gate: '44d', title: 'at 1920x1080 the container has no max-width, gutters under 120px, or a grid gains columns', fix: 'cap 1360 to 1520px and centre; hold column counts; cap vw visuals', pageOnly: true },
|
||||
'bare-fr-track': { tier: 'blocks', gate: '50', title: 'an image-bearing grid track on bare 1fr', fix: 'minmax(0, 1fr)', pageOnly: true },
|
||||
'display-head-unwrapped': { tier: 'blocks', gate: '50', title: 'a display head without overflow-wrap: break-word and min-width: 0', fix: 'overflow-wrap: break-word; min-width: 0 (never anywhere)', pageOnly: true },
|
||||
'radio-tab-jump': { tier: 'blocks', gate: '53', title: 'CSS-only radio tabs at position: absolute; top: 0 with no JS guard', fix: 'radios in flow at zero size, or a handler with preventScroll' },
|
||||
'double-sticky': { tier: 'blocks', gate: '56', title: 'two position: sticky; top: 0 elements', fix: 'top: var(--banner-height) on the second' },
|
||||
'screen-word-budget': { tier: 'ask', gate: '58', title: 'more than about 80 words in one 900px window, or a hero with more than two text blocks before its actions', fix: 'headline, one sentence, actions; a section is a head block, then its composition' },
|
||||
'double-sticky': { tier: 'blocks', gate: '56', title: 'two position: sticky; top: 0 elements', fix: 'top: var(--banner-height) on the second', pageOnly: true },
|
||||
'screen-word-budget': { tier: 'ask', gate: '58', title: 'more than about 80 words in one 900px window, or a hero with more than two text blocks before its actions', fix: 'headline, one sentence, actions; a section is a head block, then its composition', pageOnly: true },
|
||||
'section-gap': { tier: 'ask', gate: '59', title: 'a gap under 120px between sections at 1440, every gap the same, or more than nine sections', fix: '120px at least, varied; six to nine sections a page', pageOnly: true, render: true },
|
||||
'heading-rhythm': { tier: 'ask', gate: '71', title: 'a heading nearer what precedes it than what it heads', fix: 'at least twice the space above a head as below it', render: true },
|
||||
'heading-rhythm': { tier: 'ask', gate: '71', title: 'a heading nearer what precedes it than what it heads', fix: 'at least twice the space above a head as below it', render: true, pageOnly: true },
|
||||
'cramped-padding': { tier: 'ask', gate: '72', title: 'text closer than 8px (or half an em) to the side of its filled box', fix: 'pad the box; a 16px gutter', render: true },
|
||||
'thin-section': { tier: 'ask', gate: '77', title: 'a section below the hero with no mass: a narrow body on a wide canvas', fix: 'widen the body, add a visual, set a statement, or merge the section', render: true },
|
||||
'small-section-heads': { tier: 'ask', gate: '78', title: 'the median h2 below the hero renders under 40px at 1440', fix: 'h2 at the section size, item titles at the title size', render: true },
|
||||
'dead-band': { tier: 'ask', gate: '79', title: 'more than 160px of nothing between the hero\'s lowest content and the fold', fix: 'centre the content and grow the visual or the head; never fill with copy', render: true },
|
||||
'hand-drawn-svg': { tier: 'ask', gate: '80', title: 'an inline SVG of 120px or more with three curves or fills and no text: a drawn illustration', fix: 'a photograph, the product built, or type at scale', render: true },
|
||||
'empty-picture': { tier: 'ask', gate: '81', title: 'a slot or gradient block of 300x200px or more showing nothing but its wash', fix: 'the user\'s image, a credited photograph, or the product at full fidelity', render: true },
|
||||
'counting-marks': { tier: 'ask', gate: '82', title: '24 or more identical marks standing for a quantity', fix: 'set the number in type', render: true },
|
||||
'photo-wall': { tier: 'ask', gate: '83', title: 'photographs cover more than a third of the page below the hero', fix: 'keep the strongest pictures, set big; carry the rest in type, figures, the product, a table', render: true },
|
||||
'thin-section': { tier: 'ask', gate: '77', title: 'a section below the hero with no mass: a narrow body on a wide canvas', fix: 'widen the body, add a visual, set a statement, or merge the section', render: true, pageOnly: true },
|
||||
'small-section-heads': { tier: 'ask', gate: '78', title: 'the median h2 below the hero renders under 40px at 1440', fix: 'h2 at the section size, item titles at the title size', render: true, pageOnly: true },
|
||||
'dead-band': { tier: 'ask', gate: '79', title: 'more than 160px of nothing between the hero\'s lowest content and the fold', fix: 'centre the content and grow the visual or the head; never fill with copy', render: true, pageOnly: true },
|
||||
'hand-drawn-svg': { tier: 'ask', gate: '80', title: 'an inline SVG of 120px or more with three curves or fills and no text: a drawn illustration', fix: 'a photograph, the product built, or type at scale', render: true, pageOnly: true },
|
||||
'empty-picture': { tier: 'ask', gate: '81', title: 'a slot or gradient block of 300x200px or more showing nothing but its wash', fix: 'the user\'s image, a credited photograph, or the product at full fidelity', render: true, pageOnly: true },
|
||||
'counting-marks': { tier: 'ask', gate: '82', title: '24 or more identical marks standing for a quantity', fix: 'set the number in type', render: true, pageOnly: true },
|
||||
'photo-wall': { tier: 'ask', gate: '83', title: 'photographs cover more than a third of the page below the hero', fix: 'keep the strongest pictures, set big; carry the rest in type, figures, the product, a table', render: true, pageOnly: true },
|
||||
'email-band-close': { tier: 'ask', gate: '85', title: 'the page ends on an email sign-up form on a page whose job is not subscribe, join, attend, listen or visit', fix: 'end on what the visitor came to do, or on the last strong section', pageOnly: true },
|
||||
// ── Chrome and copy ──
|
||||
'aphoristic-cadence': { tier: 'blocks', gate: '19', title: 'the rebuttal cadence three or more times ("Not a feature. A platform.")', fix: 'say what the product does, in its own nouns' },
|
||||
'generic-hero-copy': { tier: 'blocks', gate: '19', title: 'a filler opener ("Built for the modern team", "In today\'s landscape") or "quietly trusted by"', fix: 'say what the product does, in its own nouns' },
|
||||
'generic-cta-copy': { tier: 'ask', gate: '19/42', title: 'a stock second action beside the primary (Learn more, Get started, Book a demo)', fix: 'one action, or a second that names where it goes' },
|
||||
'generic-cta-copy': { tier: 'ask', gate: '19/42', title: 'a stock second action beside the primary (Learn more, Get started, Book a demo)', fix: 'one action, or a second that names where it goes', pageOnly: true },
|
||||
'em-dash-overuse': { tier: 'ask', gate: '19', title: 'em or en dashes carrying the prose', fix: 'a comma, a colon, a full stop or a line break' },
|
||||
'template-footer-line': { tier: 'ask', gate: '19', title: 'a stock footer line (All rights reserved)', fix: 'the page\'s own line, or nothing' },
|
||||
'template-nav': { tier: 'ask', gate: '42', title: 'the reflex nav: wordmark left, inline links, a button right, hairline under', fix: 'decide the nav and build another shape, or keep it on the record', pageOnly: true },
|
||||
'template-footer': { tier: 'ask', gate: '42', title: 'the reflex footer: four link columns, a social row, a copyright', fix: 'decide the footer and build another shape, or keep it on the record', pageOnly: true },
|
||||
'invented-proof': { tier: 'blocks', gate: '46a', title: 'an invented metric, testimonial, logo wall or case-study count', fix: 'the user\'s number, a text slot, or nothing' },
|
||||
'drawn-browser-chrome': { tier: 'blocks', gate: '47a', title: 'a hand-drawn browser bar (traffic lights, URL pill) or OS window title bar', fix: 'a real screenshot in a <figure>, or no chrome' },
|
||||
'device-frame': { tier: 'ask', gate: '47b', title: 'a phone, laptop, terminal or editor frame', fix: 'one frame with real content inside, or none' },
|
||||
'imitation-material': { tier: 'ask', gate: '47c', title: 'styled divs standing in for a product, a photo edge or a physical object', fix: 'the product built inside a slot, a real asset, or nothing' },
|
||||
'device-frame': { tier: 'ask', gate: '47b', title: 'a phone, laptop, terminal or editor frame', fix: 'one frame with real content inside, or none', pageOnly: true },
|
||||
'imitation-material': { tier: 'ask', gate: '47c', title: 'styled divs standing in for a product, a photo edge or a physical object', fix: 'the product built inside a slot, a real asset, or nothing', pageOnly: true },
|
||||
'kicker-above-heading': { tier: 'ask', gate: '54', title: 'the eyebrow: short inert type before a heading that only announces it', fix: 'open the section another way; where the heading is figurative, the label becomes the heading', pageOnly: true },
|
||||
'template-punctuation': { tier: 'blocks', gate: '63', title: 'template punctuation: middle-dot meta strings, an arrow glued to link text, decorative status dots', fix: 'line breaks, columns, hairlines; a verb as the label' },
|
||||
'wireframe-slots': { tier: 'ask', gate: '68', title: 'more than five empty slots on one page, or four or more in one row', fix: 'set the set in type; a slot that stays holds a real photo or the product', pageOnly: true },
|
||||
@@ -111,9 +111,9 @@ export const RULES: Record<string, RuleMeta> = {
|
||||
'missing-focus-visible': { tier: 'blocks', gate: '26a', title: 'an interactive element with no :focus-visible ring, or an outline removed with nothing in its place', fix: 'a ring at 3:1 or better, instant' },
|
||||
'missing-states': { tier: 'ask', gate: '26b', title: 'no :active or :disabled state', fix: 'all the states a control can be in' },
|
||||
'missing-reduced-motion': { tier: 'blocks', gate: '27', title: 'a keyframe or transform transition without a prefers-reduced-motion fallback', fix: 'add @media (prefers-reduced-motion: reduce)' },
|
||||
'autoplay-unmuted': { tier: 'blocks', gate: '28a', title: 'a video autoplaying without muted', fix: 'autoplay muted loop playsinline' },
|
||||
'lazy-lcp': { tier: 'blocks', gate: '28a', title: 'loading="lazy" on the first image or the hero video', fix: 'fetchpriority="high" on the LCP image' },
|
||||
'video-without-poster': { tier: 'ask', gate: '28b', title: 'a video without poster or a priority hint', fix: 'add them' },
|
||||
'autoplay-unmuted': { tier: 'blocks', gate: '28a', title: 'a video autoplaying without muted', fix: 'autoplay muted loop playsinline', pageOnly: true },
|
||||
'lazy-lcp': { tier: 'blocks', gate: '28a', title: 'loading="lazy" on the first image or the hero video', fix: 'fetchpriority="high" on the LCP image', pageOnly: true },
|
||||
'video-without-poster': { tier: 'ask', gate: '28b', title: 'a video without poster or a priority hint', fix: 'add them', pageOnly: true },
|
||||
'input-state-shift': { tier: 'blocks', gate: '39a', title: 'input states that break: a border that changes width, a focus ring built from border, disabled by opacity alone', fix: 'constant border, outline, shared height, three disabled channels' },
|
||||
'control-height': { tier: 'ask', gate: '39b', title: 'control height outside 44 to 52px on a marketing page', fix: 'the number', pageOnly: true },
|
||||
'content-hidden-at-rest': { tier: 'blocks', gate: '62', title: 'a reveal class hiding content until JavaScript runs', fix: 'visible by default; hide only under a .js class, or animate from visible with @starting-style' },
|
||||
@@ -127,8 +127,8 @@ export const RULES: Record<string, RuleMeta> = {
|
||||
'low-contrast': { tier: 'blocks', gate: '40', title: 'text, an icon, a focus ring or a field edge under its contrast threshold', fix: 'fix the pair; body 4.5:1, large text and edges 3:1' },
|
||||
'gray-on-color': { tier: 'blocks', gate: '41', title: 'button text within a step of its fill, or grey text on a coloured surface', fix: 'the paired ink token' },
|
||||
'literal-outside-tokens': { tier: 'blocks', gate: '48', title: 'a colour or font-family literal outside the token block', fix: 'lift it into the tokens' },
|
||||
'button-label-wraps': { tier: 'blocks', gate: '49a', title: 'a button label or primary nav link that wraps', fix: 'shorten it; white-space: nowrap' },
|
||||
'link-wraps': { tier: 'ask', gate: '49b', title: 'a footer link, tab or breadcrumb that wraps', fix: 'shorten it' },
|
||||
'button-label-wraps': { tier: 'blocks', gate: '49a', title: 'a button label or primary nav link that wraps', fix: 'shorten it; white-space: nowrap', pageOnly: true },
|
||||
'link-wraps': { tier: 'ask', gate: '49b', title: 'a footer link, tab or breadcrumb that wraps', fix: 'shorten it', pageOnly: true },
|
||||
// ── Finish (polish) ──
|
||||
'straight-quotes': { tier: 'polish', gate: 'F1', title: 'straight quotes in prose', fix: 'curly quotes' },
|
||||
'three-dot-ellipsis': { tier: 'polish', gate: 'F2', title: 'three dots for an ellipsis', fix: 'the ellipsis character' },
|
||||
@@ -136,16 +136,16 @@ export const RULES: Record<string, RuleMeta> = {
|
||||
'two-primary-buttons': { tier: 'polish', gate: 'F4', title: 'two filled primary buttons on one page, or side by side', fix: 'one filled primary; the second as a text or outline action' },
|
||||
'icon-coin': { tier: 'polish', gate: 'F5', title: 'an icon in a coloured coin', fix: 'the icon inline, bare' },
|
||||
'display-figures-tabular': { tier: 'polish', gate: 'F6', title: 'display-size figures without tabular numerals', fix: 'font-variant-numeric: tabular-nums' },
|
||||
'stamped-recipe': { tier: 'polish', gate: 'F7', title: 'one container recipe stamped on six or more selectors, or the pill radius on five or more non-controls', fix: 'vary the recipe; pills on controls, tags and badges only' },
|
||||
'scroll-choreography-mix': { tier: 'polish', gate: 'F8', title: 'more than one scroll-choreography pattern', fix: 'one pattern a page' },
|
||||
'stamped-recipe': { tier: 'polish', gate: 'F7', title: 'one container recipe stamped on six or more selectors, or the pill radius on five or more non-controls', fix: 'vary the recipe; pills on controls, tags and badges only', pageOnly: true },
|
||||
'scroll-choreography-mix': { tier: 'polish', gate: 'F8', title: 'more than one scroll-choreography pattern', fix: 'one pattern a page', pageOnly: true },
|
||||
'fake-avatar': { tier: 'blocks', gate: 'F9', title: 'a fake-human avatar hotlink', fix: 'the user\'s people, initials, or no portrait' },
|
||||
'stock-person-photo': { tier: 'polish', gate: 'F9', title: 'a person-keyword stock photo URL', fix: 'the user\'s photograph' },
|
||||
'page-invariants': { tier: 'polish', gate: 'F15', title: 'page invariants broken: primary button style, container width, column gap or radius drifting', fix: 'one recipe for each' },
|
||||
'unthemed-browser-surfaces': { tier: 'polish', gate: 'F16', title: 'no ::selection, caret-color or scrollbar-color on a page with a committed palette', fix: 'theme the browser surfaces' },
|
||||
'page-invariants': { tier: 'polish', gate: 'F15', title: 'page invariants broken: primary button style, container width, column gap or radius drifting', fix: 'one recipe for each', pageOnly: true },
|
||||
'unthemed-browser-surfaces': { tier: 'polish', gate: 'F16', title: 'no ::selection, caret-color or scrollbar-color on a page with a committed palette', fix: 'theme the browser surfaces', pageOnly: true },
|
||||
'text-sliver': { tier: 'polish', gate: 'F17', title: 'a text block squeezed into a sliver at 1440', fix: 'give the block room or merge the columns', render: true },
|
||||
'tiny-slot': { tier: 'polish', gate: 'F18', title: 'a slot rendering under 40px in either dimension', fix: 'size the slot for what it will hold', render: true },
|
||||
'font-weight-count': { tier: 'polish', gate: 'F19', title: 'more than three font weights in running use', fix: 'three weights', render: true },
|
||||
'lonely-grid-item': { tier: 'polish', gate: 'F20', title: 'a grid or wrapping row leaving one item alone on its last row', fix: 'change the count or the columns', render: true },
|
||||
'lonely-grid-item': { tier: 'polish', gate: 'F20', title: 'a grid or wrapping row leaving one item alone on its last row', fix: 'change the count or the columns', render: true, pageOnly: true },
|
||||
'skipped-heading': { tier: 'polish', gate: 'F21', title: 'a heading level skipped', fix: 'step the levels one at a time' },
|
||||
'justified-text': { tier: 'polish', gate: 'F22', title: 'justified text without hyphens: auto', fix: 'hyphens: auto, or left-aligned' },
|
||||
'missing-lang': { tier: 'polish', gate: 'F23', title: 'a document without lang', fix: 'lang on <html>' },
|
||||
@@ -162,11 +162,11 @@ export const RULES: Record<string, RuleMeta> = {
|
||||
'dead-link': { tier: 'ask', gate: 'craft', title: 'a dead link (href="#")', fix: 'link it, or render the control disabled' },
|
||||
'alert': { tier: 'ask', gate: 'craft', title: 'alert() for a message', fix: 'show the message inline' },
|
||||
'img-no-alt': { tier: 'blocks', gate: 'craft', title: 'an <img> without alt', fix: 'add alt text (alt="" when decorative)' },
|
||||
'missing-title': { tier: 'blocks', gate: 'craft', title: 'a document without a <title>', fix: 'add a <title>' },
|
||||
'missing-meta-description': { tier: 'ask', gate: 'craft', title: 'no meta description', fix: 'add <meta name="description">' },
|
||||
'missing-favicon': { tier: 'polish', gate: 'craft', title: 'a document without a favicon', fix: 'add a favicon <link rel="icon">' },
|
||||
'missing-skip-link': { tier: 'ask', gate: 'craft', title: 'no skip link reaching <main>', fix: 'add a "Skip to content" link first in <body> whose #id exists on <main>' },
|
||||
'missing-aria-current': { tier: 'ask', gate: 'craft', title: 'route navigation without aria-current', fix: 'mark the current nav link aria-current="page"' },
|
||||
'missing-title': { tier: 'blocks', gate: 'craft', title: 'a document without a <title>', fix: 'add a <title>', pageOnly: true },
|
||||
'missing-meta-description': { tier: 'ask', gate: 'craft', title: 'no meta description', fix: 'add <meta name="description">', pageOnly: true },
|
||||
'missing-favicon': { tier: 'polish', gate: 'craft', title: 'a document without a favicon', fix: 'add a favicon <link rel="icon">', pageOnly: true },
|
||||
'missing-skip-link': { tier: 'ask', gate: 'craft', title: 'no skip link reaching <main>', fix: 'add a "Skip to content" link first in <body> whose #id exists on <main>', pageOnly: true },
|
||||
'missing-aria-current': { tier: 'ask', gate: 'craft', title: 'route navigation without aria-current', fix: 'mark the current nav link aria-current="page"', pageOnly: true },
|
||||
// ── The lint's own bookkeeping ──
|
||||
'waiver': { tier: 'ask', gate: 'waiver', title: 'a waiver the lint refused', fix: 'fix the finding, or waive it with the rule id and a reason about this page' },
|
||||
'house-style': { tier: 'polish', gate: 'waiver', title: 'more than three rules waived on one page', fix: 'lock the choices into DESIGN.md so later pages inherit them instead of re-arguing them' },
|
||||
|
||||
Reference in new issue
Block a user