diff --git a/frontend/libs/components/pagescene/package.json b/frontend/libs/components/pagescene/package.json new file mode 100644 index 0000000000..07427048f3 --- /dev/null +++ b/frontend/libs/components/pagescene/package.json @@ -0,0 +1,17 @@ +{ + "name": "@storyteller/ui-pagescene", + "version": "0.0.1", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + } +} diff --git a/frontend/libs/components/pagescene/src/index.ts b/frontend/libs/components/pagescene/src/index.ts new file mode 100644 index 0000000000..95d0f6301e --- /dev/null +++ b/frontend/libs/components/pagescene/src/index.ts @@ -0,0 +1,4 @@ +// Public API for @storyteller/ui-pagescene. +// (Stub during scaffold; populated incrementally as files migrate.) + +export {}; diff --git a/frontend/libs/components/pagescene/src/lib/adapter.ts b/frontend/libs/components/pagescene/src/lib/adapter.ts new file mode 100644 index 0000000000..ce5add8bd2 --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/adapter.ts @@ -0,0 +1,111 @@ +// Single platform-abstraction surface the host (artcraft Tauri app or +// any future web host) injects into PageScene. Mirrors the shape of +// PageDrawAdapter — methods for Tauri-specific generation/upload, slot +// renderers for host-owned modals, and optional event hooks for +// telemetry / error surfacing / tab persistence. +// +// The library itself has zero Tauri imports. Everything platform- +// specific is on this interface; the adapter is constructed in the +// host wrapper and passed to . + +import type { ReactNode } from "react"; +import type { + CommonAspectRatio, + CommonResolution, + ImageModel, +} from "@storyteller/model-list"; +import type { GenerationProvider } from "@storyteller/api-enums"; + +// ─── Generation ──────────────────────────────────────────────────────── + +export interface PageSceneGenerateRequest { + model?: ImageModel; + provider?: GenerationProvider; + prompt?: string; + imageMediaTokens?: string[]; + sceneImageMediaToken?: string; + imageCount?: number; + aspectRatio?: CommonAspectRatio; + resolution?: CommonResolution; + frontendCaller?: string; + frontendSubscriberId?: string; +} + +// ─── Scene I/O ───────────────────────────────────────────────────────── + +export interface PageSceneSavePayload { + saveJson: string; + sceneTitle: string; + sceneToken?: string; + sceneThumbnail: Blob | undefined; +} + +// ─── Adapter ─────────────────────────────────────────────────────────── + +export interface PageSceneAdapter { + // Generation enqueue. Same shape as PageDrawAdapter.enqueueEditImage. + enqueueGeneration( + req: PageSceneGenerateRequest, + ): Promise<{ status: string }>; + + // Scene I/O — replaces the previous engine/api_manager.ts + + // engine/api_fetchers.ts. The host owns all HTTP / FetchProxy + // plumbing. + saveScene(payload: PageSceneSavePayload): Promise; + loadScene(token: string): Promise; + // Wraps Tauri-flavored CORS-bypassed fetches. Used by Scene's GLTF + // loader paths that resolve CDN URLs the browser can't fetch directly. + fetchAsset(url: string): Promise; + + // Hosts. Engine builds CDN URLs (`${cdnOrigin}${bucket_path}`) and + // API URLs (`${apiSchemeAndHost}/v1/...`); the host supplies both. + getCdnOrigin(): string; + getApiSchemeAndHost(): string; + getCurrentUserToken?(): string | undefined; + + // Slot renders for host-owned UI. The library renders these inside + // its own AssetMenu / scene-load modal containers — same shape as + // PageDrawAdapter.renderBaseImageSelector. + renderAssetBrowser(props: { + onAssetSelect: (asset: { + mediaToken: string; + name: string; + kind: string; + }) => void; + }): ReactNode; + renderSceneLoader(props: { + onSceneSelect: (token: string) => void; + }): ReactNode; + + // Optional event hooks — telemetry, host-side modals, tab title sync. + onSelectionChange?( + sel: { uuid: string; assetType: string } | null, + ): void; + onSceneDirty?(dirty: boolean): void; + onError?(err: { title: string; message: string }): void; + onSceneSaved?(token: string): void; + // Wraps the host's `signalScene(...)` so the artcraft TopBar (and + // other app-wide consumers) keeps seeing scene title/owner/dirty + // state without the lib importing the host signal. + onSceneTitleChange?(meta: { + title: string; + token?: string; + ownerToken?: string; + isModified: boolean; + }): void; + onEnqueueMeta?(meta: { + prompt: string; + refImageUrls: string[]; + modelType: string; + timestamp: number; + }): void; + + // Tab-cache integration. The host (artcraft useTabStore) reads/ + // writes the serialized scene JSON between tab switches; the library + // is single-instance and tab-agnostic. + cacheJsonString?: string; + onSceneSerialized?(json: string): void; + + // Initial scene to load on mount (the route param in artcraft). + initialSceneToken?: string; +} diff --git a/frontend/libs/components/pagescene/tsconfig.json b/frontend/libs/components/pagescene/tsconfig.json new file mode 100644 index 0000000000..162ab9faec --- /dev/null +++ b/frontend/libs/components/pagescene/tsconfig.json @@ -0,0 +1,70 @@ +{ + "files": [], + "include": [], + "references": [ + { + "path": "../../common" + }, + { + "path": "../../api" + }, + { + "path": "../../api-enums" + }, + { + "path": "../../model-list" + }, + { + "path": "../../soundboard" + }, + { + "path": "../promptbox" + }, + { + "path": "../popover" + }, + { + "path": "../loading" + }, + { + "path": "../toaster" + }, + { + "path": "../gallery-modal" + }, + { + "path": "../model-selector" + }, + { + "path": "../help-menu" + }, + { + "path": "../login-modal" + }, + { + "path": "../settings-modal" + }, + { + "path": "../modal" + }, + { + "path": "../tooltip" + }, + { + "path": "../button" + }, + { + "path": "../input" + }, + { + "path": "../slider-v2" + }, + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/frontend/libs/components/pagescene/tsconfig.lib.json b/frontend/libs/components/pagescene/tsconfig.lib.json new file mode 100644 index 0000000000..17841e8e11 --- /dev/null +++ b/frontend/libs/components/pagescene/tsconfig.lib.json @@ -0,0 +1,105 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "lib": ["dom", "esnext"], + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "vite/client" + ], + "rootDir": "src", + "jsx": "react-jsx", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo" + }, + "exclude": [ + "out-tsc", + "dist", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ], + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx" + ], + "references": [ + { + "path": "../../common/tsconfig.lib.json" + }, + { + "path": "../../api/tsconfig.lib.json" + }, + { + "path": "../../api-enums/tsconfig.lib.json" + }, + { + "path": "../../model-list/tsconfig.lib.json" + }, + { + "path": "../../soundboard/tsconfig.lib.json" + }, + { + "path": "../promptbox/tsconfig.lib.json" + }, + { + "path": "../popover/tsconfig.lib.json" + }, + { + "path": "../loading/tsconfig.lib.json" + }, + { + "path": "../toaster/tsconfig.lib.json" + }, + { + "path": "../gallery-modal/tsconfig.lib.json" + }, + { + "path": "../model-selector/tsconfig.lib.json" + }, + { + "path": "../help-menu/tsconfig.lib.json" + }, + { + "path": "../login-modal/tsconfig.lib.json" + }, + { + "path": "../settings-modal/tsconfig.lib.json" + }, + { + "path": "../modal/tsconfig.lib.json" + }, + { + "path": "../tooltip/tsconfig.lib.json" + }, + { + "path": "../button/tsconfig.lib.json" + }, + { + "path": "../input/tsconfig.lib.json" + }, + { + "path": "../slider-v2/tsconfig.lib.json" + } + ] +} diff --git a/frontend/libs/components/pagescene/tsconfig.spec.json b/frontend/libs/components/pagescene/tsconfig.spec.json new file mode 100644 index 0000000000..18b7de51b8 --- /dev/null +++ b/frontend/libs/components/pagescene/tsconfig.spec.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "jsx": "react-jsx" + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/frontend/libs/components/pagescene/vite.config.ts b/frontend/libs/components/pagescene/vite.config.ts new file mode 100644 index 0000000000..c774f0df5a --- /dev/null +++ b/frontend/libs/components/pagescene/vite.config.ts @@ -0,0 +1,40 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; +import * as path from 'path'; +import { isExternal } from '../../shared-vite-config'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/components/pagescene', + plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })], + build: { + outDir: './dist', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + entry: 'src/index.ts', + name: '@storyteller/ui-pagescene', + fileName: 'index', + formats: ['es' as const] + }, + rollupOptions: { + external: isExternal + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + } + }, +}));