feat(studio+render): wire theme picker → saved_shared_colors → FlexStory render

Closes the theme→render gap: the studio theme picker now actually drives a
FlexStory render's colours. GetFlexStoryProps reads saved_shared_colors by
element_key (accentColor/secondaryColor/backgroundColor/textColor), but the studio
only wrote the theme into scene_data — so the picker never reached the MP4.

- studio-svc: UpdateSharedColorsAsync upserts saved_shared_colors by (project,
  element_key) + PATCH /v1/saved-projects/{id}/shared-colors endpoint +
  UpdateColorsRequest/UpdateColorItem. Mirrors UpdateSceneContentsAsync. (dotnet
  build: 0 errors.)
- gateway already wildcard-routes /v1/saved-projects/*path → studio-svc (no change).
- Next: /api/projects/[id]/colors route → gateway; project-api patchProjectColors
  + themeColorsFromSceneData (maps scene_data sceneAccentColor… → the colorSchema
  keys); performSave best-effort pushes the 4 colours alongside contents.

Chain: theme picker → store → scene_data → performSave → patchProjectColors →
gateway → studio-svc upsert → saved_shared_colors → GetFlexStoryProps → render.
Verified: Next build + dotnet build both clean; theme presets render cohesively
across all 6 (incl. dark Midnight). End-to-end studio→render needs the live stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-23 17:04:47 +03:30
parent c1747167f3
commit c0d04fa855
6 changed files with 143 additions and 0 deletions
@@ -0,0 +1,46 @@
import { NextResponse } from "next/server";
import { gatewayFetch } from "@/lib/api/gateway";
import { getAccessToken } from "@/lib/auth/session";
export const dynamic = "force-dynamic";
/**
* Persist the studio theme picker's brand colours to the saved project's shared
* colours (studio-svc PATCH /v1/saved-projects/{id}/shared-colors). The FlexStory
* render binder (GetFlexStoryProps) reads saved_shared_colors by element_key, so this
* is what makes the theme picker reach the MP4.
* Body: { items: [{ key, value }] } — key ∈ accentColor/secondaryColor/backgroundColor/textColor
*/
export async function PATCH(
req: Request,
ctx: { params: Promise<{ projectId: string }> }
) {
const { projectId } = await ctx.params;
const token = await getAccessToken();
if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = (await req.json().catch(() => null)) as {
items?: Array<{ key?: string; value?: string }>;
} | null;
const items = Array.isArray(body?.items)
? body!.items.filter(
(i) => typeof i?.key === "string" && i.key.length > 0 && typeof i?.value === "string"
)
: [];
if (items.length === 0) return NextResponse.json({ updated: 0 });
const res = await gatewayFetch(`/v1/saved-projects/${projectId}/shared-colors`, {
method: "PATCH",
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ items }),
});
if (!res.ok) {
const d = await res.json().catch(() => null);
return NextResponse.json(
{ error: d?.message ?? "Could not save colours" },
{ status: res.status }
);
}
return NextResponse.json(await res.json().catch(() => ({ updated: items.length })));
}
+8
View File
@@ -12,8 +12,10 @@ import {
contentValuesFromSceneData,
fetchProject,
isProjectNotFoundError,
patchProjectColors,
patchProjectContents,
patchProjectSceneData,
themeColorsFromSceneData,
} from "@/lib/project-api";
import { isDevProjectId } from "@/lib/project-ids";
import {
@@ -159,6 +161,12 @@ export function useStudioProjectPersistence(
projectId,
contentValuesFromSceneData(scene_data)
).catch(() => {});
// Persist the theme colours to saved_shared_colors so the FlexStory render
// reads the theme picker (best-effort).
void patchProjectColors(
projectId,
themeColorsFromSceneData(scene_data)
).catch(() => {});
setUsingLocalStorage(false);
markSaved(payloadJson, false);
} catch (error) {
+43
View File
@@ -98,6 +98,31 @@ export async function patchProjectContents(
}
}
export interface ColorValueUpdate {
key: string;
value: string;
}
/**
* Persist the project's theme colours (accentColor/secondaryColor/backgroundColor/
* textColor) to saved_shared_colors so the FlexStory render binder reads them.
* Best-effort (callers may ignore failures).
*/
export async function patchProjectColors(
projectId: string,
items: ColorValueUpdate[]
): Promise<void> {
if (items.length === 0) return;
const response = await fetch(`/api/projects/${projectId}/colors`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items }),
});
if (!response.ok) {
throw new ProjectApiError("Failed to save colours", response.status);
}
}
/** Extract editable input values from studio scene_data (bridged `c-<key>` layers). */
export function contentValuesFromSceneData(
sceneData: Record<string, unknown>
@@ -126,3 +151,21 @@ export function contentValuesFromSceneData(
}
return items;
}
/** Map studio scene_data theme colours → the FlexStory colorSchema shared-colour keys. */
export function themeColorsFromSceneData(
sceneData: Record<string, unknown>
): ColorValueUpdate[] {
const map: Array<[string, string]> = [
["accentColor", "sceneAccentColor"],
["secondaryColor", "sceneSecondaryColor"],
["backgroundColor", "sceneBackgroundColor"],
["textColor", "sceneTextColor"],
];
const items: ColorValueUpdate[] = [];
for (const [key, field] of map) {
const v = sceneData[field];
if (typeof v === "string" && v) items.push({ key, value: v });
}
return items;
}