Add /set: on-set screen tracking markers for crews
Deploy / deploy (push) Successful in 30s

- Unlinked, noindex page listing marker PNGs from public/set/markers
  (discovered at build time; filenames encode device/resolution/orientation)
- Detects the current screen and offers the closest match first
- One-tap full-screen viewer: wake lock, hidden controls, full grid/edges
  toggle, fill vs fit by aspect, back button closes, deep links via hash
- Offline service worker + home-screen manifest for iPhone full screen
- Marketing chrome moved into an (site) route group so /set has none

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-10-06 09:11:36 +02:00
parent e08bc7e758
commit ca122ad723
43 changed files with 692 additions and 17 deletions
+85
View File
@@ -0,0 +1,85 @@
import fs from "node:fs";
import path from "node:path";
// Screen-tracking marker images for on-set use (served at /set).
// Files live in public/set/markers and are discovered at build time, so adding
// a device is just dropping in a PNG named like:
// markers_phone_iPhone-Pro_1179x2556.png
// edge-markers_phone_iPhone-Pro_2556x1179_landscape.png
// markers_laptop_1920x1080.png (no device name)
// plain_grey_128.png (anything else = "extra")
export type Kind = "phone" | "tablet" | "laptop" | "monitor";
export type MarkerScreen = {
id: string; // stable key, e.g. "phone-iphone-pro-1179x2556"
kind: Kind;
name: string; // "iPhone Pro"
width: number;
height: number;
landscape: boolean;
full?: string; // /set/markers/markers_….png
edge?: string; // /set/markers/edge-markers_….png
};
export type MarkerExtra = { id: string; name: string; src: string };
const DIR = path.join(process.cwd(), "public", "set", "markers");
const URL_BASE = "/set/markers/";
const ORDER: Kind[] = ["phone", "tablet", "laptop", "monitor"];
const PATTERN = /^(edge-markers|markers)_(phone|tablet|laptop|monitor)_(?:(.+?)_)?(\d+)x(\d+)(_landscape)?\.png$/i;
function prettyName(raw: string | undefined, kind: Kind, w: number, h: number) {
if (!raw) {
if (kind === "laptop" && Math.max(w, h) === 1920) return "Laptop Full HD";
return `${kind[0].toUpperCase()}${kind.slice(1)} ${w}×${h}`;
}
return raw
.replace(/-/g, " ")
.replace(/ProMax/g, "Pro Max")
.replace(/(\d+(\.\d+)?)$/, "$1″")
.replace(/^4K$/, "4K Monitor");
}
export function loadMarkers(): { screens: MarkerScreen[]; extras: MarkerExtra[] } {
const files = fs.existsSync(DIR) ? fs.readdirSync(DIR).filter((f) => f.toLowerCase().endsWith(".png")) : [];
const screens = new Map<string, MarkerScreen>();
const extras: MarkerExtra[] = [];
for (const file of files.sort()) {
const m = file.match(PATTERN);
if (!m) {
const name = file
.replace(/\.png$/i, "")
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
extras.push({ id: file.replace(/\.png$/i, "").toLowerCase(), name, src: URL_BASE + file });
continue;
}
const [, variant, kindRaw, rawName, w, h, land] = m;
const kind = kindRaw.toLowerCase() as Kind;
const width = Number(w);
const height = Number(h);
const id = `${kind}-${(rawName ?? "generic").toLowerCase()}-${width}x${height}`;
const screen = screens.get(id) ?? {
id,
kind,
name: prettyName(rawName, kind, width, height),
width,
height,
landscape: Boolean(land) || width > height,
};
if (variant.toLowerCase() === "edge-markers") screen.edge = URL_BASE + file;
else screen.full = URL_BASE + file;
screens.set(id, screen);
}
const sorted = [...screens.values()].sort(
(a, b) =>
ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind) ||
a.name.localeCompare(b.name) ||
Number(a.landscape) - Number(b.landscape),
);
return { screens: sorted, extras };
}