mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-17 22:08:48 +08:00
Improve Backlot light theme
This commit is contained in:
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from backlot.state import PROJECTS_DIR, REPO_ROOT, list_projects, load_board_state, summarize_project
|
||||
@@ -29,6 +29,16 @@ _IGNORE_PARTS = {"node_modules", ".git", "__pycache__", ".cache"}
|
||||
SSE_HEARTBEAT_SECONDS = 15
|
||||
|
||||
|
||||
def _ui_html(name: str, assets: tuple[str, ...]) -> HTMLResponse:
|
||||
html = (UI_DIR / name).read_text(encoding="utf-8")
|
||||
for asset in assets:
|
||||
path = UI_DIR / asset
|
||||
if path.is_file():
|
||||
version = str(int(path.stat().st_mtime))
|
||||
html = html.replace(f"/ui/{asset}", f"/ui/{asset}?v={version}")
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
class ChangeHub:
|
||||
"""Fan-out of project-change notifications to SSE subscribers.
|
||||
|
||||
@@ -263,20 +273,32 @@ def create_app() -> FastAPI:
|
||||
# ---- UI ------------------------------------------------------------
|
||||
|
||||
@app.get("/p/{project_id}")
|
||||
async def board_page(project_id: str) -> FileResponse:
|
||||
return FileResponse(UI_DIR / "board.html")
|
||||
async def board_page(project_id: str) -> HTMLResponse:
|
||||
return _ui_html("board.html", ("board.css", "board.js"))
|
||||
|
||||
@app.get("/p/{project_path:path}")
|
||||
async def board_page_path(project_path: str) -> FileResponse:
|
||||
return FileResponse(UI_DIR / "board.html")
|
||||
async def board_page_path(project_path: str) -> HTMLResponse:
|
||||
return _ui_html("board.html", ("board.css", "board.js"))
|
||||
|
||||
@app.get("/")
|
||||
async def library_page() -> FileResponse:
|
||||
return FileResponse(UI_DIR / "index.html")
|
||||
async def library_page() -> HTMLResponse:
|
||||
return _ui_html("index.html", ("board.css", "library.js"))
|
||||
|
||||
if UI_DIR.is_dir():
|
||||
app.mount("/ui", StaticFiles(directory=UI_DIR), name="ui")
|
||||
|
||||
# The board is a long-lived SPA: a tab keeps running whatever board.js it
|
||||
# loaded, and browsers heuristically cache /ui assets. no-cache forces a
|
||||
# conditional revalidation (cheap 304 via ETag) on every load so UI fixes
|
||||
# show up on a plain refresh. Media/thumb responses keep normal caching.
|
||||
@app.middleware("http")
|
||||
async def ui_no_cache(request, call_next):
|
||||
response = await call_next(request)
|
||||
path = request.url.path
|
||||
if path == "/" or path.startswith("/ui") or path.startswith("/p/"):
|
||||
response.headers["Cache-Control"] = "no-cache"
|
||||
return response
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,17 @@
|
||||
--cream-shade: #e5d9be;
|
||||
--cream-ink: #29231a;
|
||||
--cream-ink-2: #6b5f4a;
|
||||
--bg-vignette: radial-gradient(1200px 600px at 50% -100px, #111116 0%, var(--bg) 70%);
|
||||
--grain-opacity: .035;
|
||||
--scroll-thumb: #26262e;
|
||||
--scroll-thumb-hover: #34343e;
|
||||
--clapper-a: #2c2c33;
|
||||
--clapper-b: #101013;
|
||||
--script-shadow: 0 18px 50px -18px rgba(0,0,0,.85), 0 1px 0 rgba(255,255,255,.06) inset;
|
||||
--modal-backdrop: rgba(5,5,7,.82);
|
||||
--media-bg: #000;
|
||||
--textcard-bg: #0d0d10;
|
||||
--badge-bg: rgba(8,8,10,.72);
|
||||
--sans: 'Inter', -apple-system, sans-serif;
|
||||
--mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||
--screenplay: 'Courier Prime', 'Courier New', monospace;
|
||||
@@ -35,13 +46,52 @@
|
||||
--fs-scale: 1.16;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bg: #efe4c9;
|
||||
--surface: #fff9ec;
|
||||
--surface-2: #f7edda;
|
||||
--surface-3: #ead8b5;
|
||||
--border: #d7c291;
|
||||
--border-soft: #e7d7b1;
|
||||
--text: #2c2419;
|
||||
--text-2: #766750;
|
||||
--text-3: #a39170;
|
||||
--amber: #b8522d;
|
||||
--amber-dim: rgba(184, 82, 45, 0.12);
|
||||
--green: #2f7b4e;
|
||||
--green-dim: rgba(47, 123, 78, 0.12);
|
||||
--red: #b84a3b;
|
||||
--red-dim: rgba(184, 74, 59, 0.11);
|
||||
--blue: #3f6f8f;
|
||||
--cream: #fff1c8;
|
||||
--cream-shade: #ead39d;
|
||||
--cream-ink: #2b251b;
|
||||
--cream-ink-2: #806f50;
|
||||
--bg-vignette:
|
||||
radial-gradient(1200px 560px at 50% -120px, rgba(255,248,224,.8), transparent 72%),
|
||||
linear-gradient(180deg, rgba(255,248,229,.96) 0%, rgba(244,230,195,.94) 54%, rgba(232,213,171,.98) 100%),
|
||||
repeating-linear-gradient(90deg, rgba(116,83,41,.04) 0 1px, transparent 1px 34px),
|
||||
repeating-linear-gradient(0deg, rgba(116,83,41,.032) 0 1px, transparent 1px 28px);
|
||||
--grain-opacity: .025;
|
||||
--scroll-thumb: #c5ad78;
|
||||
--scroll-thumb-hover: #9b7b47;
|
||||
--clapper-a: #2f2619;
|
||||
--clapper-b: #d9a836;
|
||||
--script-shadow: 0 34px 80px -48px rgba(93,68,35,.78), 0 1px 0 rgba(255,252,238,.86) inset;
|
||||
--modal-backdrop: rgba(52,38,22,.42);
|
||||
--media-bg: #201a13;
|
||||
--textcard-bg: #2b2419;
|
||||
--badge-bg: rgba(42,31,20,.78);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html { color-scheme: dark; }
|
||||
::-webkit-scrollbar { width: 10px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #26262e; border-radius: 6px; border: 2px solid var(--bg); }
|
||||
::-webkit-scrollbar-thumb:hover { background: #34343e; }
|
||||
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 6px; border: 2px solid var(--bg); }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
@@ -51,7 +101,7 @@ body {
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
/* faint vignette so media pops */
|
||||
background-image: radial-gradient(1200px 600px at 50% -100px, #111116 0%, var(--bg) 70%);
|
||||
background-image: var(--bg-vignette);
|
||||
}
|
||||
|
||||
.wrap { max-width: 1440px; margin: 0 auto; padding: 0 28px 80px; }
|
||||
@@ -60,7 +110,7 @@ body {
|
||||
body::after {
|
||||
content: ''; position: fixed; inset: -50%; pointer-events: none; z-index: 90;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
|
||||
opacity: .035; animation: grain 1.2s steps(4) infinite;
|
||||
opacity: var(--grain-opacity); animation: grain 1.2s steps(4) infinite;
|
||||
}
|
||||
@keyframes grain {
|
||||
0%, 100% { transform: translate(0,0); }
|
||||
@@ -101,7 +151,7 @@ aside .panel:nth-of-type(1) { animation-delay: .32s } aside .panel:nth-of-type(2
|
||||
}
|
||||
.clapper {
|
||||
width: 34px; height: 26px; border-radius: 4px; flex: none;
|
||||
background: repeating-linear-gradient(-45deg, #2c2c33 0 6px, #101013 6px 12px);
|
||||
background: repeating-linear-gradient(-45deg, var(--clapper-a) 0 6px, var(--clapper-b) 6px 12px);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.slate h1 {
|
||||
@@ -143,6 +193,256 @@ aside .panel:nth-of-type(1) { animation-delay: .32s } aside .panel:nth-of-type(2
|
||||
.cost .bar i.warn { background: var(--amber); }
|
||||
.cost .label { font-size: calc(10px * var(--fs-scale)); color: var(--text-3); letter-spacing: .08em; text-transform: uppercase; margin-top: 3px; }
|
||||
|
||||
.theme-toggle {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-2);
|
||||
background:
|
||||
radial-gradient(circle at 35% 25%, rgba(255,255,255,.16), transparent 42%),
|
||||
var(--surface-2);
|
||||
cursor: pointer;
|
||||
transition: transform .16s ease, border-color .16s ease, color .16s ease, background .16s ease;
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
color: var(--amber);
|
||||
border-color: rgba(240,168,60,.42);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.theme-toggle:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
|
||||
.theme-toggle-icon { font-size: calc(15px * var(--fs-scale)); line-height: 1; transform: translateY(-.5px); }
|
||||
|
||||
:root[data-theme="light"] .theme-toggle {
|
||||
background:
|
||||
radial-gradient(circle at 35% 24%, rgba(255,255,255,.9), transparent 42%),
|
||||
linear-gradient(145deg, #ffffff, #e9eef6);
|
||||
box-shadow: 0 10px 22px -18px rgba(33,45,70,.75);
|
||||
}
|
||||
:root[data-theme="light"] body {
|
||||
background-size: auto, auto, 34px 34px, 28px 28px;
|
||||
background-position: 0 0, 0 0, 12px 8px, 10px 6px;
|
||||
}
|
||||
:root[data-theme="light"] .wrap {
|
||||
max-width: 1480px;
|
||||
}
|
||||
:root[data-theme="light"] .slate {
|
||||
margin: 22px 0 0;
|
||||
padding: 22px 0 26px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
:root[data-theme="light"] .slate h1 {
|
||||
color: #2c2419;
|
||||
font-size: calc(24px * var(--fs-scale));
|
||||
letter-spacing: .18em;
|
||||
text-shadow: 0 1px 0 rgba(255,247,222,.75);
|
||||
}
|
||||
:root[data-theme="light"] .slate .wordmark {
|
||||
color: #aa9368;
|
||||
font-weight: 700;
|
||||
}
|
||||
:root[data-theme="light"] .chip {
|
||||
background: rgba(236,216,169,.46);
|
||||
border-color: #d8c391;
|
||||
color: #746144;
|
||||
box-shadow: 0 1px 0 rgba(255,248,224,.86) inset;
|
||||
}
|
||||
:root[data-theme="light"] .live.idle .dot {
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 0 4px rgba(47,123,78,.12);
|
||||
}
|
||||
:root[data-theme="light"] .live.idle {
|
||||
color: #8c7958;
|
||||
}
|
||||
:root[data-theme="light"] .cost .bar {
|
||||
background: #dfc994;
|
||||
}
|
||||
:root[data-theme="light"] .rail {
|
||||
margin: 24px 0 28px;
|
||||
padding: 32px 28px 30px;
|
||||
border: 1px solid rgba(214,193,142,.9);
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,250,235,.82), rgba(248,237,211,.7)),
|
||||
repeating-linear-gradient(90deg, transparent 0 52px, rgba(159,122,61,.055) 52px 53px);
|
||||
box-shadow: 0 24px 55px -42px rgba(100,75,37,.7);
|
||||
}
|
||||
:root[data-theme="light"] .panel,
|
||||
:root[data-theme="light"] .drawer,
|
||||
:root[data-theme="light"] .replay-bar,
|
||||
:root[data-theme="light"] .lib-card {
|
||||
border-color: rgba(214,193,142,.9);
|
||||
box-shadow:
|
||||
0 24px 60px -44px rgba(99,73,35,.72),
|
||||
0 1px 0 rgba(255,249,228,.82) inset;
|
||||
}
|
||||
:root[data-theme="light"] .panel {
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(112,84,42,.025) 0 1px, transparent 1px 30px),
|
||||
linear-gradient(180deg, rgba(255,250,236,.96), rgba(249,239,214,.92)),
|
||||
var(--surface);
|
||||
}
|
||||
:root[data-theme="light"] .panel-head,
|
||||
:root[data-theme="light"] .drawer .drawer-head {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(255,249,230,.96), rgba(245,232,199,.72)),
|
||||
linear-gradient(180deg, rgba(184,82,45,.055), transparent);
|
||||
}
|
||||
:root[data-theme="light"] .panel-head h2,
|
||||
:root[data-theme="light"] .section-title {
|
||||
color: #7c6340;
|
||||
}
|
||||
:root[data-theme="light"] .stage .node {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-width: 2px;
|
||||
background: linear-gradient(180deg, #fff8e7, #ead8b5);
|
||||
box-shadow: 0 12px 24px -18px rgba(64,47,23,.88);
|
||||
}
|
||||
:root[data-theme="light"] .stage .line {
|
||||
top: 17px;
|
||||
left: calc(-50% + 17px);
|
||||
right: calc(50% + 17px);
|
||||
border-top: 1px dotted rgba(47,123,78,.24);
|
||||
background: transparent;
|
||||
}
|
||||
:root[data-theme="light"] .stage:hover { background: rgba(184,82,45,.045); }
|
||||
:root[data-theme="light"] .stage.done .node {
|
||||
background: linear-gradient(180deg, #f7fff5, #dcefdc);
|
||||
border-color: #3f8f5a;
|
||||
color: var(--green);
|
||||
}
|
||||
:root[data-theme="light"] .stage.done .line {
|
||||
border-color: rgba(47,123,78,.22);
|
||||
background: transparent;
|
||||
}
|
||||
:root[data-theme="light"] .stage.active .line,
|
||||
:root[data-theme="light"] .stage.await .line {
|
||||
border-color: rgba(184,82,45,.36);
|
||||
background: transparent;
|
||||
}
|
||||
:root[data-theme="light"] .stage .name {
|
||||
color: #6a5537;
|
||||
font-weight: 700;
|
||||
}
|
||||
:root[data-theme="light"] .stage .sub {
|
||||
color: #9d875f;
|
||||
}
|
||||
:root[data-theme="light"] .stage.done .sub {
|
||||
color: #8a7858;
|
||||
}
|
||||
:root[data-theme="light"] .script-card {
|
||||
background:
|
||||
linear-gradient(178deg, rgba(255,250,226,.28), transparent 38%),
|
||||
repeating-linear-gradient(0deg, rgba(93,72,35,.036) 0 1px, transparent 1px 24px),
|
||||
linear-gradient(178deg, var(--cream) 0%, var(--cream-shade) 132%);
|
||||
border: 1px solid rgba(198,158,84,.58);
|
||||
box-shadow:
|
||||
0 38px 90px -50px rgba(88,61,24,.78),
|
||||
0 1px 0 rgba(255,251,232,.84) inset;
|
||||
}
|
||||
:root[data-theme="light"] .script-card::after {
|
||||
background: rgba(112,84,40,.16);
|
||||
}
|
||||
:root[data-theme="light"] .script-preview::before {
|
||||
background: linear-gradient(to bottom, rgba(255,241,200,0), rgba(234,211,157,.96) 74%);
|
||||
}
|
||||
:root[data-theme="light"] .script-approved {
|
||||
background: rgba(242,253,238,.66);
|
||||
color: #3f8f5a;
|
||||
border-color: #3f8f5a;
|
||||
}
|
||||
:root[data-theme="light"] .filmstrip {
|
||||
background:
|
||||
radial-gradient(circle 3.5px, #ceb982 97%, transparent) 0 6px / 26px 10px repeat-x,
|
||||
radial-gradient(circle 3.5px, #ceb982 97%, transparent) 0 calc(100% - 16px) / 26px 10px repeat-x,
|
||||
linear-gradient(180deg, rgba(255,246,222,.72), rgba(237,220,181,.6));
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(214,193,142,.84);
|
||||
scrollbar-color: #c5ad78 transparent;
|
||||
}
|
||||
:root[data-theme="light"] .scene-card .sc-slate {
|
||||
color: #9a845e;
|
||||
}
|
||||
:root[data-theme="light"] .scene-card .sc-slate .num {
|
||||
color: #5f4b30;
|
||||
}
|
||||
:root[data-theme="light"] .thumb {
|
||||
border-color: rgba(181,143,78,.75);
|
||||
box-shadow: 0 16px 34px -24px rgba(82,57,25,.72);
|
||||
}
|
||||
:root[data-theme="light"] .thumb.generating {
|
||||
background: linear-gradient(100deg, #f7ecd2 32%, #ead8b5 48%, #f7ecd2 64%);
|
||||
}
|
||||
:root[data-theme="light"] .thumb.spec { border-color: #d8bf83; }
|
||||
:root[data-theme="light"] .thumb.spec .spec-shot,
|
||||
:root[data-theme="light"] .empty .big,
|
||||
:root[data-theme="light"] .lib-poster .lp-txt {
|
||||
color: #ad9469;
|
||||
}
|
||||
:root[data-theme="light"] .wave i { background: #d8c391; }
|
||||
:root[data-theme="light"] .wave.played i { background: #aa9368; }
|
||||
:root[data-theme="light"] .drawer pre { background: #fff5dc; }
|
||||
:root[data-theme="light"] .replay-bar {
|
||||
background:
|
||||
repeating-linear-gradient(90deg, transparent 0 19px, rgba(171,139,80,.24) 19px 20px),
|
||||
linear-gradient(90deg, rgba(255,250,235,.9), rgba(246,232,199,.78));
|
||||
}
|
||||
:root[data-theme="light"] .replay-bar .rp-btn {
|
||||
color: #fff9ed;
|
||||
background: linear-gradient(180deg, #c15a34, #a84224);
|
||||
border-color: rgba(120,55,31,.42);
|
||||
box-shadow: 0 10px 22px -14px rgba(126,56,28,.9), 0 1px 0 rgba(255,225,197,.38) inset;
|
||||
}
|
||||
:root[data-theme="light"] .replay-bar .rp-btn:hover {
|
||||
color: #fff;
|
||||
border-color: rgba(120,55,31,.55);
|
||||
background: linear-gradient(180deg, #cc6239, #a84224);
|
||||
}
|
||||
:root[data-theme="light"] .replay-bar input[type=range] {
|
||||
accent-color: var(--amber);
|
||||
}
|
||||
:root[data-theme="light"] .render-hero {
|
||||
box-shadow: 0 22px 55px -38px rgba(64,45,21,.68);
|
||||
}
|
||||
:root[data-theme="light"] .lib-card:hover {
|
||||
border-color: #b9894e;
|
||||
box-shadow:
|
||||
0 28px 68px -44px rgba(93,64,28,.8),
|
||||
0 1px 0 rgba(255,249,228,.82) inset;
|
||||
}
|
||||
:root[data-theme="light"] .decision {
|
||||
border-color: rgba(214,193,142,.72);
|
||||
}
|
||||
:root[data-theme="light"] .decision .d-cat {
|
||||
color: #b8522d;
|
||||
}
|
||||
:root[data-theme="light"] .decision .d-pick {
|
||||
color: #312719;
|
||||
}
|
||||
:root[data-theme="light"] .decision .d-pick .arrow {
|
||||
color: var(--green);
|
||||
}
|
||||
:root[data-theme="light"] .act-row {
|
||||
border-color: rgba(214,193,142,.58);
|
||||
}
|
||||
:root[data-theme="light"] .clapper {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 10px;
|
||||
background:
|
||||
linear-gradient(135deg, transparent 39%, #d9a836 40% 44%, transparent 45%),
|
||||
linear-gradient(180deg, #352a1a, #251d13);
|
||||
border-color: rgba(66,49,24,.32);
|
||||
box-shadow: 0 14px 28px -18px rgba(58,42,20,.8);
|
||||
}
|
||||
|
||||
/* ---------- stage rail ---------- */
|
||||
.rail { display: flex; align-items: flex-start; padding: 26px 0 22px; }
|
||||
.stage { flex: 1; display: flex; flex-direction: column; align-items: center; position: relative; min-width: 0; }
|
||||
@@ -196,7 +496,7 @@ aside .panel:nth-of-type(1) { animation-delay: .32s } aside .panel:nth-of-type(2
|
||||
|
||||
/* ---------- layout ---------- */
|
||||
.board { display: grid; grid-template-columns: 1fr 320px; gap: 22px; align-items: start; }
|
||||
.main-col { min-width: 0; }
|
||||
.main-col { min-width: 0; display: flex; flex-direction: column; gap: 22px; }
|
||||
|
||||
.panel { background: var(--surface); border: 1px solid var(--border-soft); border-radius: 12px; }
|
||||
.panel + .panel { margin-top: 18px; }
|
||||
@@ -217,10 +517,26 @@ aside .panel:nth-of-type(1) { animation-delay: .32s } aside .panel:nth-of-type(2
|
||||
max-width: 700px; /* screenplay pages are narrow — paper on a dark desk */
|
||||
margin: 0 auto;
|
||||
font-family: var(--screenplay);
|
||||
box-shadow: 0 18px 50px -18px rgba(0,0,0,.85), 0 1px 0 rgba(255,255,255,.06) inset;
|
||||
box-shadow: var(--script-shadow);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.script-preview {
|
||||
max-height: min(430px, calc(100vh - 360px));
|
||||
min-height: 320px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.script-preview::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 8px;
|
||||
bottom: 0;
|
||||
height: 88px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to bottom, rgba(242,233,213,0), var(--cream-shade) 72%);
|
||||
z-index: 1;
|
||||
}
|
||||
.script-card::after { /* page edge */
|
||||
content: ''; position: absolute; right: 7px; top: 7px; bottom: 7px; width: 1px;
|
||||
background: rgba(0,0,0,.07);
|
||||
@@ -247,6 +563,7 @@ aside .panel:nth-of-type(1) { animation-delay: .32s } aside .panel:nth-of-type(2
|
||||
.script-card .sp-expand {
|
||||
position: absolute; right: 16px; bottom: 12px;
|
||||
font-family: var(--mono); font-size: calc(10px * var(--fs-scale)); color: var(--cream-ink-2); letter-spacing: .06em;
|
||||
z-index: 2;
|
||||
}
|
||||
.script-approved {
|
||||
position: absolute; top: 20px; right: 26px;
|
||||
@@ -319,7 +636,7 @@ aside .panel:nth-of-type(1) { animation-delay: .32s } aside .panel:nth-of-type(2
|
||||
.thumb .badge {
|
||||
position: absolute; left: 7px; bottom: 7px;
|
||||
font-family: var(--mono); font-size: calc(9px * var(--fs-scale)); letter-spacing: .06em;
|
||||
background: rgba(8,8,10,.72); color: var(--text-2);
|
||||
background: var(--badge-bg); color: #eef2f8;
|
||||
padding: 2px 7px; border-radius: 3px; backdrop-filter: blur(4px);
|
||||
}
|
||||
.thumb .play {
|
||||
@@ -362,7 +679,7 @@ aside .panel:nth-of-type(1) { animation-delay: .32s } aside .panel:nth-of-type(2
|
||||
.thumb.missing .spec-desc { color: var(--amber); -webkit-line-clamp: 2; }
|
||||
|
||||
/* text-card scene (typographic placeholder) */
|
||||
.thumb.textcard { display: flex; align-items: center; justify-content: center; background: #0d0d10; }
|
||||
.thumb.textcard { display: flex; align-items: center; justify-content: center; background: var(--textcard-bg); }
|
||||
.thumb.textcard .tc-copy {
|
||||
font-family: var(--mono); font-weight: 500; text-align: center;
|
||||
letter-spacing: .2em; font-size: calc(11px * var(--fs-scale)); color: #d8d8de; padding: 0 10px;
|
||||
@@ -413,7 +730,7 @@ aside .panel:nth-of-type(1) { animation-delay: .32s } aside .panel:nth-of-type(2
|
||||
|
||||
/* ---------- modal ---------- */
|
||||
.modal-bg {
|
||||
position: fixed; inset: 0; background: rgba(5,5,7,.82); backdrop-filter: blur(6px);
|
||||
position: fixed; inset: 0; background: var(--modal-backdrop); backdrop-filter: blur(6px);
|
||||
display: none; align-items: flex-start; justify-content: center; overflow-y: auto;
|
||||
padding: 48px 20px; z-index: 50;
|
||||
}
|
||||
@@ -505,8 +822,10 @@ a.backlink:hover { color: var(--text-2); }
|
||||
}
|
||||
|
||||
/* render section */
|
||||
.render-hero { position: relative; border-radius: 12px; overflow: hidden; border: 1px solid var(--border); background: #000; }
|
||||
.render-hero video { width: 100%; display: block; max-height: 560px; }
|
||||
.render-hero { position: relative; border-radius: 12px; overflow: hidden; border: 1px solid var(--border); background: var(--media-bg); display: flex; justify-content: center; }
|
||||
/* natural width up to the container: a 9:16 render shows as a centered
|
||||
portrait frame instead of stretching into a letterboxed landscape box */
|
||||
.render-hero video { display: block; width: auto; max-width: 100%; max-height: 560px; }
|
||||
.render-meta { font-family: var(--mono); font-size: calc(10.5px * var(--fs-scale)); color: var(--text-3); padding: 8px 2px; display: flex; gap: 14px; flex-wrap: wrap; }
|
||||
.render-meta .v { color: var(--text-2); cursor: pointer; }
|
||||
.render-meta .v.active { color: var(--amber); }
|
||||
|
||||
@@ -12,12 +12,37 @@ const app = document.getElementById("app");
|
||||
const modal = document.getElementById("modal");
|
||||
const player = document.getElementById("player");
|
||||
|
||||
const THEME_KEY = "backlot.theme";
|
||||
let currentTheme = localStorage.getItem(THEME_KEY) === "light" ? "light" : "dark";
|
||||
let state = null;
|
||||
let selectedStage = null; // stage drawer open for this stage name
|
||||
let activeRender = 0;
|
||||
let replay = null; // {t0, t1, t, playing} — replay mode when non-null
|
||||
let firstPaint = true;
|
||||
|
||||
function applyTheme(theme) {
|
||||
currentTheme = theme === "light" ? "light" : "dark";
|
||||
document.documentElement.dataset.theme = currentTheme;
|
||||
localStorage.setItem(THEME_KEY, currentTheme);
|
||||
}
|
||||
|
||||
function renderThemeToggle() {
|
||||
const next = currentTheme === "light" ? "dark" : "light";
|
||||
return el("button", {
|
||||
class: "theme-toggle",
|
||||
type: "button",
|
||||
title: `Switch to ${next} theme`,
|
||||
"aria-label": `Switch to ${next} theme`,
|
||||
"aria-pressed": currentTheme === "light" ? "true" : "false",
|
||||
onclick: () => {
|
||||
applyTheme(next);
|
||||
render();
|
||||
},
|
||||
}, el("span", { class: "theme-toggle-icon", "aria-hidden": "true" }, currentTheme === "light" ? "☾" : "☀"));
|
||||
}
|
||||
|
||||
applyTheme(currentTheme);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// header slate
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -72,6 +97,7 @@ function renderSlate(s) {
|
||||
),
|
||||
...chips,
|
||||
el("div", { class: "spacer" }),
|
||||
renderThemeToggle(),
|
||||
liveEl,
|
||||
cost,
|
||||
);
|
||||
@@ -219,7 +245,7 @@ function renderScriptCard(s) {
|
||||
const scriptStage = s.stages.find((x) => x.name === "script");
|
||||
const approved = scriptStage && scriptStage.status === "completed";
|
||||
|
||||
const card = el("div", { class: "script-card", title: "Click to expand full script", onclick: openScriptModal },
|
||||
const card = el("div", { class: "script-card script-preview", title: "Click to expand full script", onclick: openScriptModal },
|
||||
approved ? el("span", { class: "script-approved" }, "APPROVED") : null,
|
||||
el("div", { class: "sp-title" }, script.title || s.title),
|
||||
el("div", { class: "sp-meta" },
|
||||
@@ -528,7 +554,10 @@ function renderRenders(s) {
|
||||
// watch: carry playback position/state over to the recreated element.
|
||||
const prev = document.querySelector(".render-hero video");
|
||||
const src = mediaURL(s.project_id, current.path);
|
||||
const video = el("video", { src, controls: "", preload: "none" });
|
||||
// preload="metadata" gives the element its intrinsic aspect ratio (and a
|
||||
// poster frame) before playback — without it a portrait 9:16 render sits
|
||||
// in a letterboxed 100%-wide black box that reads as landscape.
|
||||
const video = el("video", { src, controls: "", preload: "metadata" });
|
||||
// Click the frame to start playback (controls handle pause/scrub) — the
|
||||
// big player was inert to a click on the picture itself.
|
||||
video.addEventListener("click", () => { if (video.paused) video.play().catch(() => {}); });
|
||||
@@ -778,16 +807,22 @@ function render() {
|
||||
if (decisions) aside.append(decisions);
|
||||
if (activity) aside.append(activity);
|
||||
|
||||
if (script || decisions || activity) {
|
||||
app.append(el("div", { class: "board" }, main, aside));
|
||||
}
|
||||
|
||||
// Media sections live INSIDE the main column so a tall decisions rail
|
||||
// never pushes them below the fold — the column flows beside the rail.
|
||||
const storyboard = renderStoryboard(s);
|
||||
if (storyboard) app.append(storyboard);
|
||||
const found = renderFoundMedia(s);
|
||||
if (found) app.append(found);
|
||||
const renders = renderRenders(s);
|
||||
if (renders) app.append(renders);
|
||||
|
||||
if (script || decisions || activity) {
|
||||
for (const section of [storyboard, found, renders]) {
|
||||
if (section) main.append(section);
|
||||
}
|
||||
app.append(el("div", { class: "board" }, main, aside));
|
||||
} else {
|
||||
for (const section of [storyboard, found, renders]) {
|
||||
if (section) app.append(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Defensive normalization (F-02): the server contract guarantees these
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import { el, fmtAgo, getJSON, subscribe, thumbURL } from "/ui/lib.js";
|
||||
|
||||
const grid = document.getElementById("grid");
|
||||
const THEME_KEY = "backlot.theme";
|
||||
let currentTheme = localStorage.getItem(THEME_KEY) === "light" ? "light" : "dark";
|
||||
|
||||
function applyTheme(theme) {
|
||||
currentTheme = theme === "light" ? "light" : "dark";
|
||||
document.documentElement.dataset.theme = currentTheme;
|
||||
localStorage.setItem(THEME_KEY, currentTheme);
|
||||
}
|
||||
|
||||
function renderThemeToggle() {
|
||||
const next = currentTheme === "light" ? "dark" : "light";
|
||||
return el("button", {
|
||||
class: "theme-toggle",
|
||||
type: "button",
|
||||
title: `Switch to ${next} theme`,
|
||||
"aria-label": `Switch to ${next} theme`,
|
||||
"aria-pressed": currentTheme === "light" ? "true" : "false",
|
||||
onclick: () => {
|
||||
applyTheme(next);
|
||||
const replacement = renderThemeToggle();
|
||||
document.querySelector(".theme-toggle").replaceWith(replacement);
|
||||
},
|
||||
}, el("span", { class: "theme-toggle-icon", "aria-hidden": "true" }, currentTheme === "light" ? "☾" : "☀"));
|
||||
}
|
||||
|
||||
applyTheme(currentTheme);
|
||||
document.getElementById("liveBadge").before(renderThemeToggle());
|
||||
|
||||
function miniRail(states) {
|
||||
const rail = el("div", { class: "mini-rail" });
|
||||
|
||||
Reference in New Issue
Block a user