style: inspector cards, readiness rows, list cards, responsive columns, welcome; remove Simple mode (replaced by Disable all enhancements)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6e104e02a2
commit
046e24dea1
+1
-2
@@ -13,7 +13,6 @@ import { createMaintenance } from "./shell/maintenance.js";
|
||||
import { syncSeparated } from "./shell/audio.js";
|
||||
import { createKeys } from "./shell/keys.js";
|
||||
import { createDock } from "./shell/dock.js";
|
||||
import { createMode } from "./shell/mode.js";
|
||||
import { hostFor } from "./shell/mounts.js";
|
||||
import paths from "./modules.js";
|
||||
|
||||
@@ -29,7 +28,7 @@ linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "o
|
||||
const frame = build(document.getElementById("app"));
|
||||
const selection = createSelection({ project });
|
||||
selection.on(() => say("select"));
|
||||
const app = { editorSettings: [], timelineLanes: [], timelineView: {}, mediaMenu: [], mediaPeek: {}, sceneSections: [], clipTags: [], has: new Set(), actions: [], inputHooks: [], promptPrefix: [], lastRun: { typed: undefined, n: 0 }, runner: {}, project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, mode: createMode(), panel: frame.togglePanel, closePanels: frame.closePanels, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const app = { editorSettings: [], timelineLanes: [], timelineView: {}, mediaMenu: [], mediaPeek: {}, sceneSections: [], clipTags: [], has: new Set(), actions: [], inputHooks: [], promptPrefix: [], lastRun: { typed: undefined, n: 0 }, runner: {}, project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const settings = createSettings({ menubar: hostFor("menubar.menus") });
|
||||
app.openSettings = (id) => settings.open(id);
|
||||
const result = await loadFeatures(paths, { app });
|
||||
|
||||
@@ -300,7 +300,7 @@ export const DEMOS = {
|
||||
}),
|
||||
"overlay.blocking": { message: "Restarting ComfyUI…" },
|
||||
"slideOver.md": (c) => ({ side: "right", title: "Properties",
|
||||
body: c.hint.default({ text: "Simple mode slides this over the preview." }) }),
|
||||
body: c.hint.default({ text: "A panel that slides in over the page." }) }),
|
||||
|
||||
// --- gallery + wheel ---
|
||||
"gallery.adaptive": { id: "demo-gallery", cols: 3, items: [
|
||||
|
||||
@@ -64,7 +64,8 @@ body { -webkit-font-smoothing: antialiased; letter-spacing: -.003em; }
|
||||
.cx-field > .cx-label, .cx-field > label { color: var(--muted); font-size: var(--fs-5); font-weight: var(--fw-regular); }
|
||||
.cx-eyebrow { color: var(--muted); font-weight: var(--fw-semibold); }
|
||||
.cx-banner { border-radius: var(--r-4); }
|
||||
.cx-chip { border-color: transparent; background: var(--elev-2); }
|
||||
.cx-chip { border-color: transparent; }
|
||||
.cx-chip-neutral { background: var(--elev-2); }
|
||||
|
||||
/* inside a settings card the buttons are quiet: a tinted fill with accent text, never a row of solid amber */
|
||||
.cx-settings-row .cx-btn-primary { background: var(--accent-soft); color: var(--accent-hi); }
|
||||
@@ -87,3 +88,21 @@ body { -webkit-font-smoothing: antialiased; letter-spacing: -.003em; }
|
||||
.cx-menu-item:hover:not(:disabled) .cx-menu-icon, .cx-menu-item:hover:not(:disabled) .cx-menu-hint { color: inherit; }
|
||||
.cx-menu-sep { margin: var(--sp-3) var(--sp-3); }
|
||||
.cx-menu-heading { padding: var(--sp-3) var(--sp-5) var(--sp-2); font-size: var(--fs-7); }
|
||||
|
||||
/* media: tiles and the viewer are rounded, with no outline until chosen */
|
||||
.cx-cell { border-color: transparent; border-radius: var(--r-4); }
|
||||
.cx-cell:hover { border-color: var(--line-hover); }
|
||||
.cx-cell-badge, .cx-cell-duration { border-radius: var(--r-2); }
|
||||
.cx-viewer-stage { border-radius: var(--r-4); }
|
||||
.cx-viewer-media { border-radius: var(--r-3); }
|
||||
|
||||
/* inspector: related fields share one rounded card */
|
||||
.fp-card { background: var(--elev-2); border-radius: var(--r-5); padding: var(--sp-6); gap: var(--sp-5); }
|
||||
.fp-card .cx-input, .fp-card .cx-textarea, .fp-card .cx-select { background: var(--elev-3); }
|
||||
.fp-card .cx-segmented { background: var(--elev-3); }
|
||||
.cx-panel-body > .cx-eyebrow, .cx-stack > .cx-eyebrow { margin-block-start: var(--sp-4); }
|
||||
|
||||
/* list views read as a grouped card, like a settings list */
|
||||
.cx-settings-content .cx-media-list { background: var(--elev-2); border-radius: var(--r-5); padding: var(--sp-3); gap: 0; }
|
||||
.cx-settings-content .cx-media-row { border-radius: var(--r-3); border-color: transparent; background: transparent; }
|
||||
.cx-settings-content .cx-media-row:hover { background: var(--hover); }
|
||||
|
||||
@@ -32,13 +32,13 @@
|
||||
}
|
||||
|
||||
/* a run of rows (and switches) is one card */
|
||||
.cx-settings-content .cx-stack > :is(.cx-settings-row, .cx-toggle-row) {
|
||||
:is(.cx-settings-content, .cx-modal-body) .cx-stack > :is(.cx-settings-row, .cx-toggle-row) {
|
||||
background-color: var(--elev-2); border: 0; border-radius: var(--r-5);
|
||||
padding: var(--sp-5) var(--sp-7); min-block-size: 40px; display: flex; align-items: center;
|
||||
}
|
||||
.cx-settings-content .cx-stack > .cx-toggle-row > label { inline-size: 100%; display: flex; }
|
||||
.cx-settings-content .cx-stack > :is(.cx-settings-row, .cx-toggle-row):has(+ :is(.cx-settings-row, .cx-toggle-row)) { border-end-start-radius: 0; border-end-end-radius: 0; }
|
||||
.cx-settings-content .cx-stack > :is(.cx-settings-row, .cx-toggle-row) + :is(.cx-settings-row, .cx-toggle-row) {
|
||||
:is(.cx-settings-content, .cx-modal-body) .cx-stack > .cx-toggle-row > label { inline-size: 100%; display: flex; }
|
||||
:is(.cx-settings-content, .cx-modal-body) .cx-stack > :is(.cx-settings-row, .cx-toggle-row):has(+ :is(.cx-settings-row, .cx-toggle-row)) { border-end-start-radius: 0; border-end-end-radius: 0; }
|
||||
:is(.cx-settings-content, .cx-modal-body) .cx-stack > :is(.cx-settings-row, .cx-toggle-row) + :is(.cx-settings-row, .cx-toggle-row) {
|
||||
border-start-start-radius: 0; border-start-end-radius: 0;
|
||||
margin-block-start: calc(-1 * var(--cx-gap, var(--sp-4)));
|
||||
background-image: linear-gradient(var(--line), var(--line)); background-repeat: no-repeat;
|
||||
|
||||
@@ -23,7 +23,7 @@ export function enhance(app, own) {
|
||||
function draw() {
|
||||
if (!alive) return;
|
||||
const sl = slot();
|
||||
if (!sl) return page.set([c.hint.default({ text: "The open pipeline has no prompt enhancer. Pick a preset that has one, or add the “FunPack Enhance Prompt” node in Models & Pipeline." })]);
|
||||
if (!sl) return page.set([c.emptyState.default({ icon: "✎", title: "No prompt enhancer here", hint: "Pick a preset that has one, or add the “FunPack Enhance Prompt” node in Settings ▸ Models & Pipeline." })]);
|
||||
const v = sl.inputs || {}, val = (n, d) => (v[n] === undefined ? d : v[n]);
|
||||
const on = val("enabled", false), greedy = val("greedy", false);
|
||||
page.set([
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// The Project tab: what the whole project makes.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { DRIVEN, isVideoClip, segments } from "../../shell/scenes.js";
|
||||
import { card } from "./scene.js";
|
||||
|
||||
const STARTS = [{ value: "i2v", label: "From an image" }, { value: "t2v", label: "From a prompt" }];
|
||||
|
||||
@@ -60,20 +61,20 @@ export function projectRows(p, slots, api) {
|
||||
const per = (open.num_frames_per_scene || 97) / (open.frame_rate || 25);
|
||||
return [
|
||||
c.label.section({ text: "Project" }),
|
||||
c.field.default({ label: "Project name", control: c.input.md({ label: "Project name", value: open.name || "", onCommit: (v) => p.rename(v) }) }),
|
||||
card(c.field.default({ label: "Project name", control: c.input.md({ label: "Project name", value: open.name || "", onCommit: (v) => p.rename(v) }) })),
|
||||
c.label.section({ text: "Video settings" }),
|
||||
c.field.row({ fields: [frames.field, fps.field] }),
|
||||
c.hint.default({ text: `≈ ${per.toFixed(2)} s per shot` }),
|
||||
...(size.length ? [c.field.row({ fields: size.map((x) => x.field) })] : []),
|
||||
...custom(p, open),
|
||||
c.field.default({ label: "Final render size", control: c.select.md({ label: "Final render size", options: sizes(open), value: open.export_size_from || "",
|
||||
onChange: (v) => p.setField("export_size_from", v) }) }),
|
||||
c.field.default({ label: "Start shots", control: c.select.md({ label: "Start shots", options: STARTS, value: open.generation_mode === "t2v" ? "t2v" : "i2v", onChange: (v) => p.setField("generation_mode", v) }) }),
|
||||
card(c.field.row({ fields: [frames.field, fps.field] }),
|
||||
c.hint.default({ text: `≈ ${per.toFixed(2)} s per shot` }),
|
||||
size.length ? c.field.row({ fields: size.map((x) => x.field) }) : null,
|
||||
...custom(p, open),
|
||||
c.field.default({ label: "Final render size", control: c.select.md({ label: "Final render size", options: sizes(open), value: open.export_size_from || "",
|
||||
onChange: (v) => p.setField("export_size_from", v) }) }),
|
||||
c.field.default({ label: "Start shots", control: c.select.md({ label: "Start shots", options: STARTS, value: open.generation_mode === "t2v" ? "t2v" : "i2v", onChange: (v) => p.setField("generation_mode", v) }) })),
|
||||
c.label.section({ text: "Prompt" }),
|
||||
c.field.default({ label: "Anchor", control: c.textarea.md({ label: "Anchor", value: p.anchor, rows: 2, onInput: (v) => p.setAnchor(v) }) }),
|
||||
c.field.default({ label: "Negative prompt", control: c.textarea.md({ label: "Negative prompt", value: p.negative, rows: 2, onInput: (v) => p.setNegative(v) }) }),
|
||||
c.toggle.default({ label: "Postfix", hint: "Appended to every scene (a style or quality tag, say).", checked: open.postfix_enabled !== false, onChange: (v) => p.setField("postfix_enabled", v) }),
|
||||
c.textarea.md({ label: "Postfix", value: p.postfix, rows: 2, disabled: open.postfix_enabled === false, onInput: (v) => p.setPostfix(v) }),
|
||||
card(c.field.default({ label: "Anchor", control: c.textarea.md({ label: "Anchor", value: p.anchor, rows: 2, onInput: (v) => p.setAnchor(v) }) }),
|
||||
c.field.default({ label: "Negative prompt", control: c.textarea.md({ label: "Negative prompt", value: p.negative, rows: 2, onInput: (v) => p.setNegative(v) }) }),
|
||||
c.toggle.default({ label: "Postfix", hint: "Appended to every scene (a style or quality tag, say).", checked: open.postfix_enabled !== false, onChange: (v) => p.setField("postfix_enabled", v) }),
|
||||
c.textarea.md({ label: "Postfix", value: p.postfix, rows: 2, disabled: open.postfix_enabled === false, onInput: (v) => p.setPostfix(v) })),
|
||||
c.collapsible.default({ label: "ADVANCED PROJECT SETTINGS", body: c.region.stack({ gap: "sm", children: [
|
||||
c.collapsible.default({ label: "Split preview (generation prompt)", body: splitPreview(p, api) }),
|
||||
] }) }),
|
||||
|
||||
@@ -36,12 +36,15 @@ function drift(sc, open) {
|
||||
/** The slip: where in the rendered file this clip starts and how much of it plays. Only a clip that has a render has one. */
|
||||
function trimRows(p, sc, open) {
|
||||
if (!((open.scene_renders || {})[sc.id] || {}).media) return [];
|
||||
return [c.label.section({ text: "Source trim (slip)" }), c.field.row({ fields: [
|
||||
return [c.label.section({ text: "Source trim (slip)" }), card(c.field.row({ fields: [
|
||||
c.field.default({ label: "Source in (s)", control: c.number.md({ label: "Source in", value: sc.source_in || 0, min: 0, step: 0.05, onChange: (v) => p.setScene(sc.id, "source_in", v) }) }),
|
||||
c.field.default({ label: "Source dur (s, 0 = full)", control: c.number.md({ label: "Source duration", value: sc.source_dur != null ? sc.source_dur : 0, min: 0, step: 0.05, onChange: (v) => p.setScene(sc.id, "source_dur", v > 0 ? Math.max(0.1, v) : null) }) })] }),
|
||||
c.button.sm({ label: "Reset source trim", tone: "ghost", disabled: !sc.source_in && sc.source_dur == null, onClick: () => { p.setScene(sc.id, "source_in", 0); p.setScene(sc.id, "source_dur", null); } })];
|
||||
c.button.sm({ label: "Reset source trim", tone: "ghost", disabled: !sc.source_in && sc.source_dur == null, onClick: () => { p.setScene(sc.id, "source_in", 0); p.setScene(sc.id, "source_dur", null); } }))];
|
||||
}
|
||||
|
||||
/** A group of fields on one rounded card. */
|
||||
export const card = (...children) => { const n = c.region.stack({ gap: "md", children: children.flat().filter(Boolean) }); n.node.classList.add("fp-card"); return n; };
|
||||
|
||||
export function sceneRows(p, app) {
|
||||
const sc = p.selected, open = p.project;
|
||||
if (!sc) return [c.emptyState.default({ icon: "▭", title: "No scene", hint: "Add one on the timeline." })];
|
||||
@@ -50,22 +53,22 @@ export function sceneRows(p, app) {
|
||||
c.banner.info({ text: "Generating with FunPack Studio + Chain Sampler", action: { label: "Engine settings →", onClick: () => app.openSettings && app.openSettings("engine") } }),
|
||||
...(sc.removed_from_plan ? [c.banner.info({ text: "Removed from plan — its generated clip stays on the timeline.", action: { label: "Restore to plan", onClick: () => p.edit((pr) => restoreToPlan(pr, sc.id)) } })] : []),
|
||||
...(cuts ? [c.hint.default({ text: "This scene has editorial cuts — Generate regens the whole uncut scene." })] : []),
|
||||
...(isSubclip(sc) ? [c.hint.default({ text: "This is a cut: its prompt and source belong to the first part." })] : [
|
||||
...(isSubclip(sc) ? [c.hint.default({ text: "This is a cut: its prompt and source belong to the first part." })] : [card(
|
||||
c.field.default({ label: "Prompt", control: c.textarea.md({ label: "Prompt", value: sc.text || "", rows: 4, onInput: (v) => p.setText(sc.id, v) }) }),
|
||||
c.field.default({ label: "Source", control: c.select.md({ label: "Source", options: SOURCES, value: (sc.source || {}).type || "carry",
|
||||
onChange: (v) => p.setScene(sc.id, "source", { ...(sc.source || {}), type: v }) }) }),
|
||||
]),
|
||||
onChange: (v) => p.setScene(sc.id, "source", { ...(sc.source || {}), type: v }) }) }))]),
|
||||
c.label.section({ text: "Reference media (v5 pipeline)" }),
|
||||
mediaField(app, p, sc, { label: "Resolution source", hint: "Sets this scene's aspect ratio for generation — the project's own Width/Height set the actual resolution; this image's pixels are not used.",
|
||||
card(mediaField(app, p, sc, { label: "Resolution source", hint: "Sets this scene's aspect ratio for generation — the project's own Width/Height set the actual resolution; this image's pixels are not used.",
|
||||
key: "source_image", multiple: false, kinds: ["image"], empty: "— choose resolution source —" }),
|
||||
mediaField(app, p, sc, { label: "References", key: "references", multiple: true, kinds: ["image", "video", "audio"], empty: "+ Add reference" }),
|
||||
c.field.row({ fields: [
|
||||
mediaField(app, p, sc, { label: "References", key: "references", multiple: true, kinds: ["image", "video", "audio"], empty: "+ Add reference" })),
|
||||
c.label.section({ text: "Length" }),
|
||||
card(c.field.row({ fields: [
|
||||
lengthField(p, sc, "Frames", "frames_mode", "frames", effFrames(sc, open)),
|
||||
lengthField(p, sc, "FPS", "fps_mode", "fps", effFps(sc, open)),
|
||||
] }),
|
||||
drift(sc, open),
|
||||
c.button.sm({ label: "Generate this scene", tone: "primary", onClick: () => app.say("generate.scene") }),
|
||||
c.button.md({ label: "Generate this scene", tone: "primary", onClick: () => app.say("generate.scene") })),
|
||||
...trimRows(p, sc, open),
|
||||
c.checkbox.default({ label: "Exclude from full generation", checked: Boolean(sc.excluded), onChange: (v) => p.setScene(sc.id, "excluded", v) }),
|
||||
card(c.checkbox.default({ label: "Exclude from full generation", checked: Boolean(sc.excluded), onChange: (v) => p.setScene(sc.id, "excluded", v) })),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { segments } from "../../shell/scenes.js";
|
||||
export default {
|
||||
id: "keys",
|
||||
mount: "menubar.menus",
|
||||
needs: ["project", "selection", "keys", "mode"],
|
||||
needs: ["project", "selection", "keys"],
|
||||
setup({ app }) {
|
||||
const p = app.project, sel = app.selection;
|
||||
const scene = (fn) => (e) => (app.mode.now === "simple" ? false : fn(e));
|
||||
const scene = (fn) => (e) => fn(e);
|
||||
const frame = () => (p.project && p.project.frame_rate) || 25;
|
||||
const step = (dir) => scene((e) => (e && e.target && e.target.closest && e.target.closest("button, a, [role=slider], [role=tab], [role=radio], [role=option], [role=menuitem], [role=separator]") ? false : app.playhead.set(app.playhead.at + dir / frame()))); // a widget that uses the arrows keeps them
|
||||
const edge = (which) => { // I / O: the playhead to the picked clip's in / out point
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// The Simple | Editor switch in the menubar, and Simple's own surface: the one-prompt bar under the monitor.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
|
||||
const switcher = {
|
||||
id: "mode-switch",
|
||||
mount: "menubar.mode",
|
||||
needs: ["mode"],
|
||||
setup({ host, app }) {
|
||||
const seg = c.segmented.sm({ label: "Mode", value: app.mode.now, onChange: (v) => app.mode.set(v),
|
||||
options: [{ value: "simple", label: "Simple" }, { value: "editor", label: "Editor" }] });
|
||||
host.append(seg.node);
|
||||
return app.mode.on((v) => seg.setValue && seg.setValue(v));
|
||||
},
|
||||
};
|
||||
|
||||
// "Describe the shot you want…" over two buttons that slide Assets and Properties in. Only shown in Simple mode.
|
||||
const bar = {
|
||||
id: "simple-bar",
|
||||
mount: "preview.simple",
|
||||
needs: ["mode", "project", "panel"],
|
||||
setup({ host, app }) {
|
||||
const p = app.project;
|
||||
const area = c.textarea.md({ label: "Prompt", rows: 3, placeholder: "Describe the shot you want…", value: "",
|
||||
onInput: (v) => { if (p.selected) p.setText(p.selected.id, v); } });
|
||||
const row = c.toolbar.default({ items: [], trailing: [c.button.sm({ label: "Media", tone: "ghost", onClick: () => app.panel("assets") }),
|
||||
c.button.sm({ label: "Advanced settings", tone: "ghost", onClick: () => app.panel("props") })] });
|
||||
host.append(area.node, row.node);
|
||||
const draw = () => {
|
||||
const simple = app.mode.now === "simple";
|
||||
area.node.hidden = row.node.hidden = !simple;
|
||||
if (!simple) app.closePanels();
|
||||
else if (!area.node.matches(":focus")) area.setValue(p.selected ? p.selected.text || "" : "");
|
||||
};
|
||||
draw();
|
||||
const off = [app.mode.on(draw), app.on(draw)];
|
||||
return () => off.forEach((f) => f());
|
||||
},
|
||||
};
|
||||
|
||||
// A slid-over zone covers the button that opened it, so each carries its own close (Simple mode only).
|
||||
const closer = (mount) => ({
|
||||
id: `panel-close:${mount}`, mount, needs: ["mode", "closePanels"],
|
||||
setup({ host, app }) {
|
||||
const b = c.button.sm({ label: "✕", tone: "ghost", title: "Close", onClick: () => app.closePanels() }).node;
|
||||
host.append(b);
|
||||
const draw = () => { b.hidden = app.mode.now !== "simple"; };
|
||||
draw();
|
||||
return app.mode.on(draw);
|
||||
},
|
||||
});
|
||||
|
||||
export default [switcher, bar, closer("assets.actions"), closer("inspector.actions")];
|
||||
@@ -33,14 +33,14 @@ export const about = (api) => function mount() {
|
||||
/** "Is this machine ready to generate?": the rows core/readiness.py reports, worst first. */
|
||||
export const readiness = (api) => function mount() {
|
||||
const body = c.region.stack({ gap: "sm", label: "Ready to generate" });
|
||||
const MARK = { ok: "✓", warn: "!", fail: "✗" }, ORDER = { fail: 0, warn: 1, ok: 2 };
|
||||
const MARK = { ok: "✓", warn: "!", fail: "✗" }, TONE = { ok: "good", warn: "warn", fail: "danger" }, ORDER = { fail: 0, warn: 1, ok: 2 };
|
||||
const run = async () => {
|
||||
body.set([c.hint.default({ text: "Checking…" })]);
|
||||
let rows;
|
||||
try { rows = (await api.readiness()).rows; } catch (err) { return body.set([c.banner.warn({ text: `Could not check: ${err.message}` }), check]); }
|
||||
const bad = rows.filter((r) => r.level !== "ok").length;
|
||||
body.set([check, bad ? c.banner.warn({ text: `${bad} thing${bad > 1 ? "s" : ""} to look at.` }) : c.banner.info({ text: "Nothing in the way." }),
|
||||
...[...rows].sort((a, b) => ORDER[a.level] - ORDER[b.level]).map((r) => c.hint.default({ text: `${MARK[r.level]} ${r.text}` }))]);
|
||||
...[...rows].sort((a, b) => ORDER[a.level] - ORDER[b.level]).map((r) => c.settingsRow.default({ label: r.text, control: c.chip[TONE[r.level]]({ label: MARK[r.level] }) }))]);
|
||||
};
|
||||
const check = c.button.sm({ label: "Check this machine", tone: "ghost", title: "Look for what a first run on a new GPU box trips over", onClick: run });
|
||||
body.set([c.hint.default({ text: "Run this on a new rental before the first generation: it checks ffmpeg, the GPU, model files, installed nodes and loaded modules." }), check]);
|
||||
|
||||
@@ -94,7 +94,9 @@ const zoom = {
|
||||
needs: ["say"],
|
||||
setup({ host, app }) {
|
||||
const b = (label, what, title) => c.button.sm({ label, tone: "ghost", title, onClick: () => app.say(what) }).node;
|
||||
host.append(c.hint.default({ text: "J K L · S split · I O in/out · + − zoom" }).node,
|
||||
const keys = c.hint.default({ text: "J K L · S split · I O in/out · + − zoom" }).node;
|
||||
keys.classList.add("fp-keyhint"); // the first thing to go when the row runs out of room
|
||||
host.append(keys,
|
||||
b("−", "zoom.out", "Zoom out"), b("+", "zoom.in", "Zoom in"), b("fit", "zoom.fit", "Fit the cut to the window"));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Project Setup Wizard: look, how much of the app, a name and a model's starting point, then the tour offer. Opens from File ▸ Project Setup Wizard.
|
||||
// Project Setup Wizard: look, a name and a model's starting point, then the tour offer. Opens from File ▸ Project Setup Wizard.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
|
||||
export default {
|
||||
id: "wizard",
|
||||
mount: "menubar.menus",
|
||||
needs: ["project", "api", "pipeline", "theme", "mode"],
|
||||
needs: ["project", "api", "pipeline", "theme"],
|
||||
setup({ app }) {
|
||||
let win = null;
|
||||
const ctx = { name: "Untitled montage", preset: "", tour: true };
|
||||
@@ -13,8 +13,6 @@ export default {
|
||||
const STEPS = [
|
||||
{ title: "Choose your look", sub: "You can change this any time in Settings ▸ Appearance.", body: () => c.segmented.md({ label: "Colour scheme", value: app.theme.get(), onChange: (v) => app.theme.apply(v),
|
||||
options: [{ value: "dark", label: "Dark" }, { value: "light", label: "Light" }, { value: "auto", label: "Auto" }] }) },
|
||||
{ title: "How much do you want in front of you?", sub: "You can switch at any time from the control beside the FunPack wordmark.", body: () => c.radioGroup.default({ label: "Mode", value: app.mode.now, onChange: (v) => app.mode.set(v),
|
||||
options: [{ value: "simple", label: "Simple — one prompt and Generate. Rating-driven steering and experimental sampling stay off." }, { value: "editor", label: "Editor — the full cutting room: timeline, ratings, every Studio and sampler setting." }] }) },
|
||||
{ title: "Name it, and pick a model", sub: "The model decides which nodes and which files this project needs.", body: () => c.region.stack({ gap: "sm", children: [
|
||||
c.field.default({ label: "Project name", control: c.input.md({ label: "Project name", value: ctx.name, onInput: (v) => { ctx.name = v; } }) }),
|
||||
c.field.default({ label: "Start from", hint: "A model's own pipeline, ready to fill with files. “Keep the current pipeline” leaves Models & Pipeline as it is.",
|
||||
|
||||
+15
-30
@@ -1,5 +1,5 @@
|
||||
/* The frame's proportions, as v4 had them: fixed side columns and a fixed-height timeline across the bottom. */
|
||||
:root { --media-w: 280px; --properties-w: 340px; --timeline-h: 320px; --pad: 10px; --pgap: 10px; }
|
||||
:root { --media-w: clamp(180px, 20vw, 280px); --properties-w: clamp(270px, 28vw, 360px); --timeline-h: clamp(270px, 42vh, 340px); --pad: 10px; --pgap: 10px; }
|
||||
.fp-main { flex-direction: column; padding: 0 var(--pad) var(--pad); gap: var(--pgap); }
|
||||
.fp-row { flex: 1 1 auto; flex-direction: row; min-block-size: 0; gap: var(--pgap); }
|
||||
.fp-row > *, .fp-main > :nth-child(2) { min-inline-size: 0; border: var(--bw) solid var(--line); border-radius: var(--card-r); overflow: hidden; background: var(--elev-1); }
|
||||
@@ -18,21 +18,6 @@
|
||||
/* An empty zone says so in the middle of it, with the controls kept at its foot. */
|
||||
.cx-zone .cx-stack-fill > .cx-empty { flex: 1 1 auto; justify-content: center; }
|
||||
|
||||
/* Simple mode: one prompt and one Generate. The timeline, splitters and dock go; Assets and Properties slide over the preview. */
|
||||
.fp-row { position: relative; }
|
||||
:root[data-ui-mode="simple"] .fp-main > :nth-child(2),
|
||||
:root[data-ui-mode="simple"] .fp-simple-hide { display: none !important; }
|
||||
:root[data-ui-mode="simple"] .fp-row > :first-child,
|
||||
:root[data-ui-mode="simple"] .fp-row > :nth-child(3) {
|
||||
position: absolute; top: 0; bottom: 0; z-index: 60; inline-size: min(var(--properties-w), 82vw);
|
||||
background: var(--bg, var(--elev-1)); box-shadow: 0 0 40px -8px var(--shadow-ink, #000);
|
||||
transition: transform .18s cubic-bezier(.22, .61, .36, 1);
|
||||
}
|
||||
:root[data-ui-mode="simple"] .fp-row > :first-child { left: 0; transform: translateX(-101%); }
|
||||
:root[data-ui-mode="simple"] .fp-row > :nth-child(3) { right: 0; transform: translateX(101%); }
|
||||
:root[data-ui-mode="simple"] .fp-row.fp-show-assets > :first-child,
|
||||
:root[data-ui-mode="simple"] .fp-row.fp-show-props > :nth-child(3) { transform: none; }
|
||||
|
||||
/* Splitters: thin hit areas straddling the zone borders. */
|
||||
.fp-main { position: relative; }
|
||||
.fp-split { position: absolute; z-index: 20; background: transparent; touch-action: none; }
|
||||
@@ -41,25 +26,24 @@
|
||||
.fp-split-left { left: calc(var(--pad) + var(--media-w) + var(--pgap) / 2 - 3.5px); }
|
||||
.fp-split-right { right: calc(var(--pad) + var(--properties-w) + var(--pgap) / 2 - 3.5px); }
|
||||
.fp-split-bottom { left: 0; right: 0; block-size: 7px; bottom: calc(var(--pad) + var(--timeline-h) + var(--pgap) / 2 - 3.5px); cursor: row-resize; }
|
||||
:root[data-ui-mode="simple"] .fp-split { display: none; }
|
||||
|
||||
/* Dock: a hidden zone leaves the row (and the splitter beside it); Simple mode ignores the dock. */
|
||||
:root:not([data-ui-mode="simple"]).fp-hide-assets .fp-row > :nth-child(1),
|
||||
:root:not([data-ui-mode="simple"]).fp-hide-preview .fp-row > :nth-child(2),
|
||||
:root:not([data-ui-mode="simple"]).fp-hide-properties .fp-row > :nth-child(3) { display: none; }
|
||||
/* Dock: a hidden zone leaves the row (and the splitter beside it). */
|
||||
.fp-hide-assets .fp-row > :nth-child(1),
|
||||
.fp-hide-preview .fp-row > :nth-child(2),
|
||||
.fp-hide-properties .fp-row > :nth-child(3) { display: none; }
|
||||
.fp-hide-assets .fp-split-left, .fp-hide-properties .fp-split-right, .fp-hide-preview .fp-split-right { display: none; }
|
||||
:root:not([data-ui-mode="simple"]).fp-hide-preview .fp-row > :nth-child(3), :root:not([data-ui-mode="simple"]).fp-hide-preview.fp-hide-properties .fp-row > :nth-child(1) { flex: 1 1 auto; }
|
||||
:root:not([data-ui-mode="simple"]).fp-hide-assets.fp-hide-preview .fp-row > :nth-child(3) { flex: 1 1 auto; }
|
||||
.fp-hide-preview .fp-row > :nth-child(3), .fp-hide-preview.fp-hide-properties .fp-row > :nth-child(1) { flex: 1 1 auto; }
|
||||
.fp-hide-assets.fp-hide-preview .fp-row > :nth-child(3) { flex: 1 1 auto; }
|
||||
.fp-split { border: 0 !important; }
|
||||
|
||||
/* Auto-hide timeline: a strip the height of its title bar, full height under the pointer or during a drag. */
|
||||
:root.fp-peek:not([data-ui-mode="simple"]) .fp-main { position: relative; padding-block-end: 34px; }
|
||||
:root.fp-peek:not([data-ui-mode="simple"]) .fp-main > :nth-child(2) { position: absolute; inset-inline: 0; bottom: 0; block-size: 34px; min-block-size: 0; z-index: 20; overflow: hidden; }
|
||||
:root.fp-peek:not([data-ui-mode="simple"]) .fp-main > :nth-child(2) .cx-panel-body { display: none; }
|
||||
:root.fp-peek:not([data-ui-mode="simple"]) .fp-main > :nth-child(2):hover,
|
||||
:root.fp-peek.fp-peek-open:not([data-ui-mode="simple"]) .fp-main > :nth-child(2) { block-size: var(--timeline-h); overflow: visible; }
|
||||
:root.fp-peek:not([data-ui-mode="simple"]) .fp-main > :nth-child(2):hover .cx-panel-body,
|
||||
:root.fp-peek.fp-peek-open:not([data-ui-mode="simple"]) .fp-main > :nth-child(2) .cx-panel-body { display: block; }
|
||||
:root.fp-peek .fp-main { position: relative; padding-block-end: 34px; }
|
||||
:root.fp-peek .fp-main > :nth-child(2) { position: absolute; inset-inline: 0; bottom: 0; block-size: 34px; min-block-size: 0; z-index: 20; overflow: hidden; }
|
||||
:root.fp-peek .fp-main > :nth-child(2) .cx-panel-body { display: none; }
|
||||
:root.fp-peek .fp-main > :nth-child(2):hover,
|
||||
:root.fp-peek.fp-peek-open .fp-main > :nth-child(2) { block-size: var(--timeline-h); overflow: visible; }
|
||||
:root.fp-peek .fp-main > :nth-child(2):hover .cx-panel-body,
|
||||
:root.fp-peek.fp-peek-open .fp-main > :nth-child(2) .cx-panel-body { display: block; }
|
||||
:root.fp-peek .fp-split-bottom { display: none; }
|
||||
|
||||
/* Welcome: the maintenance actions as a 2x2 of tiles */
|
||||
@@ -68,3 +52,4 @@
|
||||
.fp-tile:hover { background: var(--hover); }
|
||||
.fp-tile-title { font-size: var(--fs-4); font-weight: var(--fw-medium); }
|
||||
.fp-tile-hint { font-size: var(--fs-6); color: var(--muted); }
|
||||
@media (max-width: 1150px) { .fp-keyhint { display: none; } }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// The one list of UI features. Adding a feature is a folder under ./features and a line here.
|
||||
export default [
|
||||
"./features/modes/index.js",
|
||||
"./features/log/index.js",
|
||||
"./features/projects/index.js",
|
||||
"./features/media/index.js",
|
||||
|
||||
+6
-11
@@ -1,7 +1,6 @@
|
||||
// The app's regions, laid out as v4 had them: a menubar, then Assets | Preview | Properties side by
|
||||
// side with the Timeline under all three. Every region offers mount points; no feature is named here.
|
||||
// menubar.mode | menubar.menus | menubar.right the bar
|
||||
// preview.simple (under the monitor, Simple mode only)
|
||||
// menubar.menus | menubar.right the bar
|
||||
// timeline.toolbar | timeline.toolbar.end (the row over the stage; the stage itself is `timeline`)
|
||||
// assets.projects | assets.media (stacked in the Assets zone)
|
||||
// <zone> (body) | <zone>.actions (after the title) | <zone>.status (far end) for assets, preview, inspector, timeline
|
||||
@@ -33,13 +32,11 @@ export function build(root) {
|
||||
});
|
||||
return composer.region.stack({ gap: "md", fill: true, children: sheets });
|
||||
};
|
||||
// Preview: the monitor's stack, then (Simple mode only) the one-prompt bar under it.
|
||||
// Preview: the monitor's stack.
|
||||
const preview = () => {
|
||||
const stack = composer.region.stack({ gap: "sm", fill: true });
|
||||
offer("preview", stack.node);
|
||||
const bar = composer.region.stack({ gap: "sm" });
|
||||
offer("preview.simple", bar.node);
|
||||
return composer.region.stack({ gap: "sm", fill: true, children: [stack, bar] });
|
||||
return stack;
|
||||
};
|
||||
// The timeline zone: a toolbar (what acts on the cut | zoom and hints), then the stage.
|
||||
const timeline = () => {
|
||||
@@ -54,13 +51,10 @@ export function build(root) {
|
||||
main.node.classList.add("fp-main");
|
||||
|
||||
const header = composer.toolbar.default({
|
||||
items: [composer.brand.default({}), group("menubar.mode"), group("menubar.menus")],
|
||||
items: [composer.brand.default({}), group("menubar.menus")],
|
||||
trailing: [group("menubar.right")],
|
||||
});
|
||||
const frame = composer.frame.app({ header, main });
|
||||
// In Simple mode Assets and Properties slide over the preview on request.
|
||||
frame.togglePanel = (which) => { const open = row.node.classList.contains(`fp-show-${which}`); row.node.classList.remove("fp-show-assets", "fp-show-props"); if (!open) row.node.classList.add(`fp-show-${which}`); };
|
||||
frame.closePanels = () => row.node.classList.remove("fp-show-assets", "fp-show-props");
|
||||
root.append(frame.node);
|
||||
// v4's draggable splitters: three 7px bars that set the zone sizes; sizes are remembered.
|
||||
const KEY = "funpack_layout", css = document.documentElement.style;
|
||||
@@ -79,7 +73,8 @@ export function build(root) {
|
||||
bar.className = `fp-split ${cls}`; bar.setAttribute("role", "separator");
|
||||
let from = 0;
|
||||
drag(bar, {
|
||||
onStart: () => { from = parseFloat(getComputedStyle(document.documentElement).getPropertyValue(prop)); },
|
||||
// the default is a clamp(), which has no number in it: measure the zone itself
|
||||
onStart: () => { const zone = prop === "--media-w" ? row.node.children[0] : prop === "--properties-w" ? row.node.children[2] : main.node.children[1]; const r = zone.getBoundingClientRect(); from = axis === "dx" ? r.width : r.height; },
|
||||
onMove: (m) => { saved[prop] = Math.round(Math.min(size[prop][1], Math.max(size[prop][0], from + sign * m[axis]))); apply(); },
|
||||
onEnd: keep,
|
||||
});
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Simple or Editor: how much of the screen is showing. One word, kept in this browser, written onto the page
|
||||
// (`data-ui-mode`) where the stylesheet reads it.
|
||||
const KEY = "funpack_ui_mode";
|
||||
|
||||
export function createMode(root = document.documentElement) {
|
||||
const heard = new Set();
|
||||
const stored = () => { try { return localStorage.getItem(KEY) === "simple" ? "simple" : "editor"; } catch { return "editor"; } };
|
||||
let now = stored();
|
||||
root.dataset.uiMode = now;
|
||||
return {
|
||||
get now() { return now; },
|
||||
set(next) {
|
||||
if (next !== "simple" && next !== "editor") return;
|
||||
if (next === now) return;
|
||||
now = next;
|
||||
root.dataset.uiMode = now;
|
||||
try { localStorage.setItem(KEY, now); } catch { /* private window: lasts until reload */ }
|
||||
heard.forEach((fn) => fn(now));
|
||||
},
|
||||
on(fn) { heard.add(fn); return () => heard.delete(fn); },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user