Skip to content
Closed
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
22 changes: 13 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ SLASHED has **one version number**. It must be identical in every artifact.
If you touch anything version-related, you are responsible for keeping all
of these in sync:

| File | Field |
|------|-------|
| `package.json` | `.version` ← **source of truth** |
| `package-lock.json` | `.version` + `.packages[""].version` |
| `docs/roadmap.md` | `Current version: **X.Y.Z**` line |
| `configurator/package.json` | `.version` |
| `configurator/package-lock.json` | `.version` + `.packages[""].version` |
| `dist/*.css` (unminified) | `/*! SLASHED vX.Y.Z */` comment header |
| Configurator UI version pill | baked in via Vite `__SLASHED_VERSION__` at build time |
| File | Field | How it's kept correct |
|------|-------|-----------------------|
| `package.json` | `.version` ← **source of truth** | you bump it (`npm version`) |
| `package-lock.json` | `.version` + `.packages[""].version` | `npm version`; checked by `check:version` (CI) |
| `docs/roadmap.md` | `Current version: **X.Y.Z**` line | `version-sync`; checked by `check:version` (CI) |
| `configurator/package.json` | `.version` | `version-sync`; checked by `check:version` (CI) |
| `configurator/package-lock.json` | `.version` + `.packages[""].version` | `version-sync`; checked by `check:version` (CI) |
| `dist/*.css` (unminified) | `/*! SLASHED vX.Y.Z */` comment header | **build-derived** — stamped from `package.json`/tag by `bundle.js`; `dist/*.css` is git-ignored, so it cannot drift. `release.yml` re-verifies the stamp before publishing |
| Configurator UI version pill | baked in via Vite `__SLASHED_VERSION__` at build time | **build-derived** — injected from root `package.json` at Vite build; cannot drift |

The first five rows are the ones you ever sync; the last two are **build-derived**
(regenerated from `package.json` at build time and not committed as text you edit),
so `version-sync`/`check:version` intentionally don't touch them — never hand-edit them.

**Never edit version numbers by hand.** Use `npm version` then the sync script:

Expand Down
5 changes: 5 additions & 0 deletions analysis/gap-analysis.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<!-- STATUS: historical one-off snapshot, not a living document. Captured at
v0.5.28 for a competitive review; not wired into any build/CI gate and not
auto-updated. Keep for reference; supersede with a fresh dated analysis
rather than editing in place. -->

# SLASHED — Competitive Gap Analysis (v0.5.28)

> **Question:** what is still missing from SLASHED that holds it back versus
Expand Down
2 changes: 1 addition & 1 deletion demos/full-api-demo-with-overrides.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
</nav>
<main id="main">
<h1>SLASHED Full API Demo</h1>
<p style="color:var(--sf-color-text--muted)">v0.7.0 · optimal-components bundle from jsDelivr CDN · 324 classes · 729 tokens (238 configurable)</p>
<p style="color:var(--sf-color-text--muted)">v0.7.3 · optimal-components bundle from jsDelivr CDN · 324 classes · 729 tokens (238 configurable)</p>
<div class="notice"><strong>ultimate-override.css is ACTIVE.</strong> Every value below is recomputed from perturbed knob tokens — toggle it off in the toolbar, or compare against <a href="full-api-demo.html">the un-overridden page</a>.</div>

<section id="tokens">
Expand Down
2 changes: 1 addition & 1 deletion demos/full-api-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
</nav>
<main id="main">
<h1>SLASHED Full API Demo</h1>
<p style="color:var(--sf-color-text--muted)">v0.7.0 · optimal-components bundle from jsDelivr CDN · 324 classes · 729 tokens (238 configurable)</p>
<p style="color:var(--sf-color-text--muted)">v0.7.3 · optimal-components bundle from jsDelivr CDN · 324 classes · 729 tokens (238 configurable)</p>
<div class="notice">Baseline render with default tokens. Use the toolbar to switch theme, toggle the ultimate override live, or replay motion. The always-on override variant is <a href="full-api-demo-with-overrides.html">full-api-demo-with-overrides.html</a>.</div>

<section id="tokens">
Expand Down
2 changes: 1 addition & 1 deletion demos/ultimate-override.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* ============================================================================
ULTIMATE OVERRIDE — generated by demos/generate.mjs (do not edit by hand)
SLASHED v0.7.0
SLASHED v0.7.3

Perturbs every CONFIGURABLE (role: "knob") token to a valid, visibly-distinct
value so full-api-demo.html can prove each one is wired end to end.
Expand Down
16 changes: 13 additions & 3 deletions optional/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@
border-color: var(--sf-btn-color);
}
.sf-btn--secondary:hover:not(:disabled, .sf-is-disabled, .sf-is-loading) {
background: color-mix(in oklab, var(--sf-btn-color) 8%, transparent);
border-color: var(--sf-btn-color);
}

