artcraft: scaffold @storyteller/ui-pagescene library + PageSceneAdapter

Empty Nx library at frontend/libs/components/pagescene/ mirroring the
@storyteller/ui-pagedraw shape (package.json with development export,
tsconfig.lib.json with workspace lib references, vite.config.ts).

Defines the platform-abstraction surface PageSceneAdapter:
- Generation enqueue (model/provider/prompt/tokens/aspect/resolution)
- Scene I/O (saveScene, loadScene, fetchAsset, host CDN/API getters)
- Slot renderers for host-owned modals (renderAssetBrowser,
  renderSceneLoader)
- Optional event hooks (onError, onSceneSaved, onSceneTitleChange,
  onEnqueueMeta, onSelectionChange, onSceneDirty)
- Tab-cache integration (cacheJsonString + onSceneSerialized) so the
  artcraft useTabStore can roundtrip serialized scene JSON without
  the lib knowing about tabs

Library code is empty for now -- file migrations (PageSceneStore +
engine + comps + actions + hooks etc.) land in follow-up commits on
this branch.
This commit is contained in:
Tanish Manku committed 2026-05-08 12:09:47 -07:00
1 parent ca713ba1f9
commit 7465d2a533
7 files changed
+381

No files matched your search

@@ -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"
}
}
}
@@ -0,0 +1,4 @@
// Public API for @storyteller/ui-pagescene.
// (Stub during scaffold; populated incrementally as files migrate.)
export {};
@@ -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 <PageScene adapter={adapter} />.
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<string>;
loadScene(token: string): Promise<unknown>;
// 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<Response>;
// 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;
}
@@ -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"
}
@@ -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"
}
]
}
@@ -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"
}
]
}
@@ -0,0 +1,40 @@
/// <reference types='vitest' />
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,
}
},
}));