From 7131f88bf4ac4857677f3c2555b536f7cb54b6cb Mon Sep 17 00:00:00 2001 From: kseungyong Date: Mon, 18 May 2026 17:41:01 +0900 Subject: [PATCH] feat: add projectType metadata field (standard | shorts) Adds optional projectType discriminator on TProjectMetadata so downstream features (preset pickers, export defaults, UI affordances) can branch on intent without inferring from canvas dimensions. - New const PROJECT_TYPES and type TProjectType - TProjectMetadata.projectType (optional for back-compat with stored projects) - ProjectManager.createNewProject accepts optional projectType, defaults to 'standard' - Unit tests for PROJECT_TYPES invariants --- apps/web/src/core/managers/project-manager.ts | 10 ++++++++- .../project/__tests__/project-type.test.ts | 22 +++++++++++++++++++ apps/web/src/project/types.ts | 16 ++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/project/__tests__/project-type.test.ts diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts index b38fd50d..8b593fec 100644 --- a/apps/web/src/core/managers/project-manager.ts +++ b/apps/web/src/core/managers/project-manager.ts @@ -5,6 +5,7 @@ import type { TProjectSortKey, TProjectSortOption, TProjectSettings, + TProjectType, TTimelineViewState, } from "@/project/types"; import type { ExportOptions, ExportResult, ExportState } from "@/export"; @@ -79,7 +80,13 @@ export class ProjectManager { await this.storageMigrationPromise; } - async createNewProject({ name }: { name: string }): Promise { + async createNewProject({ + name, + projectType, + }: { + name: string; + projectType?: TProjectType; + }): Promise { const mainScene = buildDefaultScene({ name: "Main scene", isMain: true }); const newProject: TProject = { metadata: { @@ -88,6 +95,7 @@ export class ProjectManager { duration: getProjectDurationFromScenes({ scenes: [mainScene] }), createdAt: new Date(), updatedAt: new Date(), + projectType: projectType ?? "standard", }, scenes: [mainScene], currentSceneId: mainScene.id, diff --git a/apps/web/src/project/__tests__/project-type.test.ts b/apps/web/src/project/__tests__/project-type.test.ts new file mode 100644 index 00000000..a7e35b8d --- /dev/null +++ b/apps/web/src/project/__tests__/project-type.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "bun:test"; +import { PROJECT_TYPES, type TProjectType } from "@/project/types"; + +describe("PROJECT_TYPES", () => { + test("contains standard and shorts", () => { + expect(PROJECT_TYPES).toContain("standard"); + expect(PROJECT_TYPES).toContain("shorts"); + }); + + test("project metadata uses 'standard' when projectType is undefined", () => { + function buildMetadataProjectType(input?: TProjectType): TProjectType { + return input ?? "standard"; + } + expect(buildMetadataProjectType(undefined)).toBe("standard"); + expect(buildMetadataProjectType("shorts")).toBe("shorts"); + expect(buildMetadataProjectType("standard")).toBe("standard"); + }); + + test("PROJECT_TYPES is stable in declared order", () => { + expect([...PROJECT_TYPES]).toEqual(["standard", "shorts"]); + }); +}); diff --git a/apps/web/src/project/types.ts b/apps/web/src/project/types.ts index dca2d785..0608dd0e 100644 --- a/apps/web/src/project/types.ts +++ b/apps/web/src/project/types.ts @@ -2,6 +2,15 @@ import type { FrameRate } from "opencut-wasm"; import type { TScene } from "@/timeline/types"; import type { MediaTime } from "@/wasm"; +export const PROJECT_TYPES = ["standard", "shorts"] as const; +/** + * Project intent. "shorts" indicates a vertical 9:16 short-form project + * (e.g. YouTube Shorts) — drives preset pickers, export defaults, and + * vertical-safe text positioning. "standard" covers landscape, square, + * and other non-Shorts aspect ratios. + */ +export type TProjectType = (typeof PROJECT_TYPES)[number]; + export type TBackground = | { type: "color"; @@ -24,6 +33,13 @@ export interface TProjectMetadata { duration: MediaTime; createdAt: Date; updatedAt: Date; + /** + * Discriminator for project intent. Optional for back-compat — undefined on + * projects created before this field existed; consumers MUST treat undefined + * as "standard" (e.g. `projectType ?? "standard"`). New projects always set + * this explicitly via ProjectManager.createNewProject. + */ + projectType?: TProjectType; } export interface TProjectSettings {