Expand All @@ -120,8 +119,19 @@
color: var(--sf-btn-color);
border-color: transparent;
}
.sf-btn--ghost:hover:not(:disabled, .sf-is-disabled, .sf-is-loading) {
background: color-mix(in oklab, var(--sf-btn-color) 8%, transparent);

/* The subtle hover tint uses color-mix() — a modern colour function. Gate it
with @supports so engines without color-mix() skip the tint (the border /
foreground hover cues still apply) instead of dropping the background
declaration mid-rule. Mirrors the framework-wide gating policy enforced by
tests/tier1-p2-coverage.test.js + tests/tier1-p7-oldengine.test.js. */
@supports (background: color-mix(in oklab, red, red)) {
.sf-btn--secondary:hover:not(:disabled, .sf-is-disabled, .sf-is-loading) {
background: color-mix(in oklab, var(--sf-btn-color) 8%, transparent);
}
.sf-btn--ghost:hover:not(:disabled, .sf-is-disabled, .sf-is-loading) {
background: color-mix(in oklab, var(--sf-btn-color) 8%, transparent);
}
}

/* Outline: transparent to start, fills with the family colour on hover. */
Expand Down
16 changes: 2 additions & 14 deletions package-lock.json

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

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
"@release-it/conventional-changelog": "^11.0.1",
"culori": "^4.0.2",
"fast-check": "^4.8.0",
"lightningcss": "^1.32.0",
"release-it": "^20.2.1",
"stylelint": "^17.14.0",
"stylelint-config-standard": "^40.0.0"
Expand Down
11 changes: 11 additions & 0 deletions reports/full-api-audit/REPORT.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
<!-- OWNERSHIP / STATUS
This directory is a MANUALLY-RUN audit harness, not part of `npm run
build`/`docs` or any CI job. The check-*.mjs / probe-*.mjs scripts here are
re-runnable but require a configurator dev server + Playwright, so they are
intentionally not wired into the standard pipeline.
NOTE: the committed snapshots under `results/` (tokens-report.json,
reachable-ui.json, …) are consumed by tests/docs-artifacts-sync.test.js,
which asserts they reference no tokens absent from docs/api-index.json. That
test guards against stale token *names* only — it does not re-run the audit,
so refresh these snapshots by hand when the audited surface changes. -->

# SLASHED Full-API Testing Operation — Report

**Framework:** SLASHED v0.6.25 · **Date:** 2026-06-30
Expand Down
12 changes: 12 additions & 0 deletions scripts/artifacts.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"token-registry.json",
"dist/css-custom-data.json",
"configurator/src/data/api-index.generated.json",
"configurator/src/data/classes.generated.json",
"configurator/src/data/bundles.generated.json",
"configurator/src/data/token-registry.generated.json"
]
Expand All @@ -23,8 +24,19 @@
"buildCmd": "npm run configurator:sync",
"outputs": [
"configurator/src/data/api-index.generated.json",
"configurator/src/data/classes.generated.json",
"configurator/src/data/bundles.generated.json",
"configurator/src/data/token-registry.generated.json"
]
},
{
"name": "full-api demos",
"srcPrefixes": ["docs/api-index.json", "package.json", "core/", "optional/"],
"buildCmd": "node demos/generate.mjs",
"outputs": [
"demos/full-api-demo.html",
"demos/full-api-demo-with-overrides.html",
"demos/ultimate-override.css"
]
}
]
13 changes: 13 additions & 0 deletions scripts/check-artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ if (mode === 'fix') {
run(cmd, artifact.cwd ? resolve(root, artifact.cwd) : root);

for (const out of artifact.outputs) {
// A declared output that isn't tracked can never fail the diff check
// below (`git diff` ignores untracked/ignored paths), so it would be
// silently unguarded — exactly the drift class this gate exists to catch.
// Fail loudly instead.
if (!isTracked(out)) {
console.error(
`::error::${out} is a declared artifact of "${artifact.name}" but is not git-tracked ` +
`(untracked or .gitignore'd) — the freshness check cannot guard it. ` +
`Commit the file, or remove it from scripts/artifacts.json.`,
);
failed = true;
continue;
}
try {
gitFile('diff', '--exit-code', '--', out);
} catch {
Expand Down
56 changes: 32 additions & 24 deletions scripts/gen-class-reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,45 @@
import fs from 'node:fs';
import path from 'node:path';
import { CLASS_FILES } from './registry-sources.js';
import { stripComments, stripStrings, requireFile } from './lib/parse.js';

const ROOT = path.resolve(import.meta.dirname, '..');

const FILE_META = {
'core/layout.css': { title: 'Layout primitives', prefix: 'sf-' },
'core/macros.css': { title: 'Macro classes', prefix: 'sf-' },
'core/states.css': { title: 'State classes', prefix: 'sf-is-' },
'core/accessibility.css': { title: 'Accessibility', prefix: 'sf-' },
'core/motion.css': { title: 'Motion / entrances', prefix: 'sf-' },
'core/print.css': { title: 'Print utilities', prefix: '' },
'core/themes.css': { title: 'Theme utilities', prefix: 'sf-' },
'optional/forms.css': { title: 'Forms', prefix: 'sf-' },
'optional/components.css':{ title: 'Components', prefix: 'sf-' },
'optional/theme-example.css': { title: 'Theme example', prefix: '' },
'optional/utilities.css': { title: 'Utilities', prefix: 'sf-' },
// Human-facing section title per class-source file. Keyed by the same paths
// as registry-sources.js CLASS_FILES — a file added there without a title here
// fails loudly below rather than emitting an "undefined" section.
const FILE_TITLES = {
'core/layout.css': 'Layout primitives',
'core/macros.css': 'Macro classes',
'core/states.css': 'State classes',
'core/accessibility.css': 'Accessibility',
'core/motion.css': 'Motion / entrances',
'core/print.css': 'Print utilities',
'core/themes.css': 'Theme utilities',
'optional/forms.css': 'Forms',
'optional/components.css': 'Components',
'optional/theme-example.css': 'Theme example',
'optional/utilities.css': 'Utilities',
};

const SOURCES = CLASS_FILES.map(f => ({
file: f,
title: `${FILE_META[f].title} (\`${f}\`)`,
prefix: FILE_META[f].prefix,
}));
const SOURCES = CLASS_FILES.map(f => {
const title = FILE_TITLES[f];
if (!title) {
throw new Error(
`[docs:classes] ${f} is in registry-sources.js CLASS_FILES but has no title ` +
`in FILE_TITLES (scripts/gen-class-reference.js). Add one.`,
);
}
return { file: f, title: `${title} (\`${f}\`)` };
});

function extract(file) {
const abs = path.join(ROOT, file);
if (!fs.existsSync(abs)) {
throw new Error(`[docs:classes] Missing canonical class source file: ${abs}`);
}
const css = fs.readFileSync(abs, 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, '') // strip block comments
.replace(/"[^"]*"|'[^']*'/g, '""'); // strip string literals
// Same parsing contract as scripts/audit.js (the authoritative counter):
// strip comments then string literals so content:"…" can't yield false
// `.sf-*` matches. Shared via scripts/lib/parse.js so the two never diverge.
const css = stripStrings(stripComments(
requireFile(file, ROOT, `[docs:classes] Missing canonical class source file: ${file}`),
));
const names = new Set();
for (const m of css.matchAll(/\.(sf-[\w-]+)/g)) names.add(m[1]);
return [...names].sort();
Expand Down
22 changes: 20 additions & 2 deletions scripts/lib/api-index/extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ export const FILE_META = {
'optional/utilities.css': { category: 'Utilities', area: 'utilities', kind: 'utility' },
};

/**
* FILE_META lookup that fails loudly for a source file added to
* registry-sources.js (TOKEN_FILES/CLASS_FILES) but not described here — instead
* of the opaque `Cannot read properties of undefined (reading 'kind')` crash.
* @param {string} rel source path relative to repo root
* @returns {{category:string, area:string, kind:string}}
*/
function metaFor(rel) {
const meta = FILE_META[rel];
if (!meta) {
throw new Error(
`[api-index] ${rel} is in registry-sources.js but has no FILE_META entry ` +
`(scripts/lib/api-index/extract.js). Add its { category, area, kind }.`,
);
}
return meta;
}

// ── Source masking (length-preserving) ─────────────────────────────────────────
// Replacing comment/string bodies with spaces (NOT removing them) keeps every
// character index stable, so a token/class match found in the masked text can
Expand Down Expand Up @@ -420,7 +438,7 @@ export function extractClassesFromFile(rel, root, bundlesFor) {
const masked = maskStrings(maskComments(original));
const comments = collectComments(original);
const layer = layerOf(masked);
const meta = FILE_META[rel];
const meta = metaFor(rel);
const rows = new Map(); // name -> entry

/**
Expand Down Expand Up @@ -488,7 +506,7 @@ export function buildTokenEntries(root, bundlesFor, tokenAnnotations = {}, exist

for (const rel of TOKEN_FILES) {
const rows = extractTokensFromFile(rel, root);
const meta = FILE_META[rel];
const meta = metaFor(rel);
for (const [name, data] of rows) {
const existing = merged.get(name);
if (!existing) {
Expand Down
Loading