v0.3 infra: dev server from worktree + quietware screenshot capture

Two small infra changes to support visual verification of v0.3 phases
from a git worktree.

  - vite.config.js. server.fs.allow extended to ["..", "../.."]. When
    the dev server runs inside .worktrees/v0.3-tactile-quietware/ the
    shared node_modules sits one extra directory up, which Vite's
    default fs.strict policy rejected with HTTP 403 on the @sveltejs
    runtime client. Allowing the parent path unblocks dev runs from
    any worktree of this repo. No effect when dev runs from the main
    checkout.

  - scripts/capture-quietware-screenshots.mjs. Headless Playwright
    driver that visits /design-system-v2 (and best-effort /) under
    five attribute combinations (v0.2 baseline, quietware-dark,
    quietware-light, quietware-hc-dark, quietware-hc-light) and
    writes one PNG per combination to /tmp/lumotia-v0.3-screenshots/.
    Used to email Jake the visual proof for Phase 4a review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 13:37:44 +01:00
parent 091780086b
commit 8e612d1753
2 changed files with 93 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
// Drives the dev server with Playwright bundled chromium.
// Captures /design-system-v2 in 3 quietware modes (dark/light/HC) + a fallback.
import { chromium } from "playwright";
import { mkdir } from "node:fs/promises";
const OUT = "/tmp/lumotia-v0.3-screenshots";
await mkdir(OUT, { recursive: true });
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({
viewport: { width: 1440, height: 1024 },
deviceScaleFactor: 2,
});
const page = await ctx.newPage();
// Capture console for diagnosis
page.on("pageerror", e => console.error("[page error]", e.message));
page.on("console", m => { if (m.type() === "error") console.error("[console]", m.text()); });
async function setMode({ design, theme, contrast }) {
await page.evaluate(({ design, theme, contrast }) => {
const h = document.documentElement;
if (design) h.setAttribute("data-design", design); else h.removeAttribute("data-design");
if (theme) h.setAttribute("data-theme", theme); else h.removeAttribute("data-theme");
if (contrast) h.setAttribute("data-contrast", contrast); else h.removeAttribute("data-contrast");
}, { design, theme, contrast });
}
const CAPTURES = [
// route, modes
{ route: "/design-system-v2", name: "design-system" },
];
const MODES = [
{ id: "v02-dark", design: null, theme: null, contrast: null, label: "v0.2 baseline (no quietware)" },
{ id: "quiet-dark", design: "quietware", theme: null, contrast: null, label: "Quietware dark" },
{ id: "quiet-light", design: "quietware", theme: "light", contrast: null, label: "Quietware light" },
{ id: "quiet-hc-dark", design: "quietware", theme: null, contrast: "high", label: "Quietware high-contrast (dark base)" },
{ id: "quiet-hc-light", design: "quietware", theme: "light", contrast: "high", label: "Quietware high-contrast (light base)" },
];
const BASE = "http://localhost:1420";
for (const capture of CAPTURES) {
console.log(`\n=== ${capture.name} (${capture.route}) ===`);
try {
await page.goto(BASE + capture.route, { waitUntil: "networkidle", timeout: 30000 });
} catch (e) {
console.error(`navigation failed: ${e.message}`);
continue;
}
// Wait a beat for Svelte hydration + font load
await page.waitForTimeout(1500);
for (const mode of MODES) {
await setMode(mode);
await page.waitForTimeout(400); // CSS transitions
const file = `${OUT}/${capture.name}__${mode.id}.png`;
await page.screenshot({ path: file, fullPage: true });
const stats = await page.evaluate(() => ({
h: document.documentElement.dataset.design || "(none)",
t: document.documentElement.dataset.theme || "(default)",
c: document.documentElement.dataset.contrast || "(default)",
}));
console.log(` ${mode.id} design=${stats.h} theme=${stats.t} contrast=${stats.c}`);
}
}
// Also try the root route — may render the Dictation/FirstRun page if Tauri APIs
// gracefully degrade in browser context. Worth attempting; if it crashes we just skip.
try {
console.log("\n=== root route (best effort) ===");
await page.goto(BASE + "/", { waitUntil: "domcontentloaded", timeout: 15000 });
await page.waitForTimeout(2000);
for (const mode of MODES.slice(0, 3)) {
await setMode(mode);
await page.waitForTimeout(400);
const file = `${OUT}/root__${mode.id}.png`;
await page.screenshot({ path: file, fullPage: false });
console.log(` root ${mode.id}`);
}
} catch (e) {
console.error(`root route skipped: ${e.message}`);
}
await browser.close();
console.log(`\nWrote screenshots to ${OUT}/`);

View File

@@ -43,6 +43,12 @@ export default defineConfig(async () => ({
watch: { watch: {
ignored: ["**/src-tauri/**"], ignored: ["**/src-tauri/**"],
}, },
fs: {
// v0.3 Phase 4a screenshot capture: when dev server runs inside a
// git worktree (.worktrees/v0.3-tactile-quietware/) the parent
// node_modules sits one extra dir up. Allow the worktree root.
allow: ["..", "../.."],
},
}, },
test: { test: {
// jsdom gives storage-shim tests a real localStorage / DOMException / // jsdom gives storage-shim tests a real localStorage / DOMException /