diff --git a/README.md b/README.md index c7fd0f1..6f7358d 100644 --- a/README.md +++ b/README.md @@ -15,29 +15,53 @@ Both share the same prompts and Claude pipeline. CSS / SCSS / HTML → Claude Audit → Review & curate → Clean tokens → Export → Apply ``` -1. **Audit** — Claude analyzes your CSS, groups near-duplicate colors into clusters, detects fonts, flags spacing values that don't fit a 4px grid, identifies duplicate transition/animation declarations, and lints layout patterns for accessibility and modern-CSS pitfalls (see [CSS layout linting](#css-layout-linting)). +1. **Audit** — Claude analyzes your CSS, groups near-duplicate colors into clusters, detects fonts, flags spacing values that don't fit a 4px grid, identifies duplicate transition/animation declarations, and lints layout patterns for accessibility, modern-CSS pitfalls, and `@property` type mismatches (see [CSS layout linting](#css-layout-linting)). 2. **Curate** — Review each cluster. Pick the canonical color, rename tokens, include or exclude entries, and select which fonts to keep. (CLI applies sensible defaults: include every cluster, keep non-system fonts, use the suggested 4px scale.) 3. **Export** — Generate production-ready output in any format. 4. **Apply** — Rewrite your source CSS in place so raw values reference the generated tokens (`#1976d2` → `var(--color-primary)`). Deterministic, no LLM — adoption becomes a reviewable git diff. See [`mint-ds apply`](#applying-tokens-to-source-css). ## CSS layout linting -Beyond color, font, and spacing tokens, the audit also lints your CSS for layout accessibility issues and modern-CSS pitfalls. These findings are returned in the raw `AuditReport` (write it to disk with `--report`); the accessibility and overflow checks also feed the chaos score. - -| Category | Rule | Severity | What it flags | -| -------------------------- | ----------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- | -| **Layout accessibility** | `order` breaks DOM order | warning | A grid/flex child reordered with `order` so the visual order no longer matches the DOM — breaks keyboard navigation and SR flow | -| | reordering without tabindex | warning | An element visually reordered via `order` with no matching `tabindex` adjustment, so keyboard users navigate in DOM order | -| **Modern best practices** | `grid-when-flexbox-wrap-would-work` | suggestion | Single-column grid that a `flex` + `flex-wrap` layout would handle more simply | -| | `legacy-centering` | suggestion | `margin: 0 auto` + fixed width, or absolute-position + `transform` centering, where modern `flex`/`grid` centering is cleaner | -| | `flex-min-width-zero-hack` | suggestion | `min-width: 0` on a flex item — the classic overflow workaround that often hides a layout misunderstanding | -| | `fragile-nested-selectors` | suggestion | Selectors coupled to a brittle DOM shape (deep `>` / `+` chains) that a small HTML refactor would break | -| **Feature adoption** | `use-css-layers` | info | Large stylesheets (20+ rules) with no `@layer` organization | -| | `use-container-queries` | info | Component-scoped width `@media` queries that a `@container` query would express better | -| **Overflow & wrap safety** | `flex-wrap-missing` | warning | Flex container without `flex-wrap` — items can't wrap and may overflow on narrow viewports | -| | `missing-overflow-wrap` | suggestion | Sized grid/flex container (fixed width/height) with no `overflow` handling, so content can be clipped | - -The chaos score gains **+1** when there are 3 or more layout-accessibility issues and **+1** when there are 4 or more overflow-safety issues. Adoption suggestions are informational only and never affect the score. +Beyond color, font, and spacing tokens, the audit also lints your CSS for layout accessibility issues, modern-CSS pitfalls, and broken `@property` type contracts. These findings are returned in the raw `AuditReport` (write it to disk with `--report`); the accessibility and overflow checks also feed the chaos score. + +| Category | Rule | Severity | What it flags | +| -------------------------- | ----------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Layout accessibility** | `order` breaks DOM order | warning | A grid/flex child reordered with `order` so the visual order no longer matches the DOM — breaks keyboard navigation and SR flow | +| | reordering without tabindex | warning | An element visually reordered via `order` with no matching `tabindex` adjustment, so keyboard users navigate in DOM order | +| **Modern best practices** | `grid-when-flexbox-wrap-would-work` | suggestion | Single-column grid that a `flex` + `flex-wrap` layout would handle more simply | +| | `legacy-centering` | suggestion | `margin: 0 auto` + fixed width, or absolute-position + `transform` centering, where modern `flex`/`grid` centering is cleaner | +| | `flex-min-width-zero-hack` | suggestion | `min-width: 0` on a flex item — the classic overflow workaround that often hides a layout misunderstanding | +| | `fragile-nested-selectors` | suggestion | Selectors coupled to a brittle DOM shape (deep `>` / `+` chains) that a small HTML refactor would break | +| **Feature adoption** | `use-css-layers` | info | Large stylesheets (20+ rules) with no `@layer` organization | +| | `use-container-queries` | info | Component-scoped width `@media` queries that a `@container` query would express better | +| **Overflow & wrap safety** | `flex-wrap-missing` | warning | Flex container without `flex-wrap` — items can't wrap and may overflow on narrow viewports | +| | `missing-overflow-wrap` | suggestion | Sized grid/flex container (fixed width/height) with no `overflow` handling, so content can be clipped | +| **@property type safety** | `invalid-initial-value` | warning | An `initial-value` that doesn't parse as the declared `syntax`, or a missing one on a non-universal syntax — the browser rejects the whole registration | +| | `fallback-type-mismatch` | warning | A `var()` fallback that contradicts the registered syntax (`var(--my-color, 14px)` on a ``), so the fallback can never apply | +| | `property-type-mismatch` | suggestion | A registered property used where its declared syntax can't apply — a `` property assigned to `color` | + +The chaos score gains **+1** when there are 3 or more layout-accessibility issues and **+1** when there are 4 or more overflow-safety issues. Adoption suggestions and `@property` findings are informational for the score and never affect it. + +### `@property` type safety + +An `@property` at-rule declares a type contract for a custom property. Mint reads every registration in your CSS and checks it — and its `var()` usages — against the declared `syntax` descriptor: + + +```css +@property --spacing-unit { + syntax: ''; + inherits: false; + initial-value: red; /* invalid-initial-value — the browser drops this registration */ +} + +.button { + background-color: var(--brand-color, 14px); /* fallback-type-mismatch — can't fall back to a length */ + color: var(--spacing-unit); /* property-type-mismatch — used as a color */ +} +``` + + +`invalid-initial-value` is the one worth fixing first: when the `initial-value` doesn't match the declared `syntax`, the browser rejects the registration outright and the property silently reverts to unregistered behaviour — losing both the type contract and the ability to animate or transition it. Registrations declaring the universal syntax (`*`) are skipped, since any value satisfies them. Findings land in `propertyTypeIssues` on the `AuditReport`. ## Example — Frankenstein diff --git a/lib/__fixtures__/at-property.css b/lib/__fixtures__/at-property.css new file mode 100644 index 0000000..ef4b28b --- /dev/null +++ b/lib/__fixtures__/at-property.css @@ -0,0 +1,48 @@ +/* Fixture for STEP 13 — CSS @property type checking. + Covers one clean registration plus one case per reported rule. */ + +/* Valid: initial-value parses as the declared syntax. */ +@property --brand-color { + syntax: ''; + inherits: false; + initial-value: #6366f1; +} + +/* invalid-initial-value: declared , initial-value is a color. + The browser rejects this registration outright. */ +@property --spacing-unit { + syntax: ''; + inherits: false; + initial-value: red; +} + +/* invalid-initial-value: non-universal syntax with no initial-value at all. */ +@property --card-radius { + syntax: ''; + inherits: true; +} + +/* Universal syntax — not type-checkable, STEP 13 must skip it. */ +@property --anything { + syntax: '*'; + inherits: false; +} + +.button { + /* fallback-type-mismatch: property falling back to a length. */ + background-color: var(--brand-color, 14px); + padding: var(--spacing-unit, 8px); + border-radius: var(--card-radius, 4px); +} + +.card { + /* property-type-mismatch: a property used as a color value. */ + color: var(--spacing-unit); + gap: var(--spacing-unit, 12px); +} + +.badge { + /* Correct usage — must not be reported. */ + background-color: var(--brand-color, #4f46e5); + border-radius: var(--card-radius, 6px); +} diff --git a/lib/__tests__/audit-summary.test.mjs b/lib/__tests__/audit-summary.test.mjs index 31cef48..e0d43d4 100644 --- a/lib/__tests__/audit-summary.test.mjs +++ b/lib/__tests__/audit-summary.test.mjs @@ -13,10 +13,16 @@ describe('formatLintSummary', () => { modernPracticeIssues: [], adoptionSuggestions: [], overflowSafetyIssues: [], + propertyTypeIssues: [], } expect(formatLintSummary(audit)).toBe('') }) + it('includes the @property type count when issues are present', () => { + const audit = { propertyTypeIssues: [{}, {}, {}] } + expect(formatLintSummary(audit)).toBe('3 property types') + }) + it('includes the layout a11y count when issues are present', () => { const audit = { layoutA11yIssues: [{}, {}] } expect(formatLintSummary(audit)).toBe('2 layout a11y') @@ -34,15 +40,16 @@ describe('formatLintSummary', () => { ) }) - it('labels all four categories in a stable order', () => { + it('labels all five categories in a stable order', () => { const audit = { layoutA11yIssues: [{}], modernPracticeIssues: [{}], adoptionSuggestions: [{}], overflowSafetyIssues: [{}], + propertyTypeIssues: [{}], } expect(formatLintSummary(audit)).toBe( - '1 layout a11y · 1 modern-practice · 1 adoption · 1 overflow' + '1 layout a11y · 1 modern-practice · 1 adoption · 1 overflow · 1 property types' ) }) }) @@ -88,11 +95,33 @@ describe('collectLintGroups', () => { layoutA11yIssues: [{ selector: '.a', reason: 'r', severity: 'warning' }], modernPracticeIssues: [], adoptionSuggestions: [{ selector: '', reason: 'r', severity: 'info' }], + propertyTypeIssues: [ + { selector: '.btn', reason: 'r', severity: 'warning' }, + ], } expect(collectLintGroups(audit).map((g) => g.key)).toEqual([ 'layoutA11yIssues', 'adoptionSuggestions', 'overflowSafetyIssues', + 'propertyTypeIssues', + ]) + }) + + it('labels the @property category for display', () => { + const issue = { + selector: '.button', + property: 'background-color', + rule: 'fallback-type-mismatch', + severity: 'warning', + reason: '`` property has a `` fallback', + declaredSyntax: '', + } + expect(collectLintGroups({ propertyTypeIssues: [issue] })).toEqual([ + { + key: 'propertyTypeIssues', + label: '@property type safety', + issues: [issue], + }, ]) }) }) diff --git a/lib/__tests__/css-auditor.test.mjs b/lib/__tests__/css-auditor.test.mjs index d9f6abf..6f1c35b 100644 --- a/lib/__tests__/css-auditor.test.mjs +++ b/lib/__tests__/css-auditor.test.mjs @@ -549,3 +549,113 @@ describe('CssAuditor overflowSafetyIssues integration', () => { expect(result.overflowSafetyIssues).toBeUndefined() }) }) + +describe('buildAuditPrompt @property type checking', () => { + it('includes STEP 13 for @property type checking', () => { + const css = 'body { color: red; }' + const prompt = buildAuditPrompt(css) + expect(prompt.content).toContain('STEP 13') + expect(prompt.content).toContain('@PROPERTY TYPE CHECKING') + }) + + it('instructs LLM to validate initial-value against the declared syntax', () => { + const css = + '@property --gap { syntax: ""; initial-value: red; inherits: false; }' + const prompt = buildAuditPrompt(css) + expect(prompt.content).toContain('invalid-initial-value') + expect(prompt.content).toContain('initial-value') + }) + + it('instructs LLM to detect var() fallbacks that contradict the syntax', () => { + const css = '.btn { color: var(--brand, 14px); }' + const prompt = buildAuditPrompt(css) + expect(prompt.content).toContain('fallback-type-mismatch') + }) + + it('instructs LLM to flag registered properties used in a foreign context', () => { + const css = '.btn { background-color: var(--spacing-unit); }' + const prompt = buildAuditPrompt(css) + expect(prompt.content).toContain('property-type-mismatch') + }) + + it('documents the syntax descriptors it can type-check', () => { + const css = 'body { color: red; }' + const prompt = buildAuditPrompt(css) + for (const syntax of [ + '', + '', + '', + '', + '', + '', + '