Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ba5d387
WIP: add feature baselines page
bartveneman Aug 26, 2026
bae6bf0
feat: render a deep link to Polypane specificity calculator (#382)
bartveneman Aug 28, 2026
29b12ed
perf: improvements to syntax highlighting code (#383)
bartveneman Aug 30, 2026
9eb5dc6
perf: make custom properties page loads faster (#384)
bartveneman Aug 30, 2026
c952694
Enforce curly braces in conditional statements (#386)
bartveneman Aug 31, 2026
47151ec
fix(a11y): remove smooth scroll from `<Pre>` (#391)
bartveneman Sep 4, 2026
765b6d5
chore: co-location of blog markdown and images (#392)
bartveneman Sep 4, 2026
ea0d8ac
chore: co-locate docs and content (#393)
bartveneman Sep 4, 2026
63d6bd4
chore: use Svelte online reactivity instead of custom class (#394)
bartveneman Sep 4, 2026
533e06e
page layout n stuff
bartveneman Sep 6, 2026
45331cb
implement devtools
bartveneman Sep 6, 2026
c05c962
hide report when o css is loaded yet
bartveneman Sep 6, 2026
caf7025
add baseline status info to table
bartveneman Sep 6, 2026
6da5c43
Merge branch 'main' into feat/web-features
bartveneman Sep 7, 2026
8623f81
Separate baseline data for status and browser support
bartveneman Sep 9, 2026
4efb2ff
wip
bartveneman Sep 9, 2026
d764757
Consolidate browser icons
bartveneman Sep 9, 2026
1900e72
sortable table columns
bartveneman Sep 9, 2026
78ac69e
correctly match `type()` used inside `attr()`
bartveneman Sep 9, 2026
600f308
fix table scroll issue and trim fat on data structure
bartveneman Sep 9, 2026
a3c9f73
Clear item usage when switching filters
bartveneman Sep 9, 2026
a151d8e
Make sure baseline feature JSON files are generated before building
bartveneman Sep 9, 2026
1d2509c
Co-locate all baseline feature functions
bartveneman Sep 9, 2026
7f562dd
self-review
bartveneman Sep 9, 2026
b7c8696
perf, better types
bartveneman Sep 9, 2026
1a487f7
fix: also include web-features that are not in css group but start wi…
bartveneman Sep 9, 2026
c624b8b
code style
bartveneman Sep 9, 2026
d162ac5
more accessible tables
bartveneman Sep 9, 2026
da26aa1
add page content
bartveneman Sep 9, 2026
5fd783a
playwright tests
bartveneman Sep 9, 2026
ebca8dd
add page to navigations etc.
bartveneman Sep 9, 2026
ae426df
rm code quality from main nav to fix PW test
bartveneman Sep 9, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package-lock.json
/playwright-report/
/playwright/.cache/
/css-coverage
/src/lib/baseline/css-features.generated.json
/src/lib/baseline/compat-keys.generated.json

# Sentry Config File
.env.sentry-build-plugin
1 change: 0 additions & 1 deletion .stylelintrc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export default {
'alpha-value-notation': 'number',
'media-feature-range-notation': 'prefix', // Safari 16.4+ support
'custom-property-pattern': [
// TODO: enfore --pw- as start
'^_?([a-z][a-z0-9]*)(-[a-z0-9]+)*$',
{
message:
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
"#lib/*": "./src/lib/*"
},
"scripts": {
"prepare": "pnpm generate:css-features",
"dev": "vite dev",
"build": "vite build",
"build": "pnpm generate:css-features && vite build",
"generate:css-features": "node scripts/generate-css-features.ts",
"preview": "vite preview",
"test": "svelte-kit sync && playwright test",
"test:unit": "vitest --exclude **/*.spec.ts",
Expand Down Expand Up @@ -50,7 +52,8 @@
"runed": "^0.37.1",
"svelte": "^5.56.8",
"typescript": "^6.0.3",
"vite": "catalog:"
"vite": "catalog:",
"web-features": "^3.34.3"
},
"devDependencies": {
"@playwright/test": "^1.62.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions scripts/generate-css-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import data from 'web-features/data.json' with { type: 'json' }
import type { Baseline, CssFeature } from '../src/lib/baseline/css-feature.js'
import { browsers, type BrowserId } from '../src/lib/baseline/browsers.ts'

type WebFeaturesData = {
features: typeof import('web-features').features
}

const { features } = data as WebFeaturesData

const css_features: Record<string, CssFeature> = {}
const compat_keys: Record<string, string> = {}

for (const [id, feature] of Object.entries(features)) {
if (feature.kind !== 'feature' || !feature.group) {
continue
}

// A feature's `group` (e.g. "view-transitions", "scrolling") doesn't
// reliably nest under a "css" root in web-features' taxonomy, so group
// membership can't tell CSS features apart from JS/HTML/SVG ones. Its
// `compat_features` can though: any feature with at least one `css.*`
// key genuinely has CSS surface worth tracking.
let css_compat_keys = (feature.compat_features ?? []).filter((key) => key.startsWith('css.'))
if (css_compat_keys.length === 0) {
continue
}

let baseline = feature.status.baseline as Baseline

css_features[id] = {
name: feature.name,
baseline,
baseline_low_date: feature.status.baseline_low_date,
baseline_high_date: feature.status.baseline_high_date,
// Only features with limited availability need this - for
// newly/widely-available ones, the baseline dates already say enough.
// web-features tracks far more browsers than we display, so this is
// filtered down to the ones in browsers.ts.
support:
baseline === false
? (Object.keys(feature.status.support ?? {}).filter((id) => id in browsers) as BrowserId[])
: undefined
}

for (const compat_feature of css_compat_keys) {
compat_keys[compat_feature] = id
}
}

function write(relative_path: string, contents: unknown) {
const out = fileURLToPath(new URL(relative_path, import.meta.url))
writeFileSync(out, JSON.stringify(contents, undefined, 2))
return out
}

const features_path = write('../src/lib/baseline/css-features.generated.json', css_features)
const compat_keys_path = write('../src/lib/baseline/compat-keys.generated.json', compat_keys)

console.log(`Wrote ${Object.keys(css_features).length} CSS features to ${features_path}`)
console.log(`Wrote ${Object.keys(compat_keys).length} compat keys to ${compat_keys_path}`)
57 changes: 57 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,63 @@
d="M23.27 19.743l-11.946-11.945c-.557-.557-.842-1.331-.783-2.115.115-1.485-.395-3.009-1.529-4.146-1.03-1.028-2.376-1.537-3.723-1.537-.507 0-1.015.072-1.505.216l3.17 3.17c.344 1.589-1.959 3.918-3.567 3.567l-3.169-3.17c-.145.492-.218 1-.218 1.509 0 1.347.51 2.691 1.538 3.721 1.135 1.136 2.66 1.646 4.146 1.53.783-.06 1.557.226 2.113.783l11.946 11.944c.468.468 1.103.73 1.763.73 1.368 0 2.494-1.108 2.494-2.494 0-.638-.244-1.276-.73-1.763zm-1.77 2.757c-.553 0-1-.448-1-1s.447-1 1-1 1 .448 1 1-.447 1-1 1zm-7.935-15.289l5.327-5.318c.584-.584 1.348-.877 2.113-.877.764 0 1.529.292 2.113.877.589.587.882 1.357.882 2.125 0 .764-.291 1.528-.873 2.11l-5.326 5.318-1.039-1.039 5.067-5.059c.169-.168.167-.441-.001-.609-.169-.169-.441-.17-.61-.001l-5.068 5.058-.89-.89 5.067-5.059c.169-.169.168-.441 0-.611-.169-.168-.443-.17-.611 0l-5.067 5.058-1.084-1.083zm-3.53 9.18l-5.227 5.185c-.227.229-.423.488-.574.774l-.301.58-2.1 1.07-.833-.834 1.025-2.146.58-.302c.286-.15.561-.329.79-.558l5.227-5.185 1.413 1.416z"
/>
</symbol>
<symbol
id="svg--chrome"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
<path d="M12 9h8.4" />
<path d="M14.598 13.5l-4.2 7.275" />
<path d="M9.402 13.5l-4.2 -7.275" />
</symbol>
<symbol
id="svg--edge"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20.978 11.372a9 9 0 1 0 -1.593 5.773" />
<path
d="M20.978 11.372c.21 2.993 -5.034 2.413 -6.913 1.486c1.392 -1.6 .402 -4.038 -2.274 -3.851c-1.745 .122 -2.927 1.157 -2.784 3.202c.28 3.99 4.444 6.205 10.36 4.79"
/>
<path d="M3.022 12.628c-.283 -4.043 8.717 -7.228 11.248 -2.688" />
<path d="M12.628 20.978c-2.993 .21 -5.162 -4.725 -3.567 -9.748" />
</symbol>
<symbol
id="svg--firefox"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4.028 7.82a9 9 0 1 0 12.823 -3.4c-1.636 -1.02 -3.064 -1.02 -4.851 -1.02h-1.647" />
<path
d="M4.914 9.485c-1.756 -1.569 -.805 -5.38 .109 -6.17c.086 .896 .585 1.208 1.111 1.685c.88 -.275 1.313 -.282 1.867 0c.82 -.91 1.694 -2.354 2.628 -2.093c-1.082 1.741 -.07 3.733 1.371 4.173c-.17 .975 -1.484 1.913 -2.76 2.686c-1.296 .938 -.722 1.85 0 2.234c.949 .506 3.611 -1 4.545 .354c-1.698 .102 -1.536 3.107 -3.983 2.727c2.523 .957 4.345 .462 5.458 -.34c1.965 -1.52 2.879 -3.542 2.879 -5.557c-.014 -1.398 .194 -2.695 -1.26 -4.75"
/>
</symbol>
<symbol
id="svg--safari"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 16l2 -6l6 -2l-2 6l-6 2" />
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
</symbol>
</svg>

<div class="app">%sveltekit.body%</div>
Expand Down
43 changes: 43 additions & 0 deletions src/lib/baseline/baseline-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, test, expect } from 'vitest'
import { get_baseline_availability, is_browser_supported } from './baseline-status'
import type { CssFeature } from './css-feature'

function feature(overrides: Partial<CssFeature> = {}): CssFeature {
return { name: 'test-feature', baseline: false, ...overrides }
}

describe('get_baseline_availability', () => {
test('widely available when baseline is high', () => {
expect(get_baseline_availability(feature({ baseline: 'high' }))).toBe('widely')
})

test('newly available when baseline is low', () => {
expect(get_baseline_availability(feature({ baseline: 'low' }))).toBe('newly')
})

test('limited availability when baseline is false', () => {
expect(get_baseline_availability(feature({ baseline: false }))).toBe('limited')
})
})

describe('is_browser_supported', () => {
test('widely and newly available features are always supported', () => {
expect(is_browser_supported(feature({ baseline: 'high' }), 'widely', [])).toBe(true)
expect(is_browser_supported(feature({ baseline: 'low' }), 'newly', [])).toBe(true)
})

test('limited availability supported when any browser id matches', () => {
let f = feature({ baseline: false, support: ['chrome', 'chrome_android'] })
expect(is_browser_supported(f, 'limited', ['chrome', 'chrome_android'])).toBe(true)
})

test('limited availability unsupported when no browser id matches', () => {
let f = feature({ baseline: false, support: ['firefox', 'firefox_android'] })
expect(is_browser_supported(f, 'limited', ['chrome', 'chrome_android'])).toBe(false)
})

test('limited availability with no support list is unsupported', () => {
let f = feature({ baseline: false })
expect(is_browser_supported(f, 'limited', ['chrome', 'chrome_android'])).toBe(false)
})
})
31 changes: 31 additions & 0 deletions src/lib/baseline/baseline-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { CssFeature } from './css-feature.js'
import type { BrowserId } from './browsers.js'

export type BaselineAvailability = 'widely' | 'newly' | 'limited'

export function get_baseline_availability(feature: CssFeature): BaselineAvailability {
if (feature.baseline === 'high') {
return 'widely'
}
if (feature.baseline === 'low') {
return 'newly'
}
return 'limited'
}

/**
* Whether a browser family (e.g. Chrome desktop + Chrome Android) supports
* a feature. A family counts as supported if any of its variants does,
* since a Baseline feature only ends up "limited" when at least one browser
* lacks support - not necessarily every variant of every browser.
*/
export function is_browser_supported(
feature: CssFeature,
availability: BaselineAvailability,
browser_ids: BrowserId[]
): boolean {
if (availability === 'limited') {
return browser_ids.some((id) => feature.support?.includes(id))
}
return true
}
37 changes: 37 additions & 0 deletions src/lib/baseline/browsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, test, expect } from 'vitest'
import { browsers, browser_families, type BrowserId } from './browsers'

describe('browsers', () => {
test('every browser has a display name', () => {
for (let id of Object.keys(browsers) as BrowserId[]) {
expect(typeof browsers[id]).toBe('string')
expect(browsers[id].length).toBeGreaterThan(0)
}
})
})

describe('browser_families', () => {
test('every family id is a known browser', () => {
for (let family of Object.values(browser_families)) {
for (let id of family.ids) {
expect(browsers[id]).toBeDefined()
}
}
})

test('every browser belongs to exactly one family', () => {
let seen = new Set<string>()
for (let family of Object.values(browser_families)) {
for (let id of family.ids) {
expect(seen.has(id)).toBe(false)
seen.add(id)
}
}
expect(seen).toEqual(new Set(Object.keys(browsers)))
})

test('desktop and mobile variants of the same vendor share a family', () => {
expect(browser_families.chrome.ids).toEqual(['chrome', 'chrome_android'])
expect(browser_families.safari.ids).toEqual(['safari', 'safari_ios'])
})
})
25 changes: 25 additions & 0 deletions src/lib/baseline/browsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const browsers = {
chrome: 'Chrome',
chrome_android: 'Chrome Android',
edge: 'Edge',
firefox: 'Firefox',
firefox_android: 'Firefox Android',
safari: 'Safari',
safari_ios: 'Safari iOS'
} as const satisfies Record<string, string>

export type BrowserId = keyof typeof browsers

export type BrowserFamily = 'chrome' | 'edge' | 'firefox' | 'safari'

/**
* One entry per browser logo. Desktop and mobile variants of the same
* vendor (Chrome/Chrome Android, etc.) share a single icon and support
* indicator, matching the original Baseline status web component.
*/
export const browser_families: Record<BrowserFamily, { name: string; ids: BrowserId[] }> = {
chrome: { name: 'Chrome', ids: ['chrome', 'chrome_android'] },
edge: { name: 'Edge', ids: ['edge'] },
firefox: { name: 'Firefox', ids: ['firefox', 'firefox_android'] },
safari: { name: 'Safari', ids: ['safari', 'safari_ios'] }
}
39 changes: 39 additions & 0 deletions src/lib/baseline/calculate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, test, expect } from 'vitest'
import { analyze } from './calculate'

describe('analyze', () => {
test('empty css has no usages', () => {
expect(analyze('').size).toBe(0)
})

test('a declaration is recorded under its feature id', () => {
let usages = analyze('a { gap: 1px }')
expect(usages.get('grid')).toHaveLength(1)
})

test('an at-rule is recorded under its feature id', () => {
let usages = analyze('@container (min-width: 100px) { a { color: red } }')
expect(usages.get('container-queries')).toHaveLength(1)
})

test('repeated usage of the same feature accumulates locations', () => {
let usages = analyze('a { gap: 1px } b { gap: 2px }')
expect(usages.get('grid')).toHaveLength(2)
})

test('a property with no compat mapping is not recorded', () => {
let usages = analyze('a { not-a-real-property: 1px }')
expect(usages.size).toBe(0)
})

test('locations carry line, column, offset and length', () => {
let usages = analyze('a {\n\tgap: 1px;\n}')
let locations = usages.get('grid')
expect(locations).toEqual([{ line: 2, column: 2, offset: 5, length: 9 }])
})

test('type() inside attr() is disambiguated from a bare type() call', () => {
let usages = analyze('a { content: attr(data-x type(<color>)) }')
expect(usages.get('attr')).toHaveLength(1)
})
})
Loading
Loading