@sudajs/cli 0.19.0 → 0.20.0

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.js CHANGED
@@ -6,7 +6,7 @@ import { createRequire } from 'module';
6
6
  import path2 from 'path';
7
7
  import { createInterface } from 'readline/promises';
8
8
  import { fileURLToPath, pathToFileURL } from 'url';
9
- import { themeDesignSystemSchema, createThemeDesignCssVariables, createThemeAgentManifest, createSudaPageAgentRuntime, checkThemeModule, formatThemeCheckResult, agentValidationResultSchema } from '@sudajs/theme-engine';
9
+ import { themeDesignSystemSchema, createThemeDesignFontStylesheetUrl, findThemeDesignSystemFieldName, resolveThemeDesignTokens, createThemeDesignCssVariables, createThemeAgentManifest, createSudaPageAgentRuntime, checkThemeModule, formatThemeCheckResult, agentValidationResultSchema } from '@sudajs/theme-engine';
10
10
  import { themeManifestSchema, runWithThemePreviewLocale } from '@sudajs/theme-engine/server';
11
11
  import { Command } from 'commander';
12
12
  import { build } from 'esbuild';
@@ -902,10 +902,13 @@ export const {
902
902
  SUDA_COLOR_TOKEN_KEYS,
903
903
  createThemeDesignCssVariables,
904
904
  createThemeDesignDefault,
905
+ createThemeDesignFontStylesheetUrl,
905
906
  resolveThemeDesignTokens,
906
907
  SUDA_CUSTOM_DESIGN_PRESET_ID,
907
908
  SUDA_DESIGN_COLOR_KEYS,
908
909
  SUDA_DESIGN_RADIUS_KEYS,
910
+ SUDA_DESIGN_TYPOGRAPHY_KEYS,
911
+ SUDA_THEME_FONT_FAMILIES,
909
912
  compileSudaBlockSlots,
910
913
  isSudaLocalBlockComponent,
911
914
  SUDA_LOCAL_BLOCK_CATEGORY,
@@ -1746,6 +1749,7 @@ var __testUtils = {
1746
1749
  listProjectsOutputSchema,
1747
1750
  renderDevStarterPageHtml,
1748
1751
  renderDevEditorPageHtml,
1752
+ readDevThemeLocaleMessages,
1749
1753
  writeVitePreviewEntries,
1750
1754
  resolveDevEditorSlug,
1751
1755
  resolveDevStarterSlug,
@@ -1879,20 +1883,23 @@ async function writeVitePreviewEntries(root) {
1879
1883
  path2.join(buildDir, "editor-client.ts"),
1880
1884
  [
1881
1885
  'import "@puckeditor/core/puck.css";',
1882
- 'import { composeUnifiedThemeEditorData, createUnifiedThemeEditorConfig, installThemePreviewLocale, resolveAssetPath, ThemeEditor } from "@sudajs/theme-engine";',
1886
+ 'import { composeUnifiedThemeEditorData, createThemeDesignFontStylesheetUrl, createUnifiedThemeEditorConfig, installThemeLocale, installThemePreviewLocale, resolveAssetPath, resolveThemeDesignTokens, ThemeEditor } from "@sudajs/theme-engine";',
1883
1887
  'import { createElement } from "react";',
1884
1888
  'import { createRoot } from "react-dom/client";',
1885
- 'import runtime from "./runtime.dev";',
1886
1889
  'const payloadElement=document.querySelector("#suda-theme-editor-data");',
1887
1890
  'if(!payloadElement){throw new Error("Missing Suda theme editor payload.")}',
1888
1891
  "const payload=JSON.parse(payloadElement.textContent ?? '{}');",
1892
+ "if(payload.themeLocaleMessages){installThemeLocale(payload.themeLocaleMessages)}",
1889
1893
  "if(payload.previewLocaleMessages){installThemePreviewLocale(payload.previewLocaleMessages)}",
1894
+ 'const runtime=(await import("./runtime.dev")).default;',
1890
1895
  'const resolveAssetUrl=(value: string | undefined)=>resolveAssetPath(value,{legacyResolve:(key)=>key.startsWith("themes/")?`/api/themes/${key.slice("themes/".length)}`:undefined});',
1891
1896
  'const config=createUnifiedThemeEditorConfig(runtime.theme,undefined,{labels:{pageContent:"Page content",themeSettings:"Theme settings"}});',
1892
1897
  "const data=composeUnifiedThemeEditorData(payload.layoutData,payload.pageData,{layoutConfig:runtime.theme.layoutConfig,pageConfig:runtime.theme.pageConfig});",
1898
+ 'function applyDesignFont(nextData){const fieldName=payload.designSystemFieldName||"designSystem";const value=nextData?.root?.props?.[fieldName];const tokens=resolveThemeDesignTokens(payload.designSystem,value);const href=createThemeDesignFontStylesheetUrl(tokens);let link=document.querySelector("#suda-theme-fonts");if(!href){link?.remove();return}if(!link){link=document.createElement("link");link.id="suda-theme-fonts";link.rel="stylesheet";document.head.append(link)}link.href=href}',
1899
+ "applyDesignFont(data);",
1893
1900
  '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"}};',
1894
1901
  "const metadata={contactForm:payload.contactForm,isEditor:true,resolveAssetUrl};",
1895
- 'createRoot(document.querySelector("#suda-theme-dev-editor-root")!).render(createElement(ThemeEditor,{config,data,editorContext,metadata,iframe:false,height:"100%",publishActionVisible:false}));',
1902
+ 'createRoot(document.querySelector("#suda-theme-dev-editor-root")!).render(createElement(ThemeEditor,{config,data,editorContext,metadata,iframe:false,height:"100%",previewStylesheetUrls:["/src/styles.css"],publishActionVisible:false,onChange:applyDesignFont}));',
1896
1903
  ""
1897
1904
  ].join("\n"),
1898
1905
  "utf8"
@@ -2015,7 +2022,8 @@ function createSudaPreviewVitePlugin(root, options = {}) {
2015
2022
  renderer,
2016
2023
  localized.locale,
2017
2024
  url.searchParams,
2018
- localized.previewLocaleMessages
2025
+ localized.previewLocaleMessages,
2026
+ localized.themeLocaleMessages
2019
2027
  ) : renderDevStarterPageHtml(
2020
2028
  localized.theme,
2021
2029
  starter,
@@ -2049,8 +2057,9 @@ async function loadLocalizedViteDevTheme(root, server, explicitLang, acceptLangu
2049
2057
  localeParams.acceptLanguage = acceptLanguage;
2050
2058
  }
2051
2059
  const locale = resolveThemePreviewLocale(localeParams);
2060
+ const themeLocaleMessages = await readDevThemeLocaleMessages(root, locale);
2052
2061
  if (!baseTheme.module.manifest.previewLocales) {
2053
- return { theme: baseTheme, locale };
2062
+ return { theme: baseTheme, locale, themeLocaleMessages };
2054
2063
  }
2055
2064
  const previewLocaleMessages = await readDevPreviewLocaleMessages(root, locale);
2056
2065
  server.moduleGraph.invalidateAll();
@@ -2058,7 +2067,7 @@ async function loadLocalizedViteDevTheme(root, server, explicitLang, acceptLangu
2058
2067
  previewLocaleMessages,
2059
2068
  () => loadViteDevTheme(root, server)
2060
2069
  );
2061
- return { theme, locale, previewLocaleMessages };
2070
+ return { theme, locale, themeLocaleMessages, previewLocaleMessages };
2062
2071
  });
2063
2072
  }
2064
2073
  var vitePreviewLocaleLoadTails = /* @__PURE__ */ new WeakMap();
@@ -2236,6 +2245,16 @@ async function readDevPreviewLocaleMessages(root, locale) {
2236
2245
  }
2237
2246
  return messages;
2238
2247
  }
