@open-mercato/core 0.10.1-develop.7808.1.8128cefd28 → 0.10.1-develop.7809.1.ea24461aeb

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 6221 entry points
1
+ [build:core] found 6223 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
5
5
  import { buildBrandingPreviewDocument } from "./storeBrandingForm.js";
6
+ import { useContentSecurityPolicyNonce } from "@open-mercato/ui/theme/ContentSecurityPolicyProvider";
6
7
  const BRANDING_PREVIEW_DEBOUNCE_MS = 250;
7
8
  const SAMPLE_PRODUCT_PRICE = 29;
8
9
  function formatPreviewPrice(amount, currencyCode, locale) {
@@ -25,6 +26,7 @@ function useDebouncedValue(value, delayMs) {
25
26
  function StoreBrandingPreview({ values, storeName, currencyCode }) {
26
27
  const t = useT();
27
28
  const locale = useLocale();
29
+ const nonce = useContentSecurityPolicyNonce();
28
30
  const debouncedValues = useDebouncedValue(values, BRANDING_PREVIEW_DEBOUNCE_MS);
29
31
  const document = React.useMemo(
30
32
  () => buildBrandingPreviewDocument(debouncedValues, {
@@ -40,8 +42,8 @@ function StoreBrandingPreview({ values, storeName, currencyCode }) {
40
42
  heroButton: t("ecommerce.backend.store.branding.preview.heroButton", "Shop now"),
41
43
  productName: t("ecommerce.backend.store.branding.preview.productName", "Sample product"),
42
44
  productPrice: formatPreviewPrice(SAMPLE_PRODUCT_PRICE, currencyCode, locale)
43
- }),
44
- [currencyCode, debouncedValues, locale, storeName, t]
45
+ }, nonce),
46
+ [currencyCode, debouncedValues, locale, storeName, t, nonce]
45
47
  );
46
48
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
47
49
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ecommerce/components/StoreBrandingPreview.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildBrandingPreviewDocument } from './storeBrandingForm'\n\nexport const BRANDING_PREVIEW_DEBOUNCE_MS = 250\n\nconst SAMPLE_PRODUCT_PRICE = 29\n\nexport function formatPreviewPrice(amount: number, currencyCode: string, locale: string): string {\n const code = currencyCode.trim().toUpperCase()\n try {\n return new Intl.NumberFormat(locale, { style: 'currency', currency: code }).format(amount)\n } catch {\n const formatted = new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount)\n return code ? `${formatted} ${code}` : formatted\n }\n}\n\ntype StoreBrandingPreviewProps = {\n values: Record<string, unknown>\n storeName: string\n currencyCode: string\n}\n\nfunction useDebouncedValue<TValue>(value: TValue, delayMs: number): TValue {\n const [debounced, setDebounced] = React.useState(value)\n React.useEffect(() => {\n const handle = setTimeout(() => setDebounced(value), delayMs)\n return () => clearTimeout(handle)\n }, [value, delayMs])\n return debounced\n}\n\nexport function StoreBrandingPreview({ values, storeName, currencyCode }: StoreBrandingPreviewProps) {\n const t = useT()\n const locale = useLocale()\n const debouncedValues = useDebouncedValue(values, BRANDING_PREVIEW_DEBOUNCE_MS)\n const document = React.useMemo(\n () =>\n buildBrandingPreviewDocument(debouncedValues, {\n storeName,\n navShop: t('ecommerce.backend.store.branding.preview.navShop', 'Shop'),\n navCategories: t('ecommerce.backend.store.branding.preview.navCategories', 'Categories'),\n navCart: t('ecommerce.backend.store.branding.preview.navCart', 'Cart'),\n heroTitle: t('ecommerce.backend.store.branding.preview.heroTitle', 'New arrivals'),\n heroText: t(\n 'ecommerce.backend.store.branding.preview.heroText',\n 'A preview of your storefront with the colours, corners and fonts you chose.',\n ),\n heroButton: t('ecommerce.backend.store.branding.preview.heroButton', 'Shop now'),\n productName: t('ecommerce.backend.store.branding.preview.productName', 'Sample product'),\n productPrice: formatPreviewPrice(SAMPLE_PRODUCT_PRICE, currencyCode, locale),\n }),\n [currencyCode, debouncedValues, locale, storeName, t],\n )\n\n return (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'ecommerce.backend.store.branding.preview.hint',\n 'The preview updates as you edit and is never saved until you save the branding.',\n )}\n </p>\n <iframe\n title={t('ecommerce.backend.store.branding.preview.title', 'Storefront preview')}\n sandbox=\"\"\n referrerPolicy=\"no-referrer\"\n srcDoc={document}\n data-testid=\"branding-preview-frame\"\n className=\"h-96 w-full rounded-md border border-border bg-background\"\n />\n </div>\n )\n}\n"],
5
- "mappings": ";AA2DI,SACE,KADF;AAzDJ,YAAY,WAAW;AACvB,SAAS,WAAW,YAAY;AAChC,SAAS,oCAAoC;AAEtC,MAAM,+BAA+B;AAE5C,MAAM,uBAAuB;AAEtB,SAAS,mBAAmB,QAAgB,cAAsB,QAAwB;AAC/F,QAAM,OAAO,aAAa,KAAK,EAAE,YAAY;AAC7C,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,YAAY,UAAU,KAAK,CAAC,EAAE,OAAO,MAAM;AAAA,EAC3F,QAAQ;AACN,UAAM,YAAY,IAAI,KAAK,aAAa,QAAQ,EAAE,uBAAuB,GAAG,uBAAuB,EAAE,CAAC,EAAE,OAAO,MAAM;AACrH,WAAO,OAAO,GAAG,SAAS,IAAI,IAAI,KAAK;AAAA,EACzC;AACF;AAQA,SAAS,kBAA0B,OAAe,SAAyB;AACzE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AACtD,QAAM,UAAU,MAAM;AACpB,UAAM,SAAS,WAAW,MAAM,aAAa,KAAK,GAAG,OAAO;AAC5D,WAAO,MAAM,aAAa,MAAM;AAAA,EAClC,GAAG,CAAC,OAAO,OAAO,CAAC;AACnB,SAAO;AACT;AAEO,SAAS,qBAAqB,EAAE,QAAQ,WAAW,aAAa,GAA8B;AACnG,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,kBAAkB,kBAAkB,QAAQ,4BAA4B;AAC9E,QAAM,WAAW,MAAM;AAAA,IACrB,MACE,6BAA6B,iBAAiB;AAAA,MAC5C;AAAA,MACA,SAAS,EAAE,oDAAoD,MAAM;AAAA,MACrE,eAAe,EAAE,0DAA0D,YAAY;AAAA,MACvF,SAAS,EAAE,oDAAoD,MAAM;AAAA,MACrE,WAAW,EAAE,sDAAsD,cAAc;AAAA,MACjF,UAAU;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,MACA,YAAY,EAAE,uDAAuD,UAAU;AAAA,MAC/E,aAAa,EAAE,wDAAwD,gBAAgB;AAAA,MACvF,cAAc,mBAAmB,sBAAsB,cAAc,MAAM;AAAA,IAC7E,CAAC;AAAA,IACH,CAAC,cAAc,iBAAiB,QAAQ,WAAW,CAAC;AAAA,EACtD;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,OAAE,WAAU,iCACV;AAAA,MACC;AAAA,MACA;AAAA,IACF,GACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,kDAAkD,oBAAoB;AAAA,QAC/E,SAAQ;AAAA,QACR,gBAAe;AAAA,QACf,QAAQ;AAAA,QACR,eAAY;AAAA,QACZ,WAAU;AAAA;AAAA,IACZ;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildBrandingPreviewDocument } from './storeBrandingForm'\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\n\nexport const BRANDING_PREVIEW_DEBOUNCE_MS = 250\n\nconst SAMPLE_PRODUCT_PRICE = 29\n\nexport function formatPreviewPrice(amount: number, currencyCode: string, locale: string): string {\n const code = currencyCode.trim().toUpperCase()\n try {\n return new Intl.NumberFormat(locale, { style: 'currency', currency: code }).format(amount)\n } catch {\n const formatted = new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount)\n return code ? `${formatted} ${code}` : formatted\n }\n}\n\ntype StoreBrandingPreviewProps = {\n values: Record<string, unknown>\n storeName: string\n currencyCode: string\n}\n\nfunction useDebouncedValue<TValue>(value: TValue, delayMs: number): TValue {\n const [debounced, setDebounced] = React.useState(value)\n React.useEffect(() => {\n const handle = setTimeout(() => setDebounced(value), delayMs)\n return () => clearTimeout(handle)\n }, [value, delayMs])\n return debounced\n}\n\nexport function StoreBrandingPreview({ values, storeName, currencyCode }: StoreBrandingPreviewProps) {\n const t = useT()\n const locale = useLocale()\n const nonce = useContentSecurityPolicyNonce()\n const debouncedValues = useDebouncedValue(values, BRANDING_PREVIEW_DEBOUNCE_MS)\n const document = React.useMemo(\n () =>\n buildBrandingPreviewDocument(debouncedValues, {\n storeName,\n navShop: t('ecommerce.backend.store.branding.preview.navShop', 'Shop'),\n navCategories: t('ecommerce.backend.store.branding.preview.navCategories', 'Categories'),\n navCart: t('ecommerce.backend.store.branding.preview.navCart', 'Cart'),\n heroTitle: t('ecommerce.backend.store.branding.preview.heroTitle', 'New arrivals'),\n heroText: t(\n 'ecommerce.backend.store.branding.preview.heroText',\n 'A preview of your storefront with the colours, corners and fonts you chose.',\n ),\n heroButton: t('ecommerce.backend.store.branding.preview.heroButton', 'Shop now'),\n productName: t('ecommerce.backend.store.branding.preview.productName', 'Sample product'),\n productPrice: formatPreviewPrice(SAMPLE_PRODUCT_PRICE, currencyCode, locale),\n }, nonce),\n [currencyCode, debouncedValues, locale, storeName, t, nonce],\n )\n\n return (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'ecommerce.backend.store.branding.preview.hint',\n 'The preview updates as you edit and is never saved until you save the branding.',\n )}\n </p>\n <iframe\n title={t('ecommerce.backend.store.branding.preview.title', 'Storefront preview')}\n sandbox=\"\"\n referrerPolicy=\"no-referrer\"\n srcDoc={document}\n data-testid=\"branding-preview-frame\"\n className=\"h-96 w-full rounded-md border border-border bg-background\"\n />\n </div>\n )\n}\n"],
5
+ "mappings": ";AA6DI,SACE,KADF;AA3DJ,YAAY,WAAW;AACvB,SAAS,WAAW,YAAY;AAChC,SAAS,oCAAoC;AAC7C,SAAS,qCAAqC;AAEvC,MAAM,+BAA+B;AAE5C,MAAM,uBAAuB;AAEtB,SAAS,mBAAmB,QAAgB,cAAsB,QAAwB;AAC/F,QAAM,OAAO,aAAa,KAAK,EAAE,YAAY;AAC7C,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,YAAY,UAAU,KAAK,CAAC,EAAE,OAAO,MAAM;AAAA,EAC3F,QAAQ;AACN,UAAM,YAAY,IAAI,KAAK,aAAa,QAAQ,EAAE,uBAAuB,GAAG,uBAAuB,EAAE,CAAC,EAAE,OAAO,MAAM;AACrH,WAAO,OAAO,GAAG,SAAS,IAAI,IAAI,KAAK;AAAA,EACzC;AACF;AAQA,SAAS,kBAA0B,OAAe,SAAyB;AACzE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AACtD,QAAM,UAAU,MAAM;AACpB,UAAM,SAAS,WAAW,MAAM,aAAa,KAAK,GAAG,OAAO;AAC5D,WAAO,MAAM,aAAa,MAAM;AAAA,EAClC,GAAG,CAAC,OAAO,OAAO,CAAC;AACnB,SAAO;AACT;AAEO,SAAS,qBAAqB,EAAE,QAAQ,WAAW,aAAa,GAA8B;AACnG,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,QAAQ,8BAA8B;AAC5C,QAAM,kBAAkB,kBAAkB,QAAQ,4BAA4B;AAC9E,QAAM,WAAW,MAAM;AAAA,IACrB,MACE,6BAA6B,iBAAiB;AAAA,MAC5C;AAAA,MACA,SAAS,EAAE,oDAAoD,MAAM;AAAA,MACrE,eAAe,EAAE,0DAA0D,YAAY;AAAA,MACvF,SAAS,EAAE,oDAAoD,MAAM;AAAA,MACrE,WAAW,EAAE,sDAAsD,cAAc;AAAA,MACjF,UAAU;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,MACA,YAAY,EAAE,uDAAuD,UAAU;AAAA,MAC/E,aAAa,EAAE,wDAAwD,gBAAgB;AAAA,MACvF,cAAc,mBAAmB,sBAAsB,cAAc,MAAM;AAAA,IAC7E,GAAG,KAAK;AAAA,IACV,CAAC,cAAc,iBAAiB,QAAQ,WAAW,GAAG,KAAK;AAAA,EAC7D;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,OAAE,WAAU,iCACV;AAAA,MACC;AAAA,MACA;AAAA,IACF,GACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,kDAAkD,oBAAoB;AAAA,QAC/E,SAAQ;AAAA,QACR,gBAAe;AAAA,QACf,QAAQ;AAAA,QACR,eAAY;AAAA,QACZ,WAAU;AAAA;AAAA,IACZ;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -139,7 +139,6 @@ const HTML_ESCAPES = {
139
139
  function escapeHtml(value) {
140
140
  return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character] ?? character);
