Files
digital-garbageandClaude Sonnet 5.5 d8a906a866 Kit: responsive system (scales, wrapping toolbars, bounded windows), viewport test, docs
Lane sizes and the window margin become tokens; toolbars wrap by default; windows are capped to the viewport; a ratchet test stops element CSS gaining literal pixel sizes. app/e2e runs the real app in Chromium and Firefox at eight screen sizes (npm run test:viewport). docs/ui-design-guidelines.md gains "Responsive by construction".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 13:54:56 +02:00

48 lines
2.6 KiB
JavaScript

// A stand-in for the FunUI server, just enough for the browser tests: serves app/ui under /fun/app/ui/ and answers
// the API with a small fixture project, a pipeline with a sampler, and Training switched on. Anything else answers {}.
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "ui");
const TYPES = { ".js": "text/javascript", ".css": "text/css", ".html": "text/html", ".woff2": "font/woff2", ".svg": "image/svg+xml" };
const json = (v) => JSON.stringify(v);
const PROJECT = {
id: "p1", name: "Demo cut", num_frames_per_scene: 97, frame_rate: 25, scene_renders: {}, timeline_manually_ordered: false,
scenes: ["A fox runs through snow", "Close-up of the fox", "Wide shot, aurora sky"].map((text, i) => ({ id: `s${i + 1}`, text, frames_mode: "project" })),
};
const PIPELINE = {
queueable: true,
slots: [{ id: "k1", node: "KSampler", inputs: { steps: 20, cfg: 6, sampler_name: "euler" },
roles: ["steps:Steps", "cfg:CFG scale", "sampler_name:Sampler"].map((r) => ({ at: "generation.sampling", input: r.split(":")[0], label: r.split(":")[1] })) }],
};
const NODES = { KSampler: { widgets: [
{ name: "steps", type: "INT", min: 1, max: 100, default: 20 },
{ name: "cfg", type: "FLOAT", min: 0, max: 30, step: 0.1, default: 6 },
{ name: "sampler_name", type: "COMBO", options: ["euler", "dpmpp_2m", "heun"], default: "euler" }] } };
const ANSWERS = {
"/fun/api/projects": { projects: [{ id: "p1", name: "Demo cut", updated: 1 }] },
"/fun/api/projects/p1": PROJECT,
"/fun/api/pipeline": PIPELINE,
"/fun/api/nodes": { nodes: NODES },
"/api/m/training_lora/state": { enabled: true },
"/fun/api/m/training_lora/state": { enabled: true },
};
export function start(port = 0) {
const server = http.createServer((req, res) => {
const url = req.url.split("?")[0];
if (url === "/" || url.startsWith("/fun/app/ui/")) {
const file = path.join(ROOT, url === "/" ? "index.html" : url.slice("/fun/app/ui/".length));
if (!file.startsWith(ROOT)) { res.writeHead(403); return res.end(); }
try { const body = fs.readFileSync(file); res.writeHead(200, { "content-type": TYPES[path.extname(file)] || "application/octet-stream" }); return res.end(body); }
catch { res.writeHead(404); return res.end(); }
}
res.writeHead(200, { "content-type": "application/json" });
res.end(json(ANSWERS[url] || {}));
});
return new Promise((resolve) => server.listen(port, "127.0.0.1", () => resolve({ server, url: `http://127.0.0.1:${server.address().port}/` })));
}