* feat(history): concept shared gallery The history page's gallery is the concept's section.history-gallery with its panel heading (Shared gallery, <name> · same positive prompt) and the Images media cards, followed by the recorded executions. A saved variant with no images shows the concept's empty copy. Recorded as D47. * feat(history): drop the shared gallery's Linked images caption (D47 ruling) The concept's "Shared gallery" heading is the gallery's only heading: the embedded collection's "Linked images" heading and "N loaded · N linked images" count repeated it. mountImages gains caption: false and a focusTarget; only the history page's shared gallery passes them, so the Images page and the All images and Repeat run dialogs keep their caption. The layout row and the selected-image side panel stay (TESTING.md D47). * test(history): concept comparison for prompt history history-concept.spec.mjs compares the Prompt history with the concept's #prompt/alpine on the concept fixture at 1600x1100 and 390x844, both themes and motion settings, on Chromium and touch, with its behaviors in both documents and a mutation check. inventoryOf gains a history section; mapHistoryConcept applies D24, D35, D45, D46, D47 and the approved Show archived button, laying the concept out at runtime. Corrections it demonstrated: each compared negative mounts in a display: contents child of the concept's .compare-item (D25), and the concept fixture now links images oldest first and counts one pair use per image, so newest-first reads and lane run counts match the concept. history-canonical-comparison.mjs uses the concept's selectors for production and drops its closed polish entries. * feat(history): drop the Linked images caption from the history dialogs (D47 ruling) The maintainer extended the D47 trim to the history page's two media dialogs. All images for this positive and Repeat run are named by their own dialog title, as in the concept (app.js a1282-1304), so their embedded collection no longer shows the "Linked images" heading or the "N loaded · N linked images" count. A stranded focus returns to the dialog's close button. The Images page keeps its caption. The history concept comparison now records each dialog's caption count in both documents, and TESTING.md D47 covers the dialogs. * refactor(workspace): retire library.css and the interim page guards Every page is rebuilt, so the rules that waited for PRs D, E and F go. library.css is deleted with its link; the overlay is now the only stylesheet after concept.css. Deleted because the concept already draws them: the .library-grid tracks and breakpoints, the history .row-actions flex row and button size, summary cursors, h2 margins, and the sender and Recorded run dialogs' colors and border. Deleted because they had no effect: min-inline-size on block children, a .recorded-run-dialog pre limit, the general .prompt-inspector margin (now scoped to the Recorded run dialog, the only mount point that is not display: contents) and a 700px padding a later rule overrode. The duplicated sender and Recorded run dialog guards are merged into one rule each. Kept with a marker: windowed Library rows (D3), the shelf states (D40), the history panel's wrap, archived-lane and coarse-target rules (D45), and under the new D48 the production surfaces the concept never shows: recorded executions and the Recorded run dialog (#209, D26), the Archive negative confirmation (#235) and the prompt sender. They now use the concept's --green and --red tokens instead of private copies with the same values. The workspace browser specs on Chromium and the concept specs on touch pass unchanged. TESTING.md records D48 and closes the role-block gap. * refactor(workspace): drop the dead embedded caption and tighten the review guards - images.mjs: an embedded collection never has a caption (D47), so the caption option, its "Linked images" heading, the embedded count string and the heading arm of the focus fallback are removed; callers no longer pass caption:false. Behavior is unchanged. - workspace.css: delete the never-applied .recorded-run-dialog::backdrop rule (the concept's .ca-tools dialog::backdrop always won) and note that the D3 row's min-inline-size: 0 is load-bearing for the concept's <=700px 1fr track. - concept-css test: the waiting-rule guard is case-insensitive and is proven against the exact retired comments and the kept provenance labels. - history-concept spec: function replacement for the singular run label, and the DOM mutation removes Compare negatives by role. - D47 markers sit where the caption trim happens.
151 lines
9.1 KiB
JavaScript
151 lines
9.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import { createHash } from 'node:crypto';
|
||
import { spawnSync } from 'node:child_process';
|
||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, copyFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join, dirname } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import test from 'node:test';
|
||
import { lintOverlay } from '../../tools/vendor/sync-concept-css.mjs';
|
||
|
||
const root = join(dirname(fileURLToPath(import.meta.url)), '../..');
|
||
const script = join(root, 'tools/vendor/sync-concept-css.mjs');
|
||
const concept = join(root, 'docs/concepts/workspace/style.css');
|
||
const copy = join(root, 'web/js/workspace/concept.css');
|
||
const sha = path => createHash('sha256').update(readFileSync(path)).digest('hex');
|
||
|
||
function walk(dir) {
|
||
return readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
||
const path = join(dir, entry.name);
|
||
return entry.isDirectory() ? walk(path) : [path];
|
||
});
|
||
}
|
||
|
||
test('concept copy is byte-identical', () => {
|
||
assert.ok(existsSync(copy), 'web/js/workspace/concept.css is missing; run sync-concept-css.mjs --write');
|
||
assert.equal(sha(copy), sha(concept));
|
||
const result = spawnSync(process.execPath, [script, '--check'], { cwd: root, encoding: 'utf8' });
|
||
assert.equal(result.status, 0, result.stderr);
|
||
});
|
||
|
||
test('check fails on a one-byte drift', () => {
|
||
const temp = mkdtempSync(join(tmpdir(), 'concept-css-'));
|
||
try {
|
||
mkdirSync(join(temp, 'docs/concepts/workspace'), { recursive: true });
|
||
mkdirSync(join(temp, 'web/js/workspace'), { recursive: true });
|
||
copyFileSync(concept, join(temp, 'docs/concepts/workspace/style.css'));
|
||
writeFileSync(join(temp, 'web/js/workspace/concept.css'), Buffer.concat([readFileSync(concept), Buffer.from(' ')]));
|
||
const run = () => spawnSync(process.execPath, [script, '--check', '--root', temp], { encoding: 'utf8' });
|
||
const drifted = run();
|
||
assert.equal(drifted.status, 1);
|
||
assert.match(drifted.stderr, /drift/i);
|
||
assert.equal(spawnSync(process.execPath, [script, '--write', '--root', temp]).status, 0);
|
||
assert.equal(run().status, 0);
|
||
} finally { rmSync(temp, { recursive: true, force: true }); }
|
||
});
|
||
|
||
test('overlay lint requires a decision marker on concept selectors', () => {
|
||
const conceptCss = '.card, .row { color: red; }\n@media (max-width: 700px) { .card .title { margin: 0; } }\n';
|
||
assert.deepEqual(lintOverlay(conceptCss, '.error-row { color: red; }').violations, []);
|
||
assert.equal(lintOverlay(conceptCss, '.x, .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '@media (max-width: 700px) { .card .title { margin: 1px; } }').violations.length, 1);
|
||
assert.deepEqual(lintOverlay(conceptCss, '/* D12 */\n.card { color: blue; }').violations, []);
|
||
assert.deepEqual(lintOverlay(conceptCss, '.card { /* D3 */ color: blue; }').violations, []);
|
||
assert.equal(lintOverlay(conceptCss, '/* tweak */ .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '/* D1 */ .card { color: blue; }\n.row { gap: 1px; }').violations.length, 1);
|
||
});
|
||
|
||
test('overlay lint accepts only this spec\'s (#249) decision numbers', () => {
|
||
const conceptCss = '.card { color: red; }\n';
|
||
assert.deepEqual(lintOverlay(conceptCss, '/* D12 of #249 */ .card { color: blue; }').violations, []);
|
||
assert.deepEqual(lintOverlay(conceptCss, '/* #249 D3: reason */ .card { color: blue; }').violations, []);
|
||
// Another issue's decision numbers are not #249 rulings, so they never mark a concept override.
|
||
assert.equal(lintOverlay(conceptCss, '/* D5 (#245) */ .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '/* D2 of #240 and #249 */ .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '/* #245 decision 5 */ .card { color: blue; }').violations.length, 1);
|
||
assert.equal(lintOverlay(conceptCss, '/* AD5 */ .card { color: blue; }').violations.length, 1);
|
||
});
|
||
|
||
test('overlay rules on concept selectors carry a decision marker', () => {
|
||
const overlay = join(root, 'web/js/workspace/workspace.css');
|
||
assert.ok(existsSync(overlay), 'web/js/workspace/workspace.css is missing');
|
||
const { violations } = lintOverlay(readFileSync(concept, 'utf8'), readFileSync(overlay, 'utf8'));
|
||
assert.deepEqual(violations, []);
|
||
});
|
||
|
||
// #249 PR F-b Task 3: every page is rebuilt, so no rule waits for a page PR any more and the history page's
|
||
// production-only rules live in the overlay in the concept's tokens (D48). A comment that opens with a page PR
|
||
// ("PR F: ...", "PR E/F: ...") or defers to one ("until ... PR", "retired in PR F") is a waiting rule; a provenance
|
||
// label inside a heading ("(#249 PR E)") is not.
|
||
const WAITING_RULE = [
|
||
/\/\*\s*PR [A-F](\/[A-F]|-[ab])?\b/i,
|
||
/\buntil\b[^*]*\b(PR [A-F]?|rebuild)/i,
|
||
/\bretired in PR\b/i,
|
||
/\binterim page guards\b/i,
|
||
];
|
||
const waitingRules = css => WAITING_RULE.filter(pattern => pattern.test(css)).map(String);
|
||
|
||
test('the waiting-rule guard catches the retired comments and spares provenance labels', () => {
|
||
// The exact comments this PR retired (workspace.css and library.css at 10a4847).
|
||
for (const retired of [
|
||
'/* Interim page guards: concept element rules (`.ca-tools dialog`, `pre`) now reach production dialogs and prompt text',
|
||
' that keep their own design until their page PR rebuilds them in concept markup. */',
|
||
'.ca-tools dialog.prompt-sender::backdrop { background: #000b; } /* PR D */',
|
||
'.ca-tools dialog.recorded-run-dialog { padding: 16px; } /* PR F */',
|
||
' .ca-tools .recorded-history .history-toolbar button { min-height: 44px; } /* PR F: concept coarse buttons are 40px */',
|
||
'/* Until their pages rebuild (PR F history): the history page\'s and the recorded run\'s prompt blocks keep their',
|
||
'/* PR E/F: the Archive dialog and the frozen field provenance keep `pre.prompt-text`, guarded against the concept `pre` rule. */',
|
||
" keeps the concept's) and are retired in PR F (history). */",
|
||
]) assert.notDeepEqual(waitingRules(retired), [], retired);
|
||
for (const kept of [
|
||
"/* Prompt library cards (#249 PR E): the concept's `article.prompt-card` (s653–708) with real prompts, titles and media. */",
|
||
'/* History variants panel (#249 PR F). */',
|
||
]) assert.deepEqual(waitingRules(kept), [], kept);
|
||
});
|
||
|
||
test('the overlay keeps no rule waiting for a page rebuild', () => {
|
||
const css = readFileSync(join(root, 'web/js/workspace/workspace.css'), 'utf8');
|
||
assert.deepEqual(waitingRules(css), []);
|
||
for (const retired of ['.library-chip', '--library-positive', '--library-negative', '--prompt-role']) {
|
||
assert.equal(css.includes(retired), false, `${retired} is retired`);
|
||
}
|
||
});
|
||
|
||
// #249 C8 and D3: the Images page and the inspector render the concept DOM, so the replaced production rules stay
|
||
// retired; the few production-only rules still consumed live in the overlay.
|
||
test('the overlay keeps no retired Images page or inspector rules', () => {
|
||
const css = readFileSync(join(root, 'web/js/workspace/workspace.css'), 'utf8');
|
||
// `.image-grid` and `.image-card-actions` are concept selectors now, so only the retired production names are listed.
|
||
for (const retired of ['.image-facts', '.image-filters', '.image-toolbar', '.image-preview', '.image-collection', '.image-continuation',
|
||
'.image-details', '.file-cover', '.file-inspector-inner', '.file-title', '.file-chip', '.file-facts', '.generation-facts', '.file-inspector-footer']) {
|
||
assert.equal(css.includes(retired), false, `${retired} is retired`);
|
||
}
|
||
});
|
||
|
||
test('only workspace.html loads workspace CSS', () => {
|
||
const offenders = walk(join(root, 'web'))
|
||
.filter(path => /\.(html|js|mjs)$/.test(path) && !path.includes('/lib/') && !path.includes('/shared/'))
|
||
.filter(path => /\b(concept|workspace)\.css\b/.test(readFileSync(path, 'utf8')))
|
||
.map(path => path.slice(root.length + 1));
|
||
assert.ok(offenders.every(path => path === 'web/workspace.html'), `unexpected consumers: ${offenders.join(', ')}`);
|
||
});
|
||
|
||
// Task A2 order: the concept's own order (toolkit, Viewer.js, concept), then the production overlay last; the page
|
||
// stylesheets that waited for their PRs are all retired (PR F-b deleted the last, library.css).
|
||
test('workspace.html loads controls, viewer, concept and the overlay in order', () => {
|
||
const html = readFileSync(join(root, 'web/workspace.html'), 'utf8');
|
||
const hrefs = [...html.matchAll(/<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"/g)].map(match => match[1]);
|
||
assert.deepEqual(hrefs, [
|
||
'js/shared/toolkit/controls.css',
|
||
'lib/viewerjs/viewer.min.css',
|
||
'js/workspace/concept.css',
|
||
'js/workspace/workspace.css',
|
||
]);
|
||
});
|
||
|
||
test('the retired shell, vocabulary, editor, Images, inspector and library stylesheets stay deleted', () => {
|
||
for (const name of ['style.css', 'vocabulary.css', 'library-editor.css', 'images.css', 'inspector.css', 'library.css']) {
|
||
assert.equal(existsSync(join(root, 'web/js/workspace', name)), false, `web/js/workspace/${name} must not come back`);
|
||
}
|
||
});
|