diff --git a/README.md b/README.md index 195e6a5..9412ab3 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ pnpm install pnpm dev # editor UI + API, with a local database & storage ``` -Open the editor, hit **New composition** for a starter, edit the HTML in the **Compose** tab, drop media in **Media**, scrub the timeline, and render from **Renders**. +Open the editor, hit **New composition** for a starter, edit the HTML in the **Compose** tab, drop media on the **Media** screen, scrub the timeline, and render from **Renders**. ## Deploy diff --git a/agent.md b/agent.md index 3963934..3bb5e36 100644 --- a/agent.md +++ b/agent.md @@ -47,7 +47,7 @@ HTML by path: `assets/`. Reference it as `` or the assets your HTML actually references — you don't attach them manually. To list what's available: `GET /api/assets` → `[{ key, name, content_type }]`. -Use the exact `key` in `assets/`. (Users upload via the Media tab; you can +Use the exact `key` in `assets/`. (Users upload on the Media screen; you can also upload programmatically with a multipart `POST /api/assets`.) ## API diff --git a/package.json b/package.json index 7244dbe..4b04422 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "deploy": "clawnify deploy" }, "dependencies": { + "@clawnify/app": "^0.2.1", "@clawnify/db": "^0.2.0", + "@phosphor-icons/react": "^2.1.10", "hono": "^4.6.0", "lucide-react": "^0.400.0", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cebe48c..34fb857 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,15 @@ importers: .: dependencies: + '@clawnify/app': + specifier: ^0.2.1 + version: 0.2.1(@cloudflare/workers-types@4.20260613.1)(@hono/zod-openapi@0.19.10(hono@4.12.25)(zod@3.25.76))(@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(hono@4.12.25)(react@19.2.7) '@clawnify/db': specifier: ^0.2.0 version: 0.2.0 + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) hono: specifier: ^4.6.0 version: 4.12.25 @@ -38,7 +44,7 @@ importers: version: 4.20260613.1 '@tailwindcss/vite': specifier: ^4.2.0 - version: 4.3.1(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)) + version: 4.3.1(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) '@types/react': specifier: ^19.0.0 version: 19.2.17 @@ -47,7 +53,7 @@ importers: version: 19.2.3(@types/react@19.2.17) '@vitejs/plugin-react': specifier: ^4.0.0 - version: 4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)) + version: 4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) clawnify: specifier: ^0.1.12 version: 0.1.17 @@ -59,13 +65,18 @@ importers: version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + version: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) wrangler: specifier: ^4.0.0 version: 4.100.0(@cloudflare/workers-types@4.20260613.1) packages: + '@asteasolutions/zod-to-openapi@7.3.4': + resolution: {integrity: sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA==} + peerDependencies: + zod: ^3.20.2 + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -149,9 +160,31 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@clawnify/app@0.2.1': + resolution: {integrity: sha512-t6fx4rnbEAewU3fH/WGKfWhfhZgVbLp1JgnpCR0YSHaE6rYwxEmgHAYlN3q5gSjYutsWmXANFqro3wuyxWZlTA==} + peerDependencies: + '@hono/zod-openapi': '>=0.18 <1' + '@phosphor-icons/react': '>=2' + hono: '*' + react: '>=18' + peerDependenciesMeta: + '@phosphor-icons/react': + optional: true + react: + optional: true + '@clawnify/db@0.2.0': resolution: {integrity: sha512-6ADNSig+qLg24xajbDcjJXktBZSNKwfTDJhh1oH5TFKh8frVOzHirVGe06XTeMn9m5lI40jlpQ2sLo0OOl0iEA==} + '@clawnify/db@0.4.1': + resolution: {integrity: sha512-Ck9RGqEElt6P/3S9quxvr2MIg8mZk1n9wFftH3PpGPFnpA6hbk0td67oaioeMRuDCn7vePcJCeyYEkbvNVYy/g==} + + '@clawnify/routes@0.2.2': + resolution: {integrity: sha512-+s7gjRyH8bmIUHQDHmmwGyUeEcV5m+IoWEYr2tLiWdVW7WKlFlgeKIuJ61vW9mc9k4TizL+B+Tg8qJ72zAbvOA==} + peerDependencies: + '@hono/zod-openapi': '>=0.18 <1' + hono: '*' + '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -523,6 +556,19 @@ packages: peerDependencies: hono: ^4 + '@hono/zod-openapi@0.19.10': + resolution: {integrity: sha512-dpoS6DenvoJyvxtQ7Kd633FRZ/Qf74+4+o9s+zZI8pEqnbjdF/DtxIib08WDpCaWabMEJOL5TXpMgNEZvb7hpA==} + engines: {node: '>=16.0.0'} + peerDependencies: + hono: '>=4.3.6' + zod: '>=3.0.0' + + '@hono/zod-validator@0.7.6': + resolution: {integrity: sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw==} + peerDependencies: + hono: '>=3.9.0' + zod: ^3.25.0 || ^4.0.0 + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -705,6 +751,13 @@ packages: '@cfworker/json-schema': optional: true + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -1139,6 +1192,98 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1506,6 +1651,9 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} + openapi3-ts@4.6.1: + resolution: {integrity: sha512-XW9MOldkhoICNeXVzzmXzmOW5G73ppOEGmh7fLCqHjgfdEYCGGN+00MlVCeUZgovjjfC56j9tvtDt1zGabNjjA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -1825,6 +1973,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -1849,6 +2002,11 @@ packages: snapshots: + '@asteasolutions/zod-to-openapi@7.3.4(zod@3.25.76)': + dependencies: + openapi3-ts: 4.6.1 + zod: 3.25.76 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -1961,8 +2119,87 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@clawnify/app@0.2.1(@cloudflare/workers-types@4.20260613.1)(@hono/zod-openapi@0.19.10(hono@4.12.25)(zod@3.25.76))(@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(hono@4.12.25)(react@19.2.7)': + dependencies: + '@clawnify/db': 0.4.1(@cloudflare/workers-types@4.20260613.1) + '@clawnify/routes': 0.2.2(@hono/zod-openapi@0.19.10(hono@4.12.25)(zod@3.25.76))(hono@4.12.25) + '@hono/zod-openapi': 0.19.10(hono@4.12.25)(zod@3.25.76) + hono: 4.12.25 + optionalDependencies: + '@phosphor-icons/react': 2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - sql.js + - sqlite3 + '@clawnify/db@0.2.0': {} + '@clawnify/db@0.4.1(@cloudflare/workers-types@4.20260613.1)': + dependencies: + drizzle-orm: 0.45.2(@cloudflare/workers-types@4.20260613.1) + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - sql.js + - sqlite3 + + '@clawnify/routes@0.2.2(@hono/zod-openapi@0.19.10(hono@4.12.25)(zod@3.25.76))(hono@4.12.25)': + dependencies: + '@hono/zod-openapi': 0.19.10(hono@4.12.25)(zod@3.25.76) + hono: 4.12.25 + '@cloudflare/kv-asset-handler@0.5.0': {} '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260611.1)': @@ -2157,6 +2394,19 @@ snapshots: dependencies: hono: 4.12.25 + '@hono/zod-openapi@0.19.10(hono@4.12.25)(zod@3.25.76)': + dependencies: + '@asteasolutions/zod-to-openapi': 7.3.4(zod@3.25.76) + '@hono/zod-validator': 0.7.6(hono@4.12.25)(zod@3.25.76) + hono: 4.12.25 + openapi3-ts: 4.6.1 + zod: 3.25.76 + + '@hono/zod-validator@0.7.6(hono@4.12.25)(zod@3.25.76)': + dependencies: + hono: 4.12.25 + zod: 3.25.76 + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -2299,6 +2549,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -2453,12 +2708,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 - '@tailwindcss/vite@4.3.1(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))': + '@tailwindcss/vite@4.3.1(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.1 '@tailwindcss/oxide': 4.3.1 tailwindcss: 4.3.1 - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) '@types/babel__core@7.20.5': dependencies: @@ -2491,7 +2746,7 @@ snapshots: dependencies: csstype: 3.2.3 - '@vitejs/plugin-react@4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -2499,7 +2754,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -2651,6 +2906,10 @@ snapshots: detect-libc@2.1.2: {} + drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260613.1): + optionalDependencies: + '@cloudflare/workers-types': 4.20260613.1 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3014,6 +3273,10 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 + openapi3-ts@4.6.1: + dependencies: + yaml: 2.9.0 + parseurl@1.3.3: {} path-key@3.1.1: {} @@ -3284,7 +3547,7 @@ snapshots: vary@1.1.2: {} - vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0): + vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -3296,6 +3559,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 + yaml: 2.9.0 which@2.0.2: dependencies: @@ -3344,6 +3608,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/src/client/app.tsx b/src/client/app.tsx index 34315b3..cd73032 100644 --- a/src/client/app.tsx +++ b/src/client/app.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { EditProjectsSection, EditRoute } from "./edit"; import { STARTER_HTML } from "./starter"; import { Group, Panel, Separator, useDefaultLayout } from "react-resizable-panels"; +import { AppNav, reportLocation, type AppNavItem } from "@clawnify/app/client"; import { ArrowLeft, Film, @@ -88,9 +89,32 @@ const api = { // ── app ────────────────────────────────────────────────────────────── -type Tab = "compose" | "timeline" | "media" | "renders"; +type Tab = "compose" | "timeline" | "renders"; -// Minimal history-based router: `/` = gallery, `/` = editor for that id. +// One definition of the navigation. paints it as this app's own +// sidebar when opened directly, and hands it to the Clawnify dashboard's +// sidebar when embedded there — so the user only ever sees one nav. +const NAV: AppNavItem[] = [ + // The gallery is home: it is not a row, the app's name opens it. + { id: "compositions", label: "Compositions", href: "/", home: true }, + { id: "media", label: "Media", href: "/media", icon: "image", color: "violet" }, +]; + +// The footage library, owned one level above the screen that shows it, so the +// sidebar count stays live when a file is uploaded or deleted. +function useAssets() { + const [assets, setAssets] = useState([]); + const reload = useCallback(async () => { + setAssets(await api.get("/api/assets")); + }, []); + useEffect(() => { + reload().catch(() => setAssets([])); + }, [reload]); + return { assets, reload }; +} + +// Minimal history-based router: `/` = gallery, `/media` = the footage +// library, `/` = editor for that id. function useRouter() { const [path, setPath] = useState(() => window.location.pathname); useEffect(() => { @@ -108,38 +132,90 @@ function useRouter() { export function App() { const { path, navigate } = useRouter(); - // "/" → gallery; "/edits/" → footage editor; "/" → composition editor. + // "/" → gallery; "/media" → footage library; "/edits/" → footage editor; + // "/" → composition editor. const id = decodeURIComponent(path.replace(/^\/+|\/+$/g, "")); + const isMedia = id === "media"; const editId = id.startsWith("edits/") ? id.slice(6) : id === "edits" ? "" : null; + const { assets, reload: reloadAssets } = useAssets(); + + // Lets the dashboard restore this exact screen on reload. + useEffect(() => { + reportLocation(path); + }, [path]); + + // The badge is just state: it moves the moment the library does. + const items = NAV.map((item) => + item.id === "media" && assets.length > 0 ? { ...item, count: assets.length } : item, + ); return ( -
- {/* Brand row: the app icon is the identity object, and the accent hue - lives here (plus count badges and the focus ring) and nowhere else. */} -
- {id && ( - + // The SDK rail is a 275px column at ≥768px and a horizontal strip below + // that, so it has to be the first child of the shell's flex container. + // The shell takes a definite `h-dvh`, never a min-height: the editor's + // resizable group sizes itself off a percentage height, and a min-height + // here leaves every descendant height indefinite, collapsing it to a + // 4px seam on the phone layout. +
+ navigate(item.href ?? "/")} + /> +
+ {/* Brand row: the app icon is the identity object, and the accent hue + lives here (plus count badges and the focus ring) and nowhere else. + It stays inside the dashboard too, where the rail paints nothing. */} +
+ {id && !isMedia && ( + + )} + + + + OpenVideo + edit & render video +
+ + {isMedia ? ( + + ) : editId ? ( + + ) : id ? ( + + ) : ( + )} - - - - OpenVideo - edit & render video -
- - {editId ? ( - - ) : id ? ( - - ) : ( - - )} +
); } +// ── media route ────────────────────────────────────────────────────── + +// The footage library on its own screen. Same panel the editor used to hold +// in a tab; it is global, so it belongs beside the gallery, not inside one +// composition. +function MediaRoute({ assets, reload }: { assets: Asset[]; reload: () => Promise }) { + return ( +
+
+

