Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ npm run preview
- Effect idea moderation uses `EFFECT_MODERATION_TOKEN` (falls back to `DOODLE_MODERATION_TOKEN` / `DOODLE_ADMIN_TOKEN`). Each submission now gets a signed review page at `/effect-review.html?id=...&token=...` with live preview + submitted code, approve/reject links still use `/api/effects?action=approve|reject&id=...&token=...`, and pending queue inspection stays at `/api/effects?includePending=1&token=...`.
- Run `npm run test:integration` in an environment with the DB2 secrets set to verify the live Upstash database exists and can be read/written. If the DB2 URL is malformed, the APIs now fail closed instead of crashing with a 500 during Redis client creation. The serverless API modules also use explicit `.js` ESM imports so Vercel can resolve the emitted files correctly.
- Append `?editor=1` in dev builds to open the Scene + Timeline Editor (or toggle "Editor mode" in the debug overlay). The editor shows a live preview, edits hot-apply to the running demo, and changes persist to localStorage.
- Mobile-first editing mode: choose **Editor layout → Mobile** in the debug overlay (or use `?editor=1&editorLayout=mobile`) to switch to a stacked touch-friendly layout while keeping the existing desktop editor intact.
- The timeline editor layout uses a narrow Scenes sidebar, a center workspace with Preview plus a single accordion stack for Basic/Main Slots/Transition, and a right Inspector for scene/layer/cue editing. The Generate Text Cues tool opens from a bottom-left button as a modal.
- The timeline area now dedicates at least half the editor height, renders all scenes on the main track, and adds extra rows for selected-scene layers and scene automation entries.
- Automation clip editing foundations now treat automation as point-driven envelopes (per-segment curve type + tension metadata), with helpers for snapped point add/remove/move, slide-mode temporal shifting, and segment-level curve/tension updates for timeline UI integration.
Expand Down
7 changes: 7 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,13 @@
<input type="checkbox" id="debug-editor-toggle" />
<span>Editor mode</span>
</label>
<label class="debug-row">
<span>Editor layout</span>
<select id="debug-editor-layout">
<option value="desktop">Desktop</option>
<option value="mobile">Mobile</option>
</select>
</label>
<label class="debug-row debug-toggle">
<input type="checkbox" id="debug-monochrome" />
<span>Force monochrome</span>
Expand Down
11 changes: 10 additions & 1 deletion src/editor/EditorRoot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import {
,
buildAutomationTrackPolyline
,
zoomPlaylistTrackHeight
zoomPlaylistTrackHeight,
getEditorLayoutClass
} from "./EditorRoot";

describe("formatTime", () => {
Expand All @@ -51,6 +52,14 @@ describe("formatTime", () => {
});
});

describe("getEditorLayoutClass", () => {
it("returns mobile modifier class only for mobile layout mode", () => {
expect(getEditorLayoutClass("mobile")).toBe("editor editor--mobile");
expect(getEditorLayoutClass("desktop")).toBe("editor");
expect(getEditorLayoutClass(undefined)).toBe("editor");
});
});

