feat(ui): one start screen, and chrome in the system face

The welcome card is gone. The full-screen splash is the start screen for both
frontends, shown whenever nothing is open — Begin, the last project, the full
project list (with import), and the maintenance actions. "Load an existing
project" now opens a picker inside that surface instead of handing off.

Chrome type follows Apple's conventions: group headers, field labels, tabs,
segmented controls, lane names and status chips are sentence-case system sans,
sized on the scale, with no letter-spacing. Monospace is now reserved for data —
timecodes, counts, paths, prompts, node classes — which is what it was doing in
the timeline ruler all along. Zone padding moves onto the 4pt grid.

Scene length/fps modes read "Project default" / "Timeline trim" instead of
labels too long for their control.
This commit is contained in:
digital-garbage
2026-08-12 19:56:10 +03:00
parent 457136ca68
commit db70062bb7
7 changed files with 146 additions and 246 deletions
+1 -1
View File
@@ -276,7 +276,7 @@
}
// First run (or a wizard interrupted by an install restart) gets the setup flow;
// everyone else gets the project picker they already know.
if (!loaded && !window.Onboarding?.maybeOpen()) window.ProjectMenu.open({ dismissable: false });
if (!loaded) window.Onboarding?.maybeOpen();
// After the project is in place, so a run targeting it re-attaches fully (result and
// all) instead of falling back to the queue-watch path. Not awaited — it runs for as
// long as the generation does.
-1
View File
@@ -136,7 +136,6 @@
<script src="autocomplete.js"></script>
<script src="suggestions.js"></script>
<script src="slot_picker.js"></script>
<script src="welcome.js"></script>
<script src="pipeline_setup.js"></script>
<script src="onboarding.js"></script>
<script src="inspector.js"></script>
+1 -1
View File
@@ -137,7 +137,7 @@
wrap.append(el("span", null, m.label));
const sel = el("select"); sel.dataset.k = "sc-" + kind + "mode";
[["project", "Inherit project global"], ["timeline", "Inherit timeline (trim)"], ["custom", "Custom"]]
[["project", "Project default"], ["timeline", "Timeline trim"], ["custom", "Custom"]]
.forEach(([v, l]) => { const o = el("option", null, l); o.value = v; if (v === mode) o.selected = true; sel.append(o); });
sel.onchange = () => {
const patch = { [kind + "_mode"]: sel.value };
+77 -34
View File
@@ -15,7 +15,6 @@
const API = () => window.MovieEditorAPI;
const LS_DONE = "funpack_onboarded";
const LS_RUN = "funpack_wizard"; // an unfinished run (see save())
const SS_SKIP = "funpack_wizard_skip"; // this tab session declined it
let root = null; // the full-screen overlay
let stage = null; // the animated content column
@@ -139,11 +138,76 @@
sub: "Markers that cut one written prompt into separate scenes.",
mount: (body) => ({ cleanup: window.Composer.mountPane("splits", body) }),
},
projects: {
title: "Your projects",
sub: "Open one, or bring in a project file from somewhere else.",
mount: (body, setActions) => mountProjects(body, setActions),
},
};
// The project picker. This screen replaced a separate welcome card — two different
// front doors for the same decision was one too many.
function mountProjects(body, setActions) {
const importInput = el("input");
importInput.type = "file";
importInput.accept = ".json,.funpack_project.json,application/json";
importInput.style.display = "none";
importInput.onchange = async () => {
const file = importInput.files?.[0];
importInput.value = "";
if (!file) return;
await S().importProject(file);
close();
};
body.append(importInput);
const imp = el("button", "btn ghost tiny", "Import project file…");
imp.type = "button";
imp.onclick = () => importInput.click();
setActions([imp]);
const list = el("div", "oo-projects");
body.append(list);
const paintList = async () => {
clear(list);
let projects = [];
try { projects = (await API().listProjects()).projects || []; } catch (_) {}
projects.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
if (!projects.length) {
list.append(el("p", "oo-body", "No projects yet. Go back and press Begin."));
return;
}
projects.forEach((p) => {
const row = el("button", "oo-project");
row.type = "button";
const main = el("div", "oo-project-main");
main.append(el("div", "oo-project-name", p.name));
const n = p.scene_count || 0;
main.append(el("div", "oo-project-meta",
`${n} ${n === 1 ? "scene" : "scenes"} · ${when(p.updated_at)}`));
row.append(main);
row.onclick = () => { close(); S().loadProject(p.id); };
list.append(row);
});
};
paintList();
return { cleanup: () => {} };
}
function when(ts) {
if (!ts) return "never opened";
const days = Math.floor((Date.now() / 1000 - ts) / 86400);
if (days <= 0) return "today";
if (days === 1) return "yesterday";
if (days < 30) return days + " days ago";
return new Date(ts * 1000).toLocaleDateString();
}
function openPane(kind) {
const spec = PANES[kind];
if (!spec) return;
const fromSplash = !panel && !!root?.querySelector(".oo-splash");
closePane();
const box = el("div", "oo-pane");
const head = el("div", "oo-pane-head");
@@ -164,17 +228,20 @@
body.append(el("p", "oo-body", "This part of the setup isn't available in this build."));
}
panel = { kind, cleanup: mounted?.cleanup || null };
panel = { kind, fromSplash, cleanup: mounted?.cleanup || null };
paint(box, "pane:" + kind, { pane: true });
root.append(actions({ primary: "Done", onPrimary: closePane }));
root.append(actions({ primary: fromSplash ? "Back" : "Done", onPrimary: closePane }));
root.classList.add("oo-has-actions");
}
function closePane() {
if (!panel) return;
const backToSplash = panel.fromSplash;
try { panel.cleanup?.(); } catch (_) {}
panel = null;
if (root) renderStep();
if (!root) return;
if (backToSplash) paint(splash(), null);
else renderStep();
}
// ── step registry ─────────────────────────────────────────────────────────
@@ -508,7 +575,6 @@
resumeBtn.textContent = "Continue with “" + p.name + "”";
resumeBtn.hidden = false;
resumeBtn.onclick = () => {
suppressSession();
close();
(S().loadProject(p.id) || Promise.resolve()).catch(() => {});
};
@@ -568,15 +634,7 @@
next();
}
// Loading a project IS an answer to the wizard's question, so it stops asking for the
// rest of this session — including the tour, which would only get in the way of work
// that is already under way.
function openExisting() {
suppressSession();
close();
if (isEasyGen()) window.ProjectMenu.open({ dismissable: true });
else window.WelcomePage.open();
}
function openExisting() { openPane("projects"); }
// ── navigation ────────────────────────────────────────────────────────────
@@ -657,16 +715,6 @@
try { localStorage.removeItem(LS_RUN); } catch (_) {}
}
// Suppressed for the rest of this tab's session: the user answered the wizard's own
// question by loading a project instead. sessionStorage, not localStorage — next time
// they open the app fresh, an unfinished setup is worth offering again.
function suppressSession() {
try { sessionStorage.setItem(SS_SKIP, "1"); } catch (_) {}
}
function sessionSuppressed() {
try { return sessionStorage.getItem(SS_SKIP) === "1"; } catch (_) { return false; }
}
// The tour lives in its own page mode against a sandbox project, so it cannot start
// over the editor — finishing navigates there.
function startTour() {
@@ -681,7 +729,6 @@
function finish() {
try { localStorage.setItem(LS_DONE, "1"); } catch (_) {}
clearSaved();
suppressSession();
// The wizard IS the pipeline setup — without this its modal reopens over the editor
// and asks for the family and the node packs all over again.
window.PipelineSetup?.markHandled?.();
@@ -747,25 +794,21 @@
try { return localStorage.getItem(LS_DONE) === "1"; } catch (_) { return false; }
}
// Called by each host's boot instead of its old welcome screen.
// The start screen for both frontends: shown whenever nothing is open, not just on a
// first run. Its splash carries Begin, the last project, the project list, and the
// maintenance actions, so there is one front door instead of two.
function maybeOpen() {
if (window.__FUNPACK_TOUR__) return false;
if (sessionSuppressed()) return false;
// Something is already open — a resumed generation, a restored project. Setup is not
// the screen anyone wants in front of work that is already there.
if (S()?.get?.().project) return false;
if (S()?.get?.().project) return false; // work already open — don't cover it
const saved = restore();
if (saved?.resumable) { open({ resume: true }); return true; }
// An unfinished run that did NOT end in a deliberate restart: start over.
if (saved) { open({ carry: true }); return true; }
if (done()) return false;
open();
open(saved ? { carry: true } : undefined);
return true;
}
// Menu entry: run setup again on demand, whatever state the app is in.
function reopen() {
try { sessionStorage.removeItem(SS_SKIP); } catch (_) {}
clearSaved();
open();
}
+3 -6
View File
@@ -824,7 +824,6 @@
refreshPreview();
await loadModels();
_validateSceneRenders();
window.WelcomePage?.close?.();
window.PipelineSetup?.maybePrompt?.();
}
@@ -849,7 +848,7 @@
if (state.projects[0]) await loadProject(state.projects[0].id);
else {
notify();
if (window.WelcomePage) window.WelcomePage.open();
window.Onboarding?.open?.();
}
}
@@ -4341,11 +4340,9 @@
}
} catch (_) { /* queue unreachable — boot normally */ }
notify();
// First run (or a wizard interrupted by an install restart) gets the setup flow;
// everyone else gets the project picker they already know.
// One front door: the full-screen welcome, unless a running generation was resumed.
if (resumed) return;
if (window.Onboarding?.maybeOpen()) return;
if (window.WelcomePage) window.WelcomePage.open();
window.Onboarding?.maybeOpen();
}
// Backstop for a suspended save the user walks away from. Unconditional is safe:
+64 -94
View File
@@ -316,7 +316,7 @@ button, input, select, textarea { font-family: inherit; color: var(--text); }
}
.brand { font-family: var(--font-display); font-weight: 800; letter-spacing: .2px; font-size: var(--fs-title-3); display: flex; align-items: center; gap: 8px; }
.brand-mark { color: var(--accent); font-size: 12px; transform: translateY(-1px); filter: drop-shadow(0 0 6px var(--accent-soft)); }
.brand-sub { color: var(--faint); font-weight: 500; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; }
.brand-sub { color: var(--faint); font-weight: 500; font-size: var(--fs-callout); }
/* The brand is a link back to the hub in both frontends. */
a.brand { text-decoration: none; color: inherit; }
a.brand:hover .brand-sub { color: var(--accent-hi); }
@@ -342,16 +342,17 @@ a.brand:hover .brand-sub { color: var(--accent-hi); }
}
.menu-item:hover { background: var(--accent-soft); color: var(--accent-hi); }
.menu-item.disabled { color: var(--faint); pointer-events: none; }
.menu-item .hint { color: var(--faint); font-family: var(--font-mono); font-size: 11px; }
.menu-item .hint { color: var(--faint); font-size: var(--fs-subhead); }
.menu-item:hover .hint { color: var(--accent); }
.menu-sep { height: 1px; background: var(--line); margin: 5px 6px; }
.menu-label { padding: 4px 10px 3px; font-size: 10px; letter-spacing: 1.4px; text-transform: uppercase; color: var(--faint); }
.menu-label { padding: 4px 10px 3px; font-size: 11px; color: var(--faint); font-weight: 600;}
.menu-veil { position: fixed; inset: 0; z-index: 150; }
.menubar-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.health-chip, .save-chip {
font-family: var(--font-mono); font-size: 11px; color: var(--muted);
display: flex; align-items: center; gap: 6px; padding: 3px 9px; border: 1px solid var(--line); border-radius: 20px;
font-size: var(--fs-subhead); line-height: var(--lh-subhead); color: var(--muted);
display: flex; align-items: center; gap: 6px; padding: 3px var(--sp-2);
border: 1px solid var(--line); border-radius: 20px;
}
.health-chip .led { width: 7px; height: 7px; border-radius: 50%; background: var(--faint); }
.health-chip.ok .led { background: var(--good); box-shadow: 0 0 8px var(--good); }
@@ -361,7 +362,7 @@ a.brand:hover .brand-sub { color: var(--accent-hi); }
/* backend log viewer */
.log-btn { background: var(--ink-2); border: 1px solid var(--line); border-radius: 20px; color: var(--muted);
font-family: var(--font-mono); font-size: 11px; letter-spacing: .5px; padding: 3px 11px; cursor: pointer; }
font-size: 11px; padding: 3px 11px; cursor: pointer; }
.log-btn:hover { color: var(--accent-hi); background: var(--ink-3); }
.log-btn.active { color: var(--accent-hi); border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
.log-panel { position: fixed; z-index: 9000; right: 14px; top: 48px; width: min(720px, 88vw); height: min(420px, 60vh);
@@ -402,9 +403,8 @@ a.brand:hover .brand-sub { color: var(--accent-hi); }
/* panel toggle tabs (timeline header, top-right) */
.dock-tabs { display: flex; gap: 4px; margin-left: 12px; flex: 0 0 auto; }
.dock-tab {
background: var(--ink-2); border: 1px solid var(--line); border-radius: 6px;
color: var(--faint); font-family: var(--font-mono); font-size: 10px; letter-spacing: .8px;
text-transform: uppercase; padding: 4px 10px; cursor: pointer; transition: background .12s, color .12s, border-color .12s;
background: var(--ink-2); border: 1px solid var(--line); border-radius: 6px;
color: var(--faint); font-size: 11px; padding: 4px 10px; cursor: pointer; transition: background .12s, color .12s, border-color .12s;
}
.dock-tab:hover { color: var(--muted); border-color: var(--muted); }
.dock-tab.on { background: var(--accent-soft); color: var(--accent-hi); border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
@@ -419,11 +419,11 @@ a.brand:hover .brand-sub { color: var(--accent-hi); }
border-bottom: 1px solid var(--line);
}
.zone-title {
font-family: var(--font-mono); font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted);
font-size: var(--fs-callout); line-height: var(--lh-callout); color: var(--text); font-weight: 600;
}
.zone-title::before { content: ""; display: inline-block; width: 6px; height: 6px; margin-right: 9px; border-radius: 1px; background: var(--accent); transform: translateY(-1px); box-shadow: 0 0 7px var(--accent-soft); }
.zone-title::before { content: ""; display: inline-block; width: 5px; height: 5px; margin-right: var(--sp-2); border-radius: 50%; background: var(--accent); transform: translateY(-1px); }
.transport-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.transport-actions .btn.compact { min-height: var(--ctl-h-sm); padding: 0 var(--sp-2); font-size: var(--fs-subhead); font-family: var(--font-mono); }
.transport-actions .btn.compact { min-height: var(--ctl-h-sm); padding: 0 var(--sp-2); font-size: var(--fs-subhead); }
.transport-actions .btn:disabled { opacity: 0.42; pointer-events: none; cursor: default; }
.transport-actions .btn.render {
background: linear-gradient(var(--teal-2), var(--teal-3)); color: var(--on-teal); border-color: var(--teal-2);
@@ -433,7 +433,7 @@ a.brand:hover .brand-sub { color: var(--accent-hi); }
.zone-head-right { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-subhead);
color: var(--faint); display: flex; gap: var(--sp-1) var(--sp-2); align-items: center;
flex-wrap: wrap; justify-content: flex-end; min-width: 0; }
.zone-body { flex: 1; overflow: auto; padding: 12px; min-height: 0; }
.zone-body { flex: 1; overflow: auto; padding: var(--sp-4); min-height: 0; }
/* splitters */
.splitter { background: transparent; position: relative; flex: 0 0 auto; }
@@ -468,11 +468,12 @@ a.brand:hover .brand-sub { color: var(--accent-hi); }
.btn.tiny { min-height: var(--ctl-h-sm); padding: 0 var(--sp-2); font-size: var(--fs-subhead); }
label.field { display: block; margin-bottom: 12px; }
label.field > span { display: block; font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--faint); margin-bottom: 5px; }
label.field > span { display: block; font-size: var(--fs-subhead); line-height: var(--lh-subhead);
color: var(--muted); margin-bottom: var(--sp-1); font-weight: 500; }
/* Field label with an inline enable toggle (e.g. Postfix). Matches the field-label look. */
.field-toggle { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; cursor: pointer; }
.field-toggle > input { width: auto; margin: 0; }
.field-toggle > span { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--faint); }
.field-toggle > span { font-size: 11px; color: var(--faint); font-weight: 600;}
.field > textarea:disabled { opacity: .45; cursor: not-allowed; }
input, select, textarea {
width: 100%; background: var(--ink-0); border: 1px solid var(--line); border-radius: var(--ctl-r);
@@ -489,7 +490,9 @@ input[type=number] { font-family: var(--font-mono); }
/* ───────────────────────── MEDIA BROWSER ───────────────────────── */
.mb-section { margin-bottom: 18px; }
.mb-section-title { display: flex; align-items: center; justify-content: space-between; font-family: var(--font-mono); font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--faint); margin-bottom: 8px; }
.mb-section-title { display: flex; align-items: center; justify-content: space-between;
font-size: var(--fs-subhead); line-height: var(--lh-subhead); color: var(--muted);
margin-bottom: var(--sp-2); font-weight: 600; }
.project-row { display: flex; align-items: center; gap: var(--sp-2); padding: 6px var(--sp-2); border-radius: var(--ctl-r); cursor: pointer; border: 1px solid transparent; }
.project-row:hover { background: var(--ink-2); }
.project-row.active { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 27%, transparent); }
@@ -616,7 +619,7 @@ input[type=number] { font-family: var(--font-mono); }
.media-bin-cols { display: flex; align-items: center; gap: 8px; }
.media-bin-cols-switch { flex: 1; min-width: 0; }
.media-bin-cols-switch .insp-seg { flex: 1; min-width: 0; padding-left: 4px; padding-right: 4px; }
.media-bin-sort-lbl { flex: 0 0 auto; font-family: var(--font-mono); font-size: 9px; letter-spacing: .8px; text-transform: uppercase; color: var(--faint); }
.media-bin-sort-lbl { flex: 0 0 auto; font-size: 11px; color: var(--faint); font-weight: 600;}
.media-bin-sort-sel { flex: 1; min-width: 0; padding: 5px 7px; font-size: 12px; }
.media-bin-count { margin: 0; }
.media-grid-empty { grid-column: 1 / -1; padding: 4px 2px; }
@@ -653,9 +656,9 @@ input[type=number] { font-family: var(--font-mono); }
/* library add/edit form */
.lib-form { border: 1px solid var(--accent); border-radius: 8px; background: var(--ink-2); padding: 10px; margin: 8px 0; display: flex; flex-direction: column; gap: 8px; }
.lib-form.lib-form-modal { border: none; background: transparent; padding: 0; margin: 0; }
.lib-form-title { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--accent); }
.lib-form-title { font-size: 11px; color: var(--accent); font-weight: 600;}
.lib-field { display: flex; flex-direction: column; gap: 3px; }
.lib-field > span { font-family: var(--font-mono); font-size: 9px; letter-spacing: .8px; text-transform: uppercase; color: var(--faint); }
.lib-field > span { font-size: 11px; color: var(--faint); font-weight: 600;}
.lib-in { width: 100%; background: var(--ink-0); border: 1px solid var(--line); border-radius: 6px; color: var(--text); font-size: 12px; padding: 5px 7px; font-family: var(--font-mono); resize: vertical; }
.lib-form-actions { display: flex; gap: 6px; }
@@ -772,14 +775,14 @@ input[type=number] { font-family: var(--font-mono); }
.pm-anchor-btn { font-size: 12px; white-space: nowrap; }
/* ───────────────────────── INSPECTOR ───────────────────────── */
.insp-tag { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; color: var(--accent); text-transform: uppercase; }
.insp-tag { font-size: var(--fs-subhead); line-height: var(--lh-subhead); color: var(--muted); font-weight: 600; }
.insp-hint { font-size: 11px; color: var(--faint); margin: -4px 0 12px; }
.insp-hint.warn { color: var(--accent-hi, var(--accent)); }
.insp-block { margin-bottom: 16px; }
/* global prompt — first entry in the inspector list (scrolls with content) */
.insp-global { margin: 0 0 14px; padding: 10px 10px 8px;
background: var(--ink-2); border: 1px solid var(--line); border-radius: 9px; }
.insp-global-title { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--accent); margin-bottom: 6px; }
.insp-global-title { font-size: 11px; color: var(--accent); margin-bottom: 6px; font-weight: 600;}
.insp-global-ta { width: 100%; background: var(--ink-0); border: 1px solid var(--line); border-radius: 7px;
color: var(--text); font-size: 12px; padding: 7px 8px; resize: vertical; font-family: var(--font-mono); line-height: 1.45; }
.insp-global .insp-hint { margin: 6px 0 0; }
@@ -787,7 +790,7 @@ input[type=number] { font-family: var(--font-mono); }
/* project / scene switch */
.insp-switch { display: flex; gap: 0; margin-bottom: 14px; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.insp-seg { flex: 1; background: var(--ink-1); border: none; color: var(--muted); padding: 7px 0; cursor: pointer;
font-family: var(--font-mono); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; transition: background .12s, color .12s; }
font-size: 11px; transition: background .12s, color .12s; }
.insp-seg + .insp-seg { border-left: 1px solid var(--line); }
.insp-seg:hover:not(:disabled) { background: var(--ink-3); color: var(--accent-hi); }
.insp-seg.active { background: var(--accent-soft); color: var(--accent-hi); }
@@ -863,8 +866,8 @@ input[type=number] { font-family: var(--font-mono); }
.tl-toolbar .btn.ghost:disabled:hover { color: var(--muted); background: transparent; border-color: transparent; }
.tl-toolbar .tl-spacer { flex: 1; }
.tl-tc { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.tl-zlabel { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--faint); }
.tl-keys { font-family: var(--font-mono); font-size: 10px; letter-spacing: .5px; color: var(--faint); margin-right: 10px; opacity: .8; }
.tl-zlabel { font-size: 11px; color: var(--faint); font-weight: 600;}
.tl-keys { font-size: 11px; color: var(--faint); margin-right: 10px; opacity: .8; }
.tl-notice { margin-left: 12px; font-size: 11px; color: var(--accent-hi); display: inline-flex; align-items: center; gap: 6px; }
.tl-notice-dismiss { border: none; background: transparent; color: var(--faint); cursor: pointer; font-size: 11px; padding: 0 2px; }
.tl-notice-dismiss:hover { color: var(--text); }
@@ -930,9 +933,8 @@ input[type=number] { font-family: var(--font-mono); }
position: sticky; top: 0; z-index: 9; background: var(--ink-1); }
.tl-gutter-tracks { flex: 0 0 auto; display: flex; flex-direction: column; }
.tl-gutter-lane { display: flex; align-items: center; justify-content: center; text-align: center;
padding: 0 4px; font-family: var(--font-mono); font-size: 9px; letter-spacing: .5px; text-transform: uppercase;
color: var(--faint); border-bottom: 1px solid var(--line-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
box-sizing: border-box; flex: 0 0 auto; }
padding: 0 4px; font-size: 11px; color: var(--faint); border-bottom: 1px solid var(--line-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
box-sizing: border-box; flex: 0 0 auto; }
.tl-gutter-lane.video { color: var(--faint); }
.tl-gutter-aud { flex: 0 0 auto; border-top: 1px solid var(--line); }
.tl-gutter-aud .tl-gutter-lane:last-child { border-bottom: none; }
@@ -1056,7 +1058,7 @@ input[type=number] { font-family: var(--font-mono); }
.ov-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ov-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.ov-field.full { grid-column: 1 / -1; }
.ov-label { font-size: 11px; font-family: var(--font-mono); letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.ov-label { font-size: 11px; color: var(--muted); font-weight: 600;}
.ov-input, .ov-textarea { width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: 10px; background: var(--ink-2); color: var(--text); padding: 10px 12px; font: inherit; }
.ov-textarea { min-height: 72px; resize: vertical; line-height: 1.35; }
.ov-input:focus, .ov-textarea:focus, .ov-color:focus { outline: none; border-color: color-mix(in srgb, var(--violet) 45%, var(--line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--violet) 12%, transparent); }
@@ -1142,7 +1144,7 @@ body.tl-spacing { cursor: grabbing; user-select: none; }
background: var(--surface-raised-7); border: 1px solid var(--line); border-radius: 8px; padding: 8px;
display: flex; flex-direction: column; gap: 7px; box-shadow: 0 8px 22px color-mix(in srgb, var(--shadow-ink) 64.7%, transparent); }
.tl-dd-panel[hidden] { display: none; }
.tl-dd-head { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.tl-dd-head { font-size: 11px; color: var(--muted); font-weight: 600;}
.tl-dd-sel { width: 100%; }
.tl-dd-val { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; }
.tl-dd-val[hidden] { display: none; }
@@ -1212,10 +1214,10 @@ body.tl-spacing { cursor: grabbing; user-select: none; }
.clip.selected.focus.clip-video::before,
.clip.selected.focus.src-v2v::before { background: var(--teal); opacity: 1; }
.tl-sel-badge {
font-family: var(--font-mono); font-size: 10px; font-weight: 600; color: var(--teal);
background: color-mix(in srgb, var(--teal) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--teal-soft) 55%, var(--line));
border-radius: 10px; padding: 2px 8px; white-space: nowrap; flex-shrink: 0;
font-size: 11px; font-weight: 600; color: var(--teal);
background: color-mix(in srgb, var(--teal) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--teal-soft) 55%, var(--line));
border-radius: 10px; padding: 2px 8px; white-space: nowrap; flex-shrink: 0;
}
.clip-src-stack { display: inline-flex; align-items: center; gap: 1px; line-height: 1; }
.clip-anchor-mark { font-size: 11px; color: var(--accent); }
@@ -1336,59 +1338,10 @@ body.tl-spacing { cursor: grabbing; user-select: none; }
.empty-stage { color: var(--faint); padding: 30px; text-align: center; width: 100%; }
/* ───────────────────────── WELCOME (first load) ───────────────────────── */
.welcome-overlay {
position: fixed;
left: 0; right: 0; bottom: 0;
top: var(--menubar-h);
z-index: 350;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background:
radial-gradient(900px 480px at 50% 20%, color-mix(in srgb, var(--accent) 8%, transparent) 0%, transparent 55%),
radial-gradient(700px 400px at 80% 80%, color-mix(in srgb, var(--teal) 6%, transparent) 0%, transparent 50%),
var(--backdrop-2);
backdrop-filter: blur(6px);
animation: fade .2s ease;
}
.welcome-card {
width: min(520px, 96vw);
padding: 34px 32px 28px;
border-radius: 16px;
border: 1px solid var(--line);
background: linear-gradient(165deg, var(--surface-raised-6) 0%, var(--surface-deep-3) 100%);
box-shadow: var(--shadow), 0 0 0 1px var(--overlay-1) inset;
animation: pop .18s ease;
}
.welcome-brand {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.welcome-mark { color: var(--accent); font-size: 13px; filter: drop-shadow(0 0 8px var(--accent-soft)); }
.welcome-title { font-family: var(--font-display); font-weight: 800; font-size: 28px; letter-spacing: .2px; }
.welcome-sub {
font-family: var(--font-display);
font-weight: 500;
font-size: 11px;
letter-spacing: 2.5px;
text-transform: uppercase;
color: var(--faint);
}
.welcome-lead {
margin: 0 0 22px;
color: var(--muted);
font-size: 13px;
line-height: 1.55;
max-width: 42em;
}
/* ── project-menu buttons (Easy Gen) ──────────────────────────────────────
The Editor's welcome card is gone — the full-screen wizard splash is the only
start screen now. Easy Gen's project menu still uses these button styles. */
.welcome-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.welcome-maint { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.welcome-maint .welcome-btn { padding-top: 8px; padding-bottom: 8px; }
.welcome-btn {
display: flex;
flex-direction: column;
@@ -1425,7 +1378,6 @@ body.tl-spacing { cursor: grabbing; user-select: none; }
.welcome-btn-hint { font-size: 11px; color: var(--faint); line-height: 1.35; }
@media (max-width: 520px) {
.welcome-actions { grid-template-columns: 1fr; }
.welcome-card { padding: 24px 20px 20px; }
}
/* ───────────────────────── MODALS (Models settings) ───────────────────────── */
@@ -1455,7 +1407,7 @@ body.tl-spacing { cursor: grabbing; user-select: none; }
.composer { background: var(--ink-0); border: 1px solid var(--line); border-radius: 10px; padding: 14px; margin-bottom: 18px; }
.composer.flash { animation: composer-flash .9s ease; }
@keyframes composer-flash { 0%, 100% { box-shadow: none; border-color: var(--line); } 25% { box-shadow: 0 0 0 2px var(--accent); border-color: var(--accent); } }
.composer-title { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--accent); margin-bottom: 10px; }
.composer-title { font-size: 11px; color: var(--accent); margin-bottom: 10px; font-weight: 600;}
.composer-row { display: flex; gap: 10px; align-items: center; }
.composer-row select { flex: 1; }
.composer-row .composer-search { flex: 1.2; min-width: 0; }
@@ -1487,8 +1439,8 @@ body.tl-spacing { cursor: grabbing; user-select: none; }
/* config validation banner */
/* Pipeline requirements panel */
.req-panel { margin-bottom: 18px; }
.req-title { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--teal); margin-bottom: 8px; }
.req-opt-head { font-size: 10px; color: var(--faint); margin: 8px 0 4px; text-transform: uppercase; letter-spacing: 1px; }
.req-title { font-size: 11px; color: var(--teal); margin-bottom: 8px; font-weight: 600;}
.req-opt-head { font-size: 11px; color: var(--faint); margin: 8px 0 4px; font-weight: 600;}
.req-row { display: flex; align-items: flex-start; gap: 8px; padding: 6px 10px; border-radius: 6px; margin-bottom: 4px; font-size: 12px; flex-wrap: wrap; }
.req-row.ok { background: color-mix(in srgb, var(--teal) 8%, transparent); }
.req-row.missing { background: color-mix(in srgb, var(--danger) 8%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent); }
@@ -1581,7 +1533,7 @@ label.field.linked input, label.field.linked select { opacity: .6; }
.chip-x:hover { color: var(--danger); }
.wire-box { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--line); }
.wire-title { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--teal); margin-bottom: 8px; }
.wire-title { font-size: 11px; color: var(--teal); margin-bottom: 8px; font-weight: 600;}
.wire-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; min-width: 0; }
.wire-out { font-family: var(--font-mono); font-size: 11px; color: var(--muted); flex: 0 0 auto; min-width: 130px; }
.wire-arrow { color: var(--teal); }
@@ -1659,7 +1611,7 @@ label.field.linked input, label.field.linked select { opacity: .6; }
.insp-fold-sum { cursor: pointer; font-size: 12px; color: var(--muted); padding: 8px 0; list-style: none; }
.insp-fold-sum::-webkit-details-marker { display: none; }
.engine-preset-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; }
.engine-preset-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; color: var(--faint); }
.engine-preset-label { font-size: 11px; color: var(--faint); font-weight: 600;}
.slot-picker-overlay { z-index: 12000; }
.slot-picker-modal { max-width: 420px; }
.slot-picker-list { display: flex; flex-direction: column; gap: 6px; margin: 12px 0; max-height: 50vh; overflow-y: auto; }
@@ -1779,8 +1731,8 @@ body.tour-mode { overflow: hidden; }
.composer-btn {
display: inline-flex; align-items: center; gap: 6px;
background: var(--ink-2); border: 1px solid var(--line); border-radius: 6px;
color: var(--faint); font-family: var(--font-mono); font-size: 10px; letter-spacing: .8px;
text-transform: uppercase; padding: 4px 10px; cursor: pointer; transition: background .12s, color .12s, border-color .12s;
color: var(--faint); font-size: var(--fs-subhead); line-height: var(--lh-subhead);
padding: 4px var(--sp-2); cursor: pointer; transition: background .12s, color .12s, border-color .12s;
}
.composer-btn:hover { color: var(--muted); border-color: var(--muted); }
.composer-btn .composer-dot { font-size: 9px; color: var(--teal); transform: translateY(-.5px); }
@@ -1811,7 +1763,7 @@ body.tour-mode { overflow: hidden; }
.fw-title { display: flex; align-items: baseline; gap: 9px; min-width: 0; }
.fw-dot { color: var(--teal); font-size: 9px; }
.fw-title-text { font-family: var(--font-display); font-weight: 700; font-size: 13px; letter-spacing: .2px; }
.fw-sub { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--faint); }
.fw-sub { font-size: 11px; color: var(--faint); font-weight: 600;}
.fw-ctrls { margin-left: auto; display: flex; gap: 4px; }
.fw-btn {
width: 24px; height: 24px; display: grid; place-items: center; padding: 0;
@@ -2397,6 +2349,24 @@ body.tour-mode { overflow: hidden; }
.oo-pane-body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: auto; }
.oo-pane-body > * { min-height: 0; }
.oo-projects {
display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: var(--sp-2); padding: var(--sp-3) 0; align-content: start;
}
.oo-project {
display: flex; align-items: center; gap: var(--sp-3); text-align: left;
padding: var(--sp-3); border: 1px solid var(--line); border-radius: 10px;
background: var(--ink-1); color: var(--text); font-family: inherit; cursor: pointer;
transition: border-color .12s, background .12s;
}
.oo-project:hover { border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); background: var(--ink-2); }
.oo-project-main { min-width: 0; }
.oo-project-name {
font-size: var(--fs-body); line-height: var(--lh-body); font-weight: 600;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.oo-project-meta { font-size: var(--fs-subhead); line-height: var(--lh-subhead); color: var(--faint); margin-top: 2px; }
@media (max-width: 700px) {
.oo-root { padding: 60px 20px 128px; }
.oo-splash-title { font-size: 40px; line-height: 46px; letter-spacing: -.8px; }
-109
View File
@@ -1,109 +0,0 @@
// First-load welcome screen: pick a project before entering the editor.
(function () {
const { el } = window.dom;
const S = window.Store;
let overlay = null;
const loadFileInput = (function () {
const inp = document.createElement("input");
inp.type = "file";
inp.accept = ".json,.funpack_project.json,application/json";
inp.style.display = "none";
inp.onchange = async () => {
const file = inp.files?.[0];
inp.value = "";
if (!file) return;
await S.importProject(file);
};
document.body.appendChild(inp);
return inp;
})();
function latestProject() {
const list = S.get().projects || [];
return list.length ? list[0] : null;
}
function promptNewProject() {
if (window.SlotPicker?.openPrompt) {
window.SlotPicker.openPrompt({
title: "New project",
value: "Untitled montage",
placeholder: "Project name",
onPick: (name) => S.newProject(name),
});
return;
}
const name = prompt("Project name:", "Untitled montage");
if (name != null) S.newProject(name || "Untitled montage");
}
function openLatest() {
const p = latestProject();
if (!p) return;
S.loadProject(p.id);
}
function loadProjectFile() {
loadFileInput.click();
}
function welcomeTour() {
close();
const u = window.TourSandbox?.tourUrl?.() || (window.location.pathname + "?mode=tour");
window.location.href = u;
}
function close() {
if (overlay) {
overlay.remove();
overlay = null;
}
}
function open() {
close();
overlay = el("div", "welcome-overlay");
const card = el("div", "welcome-card");
const brand = el("div", "welcome-brand");
brand.append(el("span", "welcome-mark", "◉"));
brand.append(el("span", "welcome-title", "FunPack"));
brand.append(el("span", "welcome-sub", "Cutting Room"));
card.append(brand);
card.append(el("p", "welcome-lead",
"Build multi-scene montages with FunPack Studio and preview them on a real timeline."));
const latest = latestProject();
const actions = el("div", "welcome-actions");
const mkBtn = (label, cls, hint, disabled, fn) => {
const b = el("button", "welcome-btn" + (cls ? " " + cls : "") + (disabled ? " disabled" : ""));
b.type = "button";
b.append(el("span", "welcome-btn-label", label));
if (hint) b.append(el("span", "welcome-btn-hint", hint));
if (!disabled && fn) b.onclick = fn;
return b;
};
actions.append(
mkBtn("New Project", "primary", "Start a blank montage", false, promptNewProject),
mkBtn("Open Latest Project", "",
latest ? latest.name : "No saved projects yet",
!latest, openLatest),
mkBtn("Load Project", "", "Import a .json project file", false, loadProjectFile),
mkBtn("Welcome Tour", "ghost", "Interactive walkthrough (demo project)", false, welcomeTour),
);
card.append(actions);
// Update / Switch branch / Restart, reachable without first loading a project — a
// branch swap shouldn't mean opening a montage you didn't want just to reach the menu.
card.append(window.FunPackGit.maintenanceRow("welcome-maint"));
overlay.append(card);
document.body.append(overlay);
}
window.WelcomePage = { open, close, isOpen: () => !!overlay, openTour: welcomeTour };
})();