+ Media + {assets.length > 0 && ( + {assets.length} + )} +

+ +
+
+ ); +} + // ── gallery ────────────────────────────────────────────────────────── function fmtDate(s: string): string { @@ -486,7 +562,7 @@ function Editor({ {/* view switcher: a segmented track, active segment raised white */}
- {(["timeline", "compose", "media", "renders"] as Tab[]).map((t) => ( + {(["timeline", "compose", "renders"] as Tab[]).map((t) => (
@@ -951,20 +1026,14 @@ function toHex(color: string): string { // ── media ──────────────────────────────────────────────────────────── -function MediaPanel() { - const [assets, setAssets] = useState([]); +// The list is owned by so the sidebar count follows it; the panel reads +// it and asks for a reload after it changes something. +function MediaPanel({ assets, reload }: { assets: Asset[]; reload: () => Promise }) { const [copied, setCopied] = useState(null); const [uploading, setUploading] = useState(false); const [confirmDel, setConfirmDel] = useState(null); const fileRef = useRef(null); - async function load() { - setAssets(await api.get("/api/assets")); - } - useEffect(() => { - load(); - }, []); - async function upload(files: FileList | null) { if (!files?.length) return; setUploading(true); @@ -974,7 +1043,7 @@ function MediaPanel() { fd.append("file", f); await fetch("/api/assets", { method: "POST", body: fd }); } - await load(); + await reload(); } finally { setUploading(false); } @@ -982,7 +1051,7 @@ function MediaPanel() { async function del(id: string) { await api.send("DELETE", `/api/assets/${id}`); - load(); + reload(); } function copy(key: string) { diff --git a/src/client/styles.css b/src/client/styles.css index 27bc8df..2628d2a 100644 --- a/src/client/styles.css +++ b/src/client/styles.css @@ -344,3 +344,79 @@ body { color: var(--faint); } } + +/* ── The standalone sidebar (SDK ) brought to the shell spec ────── + @clawnify/app injects its own