mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
refactor: extract the base styles and finish the aggregator (#266 slice 5/5)
52 блока host/переменных/кросс-поверхностных групп → src/styles/base.styles.ts; styles.ts — 19-строчный сборщик [base, plan, devices, chrome, dialogs] с задокументированным контрактом порядка каскада. Два оставшихся мутантных якоря (:host-гейт ховера устройств) переадресованы на base.styles.ts. Юниты инвариантов (test/styles-split.test.mjs): состав и порядок сборщика; непересечение (scope+селектор) между файлами — единственное именованное исключение: кросс-поверхностная группа «:host(...) .dev:hover, .dev:focus- visible» живёт в base по §1.1; выживание медиа-обёрток — 2 forced-colors и 10 prefers-reduced-motion (в ТЗ и ревью фигурировали 8 — фактический счёт по исходнику 10, юнит держит точное число). fix-test-build научился точке в имени модуля (styles/base.styles → .js). ARCHITECTURE.md — раздел Styles. Refactor-proof diff (scope-ключ) пуст; golden 129/129 без переприёмки; смоки plan_snap_overlay/preloader OK; npm test 1318/0; бандл 1 291 440 → 1 291 458 (+18 байт). Issue: #266 User-Visible: no
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
/** #266: invariants of the styles split. */
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { cardStyles, baseStyles, planStyles, devicesStyles, chromeStyles, dialogsStyles }
|
||||
from '../test-build/styles.js';
|
||||
|
||||
const FILES = ['base', 'plan', 'devices', 'chrome', 'dialogs'];
|
||||
const sourceOf = (name) =>
|
||||
readFileSync(new URL(`../src/styles/${name}.styles.ts`, import.meta.url), 'utf8');
|
||||
|
||||
const selectorsOf = (tsSource) => {
|
||||
// Only the css`` payload: the TS import/export scaffolding is not CSS.
|
||||
const css = [...tsSource.matchAll(/css`([\s\S]*?)`/g)].map((m) => m[1]).join('\n');
|
||||
const text = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
const out = new Set();
|
||||
const walk = (chunk, scope) => {
|
||||
let i = 0;
|
||||
while (i < chunk.length) {
|
||||
const open = chunk.indexOf('{', i);
|
||||
if (open === -1) break;
|
||||
const header = chunk.slice(i, open).replace(/\s+/g, ' ').trim();
|
||||
let depth = 1, j = open + 1;
|
||||
while (j < chunk.length && depth > 0) {
|
||||
if (chunk[j] === '{') depth++;
|
||||
else if (chunk[j] === '}') depth--;
|
||||
j++;
|
||||
}
|
||||
if (header.startsWith('@media') || header.startsWith('@supports')) {
|
||||
walk(chunk.slice(open + 1, j - 1), `${scope}${header} :: `);
|
||||
} else if (!header.startsWith('@')) {
|
||||
// The key includes the media scope: the SAME selector inside and
|
||||
// outside a wrapper are different cascade entries by construction
|
||||
// (a multi-zone @media block lands in base as a whole).
|
||||
for (const sel of header.split(',')) out.add(`${scope}${sel.trim()}`);
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
};
|
||||
walk(text, '');
|
||||
return out;
|
||||
};
|
||||
|
||||
test('issue 266 the aggregator is exactly the five surface files in the cascade order', () => {
|
||||
assert.deepEqual(cardStyles, [baseStyles, planStyles, devicesStyles, chromeStyles, dialogsStyles],
|
||||
'the cascade order is a contract — the golden set was accepted against it');
|
||||
});
|
||||
|
||||
test('issue 266 surface files do not share a single selector', () => {
|
||||
const sets = FILES.map((name) => ({ name, set: selectorsOf(sourceOf(name)) }));
|
||||
const clashes = [];
|
||||
for (let i = 0; i < sets.length; i++) {
|
||||
for (let j = i + 1; j < sets.length; j++) {
|
||||
for (const sel of sets[i].set) {
|
||||
if (sets[j].set.has(sel)) clashes.push(`${sel} (${sets[i].name} ∩ ${sets[j].name})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Spec §1.3.2: every allowed exception is named with its reason.
|
||||
const allowed = [
|
||||
// The `:host([data-pointer-hover]) .dev:hover, .dev:focus-visible` GROUP
|
||||
// is cross-surface (host gate + device selector) and therefore lives in
|
||||
// base per spec §1.1, while the plain `.dev:focus-visible` outline rule
|
||||
// stays with its surface. Their source order (group first, outline
|
||||
// second) is preserved by the [base, …, devices] assembly order — golden
|
||||
// holds the cascade.
|
||||
'.dev:focus-visible (base ∩ devices)',
|
||||
];
|
||||
assert.deepEqual(clashes, allowed);
|
||||
});
|
||||
|
||||
test('issue 266 the media wrappers survived the move', () => {
|
||||
const all = FILES.map((name) => sourceOf(name)).join('\n');
|
||||
assert.equal((all.match(/@media \(forced-colors: active\)/g) || []).length, 2,
|
||||
'both forced-colors blocks must survive — golden never emulates them');
|
||||
assert.equal((all.match(/@media \(prefers-reduced-motion: reduce\)/g) || []).length, 10,
|
||||
'all ten reduced-motion blocks must survive — golden always shoots reduced');
|
||||
});
|
||||
Reference in New Issue
Block a user