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>
48 lines
2.6 KiB
JavaScript
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}/` })));
|
|
}
|