Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions BUILD-PLAN-issue-13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# BUILD-PLAN-issue-13.md — Modern CSS feature adoption scan

**Card:** https://github.com/nujovich/mint-radar/issues/13

**Decision:** PLAN already defined 4 concrete code milestones (planned). Decision not required.

## Milestones

- [ ] Milestone 1 — Extend audit prompt in lib/prompts.mjs with STEP 14: modern CSS feature scan (detect @property, @layer, @container, @supports, nesting, color-mix, @scope, :has()), report presence/absence with adoption suggestions
- [ ] Milestone 2 — Add modernFeatures field to AuditReport type in lib/types.ts (Record<string, {used: boolean, suggestion?: string}>)
- [ ] Milestone 3 — Wire modern features results in playground UI as "Modern CSS Adoption" section
- [ ] Milestone 4 — Add test fixture with modern and legacy CSS and tests in lib/**tests**/
88 changes: 87 additions & 1 deletion components/AuditView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import { useState } from 'react'
import type { AuditReport, ColorDecision, UserDecisions } from '@/lib/types'
import { collectLintGroups } from '@/lib/audit-summary.mjs'
import {
collectLintGroups,
collectModernFeatures,
} from '@/lib/audit-summary.mjs'

interface Props {
audit: AuditReport
Expand Down Expand Up @@ -53,6 +56,8 @@ export default function AuditView({ audit, onResolve }: Props) {
const motionDurations = audit.motion?.durations?.suggestedScale ?? {}
const motionEasings = audit.motion?.easings?.suggestedScale ?? {}
const lintGroups = collectLintGroups(audit)
const modernFeatures = collectModernFeatures(audit)
const adoptedFeatureCount = modernFeatures.filter((f) => f.used).length

const chaosColor =
audit.chaosScore <= 3
Expand Down Expand Up @@ -882,6 +887,87 @@ export default function AuditView({ audit, onResolve }: Props) {
</section>
)}

{/* Modern CSS adoption */}
{modernFeatures.length > 0 && (
<section>
<SectionLabel>
Modern CSS Adoption — {adoptedFeatureCount} of{' '}
{modernFeatures.length} adopted
</SectionLabel>

<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{modernFeatures.map((feature) => (
<div
key={feature.id}
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 12,
padding: '10px 14px',
borderRadius: 8,
background: 'var(--panel)',
border: '1px solid var(--border)',
}}
>
<span
style={{
flexShrink: 0,
width: 12,
height: 12,
marginTop: 3,
borderRadius: '50%',
background: feature.used ? '#4ade80' : 'var(--surface-2)',
border: feature.used ? 'none' : '1px solid var(--border)',
}}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
display: 'flex',
alignItems: 'baseline',
gap: 8,
}}
>
<code
style={{
fontSize: 12,
fontFamily: 'var(--mono)',
color: 'var(--text)',
}}
>
{feature.label}
</code>
<span
style={{
fontSize: 10,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.04em',
color: feature.used ? '#4ade80' : 'var(--text-faint)',
}}
>
{feature.used ? 'adopted' : 'not adopted'}
</span>
</div>
{!feature.used && feature.suggestion && (
<div
style={{
fontSize: 11,
color: 'var(--text-muted)',
lineHeight: 1.55,
marginTop: 3,
}}
>
{feature.suggestion}
</div>
)}
</div>
</div>
))}
</div>
</section>
)}

{/* CTA */}
<div style={{ display: 'flex', justifyContent: 'center', paddingTop: 8 }}>
<button
Expand Down
78 changes: 78 additions & 0 deletions lib/__fixtures__/modern-features.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* Fixture for STEP 14 - modern CSS feature adoption scan.
Uses all eight tracked modern features plus a legacy block
that exercises none of them (so every id reports used: false
there and the scan can suggest adoption). */

/* property: typed custom property registration. */
@property --brand-color {
syntax: '<color>';
inherits: false;
initial-value: #6366f1;
}

/* layer: cascade layers in statement form. */
@layer reset, base, components;

/* layer: cascade layers in block form. */
@layer base {
body {
margin: 0;
font-family: system-ui;
}
}

/* container: container-type plus a container query. */
.product-card {
container-type: inline-size;
}

@container (min-width: 400px) {
.product-card {
grid-template-columns: 1fr 1fr;
}
}

/* supports: feature query. */
@supports (display: grid) {
.product-grid {
display: grid;
}
}

/* nesting: native CSS nesting with the & selector. */
.alert {
padding: 1rem;

& .alert-title {
font-weight: 600;
}
}

/* color-mix: runtime color derivation. */
.theme-accent {
background-color: color-mix(in srgb, #6366f1 50%, white);
}

/* scope: scoped component styles. */
@scope (.pricing) {
.price {
color: var(--brand-color);
}
}

/* has: relational pseudo-class. */
.gallery:has(> img) {
display: grid;
}

/* Legacy block: no modern features, so every tracked id is absent here. */
.legacy-sidebar {
float: left;
width: 240px;
}

.legacy-clearfix::after {
content: '';
display: table;
clear: both;
}
88 changes: 87 additions & 1 deletion lib/__tests__/audit-summary.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, it, expect } from 'vitest'
import { formatLintSummary, collectLintGroups } from '../audit-summary.mjs'
import {
formatLintSummary,
collectLintGroups,
collectModernFeatures,
} from '../audit-summary.mjs'

describe('formatLintSummary', () => {
it('returns empty string when no linting fields are present', () => {
Expand Down Expand Up @@ -125,3 +129,85 @@ describe('collectLintGroups', () => {
])
})
})

describe('collectModernFeatures', () => {
it('returns an empty array when modernFeatures is absent', () => {
expect(collectModernFeatures({ brand: 'x' })).toEqual([])
})

it('returns an empty array when modernFeatures is an empty object', () => {
expect(collectModernFeatures({ modernFeatures: {} })).toEqual([])
})

it('returns all eight features in a stable order with labels', () => {
const features = collectModernFeatures({
modernFeatures: {
property: { used: true },
layer: { used: false },
container: { used: false },
supports: { used: true },
nesting: { used: false },
'color-mix': { used: false },
scope: { used: false },
has: { used: true },
},
})
expect(features.map((f) => f.id)).toEqual([
'property',
'layer',
'container',
'supports',
'nesting',
'color-mix',
'scope',
'has',
])
expect(features.map((f) => f.label)).toEqual([
'@property',
'@layer',
'@container',
'@supports',
'Nesting',
'color-mix()',
'@scope',
':has()',
])
})

it('normalizes used to a boolean and carries suggestion only when not used', () => {
const features = collectModernFeatures({
modernFeatures: {
property: { used: true, suggestion: 'should be ignored' },
layer: {
used: false,
suggestion: 'Consider @layer to organize the cascade',
},
},
})
expect(features[0]).toEqual({
id: 'property',
label: '@property',
used: true,
suggestion: 'should be ignored',
})
expect(features[1]).toEqual({
id: 'layer',
label: '@layer',
used: false,
suggestion: 'Consider @layer to organize the cascade',
})
})

it('defaults a missing feature entry to unused without a suggestion', () => {
const features = collectModernFeatures({
modernFeatures: { property: { used: true } },
})
const layer = features.find((f) => f.id === 'layer')
expect(layer).toEqual({
id: 'layer',
label: '@layer',
used: false,
suggestion: undefined,
})
})
})
Loading
Loading