@sudajs/cli 0.18.9 → 0.18.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { ThemeModule, ThemeStarterPage } from '@sudajs/theme-engine';
3
- import { ThemeRender, extractLayoutChrome, resolveAssetPath } from '@sudajs/theme-engine/server';
3
+ import { ThemeRender, extractLayoutChrome, resolveAssetPath, ThemePreviewLocaleMessages } from '@sudajs/theme-engine/server';
4
4
  import { Command } from 'commander';
5
5
  import { createElement } from 'react';
6
6
  import { renderToString } from 'react-dom/server';
@@ -1816,6 +1816,9 @@ declare const __testUtils: {
1816
1816
  performUpdateThemeSettings: typeof performUpdateThemeSettings;
1817
1817
  getRemoteThemeSettings: typeof getRemoteThemeSettings;
1818
1818
  renderDevStarterPageHtml: typeof renderDevStarterPageHtml;
1819
+ renderDevEditorPageHtml: typeof renderDevEditorPageHtml;
1820
+ writeVitePreviewEntries: typeof writeVitePreviewEntries;
1821
+ resolveDevEditorSlug: typeof resolveDevEditorSlug;
1819
1822
  resolveDevStarterSlug: typeof resolveDevStarterSlug;
1820
1823
  resolveScreenshotOptions: typeof resolveScreenshotOptions;
1821
1824
  slugifyThemeName: typeof slugifyThemeName;
@@ -1831,7 +1834,9 @@ declare const __testUtils: {
1831
1834
  updateRemotePageDraft: typeof updateRemotePageDraft;
1832
1835
  };
1833
1836
  declare function watchTheme(root: string, port: number): Promise<void>;
1837
+ declare function writeVitePreviewEntries(root: string): Promise<void>;
1834
1838
  declare function withSerializedVitePreviewLocaleLoad<Result>(server: object, load: () => Promise<Result>): Promise<Result>;
1839
+ declare function resolveDevEditorSlug(pathname: string): string | null | undefined;
1835
1840
  declare function resolveDevStarterSlug(pathname: string): string | null | undefined;
1836
1841
  declare function resolveThemePreviewLocale(params: {
1837
1842
  previewLocales: string[] | undefined;
@@ -1856,7 +1861,8 @@ interface ThemeRenderer {
1856
1861
  renderToString: typeof renderToString;
1857
1862
  }
1858
1863
  declare function loadThemeScopedRenderer(themeRoot: string): Promise<ThemeRenderer>;
1859
- declare function renderDevStarterPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, preserveLang?: boolean): string;
1864
+ declare function renderDevStarterPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, preserveLang?: boolean, previewLocaleMessages?: ThemePreviewLocaleMessages, searchParams?: URLSearchParams): string;
1865
+ declare function renderDevEditorPageHtml(theme: ValidatedTheme, page: ThemeStarterPage, renderer: ThemeRenderer, locale?: string, searchParams?: URLSearchParams, previewLocaleMessages?: ThemePreviewLocaleMessages): string;
1860
1866
  interface ScreenshotOptions extends ThemeRootOptions {
1861
1867
  device?: string[] | string;
1862
1868
  output?: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from 'crypto';
3
3
  import { existsSync } from 'fs';
4
- import fs, { readFile, writeFile, stat, readdir, mkdir, copyFile, rm } from 'fs/promises';
4
+ import fs, { readFile, mkdir, writeFile, stat, readdir, copyFile, rm } from 'fs/promises';
5
5
  import { createRequire } from 'module';
6
6
  import path2 from 'path';
7
7
  import { createInterface } from 'readline/promises';
@@ -12,6 +12,9 @@ import { themeDesignSystemSchema, createThemeDesignCssVariables, createThemeAgen
12
12
  import { themeManifestSchema, runWithThemePreviewLocale } from '@sudajs/theme-engine/server';
13
13
  import { Command } from 'commander';
14
14
  import { build } from 'esbuild';
15
+ import { CircleCheck, Pencil } from 'lucide-react';
16
+ import { createElement } from 'react';
17
+ import { renderToStaticMarkup } from 'react-dom/server';
15
18
  import { z } from 'zod';
16
19
  import os from 'os';
17
20
  import pc from 'picocolors';
@@ -1520,26 +1523,7 @@ async function buildClientRuntime(root, minify) {
1520
1523
  await mkdir(path2.dirname(generatedEntry), { recursive: true });
1521
1524
  await writeFile(
1522
1525
  generatedEntry,
1523
- [
1524
- '"use client";',
1525
- 'import type { ThemeClientHooks, ThemeClientRuntime, ThemeRenderModule } from "@sudajs/theme-engine/runtime";',
1526
- 'import * as themeConfig from "../src/config.js";',
1527
- clientHooksImport,
1528
- "const theme: ThemeRenderModule = {",
1529
- " pageConfig: themeConfig.pageConfig,",
1530
- " layoutConfig: themeConfig.layoutConfig,",
1531
- "};",
1532
- 'if ("rendering" in themeConfig && themeConfig.rendering !== undefined) {',
1533
- " theme.rendering = themeConfig.rendering;",
1534
- "}",
1535
- "const hooks: ThemeClientHooks = clientHooks;",
1536
- "const runtime: ThemeClientRuntime = { theme };",
1537
- "if (hooks.initialize !== undefined) {",
1538
- " runtime.initialize = hooks.initialize;",
1539
- "}",
1540
- "export default runtime;",
1541
- ""
1542
- ].join("\n"),
1526
+ createClientRuntimeSource(clientHooksImport, true),
1543
1527
  "utf8"
1544
1528
  );
1545
1529
  await mkdir(path2.join(root, "dist"), { recursive: true });
@@ -1754,6 +1738,9 @@ var __testUtils = {
1754
1738
  performUpdateThemeSettings,
1755
1739
  getRemoteThemeSettings,
1756
1740
  renderDevStarterPageHtml,
1741
+ renderDevEditorPageHtml,
1742
+ writeVitePreviewEntries,
1743
+ resolveDevEditorSlug,
1757
1744
  resolveDevStarterSlug,
1758
1745
  resolveScreenshotOptions,
1759
1746
  slugifyThemeName,
@@ -1825,13 +1812,17 @@ async function watchTheme(root, port) {
1825
1812
  async function startViteDevPreviewServer(root, port) {
1826
1813
  const viteConfig = await findViteConfig(root);
1827
1814
  const vite = await loadThemeVite(root);
1815
+ await writeVitePreviewEntries(root);
1828
1816
  const host = "127.0.0.1";
1829
1817
  const server = await vite.createServer({
1830
1818
  root,
1831
1819
  configFile: viteConfig,
1832
1820
  configLoader: "runner",
1833
- cacheDir: path2.join(root, ".suda-build", "vite-cache"),
1821
+ cacheDir: path2.join(root, ".suda-build", "node_modules", ".vite"),
1834
1822
  appType: "custom",
1823
+ optimizeDeps: {
1824
+ entries: [".suda-build/preview-client.ts", ".suda-build/editor-client.ts"]
1825
+ },
1835
1826
  server: { host, port },
1836
1827
  plugins: [createSudaPreviewVitePlugin(root)]
1837
1828
  });
@@ -1843,6 +1834,77 @@ async function startViteDevPreviewServer(root, port) {
1843
1834
  close: () => server.close()
1844
1835
  };
1845
1836
  }
1837
+ async function writeVitePreviewEntries(root) {
1838
+ const clientHooksPath = path2.join(root, "src", "client.ts");
1839
+ const clientHooksImport = existsSync(clientHooksPath) ? 'import { clientHooks } from "../src/client.js";' : "const clientHooks = {};";
1840
+ const buildDir = path2.join(root, ".suda-build");
1841
+ await mkdir(buildDir, { recursive: true });
1842
+ await writeFile(
1843
+ path2.join(buildDir, "runtime.dev.ts"),
1844
+ createClientRuntimeSource(clientHooksImport, false),
1845
+ "utf8"
1846
+ );
1847
+ await writeFile(
1848
+ path2.join(buildDir, "preview-client.ts"),
1849
+ [
1850
+ 'import { hydrateThemePage, loadThemeRuntime } from "@sudajs/theme-engine/client";',
1851
+ 'import { installThemePreviewLocale, resolveAssetPath } from "@sudajs/theme-engine/site-runtime";',
1852
+ 'const payloadElement=document.querySelector("#suda-theme-hydration-data");',
1853
+ 'if(!payloadElement){throw new Error("Missing Suda theme hydration payload.")}',
1854
+ "const payload=JSON.parse(payloadElement.textContent ?? '{}');",
1855
+ "if(payload.previewLocaleMessages){installThemePreviewLocale(payload.previewLocaleMessages)}",
1856
+ 'const runtime=(await loadThemeRuntime("/.suda-build/runtime.dev.ts")).default;',
1857
+ 'const metadata={contactForm:payload.contactForm,resolveAssetUrl:(value: string | undefined)=>resolveAssetPath(value,{legacyResolve:(key)=>key.startsWith("themes/")?`/api/themes/${key.slice("themes/".length)}`:undefined})};',
1858
+ 'await hydrateThemePage({container:document.querySelector("[data-theme-hydration-root]")!,runtime,pageData:payload.pageData,layoutData:payload.layoutData,metadata,onRecoverableError:(error)=>console.error("[theme-dev] theme hydration recovered from an error",error)});',
1859
+ ""
1860
+ ].join("\n"),
1861
+ "utf8"
1862
+ );
1863
+ await writeFile(
1864
+ path2.join(buildDir, "editor-client.ts"),
1865
+ [
1866
+ 'import "@puckeditor/core/puck.css";',
1867
+ 'import { composeUnifiedThemeEditorData, createUnifiedThemeEditorConfig, installThemePreviewLocale, resolveAssetPath, ThemeEditor } from "@sudajs/theme-engine";',
1868
+ 'import { createElement } from "react";',
1869
+ 'import { createRoot } from "react-dom/client";',
1870
+ 'import runtime from "./runtime.dev";',
1871
+ 'const payloadElement=document.querySelector("#suda-theme-editor-data");',
1872
+ 'if(!payloadElement){throw new Error("Missing Suda theme editor payload.")}',
1873
+ "const payload=JSON.parse(payloadElement.textContent ?? '{}');",
1874
+ "if(payload.previewLocaleMessages){installThemePreviewLocale(payload.previewLocaleMessages)}",
1875
+ 'const resolveAssetUrl=(value: string | undefined)=>resolveAssetPath(value,{legacyResolve:(key)=>key.startsWith("themes/")?`/api/themes/${key.slice("themes/".length)}`:undefined});',
1876
+ 'const config=createUnifiedThemeEditorConfig(runtime.theme,undefined,{labels:{pageContent:"Page content",themeSettings:"Theme settings"}});',
1877
+ "const data=composeUnifiedThemeEditorData(payload.layoutData,payload.pageData,{layoutConfig:runtime.theme.layoutConfig,pageConfig:runtime.theme.pageConfig});",
1878
+ 'const editorContext={assets:[],resolveAssetUrl,currentPageId:payload.currentPageId,backHref:payload.backHref,editorPages:payload.editorPages,editorLocale:payload.locale,chromeMessages:{backToPages:"Back to preview",pageMenuLabel:"Switch starter page",themeSettings:"Theme settings",outline:"Outline",blocks:"Blocks",switchViewport:"Switch viewport"}};',
1879
+ "const metadata={contactForm:payload.contactForm,isEditor:true,resolveAssetUrl};",
1880
+ 'createRoot(document.querySelector("#suda-theme-dev-editor-root")!).render(createElement(ThemeEditor,{config,data,editorContext,metadata,iframe:false,height:"100%",publishActionVisible:false}));',
1881
+ ""
1882
+ ].join("\n"),
1883
+ "utf8"
1884
+ );
1885
+ }
1886
+ function createClientRuntimeSource(clientHooksImport, useClientDirective) {
1887
+ return [
1888
+ ...useClientDirective ? ['"use client";'] : [],
1889
+ 'import type { ThemeClientHooks, ThemeClientRuntime, ThemeRenderModule } from "@sudajs/theme-engine/runtime";',
1890
+ 'import * as themeConfig from "../src/config.js";',
1891
+ clientHooksImport,
1892
+ "const theme: ThemeRenderModule = {",
1893
+ " pageConfig: themeConfig.pageConfig,",
1894
+ " layoutConfig: themeConfig.layoutConfig,",
1895
+ "};",
1896
+ 'if ("rendering" in themeConfig && themeConfig.rendering !== undefined) {',
1897
+ " theme.rendering = themeConfig.rendering;",
1898
+ "}",
1899
+ "const hooks: ThemeClientHooks = clientHooks;",
1900
+ "const runtime: ThemeClientRuntime = { theme };",
1901
+ "if (hooks.initialize !== undefined) {",
1902
+ " runtime.initialize = hooks.initialize;",
1903
+ "}",
1904
+ "export default runtime;",
1905
+ ""
1906
+ ].join("\n");
1907
+ }
1846
1908
  function createSudaPreviewVitePlugin(root) {
1847
1909
  return {
1848
1910
  name: "suda-theme-preview",
@@ -1856,65 +1918,51 @@ function createSudaPreviewVitePlugin(root) {
1856
1918
  next();
1857
1919
  return;
1858
1920
  }
1859
- const slug = resolveDevStarterSlug(url.pathname);
1860
- if (slug === void 0) {
1921
+ const editorSlug = resolveDevEditorSlug(url.pathname);
1922
+ const previewSlug = resolveDevStarterSlug(url.pathname);
1923
+ if (editorSlug === void 0 && previewSlug === void 0) {
1861
1924
  next();
1862
1925
  return;
1863
1926
  }
1927
+ const isEditor = editorSlug !== void 0;
1928
+ const slug = isEditor ? editorSlug : previewSlug;
1864
1929
  try {
1865
1930
  const acceptLanguageHeader = request.headers["accept-language"];
1866
1931
  const acceptLanguage = Array.isArray(acceptLanguageHeader) ? acceptLanguageHeader.join(",") : acceptLanguageHeader;
1867
1932
  const explicitLang = url.searchParams.get("lang");
1868
- const { theme, locale } = await loadLocalizedViteDevTheme(
1933
+ const localized = await loadLocalizedViteDevTheme(
1869
1934
  root,
1870
1935
  server,
1871
1936
  explicitLang,
1872
1937
  acceptLanguage
1873
1938
  );
1874
1939
  const renderer = await loadThemeScopedRenderer(root);
1875
- if (slug === null) {
1876
- const homeSlug = pickStarterSlug(theme);
1877
- if (!homeSlug) {
1878
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1879
- response.end("No starter pages declared by this theme.");
1880
- return;
1881
- }
1882
- const starter2 = findStarterPage(theme, homeSlug);
1883
- if (!starter2) {
1884
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1885
- response.end(`Unknown starter page: ${homeSlug}`);
1886
- return;
1887
- }
1888
- const html2 = await server.transformIndexHtml(
1889
- url.pathname,
1890
- renderDevStarterPageHtml(
1891
- theme,
1892
- starter2,
1893
- renderer,
1894
- locale,
1895
- canonicalLocale(explicitLang) !== null
1896
- )
1897
- );
1898
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1899
- response.end(html2);
1900
- return;
1901
- }
1902
- const starter = findStarterPage(theme, slug);
1940
+ const starterSlug = slug ?? pickStarterSlug(localized.theme);
1941
+ const starter = starterSlug ? findStarterPage(localized.theme, starterSlug) : null;
1903
1942
  if (!starter) {
1904
1943
  response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1905
- response.end(`Unknown starter page: ${slug}`);
1944
+ response.end(
1945
+ starterSlug ? `Unknown starter page: ${starterSlug}` : "No starter pages declared by this theme."
1946
+ );
1906
1947
  return;
1907
1948
  }
1908
- const html = await server.transformIndexHtml(
1909
- url.pathname,
1910
- renderDevStarterPageHtml(
1911
- theme,
1912
- starter,
1913
- renderer,
1914
- locale,
1915
- canonicalLocale(explicitLang) !== null
1916
- )
1949
+ const htmlSource = isEditor ? renderDevEditorPageHtml(
1950
+ localized.theme,
1951
+ starter,
1952
+ renderer,
1953
+ localized.locale,
1954
+ url.searchParams,
1955
+ localized.previewLocaleMessages
1956
+ ) : renderDevStarterPageHtml(
1957
+ localized.theme,
1958
+ starter,
1959
+ renderer,
1960
+ localized.locale,
1961
+ canonicalLocale(explicitLang) !== null,
1962
+ localized.previewLocaleMessages,
1963
+ url.searchParams
1917
1964
  );
1965
+ const html = await server.transformIndexHtml(url.pathname, htmlSource);
1918
1966
  response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1919
1967
  response.end(html);
1920
1968
  } catch (error2) {
@@ -1940,13 +1988,13 @@ async function loadLocalizedViteDevTheme(root, server, explicitLang, acceptLangu
1940
1988
  if (!baseTheme.module.manifest.previewLocales) {
1941
1989
  return { theme: baseTheme, locale };
1942
1990
  }
1943
- const messages = await readDevPreviewLocaleMessages(root, locale);
1991
+ const previewLocaleMessages = await readDevPreviewLocaleMessages(root, locale);
1944
1992
  server.moduleGraph.invalidateAll();
1945
1993
  const theme = await runWithThemePreviewLocale(
1946
- messages,
1994
+ previewLocaleMessages,
1947
1995
  () => loadViteDevTheme(root, server)
1948
1996
  );
1949
- return { theme, locale };
1997
+ return { theme, locale, previewLocaleMessages };
1950
1998
  });
1951
1999
  }
1952
2000
  var vitePreviewLocaleLoadTails = /* @__PURE__ */ new WeakMap();
@@ -1994,10 +2042,27 @@ function pickStarterSlug(theme) {
1994
2042
  function findStarterPage(theme, slug) {
1995
2043
  return theme.module.starterPages.find((page) => page.slug === slug) ?? null;
1996
2044
  }
2045
+ var DEV_EDITOR_PATH = "/__suda/editor";
2046
+ function resolveDevEditorSlug(pathname) {
2047
+ if (pathname === DEV_EDITOR_PATH || pathname === `${DEV_EDITOR_PATH}/`) {
2048
+ return null;
2049
+ }
2050
+ if (!pathname.startsWith(`${DEV_EDITOR_PATH}/`)) {
2051
+ return void 0;
2052
+ }
2053
+ const normalized = pathname.slice(DEV_EDITOR_PATH.length + 1).replace(/\/+$/, "");
2054
+ if (!normalized || normalized.includes("/")) {
2055
+ return void 0;
2056
+ }
2057
+ return decodeURIComponent(normalized);
2058
+ }
1997
2059
  function resolveDevStarterSlug(pathname) {
1998
2060
  if (pathname === "/") {
1999
2061
  return null;
2000
2062
  }
2063
+ if (pathname.startsWith("/@")) {
2064
+ return void 0;
2065
+ }
2001
2066
  const normalized = pathname.replace(/^\/+/, "").replace(/\/+$/, "");
2002
2067
  if (!normalized || normalized.includes("/")) {
2003
2068
  return void 0;
@@ -2104,8 +2169,9 @@ async function loadThemeScopedRenderer(themeRoot) {
2104
2169
  renderToString: reactDomServerModule.renderToString
2105
2170
  };
2106
2171
  }
2107
- function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserveLang = false) {
2108
- const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
2172
+ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserveLang = false, previewLocaleMessages, searchParams = new URLSearchParams()) {
2173
+ const layoutData = createDevPreviewLayout(theme, searchParams);
2174
+ const chrome = renderer.extractLayoutChrome(layoutData);
2109
2175
  const metadata = {
2110
2176
  resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath),
2111
2177
  contactForm: PREVIEW_CONTACT_FORM
@@ -2114,14 +2180,27 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2114
2180
  renderer.createElement(renderer.ThemeRender, {
2115
2181
  theme: theme.module,
2116
2182
  pageData: page.data,
2117
- layoutData: theme.module.defaultLayout,
2183
+ layoutData,
2118
2184
  metadata
2119
2185
  })
2120
2186
  );
2121
2187
  const cssVarStyle = Object.entries(chrome.cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
2122
2188
  const customHead = chrome.customHeadCode ?? "";
2123
2189
  const customBody = chrome.customBodyCode ?? "";
2124
- const designPalette = renderDevDesignPalette(theme.module.manifest.designSystem);
2190
+ const designPalette = renderDevDesignPalette(
2191
+ theme.module.manifest.designSystem,
2192
+ createDevEditorHref(page.slug, searchParams)
2193
+ );
2194
+ const hydrationPayload = serializeInlineScriptValue({
2195
+ pageData: page.data,
2196
+ layoutData,
2197
+ contactForm: PREVIEW_CONTACT_FORM,
2198
+ previewLocaleMessages
2199
+ });
2200
+ const hydrationScript = [
2201
+ `<script id="suda-theme-hydration-data" type="application/json">${hydrationPayload}</script>`,
2202
+ '<script type="module">void import("/.suda-build/preview-client.ts").catch((error)=>console.error("[theme-dev] theme client failed to load",error))</script>'
2203
+ ].join("");
2125
2204
  return [
2126
2205
  "<!doctype html>",
2127
2206
  `<html lang="${escapeHtml(locale)}">`,
@@ -2134,9 +2213,10 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2134
2213
  customHead,
2135
2214
  "</head>",
2136
2215
  `<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
2137
- body,
2216
+ `<div data-theme-hydration-root="" style="display:contents">${body}</div>`,
2138
2217
  designPalette.html,
2139
2218
  customBody,
2219
+ hydrationScript,
2140
2220
  ...preserveLang ? [
2141
2221
  '<script>for(const a of document.querySelectorAll("a[href]")){try{const u=new URL(a.href,location.href);if(u.origin===location.origin){u.searchParams.set("lang",new URL(location.href).searchParams.get("lang"));a.href=u.pathname+u.search+u.hash}}catch{}}</script>'
2142
2222
  ] : [],
@@ -2144,31 +2224,130 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2144
2224
  "</html>"
2145
2225
  ].join("");
2146
2226
  }
2227
+ function copyDevPreviewSearchParams(searchParams) {
2228
+ const copied = new URLSearchParams();
2229
+ for (const key of ["lang", "designPreset"]) {
2230
+ const value = searchParams.get(key);
2231
+ if (value) {
2232
+ copied.set(key, value);
2233
+ }
2234
+ }
2235
+ return copied;
2236
+ }
2237
+ function createDevEditorHref(slug, searchParams) {
2238
+ const query = copyDevPreviewSearchParams(searchParams).toString();
2239
+ return `${DEV_EDITOR_PATH}/${encodeURIComponent(slug)}${query ? `?${query}` : ""}`;
2240
+ }
2241
+ function createDevPreviewHref(page, searchParams) {
2242
+ const pathname = page.isHome ? "/" : `/${encodeURIComponent(page.slug)}`;
2243
+ const query = copyDevPreviewSearchParams(searchParams).toString();
2244
+ return `${pathname}${query ? `?${query}` : ""}`;
2245
+ }
2246
+ function createDevPreviewLayout(theme, searchParams) {
2247
+ const requestedPresetId = searchParams.get("designPreset");
2248
+ const requestedPreset = requestedPresetId ? theme.module.manifest.designSystem.presets.find(
2249
+ (preset) => preset.id === requestedPresetId
2250
+ ) : void 0;
2251
+ if (!requestedPreset) {
2252
+ return theme.module.defaultLayout;
2253
+ }
2254
+ const rootFields = theme.module.layoutConfig.root?.fields ?? {};
2255
+ const designFieldName = Object.entries(rootFields).find(
2256
+ ([, field]) => field.metadata?.sudaField === "designSystem"
2257
+ )?.[0];
2258
+ if (!designFieldName) {
2259
+ return theme.module.defaultLayout;
2260
+ }
2261
+ const layoutData = structuredClone(theme.module.defaultLayout);
2262
+ layoutData.root = {
2263
+ ...layoutData.root ?? {},
2264
+ props: {
2265
+ ...layoutData.root?.props ?? {},
2266
+ [designFieldName]: { presetId: requestedPreset.id }
2267
+ }
2268
+ };
2269
+ return layoutData;
2270
+ }
2271
+ function renderDevEditorPageHtml(theme, page, renderer, locale = "en", searchParams = new URLSearchParams(), previewLocaleMessages) {
2272
+ const layoutData = createDevPreviewLayout(theme, searchParams);
2273
+ const chrome = renderer.extractLayoutChrome(layoutData);
2274
+ const cssVariables = { ...chrome.cssVariables };
2275
+ const cssVarStyle = Object.entries(cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
2276
+ const editorPages = theme.module.starterPages.map((starterPage) => ({
2277
+ id: starterPage.slug,
2278
+ handle: starterPage.slug,
2279
+ label: starterPage.title,
2280
+ group: starterPage.isHome ? "home" : "static",
2281
+ editorHref: createDevEditorHref(starterPage.slug, searchParams)
2282
+ }));
2283
+ const payload = serializeInlineScriptValue({
2284
+ pageData: page.data,
2285
+ layoutData,
2286
+ contactForm: PREVIEW_CONTACT_FORM,
2287
+ previewLocaleMessages,
2288
+ locale,
2289
+ currentPageId: page.slug,
2290
+ backHref: createDevPreviewHref(page, searchParams),
2291
+ editorPages
2292
+ });
2293
+ return [
2294
+ "<!doctype html>",
2295
+ `<html lang="${escapeHtml(locale)}">`,
2296
+ "<head>",
2297
+ '<meta charset="utf-8" />',
2298
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
2299
+ `<title>${escapeHtml(`Edit ${theme.module.manifest.name} \u2014 ${page.title}`)}</title>`,
2300
+ '<link rel="stylesheet" href="/src/styles.css" />',
2301
+ "<style>html,body,#suda-theme-dev-editor-root{height:100%;margin:0}body{overflow:hidden}</style>",
2302
+ "</head>",
2303
+ `<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
2304
+ '<div id="suda-theme-dev-editor-root"></div>',
2305
+ `<script id="suda-theme-editor-data" type="application/json">${payload}</script>`,
2306
+ '<script type="module">void import("/.suda-build/editor-client.ts").catch((error)=>console.error("[theme-dev] theme editor failed to load",error))</script>',
2307
+ "</body>",
2308
+ "</html>"
2309
+ ].join("");
2310
+ }
2147
2311
  function serializeInlineScriptValue(value) {
2148
2312
  return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
2149
2313
  }
2150
- function renderDevDesignPalette(designSystem) {
2151
- if (designSystem.presets.length < 2) {
2152
- return { style: "", html: "" };
2153
- }
2314
+ function renderDevDesignPalette(designSystem, editorHref) {
2315
+ const hasPresetSwitcher = designSystem.presets.length >= 2;
2154
2316
  const presets = designSystem.presets.map((preset) => ({
2155
2317
  id: preset.id,
2156
2318
  variables: createThemeDesignCssVariables(preset.tokens)
2157
2319
  }));
2158
- const buttons = designSystem.presets.map(
2320
+ const selectedIcon = renderToStaticMarkup(
2321
+ createElement(CircleCheck, {
2322
+ className: "suda-dev-preset-selected-icon",
2323
+ width: 32,
2324
+ height: 32,
2325
+ "aria-hidden": true,
2326
+ focusable: false
2327
+ })
2328
+ );
2329
+ const editIcon = renderToStaticMarkup(
2330
+ createElement(Pencil, {
2331
+ width: 20,
2332
+ height: 20,
2333
+ "aria-hidden": true,
2334
+ focusable: false
2335
+ })
2336
+ );
2337
+ const buttons = hasPresetSwitcher ? designSystem.presets.map(
2159
2338
  (preset) => `<button type="button" data-suda-dev-preset="${escapeHtml(preset.id)}" title="${escapeHtml(
2160
2339
  preset.description ? `${preset.label} \u2014 ${preset.description}` : preset.label
2161
2340
  )}" aria-label="${escapeHtml(preset.label)}"><span style="background:${escapeHtml(
2162
2341
  preset.tokens.colors.primary
2163
- )}"></span></button>`
2164
- ).join("");
2342
+ )}"></span>${selectedIcon}</button>`
2343
+ ).join("") : "";
2165
2344
  const config = serializeInlineScriptValue({
2166
2345
  defaultPresetId: designSystem.defaultPresetId,
2167
2346
  presets
2168
2347
  });
2169
2348
  return {
2170
- style: '<style data-suda-dev-palette-style>\n#suda-dev-palette{all:initial;position:fixed;z-index:2147483000;top:50%;right:16px;width:64px;max-height:calc(100svh - 32px);padding:8px 9px;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;background:#fff;border:1px solid #d1d5db;border-radius:999px;box-shadow:0 8px 24px rgb(15 23 42 / 12%);transform:translateY(-50%);color-scheme:light;scrollbar-width:none;-ms-overflow-style:none}\n#suda-dev-palette::-webkit-scrollbar{display:none}\n#suda-dev-palette button{all:initial;position:relative;display:grid;width:44px;height:44px;box-sizing:border-box;cursor:pointer;border-radius:999px;place-items:center}\n#suda-dev-palette button span{display:block;width:32px;height:32px;border-radius:999px;transition:transform 120ms ease}\n#suda-dev-palette button:hover span{transform:scale(1.08)}\n#suda-dev-palette button[aria-pressed="true"] span{visibility:hidden}\n#suda-dev-palette button[aria-pressed="true"]::after{position:absolute;display:grid;width:32px;height:32px;color:var(--suda-dev-preset-primary);font:700 22px/1 ui-sans-serif,system-ui,sans-serif;content:"\u2713";place-items:center}\n#suda-dev-palette button:focus-visible{outline:2px solid #94a3b8;outline-offset:2px}\n@media(max-width:640px){#suda-dev-palette{right:8px;max-height:calc(100svh - 16px)}}\n@media(prefers-reduced-motion:reduce){#suda-dev-palette button span{transition:none}}\n</style>',
2171
- html: `<aside id="suda-dev-palette" aria-label="Design presets">${buttons}</aside><script data-suda-dev-palette-script>(function(){const config=${config};const palette=document.getElementById("suda-dev-palette");if(!palette)return;const byId=new Map(config.presets.map((preset)=>[preset.id,preset]));const params=new URL(location.href).searchParams;const requested=params.get("designPreset");const selected=byId.has(requested)?requested:config.defaultPresetId;function apply(id,updateUrl){const preset=byId.get(id);if(!preset)return;const properties=Object.keys(preset.variables);const targets=[document.documentElement,document.body,...document.querySelectorAll("[style]")].filter((element,index,items)=>items.indexOf(element)===index&&(element===document.documentElement||element===document.body||properties.some((property)=>element.style.getPropertyValue(property)!=="")));for(const target of targets){for(const [property,value] of Object.entries(preset.variables))target.style.setProperty(property,value)}for(const button of palette.querySelectorAll("[data-suda-dev-preset]")){const active=button.dataset.sudaDevPreset===id;button.setAttribute("aria-pressed",String(active));button.style.setProperty("--suda-dev-preset-primary",byId.get(button.dataset.sudaDevPreset).variables["--suda-color-primary"])}if(updateUrl){const url=new URL(location.href);url.searchParams.set("designPreset",id);history.replaceState(history.state,"",url)}}if(requested&&byId.has(requested))apply(selected,false);else for(const button of palette.querySelectorAll("[data-suda-dev-preset]")){button.setAttribute("aria-pressed",String(button.dataset.sudaDevPreset===selected));button.style.setProperty("--suda-dev-preset-primary",byId.get(button.dataset.sudaDevPreset).variables["--suda-color-primary"])}palette.addEventListener("click",(event)=>{const button=event.target.closest("[data-suda-dev-preset]");if(button)apply(button.dataset.sudaDevPreset,true)})})();</script>`
2349
+ style: '<style data-suda-dev-palette-style>\n#suda-dev-palette{all:initial;position:fixed;z-index:2147483000;top:50%;right:16px;width:64px;max-height:calc(100svh - 32px);padding:8px 9px;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;background:#fff;border:1px solid #d1d5db;border-radius:999px;box-shadow:0 8px 24px rgb(15 23 42 / 12%);transform:translateY(-50%);color-scheme:light;scrollbar-width:none;-ms-overflow-style:none}\n#suda-dev-palette::-webkit-scrollbar{display:none}\n#suda-dev-palette button{all:initial;position:relative;display:grid;width:44px;height:44px;box-sizing:border-box;cursor:pointer;border-radius:999px;place-items:center}\n#suda-dev-palette button span{display:block;width:32px;height:32px;border-radius:999px;transition:transform 120ms ease}\n#suda-dev-palette button:hover span{transform:scale(1.08)}\n#suda-dev-palette button[aria-pressed="true"] span{visibility:hidden}\n#suda-dev-palette .suda-dev-preset-selected-icon{position:absolute;display:block;width:32px;height:32px;color:var(--suda-dev-preset-primary);visibility:hidden;opacity:0;transform:scale(.85);transition:opacity 120ms ease,transform 120ms ease}\n#suda-dev-palette button[aria-pressed="true"] .suda-dev-preset-selected-icon{visibility:visible;opacity:1;transform:scale(1)}\n#suda-dev-palette button:focus-visible{outline:2px solid #94a3b8;outline-offset:2px}\n#suda-dev-palette .suda-dev-edit-divider{display:block;width:28px;height:1px;margin:4px auto;background:#e2e8f0}\n#suda-dev-palette .suda-dev-edit-link{all:initial;display:grid;width:44px;height:44px;box-sizing:border-box;color:#334155;cursor:pointer;border-radius:999px;place-items:center;transition:background-color 120ms ease,color 120ms ease}\n#suda-dev-palette .suda-dev-edit-link:hover{color:#0f172a;background:#f1f5f9}\n#suda-dev-palette .suda-dev-edit-link:focus-visible{outline:2px solid #94a3b8;outline-offset:2px}\n@media(max-width:640px){#suda-dev-palette{right:8px;max-height:calc(100svh - 16px)}}\n@media(prefers-reduced-motion:reduce){#suda-dev-palette button span,#suda-dev-palette .suda-dev-preset-selected-icon,#suda-dev-palette .suda-dev-edit-link{transition:none}}\n</style>',
2350
+ html: `<aside id="suda-dev-palette" aria-label="Theme development tools">${buttons}${hasPresetSwitcher ? '<span class="suda-dev-edit-divider" aria-hidden="true"></span>' : ""}<a class="suda-dev-edit-link" data-suda-dev-editor-link="" href="${escapeHtml(editorHref)}" title="Edit starter page" aria-label="Edit starter page">${editIcon}</a></aside>` + (hasPresetSwitcher ? `<script data-suda-dev-palette-script>(function(){const config=${config};const palette=document.getElementById("suda-dev-palette");if(!palette)return;const byId=new Map(config.presets.map((preset)=>[preset.id,preset]));const params=new URL(location.href).searchParams;const requested=params.get("designPreset");const selected=byId.has(requested)?requested:config.defaultPresetId;function apply(id,updateUrl){const preset=byId.get(id);if(!preset)return;const properties=Object.keys(preset.variables);const targets=[document.documentElement,document.body,...document.querySelectorAll("[style]")].filter((element,index,items)=>items.indexOf(element)===index&&(element===document.documentElement||element===document.body||properties.some((property)=>element.style.getPropertyValue(property)!=="")));for(const target of targets){for(const [property,value] of Object.entries(preset.variables))target.style.setProperty(property,value)}for(const button of palette.querySelectorAll("[data-suda-dev-preset]")){const active=button.dataset.sudaDevPreset===id;button.setAttribute("aria-pressed",String(active));button.style.setProperty("--suda-dev-preset-primary",byId.get(button.dataset.sudaDevPreset).variables["--suda-color-primary"])}const editorLink=palette.querySelector("[data-suda-dev-editor-link]");if(editorLink){const editorUrl=new URL(editorLink.href,location.href);editorUrl.searchParams.set("designPreset",id);editorLink.href=editorUrl.pathname+editorUrl.search+editorUrl.hash}if(updateUrl){const url=new URL(location.href);url.searchParams.set("designPreset",id);history.replaceState(history.state,"",url)}}if(requested&&byId.has(requested))apply(selected,false);else for(const button of palette.querySelectorAll("[data-suda-dev-preset]")){button.setAttribute("aria-pressed",String(button.dataset.sudaDevPreset===selected));button.style.setProperty("--suda-dev-preset-primary",byId.get(button.dataset.sudaDevPreset).variables["--suda-color-primary"])}palette.addEventListener("click",(event)=>{const button=event.target.closest("[data-suda-dev-preset]");if(button)apply(button.dataset.sudaDevPreset,true)})})();</script>` : "")
2172
2351
  };
2173
2352
  }
2174
2353
  var SCREENSHOT_DEVICES = ["desktop", "tablet", "mobile"];