diff --git a/easy_gen/frontend/app.js b/easy_gen/frontend/app.js index 23fd7743..992d37a6 100644 --- a/easy_gen/frontend/app.js +++ b/easy_gen/frontend/app.js @@ -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. diff --git a/movie_editor/frontend/index.html b/movie_editor/frontend/index.html index d515eb2c..e7c874fd 100644 --- a/movie_editor/frontend/index.html +++ b/movie_editor/frontend/index.html @@ -136,7 +136,6 @@ - diff --git a/movie_editor/frontend/inspector.js b/movie_editor/frontend/inspector.js index 08d8d254..6262785f 100644 --- a/movie_editor/frontend/inspector.js +++ b/movie_editor/frontend/inspector.js @@ -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 }; diff --git a/movie_editor/frontend/onboarding.js b/movie_editor/frontend/onboarding.js index 0a5eaafc..2e166543 100644 --- a/movie_editor/frontend/onboarding.js +++ b/movie_editor/frontend/onboarding.js @@ -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(); } diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index e8eb9bc7..a900479e 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -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: diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index bc9063cc..21ce4ba2 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -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; } diff --git a/movie_editor/frontend/welcome.js b/movie_editor/frontend/welcome.js deleted file mode 100644 index 05215483..00000000 --- a/movie_editor/frontend/welcome.js +++ /dev/null @@ -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 }; -})();