From 54e6803b88cfc3fa73357dc2e8a23713ae08987a Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Tue, 12 May 2026 13:57:19 -0400 Subject: [PATCH 01/10] feat: inject default theme variables into local-editor --- packages/visual-editor/src/editor/types.ts | 3 + .../hooks/useMessageReceivers.test.ts | 44 +++++++++++++- .../src/internal/hooks/useMessageReceivers.ts | 2 +- .../internal/utils/loadMapboxIntoIframe.tsx | 49 ++++++++++++++++ .../src/local-editor/LocalEditorShell.tsx | 3 + .../src/local-editor/selection.ts | 2 + .../visual-editor/src/local-editor/types.ts | 3 + .../src/utils/applyTheme.test.ts | 25 +++++++- .../visual-editor/src/utils/applyTheme.ts | 41 +++++++------ .../local-editor/generatedFiles.ts | 24 +++++++- .../src/vite-plugin/local-editor/scaffold.ts | 57 +++++++++++++++++++ .../visual-editor/src/vite-plugin/plugin.ts | 6 +- .../registryTemplateGenerator.test.ts | 6 ++ .../vite-plugin/templates/local-editor.tsx | 2 + 14 files changed, 245 insertions(+), 22 deletions(-) diff --git a/packages/visual-editor/src/editor/types.ts b/packages/visual-editor/src/editor/types.ts index 61250d0dca..70db9ef643 100644 --- a/packages/visual-editor/src/editor/types.ts +++ b/packages/visual-editor/src/editor/types.ts @@ -1,3 +1,5 @@ +import type { ThemeData } from "../internal/types/themeData.ts"; + export type LocalDevOptions = { templateId?: string; entityId?: string | number; @@ -5,5 +7,6 @@ export type LocalDevOptions = { locales?: string[]; layoutScopeKey?: string; initialLayoutData?: Record; + initialThemeData?: ThemeData; showOverrideButtons?: boolean; }; diff --git a/packages/visual-editor/src/internal/hooks/useMessageReceivers.test.ts b/packages/visual-editor/src/internal/hooks/useMessageReceivers.test.ts index c44e5fc7e5..c42712ac96 100644 --- a/packages/visual-editor/src/internal/hooks/useMessageReceivers.test.ts +++ b/packages/visual-editor/src/internal/hooks/useMessageReceivers.test.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { locatorConfig } from "../../components/configs/locatorConfig.tsx"; -import { getLocalDevLayoutData } from "./useMessageReceivers.ts"; +import { renderHook, waitFor } from "@testing-library/react"; +import { + getLocalDevLayoutData, + useCommonMessageReceivers, +} from "./useMessageReceivers.ts"; const localDevLocatorLayout = JSON.stringify({ root: { @@ -82,3 +86,41 @@ describe("getLocalDevLayoutData", () => { expect(warnSpy).toHaveBeenCalledOnce(); }); }); + +describe("useCommonMessageReceivers", () => { + it("seeds local dev theme data from localDevOptions when provided", async () => { + vi.spyOn(window.parent, "postMessage").mockImplementation(() => {}); + + const { result } = renderHook(() => { + return useCommonMessageReceivers( + { locator: locatorConfig }, + true, + { + meta: { + entityType: { + id: "locator", + }, + }, + __: { + name: "locator", + }, + }, + { + templateId: "locator", + locale: "en", + initialThemeData: { + "--colors-palette-primary": "#CF0A2C", + }, + } + ); + }); + + await waitFor(() => { + expect(result.current.themeDataFetched).toBe(true); + }); + + expect(result.current.themeData).toEqual({ + "--colors-palette-primary": "#CF0A2C", + }); + }); +}); diff --git a/packages/visual-editor/src/internal/hooks/useMessageReceivers.ts b/packages/visual-editor/src/internal/hooks/useMessageReceivers.ts index acc728e640..7fdb55797f 100644 --- a/packages/visual-editor/src/internal/hooks/useMessageReceivers.ts +++ b/packages/visual-editor/src/internal/hooks/useMessageReceivers.ts @@ -204,7 +204,7 @@ export const useCommonMessageReceivers = ( ) ); setLayoutDataFetched(true); - setThemeData({}); + setThemeData(localDevOptions?.initialThemeData ?? {}); setThemeDataFetched(true); } }, [ diff --git a/packages/visual-editor/src/internal/utils/loadMapboxIntoIframe.tsx b/packages/visual-editor/src/internal/utils/loadMapboxIntoIframe.tsx index 6db1eb6cf4..dba3286746 100644 --- a/packages/visual-editor/src/internal/utils/loadMapboxIntoIframe.tsx +++ b/packages/visual-editor/src/internal/utils/loadMapboxIntoIframe.tsx @@ -1,5 +1,39 @@ import { ReactNode, useEffect } from "react"; import mapboxPackageJson from "mapbox-gl/package.json" with { type: "json" }; +import { THEME_STYLE_TAG_ID } from "../../utils/applyTheme.ts"; + +const syncParentStylesIntoIframe = (iframeDocument: Document) => { + const parentHeadNodes = window.document.head.querySelectorAll( + 'link[rel="stylesheet"], style' + ); + + parentHeadNodes.forEach((node, index) => { + const clonedNode = node.cloneNode(true) as HTMLElement; + const syncId = + clonedNode.id || + clonedNode.getAttribute("href") || + clonedNode.getAttribute("data-visual-editor-font") || + `visual-editor-style-sync-${index}`; + + if ( + !iframeDocument.head.querySelector( + `[data-visual-editor-sync="${CSS.escape(syncId)}"]` + ) + ) { + clonedNode.setAttribute("data-visual-editor-sync", syncId); + iframeDocument.head.appendChild(clonedNode); + return; + } + + const existingNode = iframeDocument.head.querySelector( + `[data-visual-editor-sync="${CSS.escape(syncId)}"]` + ); + if (existingNode) { + existingNode.replaceWith(clonedNode); + clonedNode.setAttribute("data-visual-editor-sync", syncId); + } + }); +}; /** * For use in Puck's iframe override. Loads the Mapbox script and stylesheet into the iframe document. @@ -16,6 +50,21 @@ export const loadMapboxIntoIframe = ({ return; } + syncParentStylesIntoIframe(document); + + const parentThemeStyleTag = + window.document.getElementById(THEME_STYLE_TAG_ID); + if (parentThemeStyleTag) { + let iframeThemeStyleTag = document.getElementById(THEME_STYLE_TAG_ID); + if (!iframeThemeStyleTag) { + iframeThemeStyleTag = document.createElement("style"); + iframeThemeStyleTag.id = THEME_STYLE_TAG_ID; + iframeThemeStyleTag.setAttribute("type", "text/css"); + document.head.appendChild(iframeThemeStyleTag); + } + iframeThemeStyleTag.textContent = parentThemeStyleTag.textContent; + } + // Ensure Mapbox script is loaded in the iframe if (!document.getElementById("mapbox-script")) { const script = document.createElement("script"); diff --git a/packages/visual-editor/src/local-editor/LocalEditorShell.tsx b/packages/visual-editor/src/local-editor/LocalEditorShell.tsx index 406ca7f82a..70c01239fa 100644 --- a/packages/visual-editor/src/local-editor/LocalEditorShell.tsx +++ b/packages/visual-editor/src/local-editor/LocalEditorShell.tsx @@ -20,6 +20,7 @@ export const LocalEditorShell = ({ componentRegistry, tailwindConfig, themeConfig, + simulatedThemeData, }: LocalEditorShellProps) => { const [locationSearch, setLocationSearch] = React.useState(() => { return typeof window === "undefined" ? "" : window.location.search; @@ -102,10 +103,12 @@ export const LocalEditorShell = ({ selectedEntity, selectedLocale, selectedTemplateDefaults, + simulatedThemeData, }); }, [ selectedEntity, selectedLocale, + simulatedThemeData, selectedTemplateDefaults, selectedTemplateId, ]); diff --git a/packages/visual-editor/src/local-editor/selection.ts b/packages/visual-editor/src/local-editor/selection.ts index c61c9a6002..bf5f6d0016 100644 --- a/packages/visual-editor/src/local-editor/selection.ts +++ b/packages/visual-editor/src/local-editor/selection.ts @@ -142,6 +142,7 @@ export const buildEditorLocalDevOptions = ({ selectedEntity, selectedLocale, selectedTemplateDefaults, + simulatedThemeData, }: BuildEditorLocalDevOptionsArgs): LocalDevOptions | undefined => { if (!selectedTemplateId) { return undefined; @@ -158,6 +159,7 @@ export const buildEditorLocalDevOptions = ({ initialLayoutData: selectedTemplateDefaults?.defaultLayoutData as | Record | undefined, + initialThemeData: simulatedThemeData, showOverrideButtons: false, }; }; diff --git a/packages/visual-editor/src/local-editor/types.ts b/packages/visual-editor/src/local-editor/types.ts index 615bbb7007..d24fdae798 100644 --- a/packages/visual-editor/src/local-editor/types.ts +++ b/packages/visual-editor/src/local-editor/types.ts @@ -1,4 +1,5 @@ import type { Config } from "@puckeditor/core"; +import type { ThemeData } from "../internal/types/themeData.ts"; import type { TailwindConfig, ThemeConfig } from "../utils/themeResolver.ts"; import type { LocalEditorDocumentResponse, @@ -20,6 +21,7 @@ export type LocalEditorShellProps = { componentRegistry: Record>; tailwindConfig: TailwindConfig; themeConfig?: ThemeConfig; + simulatedThemeData?: ThemeData; }; export type BuildEditorLocalDevOptionsArgs = { @@ -27,4 +29,5 @@ export type BuildEditorLocalDevOptionsArgs = { selectedEntity?: LocalEditorEntityOption; selectedLocale: string; selectedTemplateDefaults?: LocalEditorTemplateDefaults; + simulatedThemeData?: ThemeData; }; diff --git a/packages/visual-editor/src/utils/applyTheme.test.ts b/packages/visual-editor/src/utils/applyTheme.test.ts index d43c239f11..6256108659 100644 --- a/packages/visual-editor/src/utils/applyTheme.test.ts +++ b/packages/visual-editor/src/utils/applyTheme.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { applyTheme } from "./applyTheme.ts"; +import { + applyTheme, + THEME_STYLE_TAG_ID, + updateThemeInEditor, +} from "./applyTheme.ts"; import { ThemeConfig } from "./themeResolver.ts"; import { StreamDocument } from "./types/StreamDocument.ts"; @@ -269,6 +273,25 @@ describe("buildCssOverridesStyle", () => { "--colors-palette-secondary-contrast:#FFFFFF !important" ); }); + + it("creates the theme style tag when updating the editor without one", async () => { + document.getElementById(THEME_STYLE_TAG_ID)?.remove(); + + await updateThemeInEditor( + { + "--colors-palette-primary": "#CF0A2C", + }, + themeConfig, + true + ); + + const styleTag = document.getElementById(THEME_STYLE_TAG_ID); + expect(styleTag).not.toBeNull(); + expect(styleTag?.textContent).toContain(".components{"); + expect(styleTag?.textContent).toContain( + "--colors-palette-primary:#CF0A2C !important" + ); + }); }); const themeConfig: ThemeConfig = { diff --git a/packages/visual-editor/src/utils/applyTheme.ts b/packages/visual-editor/src/utils/applyTheme.ts index 3ba908811e..070444c8fd 100644 --- a/packages/visual-editor/src/utils/applyTheme.ts +++ b/packages/visual-editor/src/utils/applyTheme.ts @@ -145,14 +145,12 @@ const internalApplyTheme = ( devLogger.logData("THEME_VALUES_TO_APPLY", themeValuesToApply); - return ( - `.components{` + - Object.entries(themeValuesToApply) - .filter(([key]) => key.startsWith("--")) - .map(([key, value]) => `${key}:${value} !important`) - .join(";") + - "}" - ); + const cssVariables = Object.entries(themeValuesToApply) + .filter(([key]) => key.startsWith("--")) + .map(([key, value]) => `${key}:${value} !important`) + .join(";"); + + return `.components{${cssVariables}}:root{${cssVariables}}`; }; /** @@ -196,6 +194,19 @@ const updateFontLinksInDocument = ( } }; +const ensureThemeStyleTag = (document: Document): HTMLStyleElement => { + const existingStyleTag = document.getElementById(THEME_STYLE_TAG_ID); + if (existingStyleTag instanceof HTMLStyleElement) { + return existingStyleTag; + } + + const styleTag = document.createElement("style"); + styleTag.id = THEME_STYLE_TAG_ID; + styleTag.type = "text/css"; + document.head.appendChild(styleTag); + return styleTag; +}; + // Used to avoid creating multiple observers in updateThemeInEditor let pendingObserver: MutationObserver | null = null; @@ -215,10 +226,7 @@ export const updateThemeInEditor = async ( ); const newThemeTag = internalApplyTheme(newTheme, themeConfig); - const editorStyleTag = window.document.getElementById(THEME_STYLE_TAG_ID); - if (editorStyleTag) { - editorStyleTag.innerText = newThemeTag; - } + ensureThemeStyleTag(window.document).textContent = newThemeTag; // In the theme editor, all fonts are already loaded // In the layout editor, we need to load the in-use fonts after the Puck iframe has loaded @@ -233,15 +241,14 @@ export const updateThemeInEditor = async ( const iframe = document.getElementById( PUCK_PREVIEW_IFRAME_ID ) as HTMLIFrameElement; - const pagePreviewStyleTag = - iframe?.contentDocument?.getElementById(THEME_STYLE_TAG_ID); - if (pagePreviewStyleTag) { + const iframeDocument = iframe?.contentDocument; + if (iframeDocument) { observer.disconnect(); pendingObserver = null; - pagePreviewStyleTag.innerText = newThemeTag; + ensureThemeStyleTag(iframeDocument).textContent = newThemeTag; updateFontLinksInDocument( - iframe.contentDocument!, + iframeDocument, googleFontsToLoad, inUseCustomFonts ); diff --git a/packages/visual-editor/src/vite-plugin/local-editor/generatedFiles.ts b/packages/visual-editor/src/vite-plugin/local-editor/generatedFiles.ts index 67475c553c..51cd6c2052 100644 --- a/packages/visual-editor/src/vite-plugin/local-editor/generatedFiles.ts +++ b/packages/visual-editor/src/vite-plugin/local-editor/generatedFiles.ts @@ -1,9 +1,13 @@ import path from "node:path"; import fs from "fs-extra"; -import { buildLocalEditorScaffoldSource } from "./scaffold.ts"; +import { + buildLocalEditorScaffoldSource, + buildLocalEditorThemeScaffoldSource, +} from "./scaffold.ts"; export const DEFAULT_LOCAL_EDITOR_ROUTE = "/local-editor"; export const DEFAULT_LOCAL_EDITOR_STREAM_CONFIG_PATH = "stream.config.ts"; +export const DEFAULT_LOCAL_EDITOR_THEME_PATH = "localEditorTheme.ts"; export const LOCAL_EDITOR_API_BASE_PATH = "/__yext_visual_editor/local-editor"; export const LOCAL_EDITOR_DATA_TEMPLATE_PREFIX = "local-editor-data-"; export const LEGACY_LOCAL_EDITOR_DATA_TEMPLATE_PATH = @@ -104,3 +108,21 @@ export const ensureLocalEditorStreamConfig = async ( buildLocalEditorScaffoldSource(rootDir) ); }; + +/** + * Ensures the root `localEditorTheme.ts` file exists by scaffolding it on demand. + */ +export const ensureLocalEditorTheme = async ( + rootDir: string +): Promise => { + const absoluteThemePath = toAbsoluteLocalEditorPath( + rootDir, + DEFAULT_LOCAL_EDITOR_THEME_PATH + ); + if (fs.existsSync(absoluteThemePath)) { + return; + } + + fs.ensureDirSync(path.dirname(absoluteThemePath)); + fs.writeFileSync(absoluteThemePath, buildLocalEditorThemeScaffoldSource()); +}; diff --git a/packages/visual-editor/src/vite-plugin/local-editor/scaffold.ts b/packages/visual-editor/src/vite-plugin/local-editor/scaffold.ts index cecc9c2ecb..ece1f61595 100644 --- a/packages/visual-editor/src/vite-plugin/local-editor/scaffold.ts +++ b/packages/visual-editor/src/vite-plugin/local-editor/scaffold.ts @@ -77,6 +77,63 @@ export const buildLocalEditorScaffoldSource = (rootDir: string): string => { ].join("\n")}`; }; +export const buildLocalEditorThemeScaffoldSource = (): string => { + return `${[ + "const localEditorTheme = {", + ' "--colors-palette-primary": "#CF0A2C",', + ' "--colors-palette-secondary": "#737B82",', + ' "--colors-palette-tertiary": "#FF7E7E",', + ' "--colors-palette-quaternary": "#000000",', + ` "--fontFamily-h1-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-h1-fontSize": "48px",', + ' "--fontWeight-h1-fontWeight": "700",', + ' "--textTransform-h1-textTransform": "none",', + ` "--fontFamily-h2-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-h2-fontSize": "40px",', + ' "--fontWeight-h2-fontWeight": "700",', + ' "--textTransform-h2-textTransform": "none",', + ` "--fontFamily-h3-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-h3-fontSize": "32px",', + ' "--fontWeight-h3-fontWeight": "700",', + ' "--textTransform-h3-textTransform": "none",', + ` "--fontFamily-h4-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-h4-fontSize": "24px",', + ' "--fontWeight-h4-fontWeight": "700",', + ' "--textTransform-h4-textTransform": "none",', + ` "--fontFamily-h5-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-h5-fontSize": "20px",', + ' "--fontWeight-h5-fontWeight": "700",', + ' "--textTransform-h5-textTransform": "none",', + ` "--fontFamily-h6-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-h6-fontSize": "18px",', + ' "--fontWeight-h6-fontWeight": "700",', + ' "--textTransform-h6-textTransform": "none",', + ` "--fontFamily-body-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-body-fontSize": "16px",', + ' "--fontWeight-body-fontWeight": "400",', + ' "--textTransform-body-textTransform": "none",', + ' "--maxWidth-pageSection-contentWidth": "1024px",', + ' "--padding-pageSection-verticalPadding": "32px",', + ` "--fontFamily-button-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-button-fontSize": "16px",', + ' "--fontWeight-button-fontWeight": "400",', + ' "--borderRadius-button-borderRadius": "4px",', + ' "--textTransform-button-textTransform": "none",', + ' "--letterSpacing-button-letterSpacing": "0em",', + ` "--fontFamily-link-fontFamily": "'Open Sans', 'Open Sans Fallback', sans-serif",`, + ' "--fontSize-link-fontSize": "16px",', + ' "--fontWeight-link-fontWeight": "400",', + ' "--textTransform-link-textTransform": "none",', + ' "--letterSpacing-link-letterSpacing": "0em",', + ' "--display-link-caret": "block",', + ' "--borderRadius-image-borderRadius": "0px",', + "};", + "", + "export default localEditorTheme;", + "", + ].join("\n")}`; +}; + const readScaffoldTemplateIds = (rootDir: string): string[] => { const manifestPath = path.join(rootDir, ".template-manifest.json"); if (!fs.existsSync(manifestPath)) { diff --git a/packages/visual-editor/src/vite-plugin/plugin.ts b/packages/visual-editor/src/vite-plugin/plugin.ts index 9125abf334..242eb142e8 100644 --- a/packages/visual-editor/src/vite-plugin/plugin.ts +++ b/packages/visual-editor/src/vite-plugin/plugin.ts @@ -17,7 +17,10 @@ import { getEffectiveEditorTemplateNames } from "./routing/editorTemplateNames.t import { syncGeneratedEditorFiles } from "./generated/editorFiles.ts"; import { hasExplicitLocalMainTemplate } from "./generated/templateFiles.ts"; import { createLocalEditorArtifactsManager } from "./local-editor/artifacts.ts"; -import { ensureLocalEditorStreamConfig } from "./local-editor/generatedFiles.ts"; +import { + ensureLocalEditorStreamConfig, + ensureLocalEditorTheme, +} from "./local-editor/generatedFiles.ts"; import { handleLocalEditorRequest, sendJsonResponse, @@ -187,6 +190,7 @@ export const yextVisualEditorPlugin = ( if (!isBuildMode && localEditorOptions?.enabled) { await ensureLocalEditorStreamConfig(process.cwd()); + await ensureLocalEditorTheme(process.cwd()); await localEditorArtifacts.syncLocalEditorDataTemplates(); } diff --git a/packages/visual-editor/src/vite-plugin/registry/registryTemplateGenerator.test.ts b/packages/visual-editor/src/vite-plugin/registry/registryTemplateGenerator.test.ts index acb816b58d..4e4e360efa 100644 --- a/packages/visual-editor/src/vite-plugin/registry/registryTemplateGenerator.test.ts +++ b/packages/visual-editor/src/vite-plugin/registry/registryTemplateGenerator.test.ts @@ -183,6 +183,12 @@ describe.sequential("generateRegistryTemplateFiles", () => { expect(localEditorTemplate).toContain( 'const DEFAULT_LOCAL_EDITOR_ROUTE = "/local-editor";' ); + expect(localEditorTemplate).toContain( + 'import localEditorTheme from "../../localEditorTheme";' + ); + expect(localEditorTemplate).toContain( + "simulatedThemeData={localEditorTheme}" + ); expect(localEditorTemplate).not.toContain("../local-editor/generatedFiles"); }); diff --git a/packages/visual-editor/src/vite-plugin/templates/local-editor.tsx b/packages/visual-editor/src/vite-plugin/templates/local-editor.tsx index 1a8e8147b5..073458b4f4 100644 --- a/packages/visual-editor/src/vite-plugin/templates/local-editor.tsx +++ b/packages/visual-editor/src/vite-plugin/templates/local-editor.tsx @@ -1,6 +1,7 @@ /** THIS FILE IS AUTOGENERATED AND SHOULD NOT BE EDITED */ import "@yext/visual-editor/editor.css"; import "../index.css"; +import localEditorTheme from "../../localEditorTheme"; import { GetPath, Template, @@ -60,6 +61,7 @@ const LocalEditor: Template = () => { componentRegistry={componentRegistry} tailwindConfig={tailwindConfig} themeConfig={defaultThemeConfig} + simulatedThemeData={localEditorTheme} /> ); }; From 0a914583f833963a6e9b731341e4971475e4c3fb Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Mon, 18 May 2026 20:49:37 -0400 Subject: [PATCH 02/10] add full theme editor --- packages/visual-editor/src/editor/types.ts | 1 + .../hooks/useMessageReceivers.test.ts | 42 ++++++++------ .../internal/types/templateMetadata.test.ts | 14 +++++ .../src/internal/types/templateMetadata.ts | 2 + .../src/local-editor/LocalEditorControls.tsx | 30 +++++++++- .../src/local-editor/LocalEditorShell.tsx | 25 ++++++-- .../src/local-editor/selection.ts | 12 +++- .../visual-editor/src/local-editor/types.ts | 5 +- .../src/utils/applyTheme.test.ts | 14 +++++ .../local-editor/generatedFiles.ts | 24 +------- .../src/vite-plugin/local-editor/scaffold.ts | 57 ------------------- .../visual-editor/src/vite-plugin/plugin.ts | 6 +- .../registryTemplateGenerator.test.ts | 8 +-- .../vite-plugin/templates/local-editor.tsx | 2 - 14 files changed, 118 insertions(+), 124 deletions(-) diff --git a/packages/visual-editor/src/editor/types.ts b/packages/visual-editor/src/editor/types.ts index 70db9ef643..0451505ea2 100644 --- a/packages/visual-editor/src/editor/types.ts +++ b/packages/visual-editor/src/editor/types.ts @@ -6,6 +6,7 @@ export type LocalDevOptions = { locale?: string; locales?: string[]; layoutScopeKey?: string; + themeScopeKey?: string; initialLayoutData?: Record; initialThemeData?: ThemeData; showOverrideButtons?: boolean; diff --git a/packages/visual-editor/src/internal/hooks/useMessageReceivers.test.ts b/packages/visual-editor/src/internal/hooks/useMessageReceivers.test.ts index c42712ac96..d95d12223b 100644 --- a/packages/visual-editor/src/internal/hooks/useMessageReceivers.test.ts +++ b/packages/visual-editor/src/internal/hooks/useMessageReceivers.test.ts @@ -39,6 +39,27 @@ const localDevLocatorLayout = JSON.stringify({ zones: {}, }); +const localDevLocatorDocument = { + meta: { + entityType: { + id: "locator", + }, + }, + __: { + name: "locator", + }, +}; + +const localDevLocatorOptions = { + templateId: "locator", + locale: "en", + initialThemeData: { + "--colors-palette-primary": "#CF0A2C", + }, +}; + +const localDevLocatorRegistry = { locator: locatorConfig }; + afterEach(() => { vi.restoreAllMocks(); }); @@ -93,25 +114,10 @@ describe("useCommonMessageReceivers", () => { const { result } = renderHook(() => { return useCommonMessageReceivers( - { locator: locatorConfig }, + localDevLocatorRegistry, true, - { - meta: { - entityType: { - id: "locator", - }, - }, - __: { - name: "locator", - }, - }, - { - templateId: "locator", - locale: "en", - initialThemeData: { - "--colors-palette-primary": "#CF0A2C", - }, - } + localDevLocatorDocument, + localDevLocatorOptions ); }); diff --git a/packages/visual-editor/src/internal/types/templateMetadata.test.ts b/packages/visual-editor/src/internal/types/templateMetadata.test.ts index ce08755652..355015efde 100644 --- a/packages/visual-editor/src/internal/types/templateMetadata.test.ts +++ b/packages/visual-editor/src/internal/types/templateMetadata.test.ts @@ -35,4 +35,18 @@ describe("generateTemplateMetadata", () => { "type.image" ); }); + + it("uses themeScopeKey for a shared local-dev theme entity", () => { + const directoryMetadata = generateTemplateMetadata(undefined, { + templateId: "directory", + themeScopeKey: "local-editor", + }); + const locatorMetadata = generateTemplateMetadata(undefined, { + templateId: "locator", + themeScopeKey: "local-editor", + }); + + expect(directoryMetadata.themeEntityId).toBeDefined(); + expect(directoryMetadata.themeEntityId).toBe(locatorMetadata.themeEntityId); + }); }); diff --git a/packages/visual-editor/src/internal/types/templateMetadata.ts b/packages/visual-editor/src/internal/types/templateMetadata.ts index f59f67d8d4..2d10ca5468 100644 --- a/packages/visual-editor/src/internal/types/templateMetadata.ts +++ b/packages/visual-editor/src/internal/types/templateMetadata.ts @@ -133,6 +133,7 @@ export function generateTemplateMetadata( entityId: localDevOptions?.entityId ?? cleanString, locale, }); + const themeScopeKey = localDevOptions?.themeScopeKey; const locales = localDevOptions?.locales?.length ? localDevOptions.locales : ["en", "es", "fr"]; @@ -141,6 +142,7 @@ export function generateTemplateMetadata( siteId: 1337, templateId, entityId, + themeEntityId: themeScopeKey ? hashCode(themeScopeKey) : undefined, layoutId: hashCode(layoutScopeKey), assignment: "ALL", isDevMode: true, diff --git a/packages/visual-editor/src/local-editor/LocalEditorControls.tsx b/packages/visual-editor/src/local-editor/LocalEditorControls.tsx index 2cda956b13..a9dad97a64 100644 --- a/packages/visual-editor/src/local-editor/LocalEditorControls.tsx +++ b/packages/visual-editor/src/local-editor/LocalEditorControls.tsx @@ -1,5 +1,5 @@ import React from "react"; -import type { LocalEditorEntityOption } from "./types.ts"; +import type { LocalEditorEntityOption, LocalEditorMode } from "./types.ts"; type LocalEditorControlsProps = { activeEntities: LocalEditorEntityOption[]; @@ -7,9 +7,11 @@ type LocalEditorControlsProps = { controlsDisabled: boolean; selectedEntityId?: string; selectedLocale: string; + selectedMode: LocalEditorMode; selectedTemplateId: string; onEntityChange: (entityId: string) => void; onLocaleChange: (locale: string) => void; + onModeChange: (mode: LocalEditorMode) => void; onTemplateChange: (templateId: string) => void; }; @@ -19,9 +21,11 @@ export const LocalEditorControls = ({ controlsDisabled, selectedEntityId, selectedLocale, + selectedMode, selectedTemplateId, onEntityChange, onLocaleChange, + onModeChange, onTemplateChange, }: LocalEditorControlsProps) => { const selectedEntity = activeEntities.find((entity) => { @@ -53,6 +57,30 @@ export const LocalEditorControls = ({ + + + +