2248
+ async function readDevThemeLocaleMessages(root, locale) {
2249
+ const localesDir = path2.join(root, "src", "locales");
2250
+ const localizedPath = path2.join(localesDir, `${locale}.json`);
2251
+ const filePath = await pathExists(localizedPath) ? localizedPath : path2.join(localesDir, "en.json");
2252
+ const messages = await readLocaleJson(filePath);
2253
+ if (!isRecord(messages)) {
2254
+ throw new Error(`Theme locale ${locale} must be a JSON object.`);
2255
+ }
2256
+ return messages;
2257
+ }
2239
2258
  function escapeHtml(value) {
2240
2259
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2241
2260
  }
@@ -2271,6 +2290,8 @@ async function loadThemeScopedRenderer(themeRoot) {
2271
2290
  }
2272
2291
  function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserveLang = false, previewLocaleMessages, searchParams = new URLSearchParams(), options = {}) {
2273
2292
  const layoutData = createDevPreviewLayout(theme, searchParams);
2293
+ const designTokens = resolveDevDesignTokens(theme, layoutData);
2294
+ const fontStylesheetUrl = createThemeDesignFontStylesheetUrl(designTokens);
2274
2295
  const chrome = renderer.extractLayoutChrome(layoutData);
2275
2296
  const metadata = {
2276
2297
  resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath),
@@ -2310,6 +2331,7 @@ function renderDevStarterPageHtml(theme, page, renderer, locale = "en", preserve
2310
2331
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
2311
2332
  `<title>${escapeHtml(`${theme.module.manifest.name} \u2014 ${page.title}`)}</title>`,
2312
2333
  '<link rel="stylesheet" href="/src/styles.css" />',
2334
+ ...fontStylesheetUrl ? [`<link id="suda-theme-fonts" rel="stylesheet" href="${escapeHtml(fontStylesheetUrl)}" />`] : [],
2313
2335
  customHead,
2314
2336
  designPalette?.style ?? "",
2315
2337
  "</head>",
@@ -2352,10 +2374,7 @@ function createDevPreviewLayout(theme, searchParams) {
2352
2374
  if (!requestedPreset) {
2353
2375
  return theme.module.defaultLayout;
2354
2376
  }
2355
- const rootFields = theme.module.layoutConfig.root?.fields ?? {};
2356
- const designFieldName = Object.entries(rootFields).find(
2357
- ([, field]) => field.metadata?.sudaField === "designSystem"
2358
- )?.[0];
2377
+ const designFieldName = findThemeDesignSystemFieldName(theme.module.layoutConfig.root?.fields);
2359
2378
  if (!designFieldName) {
2360
2379
  return theme.module.defaultLayout;
2361
2380
  }
@@ -2369,8 +2388,16 @@ function createDevPreviewLayout(theme, searchParams) {
2369
2388
  };
2370
2389
  return layoutData;
2371
2390
  }
2372
- function renderDevEditorPageHtml(theme, page, renderer, locale = "en", searchParams = new URLSearchParams(), previewLocaleMessages) {
2391
+ function resolveDevDesignTokens(theme, layoutData) {
2392
+ const designFieldName = findThemeDesignSystemFieldName(theme.module.layoutConfig.root?.fields);
2393
+ const rootProps = layoutData.root?.props;
2394
+ const designValue = designFieldName ? rootProps?.[designFieldName] : void 0;
2395
+ return resolveThemeDesignTokens(theme.module.manifest.designSystem, designValue);
2396
+ }
2397
+ function renderDevEditorPageHtml(theme, page, renderer, locale = "en", searchParams = new URLSearchParams(), previewLocaleMessages, themeLocaleMessages) {
2373
2398
  const layoutData = createDevPreviewLayout(theme, searchParams);
2399
+ const designTokens = resolveDevDesignTokens(theme, layoutData);
2400
+ const fontStylesheetUrl = createThemeDesignFontStylesheetUrl(designTokens);
2374
2401
  const chrome = renderer.extractLayoutChrome(layoutData);
2375
2402
  const cssVariables = { ...chrome.cssVariables };
2376
2403
  const cssVarStyle = Object.entries(cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
@@ -2386,6 +2413,9 @@ function renderDevEditorPageHtml(theme, page, renderer, locale = "en", searchPar
2386
2413
  layoutData,
2387
2414
  contactForm: PREVIEW_CONTACT_FORM,
2388
2415
  previewLocaleMessages,
2416
+ designSystem: theme.module.manifest.designSystem,
2417
+ designSystemFieldName: findThemeDesignSystemFieldName(theme.module.layoutConfig.root?.fields) ?? "designSystem",
2418
+ themeLocaleMessages,
2389
2419
  locale,
2390
2420
  currentPageId: page.slug,
2391
2421
  backHref: createDevPreviewHref(page, searchParams),
@@ -2399,6 +2429,7 @@ function renderDevEditorPageHtml(theme, page, renderer, locale = "en", searchPar
2399
2429
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
2400
2430
  `<title>${escapeHtml(`Edit ${theme.module.manifest.name} \u2014 ${page.title}`)}</title>`,
2401
2431
  '<link rel="stylesheet" href="/src/styles.css" />',
2432
+ ...fontStylesheetUrl ? [`<link id="suda-theme-fonts" rel="stylesheet" href="${escapeHtml(fontStylesheetUrl)}" />`] : [],
2402
2433
  "<style>html,body,#suda-theme-dev-editor-root{height:100%;margin:0}body{overflow:hidden}</style>",
2403
2434
  "</head>",
2404
2435
  `<body${cssVarStyle ? ` style="${cssVarStyle}"` : ""}>`,
@@ -2413,10 +2444,14 @@ function serializeInlineScriptValue(value) {
2413
2444
  return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
2414
2445
  }
2415
2446
  function renderDevDesignPalette(designSystem, editorHref) {
2416
- const hasPresetSwitcher = designSystem.presets.length >= 2;
2447
+ const primaryColors = new Set(
2448
+ designSystem.presets.map((preset) => preset.tokens.colors.primary)
2449
+ );
2450
+ const hasPresetSwitcher = primaryColors.size >= 2;
2417
2451
  const presets = designSystem.presets.map((preset) => ({
2418
2452
  id: preset.id,
2419
- variables: createThemeDesignCssVariables(preset.tokens)
2453
+ variables: createThemeDesignCssVariables(preset.tokens),
2454
+ fontStylesheetUrl: createThemeDesignFontStylesheetUrl(preset.tokens)
2420
2455
  }));
2421
2456
  const selectedIcon = renderToStaticMarkup(
2422
2457
  createElement(CircleCheck, {
@@ -2448,7 +2483,7 @@ function renderDevDesignPalette(designSystem, editorHref) {
2448
2483
  });
2449
2484
  return {
2450
2485
  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>',
2451
- 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["--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["--primary"])}palette.addEventListener("click",(event)=>{const button=event.target.closest("[data-suda-dev-preset]");if(button)apply(button.dataset.sudaDevPreset,true)})})();</script>` : "")
2486
+ 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)}let fontLink=document.querySelector("#suda-theme-fonts");if(preset.fontStylesheetUrl){if(!fontLink){fontLink=document.createElement("link");fontLink.id="suda-theme-fonts";fontLink.rel="stylesheet";document.head.append(fontLink)}fontLink.href=preset.fontStylesheetUrl}else fontLink?.remove();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["--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["--primary"])}palette.addEventListener("click",(event)=>{const button=event.target.closest("[data-suda-dev-preset]");if(button)apply(button.dataset.sudaDevPreset,true)})})();</script>` : "")
2452
2487
  };
2453
2488
  }
2454
2489
  var SCREENSHOT_DEVICES = ["desktop", "tablet", "mobile"];