141
141
  }
142
- const PREVIEW_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; img-src http: https:";
143
142
  const PREVIEW_STATIC_CSS = [
144
143
  "*{box-sizing:border-box}",
145
144
  "body{margin:0;background:var(--background);color:var(--foreground);font-family:var(--font-base);font-size:14px;line-height:1.5}",
@@ -158,15 +157,19 @@ const PREVIEW_STATIC_CSS = [
158
157
  ".card-body{padding:10px}",
159
158
  ".price{font-weight:600}"
160
159
  ].join("");
161
- function buildBrandingPreviewDocument(values, labels) {
160
+ function buildBrandingPreviewDocument(values, labels, nonce) {
162
161
  const branding = sanitizePreviewBranding(values);
162
+ const styleNonce = typeof nonce === "string" && /^[A-Za-z0-9+/_-]{8,128}={0,2}$/.test(nonce) ? nonce : null;
163
+ const styleAttribute = styleNonce ? ` nonce="${styleNonce}"` : "";
164
+ const styleSource = styleNonce ? `'nonce-${styleNonce}'` : "'unsafe-inline'";
165
+ const contentSecurityPolicy = `default-src 'none'; style-src ${styleSource}; img-src http: https:`;
163
166
  const rootRule = renderBrandingRootRule(branding);
164
167
  const logo = branding.logoUrl ? `<img src="${escapeHtml(branding.logoUrl)}" alt="">` : "";
165
168
  const card = `<div class="card"><div class="thumb"></div><div class="card-body"><div>${escapeHtml(labels.productName)}</div><div class="price">${escapeHtml(labels.productPrice)}</div></div></div>`;
166
169
  return [
167
170
  '<!doctype html><html><head><meta charset="utf-8">',
168
- `<meta http-equiv="Content-Security-Policy" content="${PREVIEW_CONTENT_SECURITY_POLICY}">`,
169
- `<style>${rootRule}</style><style>${PREVIEW_STATIC_CSS}</style></head><body>`,
171
+ `<meta http-equiv="Content-Security-Policy" content="${contentSecurityPolicy}">`,
172
+ `<style${styleAttribute}>${rootRule}</style><style${styleAttribute}>${PREVIEW_STATIC_CSS}</style></head><body>`,
170
173
  `<header class="bar"><div class="brand">${logo}<span>${escapeHtml(labels.storeName)}</span></div>`,
171
174
  `<nav><span>${escapeHtml(labels.navShop)}</span><span>${escapeHtml(labels.navCategories)}</span><span>${escapeHtml(labels.navCart)}</span></nav></header>`,
172
175
  `<section class="hero"><h1>${escapeHtml(labels.heroTitle)}</h1><p>${escapeHtml(labels.heroText)}</p><span class="btn">${escapeHtml(labels.heroButton)}</span></section>`,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ecommerce/components/storeBrandingForm.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { ecommerceStoreBrandingSchema, isSafeAssetUrl } from '../data/validators'\nimport {\n ECOMMERCE_BRANDING_COLOR_KEYS,\n ECOMMERCE_BRANDING_DEFAULTS,\n ECOMMERCE_BRANDING_FONT_KEYS,\n findEcommerceBrandingFont,\n isValidBorderRadius,\n normalizeBrandingColor,\n renderBrandingRootRule,\n type EcommerceBrandingColorKey,\n type EcommerceBrandingFontKey,\n} from '../lib/brandingStyles'\nimport type { StoreAdminRecord } from './storeAdmin'\n\nexport const BRANDING_COLOR_KEYS = ECOMMERCE_BRANDING_COLOR_KEYS\nexport const BRANDING_FONT_KEYS = ECOMMERCE_BRANDING_FONT_KEYS\nexport const BRANDING_ASSET_KEYS = ['logoUrl', 'faviconUrl'] as const\n\nexport type BrandingAssetKey = (typeof BRANDING_ASSET_KEYS)[number]\nexport type StoreBrandingFormKey =\n | EcommerceBrandingColorKey\n | EcommerceBrandingFontKey\n | BrandingAssetKey\n | 'borderRadius'\n\nexport type StoreBrandingFormValues = Record<StoreBrandingFormKey, string>\n\nexport type StoreBrandingPayload = Partial<Record<StoreBrandingFormKey, string>>\n\nexport const BRANDING_FONT_INHERIT = 'inherit'\nexport const DEFAULT_BASE_FONT_ID = 'inter'\nexport const RADIUS_SLIDER_MIN = 0\nexport const RADIUS_SLIDER_MAX = 5\nexport const RADIUS_SLIDER_STEP = 0.125\nconst PX_PER_REM = 16\n\nexport const STORE_BRANDING_API_PATH = (storeId: string) => `/api/ecommerce/stores/${encodeURIComponent(storeId)}/branding`\n\nconst ALL_KEYS: readonly StoreBrandingFormKey[] = [\n ...BRANDING_COLOR_KEYS,\n 'borderRadius',\n ...BRANDING_FONT_KEYS,\n ...BRANDING_ASSET_KEYS,\n]\n\nfunction readStoredBranding(store: Pick<StoreAdminRecord, 'settings'>): Record<string, unknown> {\n const branding = store.settings?.branding\n return typeof branding === 'object' && branding !== null && !Array.isArray(branding)\n ? (branding as Record<string, unknown>)\n : {}\n}\n\nfunction readStoredString(stored: Record<string, unknown>, key: string): string {\n const value = stored[key]\n return typeof value === 'string' ? value : ''\n}\n\nexport function buildStoreBrandingInitialValues(store: Pick<StoreAdminRecord, 'settings'>): StoreBrandingFormValues {\n const stored = readStoredBranding(store)\n const values = {} as StoreBrandingFormValues\n for (const key of BRANDING_COLOR_KEYS) {\n values[key] = readStoredString(stored, key) || ECOMMERCE_BRANDING_DEFAULTS[key]\n }\n values.borderRadius = readStoredString(stored, 'borderRadius') || ECOMMERCE_BRANDING_DEFAULTS.borderRadius\n values.fontFamilyBase = readStoredString(stored, 'fontFamilyBase') || DEFAULT_BASE_FONT_ID\n values.fontFamilyHeading = readStoredString(stored, 'fontFamilyHeading') || BRANDING_FONT_INHERIT\n values.logoUrl = readStoredString(stored, 'logoUrl')\n values.faviconUrl = readStoredString(stored, 'faviconUrl')\n return values\n}\n\nfunction readFormString(values: Record<string, unknown>, key: string): string {\n const value = values[key]\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction defaultValueFor(key: StoreBrandingFormKey): string | null {\n if (key === 'fontFamilyBase') return DEFAULT_BASE_FONT_ID\n if (key === 'fontFamilyHeading') return BRANDING_FONT_INHERIT\n if (key === 'logoUrl' || key === 'faviconUrl') return null\n return ECOMMERCE_BRANDING_DEFAULTS[key]\n}\n\nexport function buildStoreBrandingInput(values: Record<string, unknown>): StoreBrandingPayload {\n const input: StoreBrandingPayload = {}\n for (const key of ALL_KEYS) {\n const value = readFormString(values, key)\n if (value === '' || value === BRANDING_FONT_INHERIT) continue\n input[key] = value\n }\n return input\n}\n\nexport function buildStoreBrandingPayload(\n values: Record<string, unknown>,\n store: Pick<StoreAdminRecord, 'settings'>,\n): StoreBrandingPayload {\n const stored = readStoredBranding(store)\n const input = buildStoreBrandingInput(values)\n const payload: StoreBrandingPayload = {}\n for (const key of ALL_KEYS) {\n const value = input[key]\n if (value === undefined) continue\n const wasStored = readStoredString(stored, key) !== ''\n if (!wasStored && value === defaultValueFor(key)) continue\n payload[key] = value\n }\n return payload\n}\n\nconst brandingFormShape = {\n primaryColor: z.string(),\n primaryForeground: z.string(),\n accentColor: z.string(),\n accentForeground: z.string(),\n backgroundColor: z.string(),\n foregroundColor: z.string(),\n borderRadius: z.string(),\n fontFamilyBase: z.string(),\n fontFamilyHeading: z.string(),\n logoUrl: z.string(),\n faviconUrl: z.string(),\n} satisfies Record<StoreBrandingFormKey, z.ZodString>\n\nexport const storeBrandingFormSchema = z.object(brandingFormShape).superRefine((values, context) => {\n const result = ecommerceStoreBrandingSchema.safeParse(buildStoreBrandingInput(values))\n if (result.success) return\n for (const issue of result.error.issues) {\n context.addIssue({ code: 'custom', path: issue.path, message: issue.message })\n }\n})\n\nexport function parseRadiusRem(value: string): number {\n const trimmed = value.trim()\n if (trimmed === '0') return 0\n const match = /^(\\d+(?:\\.\\d+)?|\\.\\d+)(rem|px)$/.exec(trimmed)\n if (!match) return parseRadiusRem(ECOMMERCE_BRANDING_DEFAULTS.borderRadius)\n const amount = Number(match[1])\n const rem = match[2] === 'px' ? amount / PX_PER_REM : amount\n return Math.min(RADIUS_SLIDER_MAX, Math.max(RADIUS_SLIDER_MIN, rem))\n}\n\nexport function formatRadiusRem(rem: number): string {\n if (rem <= 0) return '0'\n return `${Number(rem.toFixed(3))}rem`\n}\n\nexport type SanitizedPreviewBranding = Partial<Record<StoreBrandingFormKey, string>>\n\nexport function sanitizePreviewBranding(values: Record<string, unknown>): SanitizedPreviewBranding {\n const input = buildStoreBrandingInput(values)\n const sanitized: SanitizedPreviewBranding = {}\n for (const key of BRANDING_COLOR_KEYS) {\n const raw = input[key]\n const normalized = raw === undefined ? null : normalizeBrandingColor(raw)\n if (normalized !== null) sanitized[key] = normalized\n }\n const radius = input.borderRadius\n if (radius !== undefined && isValidBorderRadius(radius)) sanitized.borderRadius = radius\n for (const key of BRANDING_FONT_KEYS) {\n const raw = input[key]\n if (raw !== undefined && findEcommerceBrandingFont(raw)) sanitized[key] = raw\n }\n const logo = input.logoUrl\n if (logo !== undefined && isSafeAssetUrl(logo)) sanitized.logoUrl = logo\n return sanitized\n}\n\nexport type BrandingPreviewLabels = {\n storeName: string\n navShop: string\n navCategories: string\n navCart: string\n heroTitle: string\n heroText: string\n heroButton: string\n productName: string\n productPrice: string\n}\n\nconst HTML_ESCAPES: Readonly<Record<string, string>> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n}\n\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => HTML_ESCAPES[character] ?? character)\n}\n\nconst PREVIEW_CONTENT_SECURITY_POLICY = \"default-src 'none'; style-src 'unsafe-inline'; img-src http: https:\"\n\nconst PREVIEW_STATIC_CSS = [\n '*{box-sizing:border-box}',\n 'body{margin:0;background:var(--background);color:var(--foreground);font-family:var(--font-base);font-size:14px;line-height:1.5}',\n 'h1,.brand{font-family:var(--font-heading)}',\n '.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 20px;border-bottom:1px solid color-mix(in oklch,var(--foreground) 15%,transparent)}',\n '.brand{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:700}',\n '.brand img{max-width:120px;max-height:28px}',\n 'nav{display:flex;gap:16px;opacity:.7}',\n '.hero{padding:28px 20px;background:var(--accent);color:var(--accent-foreground)}',\n '.hero h1{margin:0 0 8px;font-size:24px;line-height:1.2}',\n '.hero p{margin:0 0 16px;max-width:36ch}',\n '.btn{display:inline-block;padding:8px 16px;border-radius:var(--radius);background:var(--primary);color:var(--primary-foreground);font-weight:600}',\n '.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;padding:20px}',\n '.card{overflow:hidden;border:1px solid color-mix(in oklch,var(--foreground) 15%,transparent);border-radius:var(--radius)}',\n '.thumb{height:56px;background:color-mix(in oklch,var(--primary) 20%,var(--background))}',\n '.card-body{padding:10px}',\n '.price{font-weight:600}',\n].join('')\n\nexport function buildBrandingPreviewDocument(values: Record<string, unknown>, labels: BrandingPreviewLabels): string {\n const branding = sanitizePreviewBranding(values)\n const rootRule = renderBrandingRootRule(branding)\n const logo = branding.logoUrl ? `<img src=\"${escapeHtml(branding.logoUrl)}\" alt=\"\">` : ''\n const card = `<div class=\"card\"><div class=\"thumb\"></div><div class=\"card-body\"><div>${escapeHtml(labels.productName)}</div><div class=\"price\">${escapeHtml(labels.productPrice)}</div></div></div>`\n return [\n '<!doctype html><html><head><meta charset=\"utf-8\">',\n `<meta http-equiv=\"Content-Security-Policy\" content=\"${PREVIEW_CONTENT_SECURITY_POLICY}\">`,\n `<style>${rootRule}</style><style>${PREVIEW_STATIC_CSS}</style></head><body>`,\n `<header class=\"bar\"><div class=\"brand\">${logo}<span>${escapeHtml(labels.storeName)}</span></div>`,\n `<nav><span>${escapeHtml(labels.navShop)}</span><span>${escapeHtml(labels.navCategories)}</span><span>${escapeHtml(labels.navCart)}</span></nav></header>`,\n `<section class=\"hero\"><h1>${escapeHtml(labels.heroTitle)}</h1><p>${escapeHtml(labels.heroText)}</p><span class=\"btn\">${escapeHtml(labels.heroButton)}</span></section>`,\n `<section class=\"grid\">${card}${card}${card}</section>`,\n '</body></html>',\n ].join('')\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { ecommerceStoreBrandingSchema, isSafeAssetUrl } from '../data/validators'\nimport {\n ECOMMERCE_BRANDING_COLOR_KEYS,\n ECOMMERCE_BRANDING_DEFAULTS,\n ECOMMERCE_BRANDING_FONT_KEYS,\n findEcommerceBrandingFont,\n isValidBorderRadius,\n normalizeBrandingColor,\n renderBrandingRootRule,\n type EcommerceBrandingColorKey,\n type EcommerceBrandingFontKey,\n} from '../lib/brandingStyles'\nimport type { StoreAdminRecord } from './storeAdmin'\n\nexport const BRANDING_COLOR_KEYS = ECOMMERCE_BRANDING_COLOR_KEYS\nexport const BRANDING_FONT_KEYS = ECOMMERCE_BRANDING_FONT_KEYS\nexport const BRANDING_ASSET_KEYS = ['logoUrl', 'faviconUrl'] as const\n\nexport type BrandingAssetKey = (typeof BRANDING_ASSET_KEYS)[number]\nexport type StoreBrandingFormKey =\n | EcommerceBrandingColorKey\n | EcommerceBrandingFontKey\n | BrandingAssetKey\n | 'borderRadius'\n\nexport type StoreBrandingFormValues = Record<StoreBrandingFormKey, string>\n\nexport type StoreBrandingPayload = Partial<Record<StoreBrandingFormKey, string>>\n\nexport const BRANDING_FONT_INHERIT = 'inherit'\nexport const DEFAULT_BASE_FONT_ID = 'inter'\nexport const RADIUS_SLIDER_MIN = 0\nexport const RADIUS_SLIDER_MAX = 5\nexport const RADIUS_SLIDER_STEP = 0.125\nconst PX_PER_REM = 16\n\nexport const STORE_BRANDING_API_PATH = (storeId: string) => `/api/ecommerce/stores/${encodeURIComponent(storeId)}/branding`\n\nconst ALL_KEYS: readonly StoreBrandingFormKey[] = [\n ...BRANDING_COLOR_KEYS,\n 'borderRadius',\n ...BRANDING_FONT_KEYS,\n ...BRANDING_ASSET_KEYS,\n]\n\nfunction readStoredBranding(store: Pick<StoreAdminRecord, 'settings'>): Record<string, unknown> {\n const branding = store.settings?.branding\n return typeof branding === 'object' && branding !== null && !Array.isArray(branding)\n ? (branding as Record<string, unknown>)\n : {}\n}\n\nfunction readStoredString(stored: Record<string, unknown>, key: string): string {\n const value = stored[key]\n return typeof value === 'string' ? value : ''\n}\n\nexport function buildStoreBrandingInitialValues(store: Pick<StoreAdminRecord, 'settings'>): StoreBrandingFormValues {\n const stored = readStoredBranding(store)\n const values = {} as StoreBrandingFormValues\n for (const key of BRANDING_COLOR_KEYS) {\n values[key] = readStoredString(stored, key) || ECOMMERCE_BRANDING_DEFAULTS[key]\n }\n values.borderRadius = readStoredString(stored, 'borderRadius') || ECOMMERCE_BRANDING_DEFAULTS.borderRadius\n values.fontFamilyBase = readStoredString(stored, 'fontFamilyBase') || DEFAULT_BASE_FONT_ID\n values.fontFamilyHeading = readStoredString(stored, 'fontFamilyHeading') || BRANDING_FONT_INHERIT\n values.logoUrl = readStoredString(stored, 'logoUrl')\n values.faviconUrl = readStoredString(stored, 'faviconUrl')\n return values\n}\n\nfunction readFormString(values: Record<string, unknown>, key: string): string {\n const value = values[key]\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction defaultValueFor(key: StoreBrandingFormKey): string | null {\n if (key === 'fontFamilyBase') return DEFAULT_BASE_FONT_ID\n if (key === 'fontFamilyHeading') return BRANDING_FONT_INHERIT\n if (key === 'logoUrl' || key === 'faviconUrl') return null\n return ECOMMERCE_BRANDING_DEFAULTS[key]\n}\n\nexport function buildStoreBrandingInput(values: Record<string, unknown>): StoreBrandingPayload {\n const input: StoreBrandingPayload = {}\n for (const key of ALL_KEYS) {\n const value = readFormString(values, key)\n if (value === '' || value === BRANDING_FONT_INHERIT) continue\n input[key] = value\n }\n return input\n}\n\nexport function buildStoreBrandingPayload(\n values: Record<string, unknown>,\n store: Pick<StoreAdminRecord, 'settings'>,\n): StoreBrandingPayload {\n const stored = readStoredBranding(store)\n const input = buildStoreBrandingInput(values)\n const payload: StoreBrandingPayload = {}\n for (const key of ALL_KEYS) {\n const value = input[key]\n if (value === undefined) continue\n const wasStored = readStoredString(stored, key) !== ''\n if (!wasStored && value === defaultValueFor(key)) continue\n payload[key] = value\n }\n return payload\n}\n\nconst brandingFormShape = {\n primaryColor: z.string(),\n primaryForeground: z.string(),\n accentColor: z.string(),\n accentForeground: z.string(),\n backgroundColor: z.string(),\n foregroundColor: z.string(),\n borderRadius: z.string(),\n fontFamilyBase: z.string(),\n fontFamilyHeading: z.string(),\n logoUrl: z.string(),\n faviconUrl: z.string(),\n} satisfies Record<StoreBrandingFormKey, z.ZodString>\n\nexport const storeBrandingFormSchema = z.object(brandingFormShape).superRefine((values, context) => {\n const result = ecommerceStoreBrandingSchema.safeParse(buildStoreBrandingInput(values))\n if (result.success) return\n for (const issue of result.error.issues) {\n context.addIssue({ code: 'custom', path: issue.path, message: issue.message })\n }\n})\n\nexport function parseRadiusRem(value: string): number {\n const trimmed = value.trim()\n if (trimmed === '0') return 0\n const match = /^(\\d+(?:\\.\\d+)?|\\.\\d+)(rem|px)$/.exec(trimmed)\n if (!match) return parseRadiusRem(ECOMMERCE_BRANDING_DEFAULTS.borderRadius)\n const amount = Number(match[1])\n const rem = match[2] === 'px' ? amount / PX_PER_REM : amount\n return Math.min(RADIUS_SLIDER_MAX, Math.max(RADIUS_SLIDER_MIN, rem))\n}\n\nexport function formatRadiusRem(rem: number): string {\n if (rem <= 0) return '0'\n return `${Number(rem.toFixed(3))}rem`\n}\n\nexport type SanitizedPreviewBranding = Partial<Record<StoreBrandingFormKey, string>>\n\nexport function sanitizePreviewBranding(values: Record<string, unknown>): SanitizedPreviewBranding {\n const input = buildStoreBrandingInput(values)\n const sanitized: SanitizedPreviewBranding = {}\n for (const key of BRANDING_COLOR_KEYS) {\n const raw = input[key]\n const normalized = raw === undefined ? null : normalizeBrandingColor(raw)\n if (normalized !== null) sanitized[key] = normalized\n }\n const radius = input.borderRadius\n if (radius !== undefined && isValidBorderRadius(radius)) sanitized.borderRadius = radius\n for (const key of BRANDING_FONT_KEYS) {\n const raw = input[key]\n if (raw !== undefined && findEcommerceBrandingFont(raw)) sanitized[key] = raw\n }\n const logo = input.logoUrl\n if (logo !== undefined && isSafeAssetUrl(logo)) sanitized.logoUrl = logo\n return sanitized\n}\n\nexport type BrandingPreviewLabels = {\n storeName: string\n navShop: string\n navCategories: string\n navCart: string\n heroTitle: string\n heroText: string\n heroButton: string\n productName: string\n productPrice: string\n}\n\nconst HTML_ESCAPES: Readonly<Record<string, string>> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n}\n\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => HTML_ESCAPES[character] ?? character)\n}\n\nconst PREVIEW_STATIC_CSS = [\n '*{box-sizing:border-box}',\n 'body{margin:0;background:var(--background);color:var(--foreground);font-family:var(--font-base);font-size:14px;line-height:1.5}',\n 'h1,.brand{font-family:var(--font-heading)}',\n '.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 20px;border-bottom:1px solid color-mix(in oklch,var(--foreground) 15%,transparent)}',\n '.brand{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:700}',\n '.brand img{max-width:120px;max-height:28px}',\n 'nav{display:flex;gap:16px;opacity:.7}',\n '.hero{padding:28px 20px;background:var(--accent);color:var(--accent-foreground)}',\n '.hero h1{margin:0 0 8px;font-size:24px;line-height:1.2}',\n '.hero p{margin:0 0 16px;max-width:36ch}',\n '.btn{display:inline-block;padding:8px 16px;border-radius:var(--radius);background:var(--primary);color:var(--primary-foreground);font-weight:600}',\n '.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;padding:20px}',\n '.card{overflow:hidden;border:1px solid color-mix(in oklch,var(--foreground) 15%,transparent);border-radius:var(--radius)}',\n '.thumb{height:56px;background:color-mix(in oklch,var(--primary) 20%,var(--background))}',\n '.card-body{padding:10px}',\n '.price{font-weight:600}',\n].join('')\n\nexport function buildBrandingPreviewDocument(values: Record<string, unknown>, labels: BrandingPreviewLabels, nonce?: string): string {\n const branding = sanitizePreviewBranding(values)\n const styleNonce = typeof nonce === 'string' && /^[A-Za-z0-9+/_-]{8,128}={0,2}$/.test(nonce) ? nonce : null\n const styleAttribute = styleNonce ? ` nonce=\"${styleNonce}\"` : ''\n const styleSource = styleNonce ? `'nonce-${styleNonce}'` : \"'unsafe-inline'\"\n const contentSecurityPolicy = `default-src 'none'; style-src ${styleSource}; img-src http: https:`\n const rootRule = renderBrandingRootRule(branding)\n const logo = branding.logoUrl ? `<img src=\"${escapeHtml(branding.logoUrl)}\" alt=\"\">` : ''\n const card = `<div class=\"card\"><div class=\"thumb\"></div><div class=\"card-body\"><div>${escapeHtml(labels.productName)}</div><div class=\"price\">${escapeHtml(labels.productPrice)}</div></div></div>`\n return [\n '<!doctype html><html><head><meta charset=\"utf-8\">',\n `<meta http-equiv=\"Content-Security-Policy\" content=\"${contentSecurityPolicy}\">`,\n `<style${styleAttribute}>${rootRule}</style><style${styleAttribute}>${PREVIEW_STATIC_CSS}</style></head><body>`,\n `<header class=\"bar\"><div class=\"brand\">${logo}<span>${escapeHtml(labels.storeName)}</span></div>`,\n `<nav><span>${escapeHtml(labels.navShop)}</span><span>${escapeHtml(labels.navCategories)}</span><span>${escapeHtml(labels.navCart)}</span></nav></header>`,\n `<section class=\"hero\"><h1>${escapeHtml(labels.heroTitle)}</h1><p>${escapeHtml(labels.heroText)}</p><span class=\"btn\">${escapeHtml(labels.heroButton)}</span></section>`,\n `<section class=\"grid\">${card}${card}${card}</section>`,\n '</body></html>',\n ].join('')\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7808.1.8128cefd28",
3
+ "version": "0.10.1-develop.7809.1.ea24461aeb",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.4.3"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7808.1.8128cefd28",
270
- "@open-mercato/shared": "0.10.1-develop.7808.1.8128cefd28",
271
- "@open-mercato/ui": "0.10.1-develop.7808.1.8128cefd28",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7809.1.ea24461aeb",
270
+ "@open-mercato/shared": "0.10.1-develop.7809.1.ea24461aeb",
271
+ "@open-mercato/ui": "0.10.1-develop.7809.1.ea24461aeb",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7808.1.8128cefd28",
277
- "@open-mercato/shared": "0.10.1-develop.7808.1.8128cefd28",
278
- "@open-mercato/ui": "0.10.1-develop.7808.1.8128cefd28",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7809.1.ea24461aeb",
277
+ "@open-mercato/shared": "0.10.1-develop.7809.1.ea24461aeb",
278
+ "@open-mercato/ui": "0.10.1-develop.7809.1.ea24461aeb",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
5
5
  import { buildBrandingPreviewDocument } from './storeBrandingForm'
6
+ import { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'
6
7
 
7
8
  export const BRANDING_PREVIEW_DEBOUNCE_MS = 250
8
9
 
@@ -36,6 +37,7 @@ function useDebouncedValue<TValue>(value: TValue, delayMs: number): TValue {
36
37
  export function StoreBrandingPreview({ values, storeName, currencyCode }: StoreBrandingPreviewProps) {
37
38
  const t = useT()
38
39
  const locale = useLocale()
40
+ const nonce = useContentSecurityPolicyNonce()
39
41
  const debouncedValues = useDebouncedValue(values, BRANDING_PREVIEW_DEBOUNCE_MS)
40
42
  const document = React.useMemo(
41
43
  () =>
@@ -52,8 +54,8 @@ export function StoreBrandingPreview({ values, storeName, currencyCode }: StoreB
52
54
  heroButton: t('ecommerce.backend.store.branding.preview.heroButton', 'Shop now'),
53
55
  productName: t('ecommerce.backend.store.branding.preview.productName', 'Sample product'),
54
56
  productPrice: formatPreviewPrice(SAMPLE_PRODUCT_PRICE, currencyCode, locale),
55
- }),
56
- [currencyCode, debouncedValues, locale, storeName, t],
57
+ }, nonce),
58
+ [currencyCode, debouncedValues, locale, storeName, t, nonce],
57
59
  )
58
60
 
59
61
  return (
@@ -191,8 +191,6 @@ export function escapeHtml(value: string): string {
191
191
  return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character] ?? character)
192
192
  }
193
193
 
194
- const PREVIEW_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; img-src http: https:"
195
-
196
194
  const PREVIEW_STATIC_CSS = [
197
195
  '*{box-sizing:border-box}',
198
196
  'body{margin:0;background:var(--background);color:var(--foreground);font-family:var(--font-base);font-size:14px;line-height:1.5}',
@@ -212,15 +210,19 @@ const PREVIEW_STATIC_CSS = [
212
210
  '.price{font-weight:600}',
213
211
  ].join('')
214
212
 
215
- export function buildBrandingPreviewDocument(values: Record<string, unknown>, labels: BrandingPreviewLabels): string {
213
+ export function buildBrandingPreviewDocument(values: Record<string, unknown>, labels: BrandingPreviewLabels, nonce?: string): string {
216
214
  const branding = sanitizePreviewBranding(values)
215
+ const styleNonce = typeof nonce === 'string' && /^[A-Za-z0-9+/_-]{8,128}={0,2}$/.test(nonce) ? nonce : null
216
+ const styleAttribute = styleNonce ? ` nonce="${styleNonce}"` : ''
217
+ const styleSource = styleNonce ? `'nonce-${styleNonce}'` : "'unsafe-inline'"
218
+ const contentSecurityPolicy = `default-src 'none'; style-src ${styleSource}; img-src http: https:`
217
219
  const rootRule = renderBrandingRootRule(branding)
218
220
  const logo = branding.logoUrl ? `<img src="${escapeHtml(branding.logoUrl)}" alt="">` : ''
219
221
  const card = `<div class="card"><div class="thumb"></div><div class="card-body"><div>${escapeHtml(labels.productName)}</div><div class="price">${escapeHtml(labels.productPrice)}</div></div></div>`
220
222
  return [
221
223
  '<!doctype html><html><head><meta charset="utf-8">',
222
- `<meta http-equiv="Content-Security-Policy" content="${PREVIEW_CONTENT_SECURITY_POLICY}">`,
223
- `<style>${rootRule}</style><style>${PREVIEW_STATIC_CSS}</style></head><body>`,
224
+ `<meta http-equiv="Content-Security-Policy" content="${contentSecurityPolicy}">`,
225
+ `<style${styleAttribute}>${rootRule}</style><style${styleAttribute}>${PREVIEW_STATIC_CSS}</style></head><body>`,
224
226
  `<header class="bar"><div class="brand">${logo}<span>${escapeHtml(labels.storeName)}</span></div>`,
225
227
  `<nav><span>${escapeHtml(labels.navShop)}</span><span>${escapeHtml(labels.navCategories)}</span><span>${escapeHtml(labels.navCart)}</span></nav></header>`,
226
228
  `<section class="hero"><h1>${escapeHtml(labels.heroTitle)}</h1><p>${escapeHtml(labels.heroText)}</p><span class="btn">${escapeHtml(labels.heroButton)}</span></section>`,