describe("getClipPercent", () => {
it("returns clip left and width percentage from start/end/duration", () => {
expect(getClipPercent(10, 30, 100)).toEqual({ left: 10, width: 20 });
Expand Down
6 changes: 6 additions & 0 deletions src/editor/EditorRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const MAIN_SLOT_LABELS: Array<{ slot: MainSlot; label: string }> = [
type EditorInit = {
container: HTMLElement;
effectNames: string[];
layoutMode?: "desktop" | "mobile";
applyTimeline: (raw: RawTimelineConfig) => Promise<string | null>;
play: () => Promise<void>;
pause: () => void;
Expand All @@ -123,6 +124,10 @@ type EditorInit = {
getAudioDuration: () => number;
};

export const getEditorLayoutClass = (layoutMode: EditorInit["layoutMode"]): string => {
return layoutMode === "mobile" ? "editor editor--mobile" : "editor";
};

export type EditorController = {
setVisible: (visible: boolean) => void;
isVisible: () => boolean;
Expand Down Expand Up @@ -905,6 +910,7 @@ export async function createEditorRoot(init: EditorInit): Promise<EditorControll
const previousSceneList = init.container.querySelector<HTMLDivElement>(".editor-scene-list");
const sceneListScrollTop = previousSceneList?.scrollTop ?? 0;

init.container.className = getEditorLayoutClass(init.layoutMode);
init.container.innerHTML = `
<div class="editor-header">
<div class="editor-title">SCENE + TIMELINE EDITOR</div>
Expand Down
20 changes: 18 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const debugSkipEndButton = document.querySelector<HTMLButtonElement>("#debug-ski
const debugSkipBackButton = document.querySelector<HTMLButtonElement>("#debug-skip-back");
const debugSkipForwardButton = document.querySelector<HTMLButtonElement>("#debug-skip-forward");
const debugEditorToggle = document.querySelector<HTMLInputElement>("#debug-editor-toggle");
const debugEditorLayoutSelect = document.querySelector<HTMLSelectElement>("#debug-editor-layout");
const debugTabButtons = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-debug-tab]"));
const editorRoot = document.querySelector<HTMLDivElement>("#editor-root");
const mobileControls = document.querySelector<HTMLDivElement>("#mobile-controls");
Expand Down Expand Up @@ -181,6 +182,7 @@ const effectIdeaSubmitButton = document.querySelector<HTMLButtonElement>("#effec
const queryParams = new URLSearchParams(window.location.search);
const releaseMode = queryParams.get("release") === "1";
const editorModeFromQuery = queryParams.get("editor") === "1";
const editorLayoutFromQuery = queryParams.get("editorLayout") === "mobile" ? "mobile" : "desktop";
const renderSettings = getRenderSettings(queryParams);
const qualityState = createQualityState(renderSettings.qualityScale, renderSettings.autoQuality);
const initialQualityPresetId = resolveInitialQualityPresetId(qualityState.qualityScale, qualityState.autoQuality);
Expand Down Expand Up @@ -213,6 +215,7 @@ let isRunning = false;
let pendingConfig: TimelineConfig | null = null;
let currentAudioSrc = "";
let editorController: EditorController | null = null;
let editorLayoutMode: "desktop" | "mobile" = editorLayoutFromQuery;
let lastFrameTimestamp = performance.now();
let currentViewCount = 0;
let overlayMode: OverlayMode = "start";
Expand Down Expand Up @@ -1732,9 +1735,10 @@ if (!releaseMode) {
if (!editorRoot) {
return;
}
createEditorRoot({
const initEditor = (): Promise<EditorController> => createEditorRoot({
container: editorRoot,
effectNames: availableEffectNames,
layoutMode: editorLayoutMode,
applyTimeline: applyRawTimeline,
play: async () => {
if (!audioPlayer) {
Expand All @@ -1760,14 +1764,26 @@ if (!releaseMode) {
},
getAudioOffset: () => timeline?.getAudioOffset() ?? 0,
getAudioDuration: () => audioPlayer?.duration ?? 0
}).then((controller) => {
});

initEditor().then((controller) => {
editorController = controller;
if (debugEditorToggle) {
debugEditorToggle.checked = editorModeFromQuery;
debugEditorToggle.addEventListener("change", () => {
controller.setVisible(debugEditorToggle.checked);
});
}
if (debugEditorLayoutSelect) {
debugEditorLayoutSelect.value = editorLayoutMode;
debugEditorLayoutSelect.addEventListener("change", () => {
editorLayoutMode = debugEditorLayoutSelect.value === "mobile" ? "mobile" : "desktop";
void initEditor().then((nextController) => {
editorController = nextController;
nextController.setVisible(debugEditorToggle?.checked ?? editorModeFromQuery);
});
});
}
controller.setVisible(debugEditorToggle?.checked ?? editorModeFromQuery);
});
})();
Expand Down
22 changes: 22 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,28 @@ body,
z-index: 2;
}

.editor--mobile .editor-body {
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
"workspace"
"scenes"
"inspector";
}

.editor--mobile .editor-top-row {
grid-template-columns: minmax(0, 1fr);
}

.editor--mobile .editor-preview canvas {
width: 100%;
height: auto;
}

.editor--mobile .editor-scenes,
.editor--mobile .editor-inspector {
max-height: none;
}

.editor-transport-row {
display: flex;
align-items: center;
Expand Down