cms-renderer 0.6.0 → 0.6.1

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.
@@ -36,6 +36,10 @@ type WalkVisitors = {
36
36
  */
37
37
  onElement?: (info: ElementInfo) => React__default.ReactElement;
38
38
  };
39
+ /**
40
+ * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.
41
+ * SSR-safe: does not touch DOM APIs.
42
+ */
39
43
  declare function walkReactNode(node: React__default.ReactNode, visitors: WalkVisitors, ctx?: {
40
44
  path?: Array<string | number>;
41
45
  parentType?: React__default.ElementType;
@@ -71,10 +71,6 @@ function extractContentValues(content, basePath = []) {
71
71
  function CmsEditableInit() {
72
72
  return /* @__PURE__ */ jsxs(Fragment, { children: [
73
73
  /* @__PURE__ */ jsx("style", { children: `
74
- [data-cms-block]:hover {
75
- outline: 2px solid #3b82f6;
76
- outline-offset: 4px;
77
- }
78
74
  [data-cms-editable] {
79
75
  cursor: pointer;
80
76
  border-radius: 2px;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { ClientEditableBlock } from './client-editable-block';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\n// Elements where injecting a <span> would break layout or produce invalid HTML\nconst SKIP_SPAN_PARENTS = new Set([\n // SVG text-bearing elements — spans are not valid SVG children\n 'text',\n 'tspan',\n 'textPath',\n 'desc',\n // Form elements whose text content must not be interrupted\n 'option',\n 'optgroup',\n // Non-rendered elements\n 'script',\n 'style',\n 'noscript',\n]);\n\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n inSvg: ctx.inSvg,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Track SVG context so we never inject <span> inside SVG subtrees\n const nextInSvg = ctx.inSvg || el.type === 'svg';\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n inSvg: nextInSvg,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// CmsEditableInit — render once per page in edit mode\n// -----------------------------------------------------------------------------\n\n/**\n * Renders the shared CMS edit-mode styles and click-routing script.\n * Place this once at the top of your page when edit_mode is active.\n */\nexport function CmsEditableInit() {\n return (\n <>\n <style>{`\n [data-cms-block]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 4px;\n }\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n `,\n }}\n />\n </>\n );\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n /**\n * The current URL path (e.g. \"/en/test\").\n * When provided, enables path-namespaced component lookup.\n * Registry keys like \"/{lang}/test Article\" will be matched against this path,\n * where `{x}` and `(x)` are treated as single-segment wildcards.\n */\n path?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Path-namespaced component resolution\n// -----------------------------------------------------------------------------\n\n/**\n * Returns true if each segment of `path` matches the corresponding segment in\n * `pattern`, where a pattern segment wrapped in `{…}` or `(…)` is a wildcard.\n */\nfunction pathMatchesPattern(path: string, pattern: string): boolean {\n const pathSegs = path.split('/').filter(Boolean);\n const patternSegs = pattern.split('/').filter(Boolean);\n if (pathSegs.length !== patternSegs.length) return false;\n for (let i = 0; i < patternSegs.length; i++) {\n const seg = patternSegs[i];\n if (!seg) return false;\n if ((seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('(') && seg.endsWith(')'))) {\n continue;\n }\n if (seg !== pathSegs[i]) return false;\n }\n return true;\n}\n\n/**\n * Resolves the component for `blockType` from the registry.\n *\n * When `path` is provided, keys of the form `\"/{pattern} BlockType\"` are\n * checked first. The first key whose path pattern matches the current path\n * and whose block-type suffix matches `blockType` wins. Falls back to a\n * direct `registry[blockType]` lookup.\n */\nfunction resolveComponent(\n registry: Partial<BlockComponentRegistry>,\n blockType: string,\n path?: string\n): BlockComponentRegistry[string] | undefined {\n if (path) {\n for (const key of Object.keys(registry)) {\n if (!key.startsWith('/')) continue;\n const spaceIdx = key.indexOf(' ');\n if (spaceIdx === -1) continue;\n const pathPattern = key.slice(0, spaceIdx);\n const registeredType = key.slice(spaceIdx + 1);\n if (registeredType !== blockType) continue;\n if (pathMatchesPattern(path, pathPattern)) {\n return registry[key];\n }\n }\n }\n return registry[blockType];\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * In editable mode, wraps the block in a ClientEditableBlock that:\n * - Stamps data-cms-block attributes directly on the component's root element\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into the component's root element\n *\n * Render CmsEditableInit once at the page level to include the shared styles\n * and click-routing script.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n path,\n}: BlockRendererProps) {\n const Component = resolveComponent(registry, block.type, path);\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Build content entries for client-side text matching\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n const contentEntries = Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p);\n\n return (\n <ClientEditableBlock blockId={block.id} blockType={block.type} contentEntries={contentEntries}>\n {component}\n </ClientEditableBlock>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { ClientEditableBlock } from './client-editable-block';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n inSvg: ctx.inSvg,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Track SVG context so we never inject <span> inside SVG subtrees\n const nextInSvg = ctx.inSvg || el.type === 'svg';\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n inSvg: nextInSvg,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// CmsEditableInit — render once per page in edit mode\n// -----------------------------------------------------------------------------\n\n/**\n * Renders the shared CMS edit-mode styles and click-routing script.\n * Place this once at the top of your page when edit_mode is active.\n */\nexport function CmsEditableInit() {\n return (\n <>\n <style>{`\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n `,\n }}\n />\n </>\n );\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n /**\n * The current URL path (e.g. \"/en/test\").\n * When provided, enables path-namespaced component lookup.\n * Registry keys like \"/{lang}/test Article\" will be matched against this path,\n * where `{x}` and `(x)` are treated as single-segment wildcards.\n */\n path?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Path-namespaced component resolution\n// -----------------------------------------------------------------------------\n\n/**\n * Returns true if each segment of `path` matches the corresponding segment in\n * `pattern`, where a pattern segment wrapped in `{…}` or `(…)` is a wildcard.\n */\nfunction pathMatchesPattern(path: string, pattern: string): boolean {\n const pathSegs = path.split('/').filter(Boolean);\n const patternSegs = pattern.split('/').filter(Boolean);\n if (pathSegs.length !== patternSegs.length) return false;\n for (let i = 0; i < patternSegs.length; i++) {\n const seg = patternSegs[i];\n if (!seg) return false;\n if ((seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('(') && seg.endsWith(')'))) {\n continue;\n }\n if (seg !== pathSegs[i]) return false;\n }\n return true;\n}\n\n/**\n * Resolves the component for `blockType` from the registry.\n *\n * When `path` is provided, keys of the form `\"/{pattern} BlockType\"` are\n * checked first. The first key whose path pattern matches the current path\n * and whose block-type suffix matches `blockType` wins. Falls back to a\n * direct `registry[blockType]` lookup.\n */\nfunction resolveComponent(\n registry: Partial<BlockComponentRegistry>,\n blockType: string,\n path?: string\n): BlockComponentRegistry[string] | undefined {\n if (path) {\n for (const key of Object.keys(registry)) {\n if (!key.startsWith('/')) continue;\n const spaceIdx = key.indexOf(' ');\n if (spaceIdx === -1) continue;\n const pathPattern = key.slice(0, spaceIdx);\n const registeredType = key.slice(spaceIdx + 1);\n if (registeredType !== blockType) continue;\n if (pathMatchesPattern(path, pathPattern)) {\n return registry[key];\n }\n }\n }\n return registry[blockType];\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * In editable mode, wraps the block in a ClientEditableBlock that:\n * - Stamps data-cms-block attributes directly on the component's root element\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into the component's root element\n *\n * Render CmsEditableInit once at the page level to include the shared styles\n * and click-routing script.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n path,\n}: BlockRendererProps) {\n const Component = resolveComponent(registry, block.type, path);\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Build content entries for client-side text matching\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n const contentEntries = Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p);\n\n return (\n <ClientEditableBlock blockId={block.id} blockType={block.type} contentEntries={contentEntries}>\n {component}\n </ClientEditableBlock>\n );\n}\n"],"mappings":"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","names":[]}
@@ -253,7 +253,6 @@ function ClientEditableBlock({
253
253
  if (text.indexOf(entry.v) !== -1 && text.trim() === entry.v.trim()) {
254
254
  used.add(entry.p);
255
255
  const span = document.createElement("span");
256
- span.style.display = "contents";
257
256
  span.setAttribute("data-cms-editable", "");
258
257
  span.setAttribute("data-block-id", blockId);
259
258
  span.setAttribute("data-block-type", blockType);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/client-editable-block.tsx","../../lib/block-outline.tsx","../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useLayoutEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { BlockOutline } from './block-outline';\nimport { BlockToolbar } from './block-toolbar';\n\n// ---------------------------------------------------------------------------\n// Global styles + click-routing — injected once into <head> on first mount.\n// We do this programmatically because Next.js RSC marks body-level <style> and\n// <script> tags as inactive (they appear greyed-out in DevTools and are not\n// applied by the browser).\n// ---------------------------------------------------------------------------\n\nconst CMS_STYLES = `\n [data-cms-editable] { cursor: pointer; border-radius: 2px; }\n [data-cms-editable]:hover { outline: 2px solid #3b82f6; outline-offset: 2px; }\n .cms-block-toolbar {\n position: fixed;\n display: flex; gap: 4px; background: #1f2937; border-radius: 6px; padding: 4px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n transition: opacity 0.15s ease; z-index: 99999;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex; align-items: center; justify-content: center;\n width: 28px; height: 28px; border: none; background: transparent;\n color: #9ca3af; border-radius: 4px; cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover { background: #374151; color: #fff; }\n .cms-block-toolbar button.delete:hover { background: #dc2626; color: #fff; }\n .cms-block-toolbar button:disabled { opacity: 0.4; cursor: not-allowed; }\n .cms-block-toolbar button:disabled:hover { background: transparent; color: #9ca3af; }\n .cms-block-toolbar svg { width: 16px; height: 16px; }\n`;\n\nlet cmsGlobalInjected = false;\n\nfunction ensureCmsGlobals() {\n if (cmsGlobalInjected) return;\n cmsGlobalInjected = true;\n\n const style = document.createElement('style');\n style.setAttribute('data-cms', '');\n style.textContent = CMS_STYLES;\n document.head.appendChild(style);\n\n if (!(window as Window & { __cmsEditableInitialized?: boolean }).__cmsEditableInitialized) {\n (window as Window & { __cmsEditableInitialized?: boolean }).__cmsEditableInitialized = true;\n document.addEventListener('click', (e) => {\n const target = e.target as Element;\n if (target.closest('.cms-block-toolbar')) return;\n\n const editable = target.closest('[data-cms-editable]');\n if (editable) {\n const msg = {\n type: 'cms-editable-click',\n blockId: editable.getAttribute('data-block-id'),\n blockType: editable.getAttribute('data-block-type'),\n contentPath: editable.getAttribute('data-content-path'),\n };\n if (window.parent && window.parent !== window) window.parent.postMessage(msg, '*');\n return;\n }\n\n const block = target.closest('[data-cms-block]');\n if (block) {\n const msg = {\n type: 'cms-editable-click',\n blockId: block.getAttribute('data-block-id'),\n blockType: block.getAttribute('data-block-type'),\n contentPath: null,\n };\n if (window.parent && window.parent !== window) window.parent.postMessage(msg, '*');\n }\n });\n }\n}\n\ninterface ContentEntry {\n v: string;\n p: string;\n}\n\ninterface ClientEditableBlockProps {\n blockId: string;\n blockType: string;\n contentEntries: ContentEntry[];\n children: React.ReactNode;\n}\n\n/**\n * Client-side block editable wrapper.\n *\n * Renders a hidden sentinel span followed by the block's children (no wrapper div).\n * On mount, finds the component's root element via nextElementSibling and:\n * - Stamps data-cms-block, data-block-id, data-block-type directly on it\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into document.body with position:fixed so it is\n * never clipped by overflow:hidden or stacking contexts on the block itself\n *\n * Uses a MutationObserver to re-inject spans after every DOM mutation\n * (e.g. animated state changes that swap visible elements).\n */\nexport function ClientEditableBlock({\n blockId,\n blockType,\n contentEntries,\n children,\n}: ClientEditableBlockProps) {\n const sentinelRef = useRef<HTMLSpanElement>(null);\n const observerRef = useRef<MutationObserver | null>(null);\n const isInjectingRef = useRef(false);\n const [toolbarStyle, setToolbarStyle] = useState<React.CSSProperties | null>(null);\n const [outlineRect, setOutlineRect] = useState<DOMRect | null>(null);\n\n const getBlockRoot = useCallback((): HTMLElement | null => {\n return (sentinelRef.current?.nextElementSibling as HTMLElement) ?? null;\n }, []);\n\n const injectSpans = useCallback(() => {\n if (isInjectingRef.current) return;\n const blockRoot = getBlockRoot();\n if (!blockRoot) return;\n\n isInjectingRef.current = true;\n // Disconnect observer while we mutate the DOM to avoid re-entrant calls.\n observerRef.current?.disconnect();\n\n try {\n // Remove previously injected text-level spans, restoring the original text nodes.\n const existing = Array.from(\n blockRoot.querySelectorAll<HTMLElement>('span[data-cms-editable][data-content-path]')\n );\n for (const span of existing) {\n const parent = span.parentNode;\n if (!parent) continue;\n while (span.firstChild) parent.insertBefore(span.firstChild, span);\n parent.removeChild(span);\n }\n\n // Walk all visible text nodes and wrap the ones that match content values.\n const used = new Set<string>();\n const walker = document.createTreeWalker(blockRoot, NodeFilter.SHOW_TEXT, null);\n const textNodes: Text[] = [];\n let n = walker.nextNode();\n while (n !== null) {\n const textNode = n as Text;\n if (\n textNode.parentElement?.closest('.cms-block-toolbar') == null &&\n textNode.nodeValue?.trim()\n ) {\n textNodes.push(textNode);\n }\n n = walker.nextNode();\n }\n\n for (const textNode of textNodes) {\n // Never inject spans inside SVG — <span> is not valid SVG content\n if (textNode.parentElement?.closest('svg') != null) continue;\n\n const text = textNode.nodeValue;\n if (!text) continue;\n for (const entry of contentEntries) {\n if (used.has(entry.p)) continue;\n if (text.indexOf(entry.v) !== -1 && text.trim() === entry.v.trim()) {\n used.add(entry.p);\n const span = document.createElement('span');\n // Inline style ensures layout-transparency before the stylesheet cascade applies\n span.style.display = 'contents';\n span.setAttribute('data-cms-editable', '');\n span.setAttribute('data-block-id', blockId);\n span.setAttribute('data-block-type', blockType);\n span.setAttribute('data-content-path', entry.p);\n textNode.parentNode?.insertBefore(span, textNode);\n span.appendChild(textNode);\n break;\n }\n }\n }\n } finally {\n isInjectingRef.current = false;\n // Reconnect after our mutations are done.\n const root = getBlockRoot();\n if (root && observerRef.current) {\n observerRef.current.observe(root, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n }, [blockId, blockType, contentEntries, getBlockRoot]);\n\n useLayoutEffect(() => {\n ensureCmsGlobals();\n\n const blockRoot = getBlockRoot();\n if (!blockRoot) return;\n\n // Stamp CMS attributes directly on the component's root element.\n blockRoot.setAttribute('data-cms-block', '');\n blockRoot.setAttribute('data-block-id', blockId);\n blockRoot.setAttribute('data-block-type', blockType);\n\n // Toolbar: portal to document.body with position:fixed so it escapes\n // any overflow:hidden or stacking context on the block or its ancestors.\n const showToolbar = () => {\n const rect = blockRoot.getBoundingClientRect();\n setToolbarStyle({\n bottom: window.innerHeight - rect.bottom + 8,\n left: rect.left + rect.width / 2,\n transform: 'translateX(-50%)',\n });\n setOutlineRect(rect);\n };\n const hideToolbar = () => {\n setToolbarStyle(null);\n setOutlineRect(null);\n };\n\n blockRoot.addEventListener('mouseenter', showToolbar);\n blockRoot.addEventListener('mouseleave', hideToolbar);\n\n // Initial span injection after React's first commit.\n injectSpans();\n\n // Re-inject whenever the child component mutates the DOM.\n const observer = new MutationObserver(injectSpans);\n observerRef.current = observer;\n observer.observe(blockRoot, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n\n return () => {\n observer.disconnect();\n observerRef.current = null;\n blockRoot.removeEventListener('mouseenter', showToolbar);\n blockRoot.removeEventListener('mouseleave', hideToolbar);\n };\n }, [injectSpans, blockId, blockType, getBlockRoot]);\n\n return (\n <>\n <span ref={sentinelRef} style={{ display: 'none' }} aria-hidden />\n {children}\n {outlineRect && createPortal(<BlockOutline blockRect={outlineRect} />, document.body)}\n {toolbarStyle &&\n createPortal(<BlockToolbar blockId={blockId} style={toolbarStyle} />, document.body)}\n </>\n );\n}\n","'use client';\n\nimport { useLayoutEffect, useRef } from 'react';\n\n/**\n * Block Outline Component\n *\n * Renders a position:fixed border overlay around a hovered block.\n * Tries up to 3 offset values (4px → 0px → -2px inset) to keep every\n * edge of the border visible inside the viewport before giving up.\n */\nexport function BlockOutline({ blockRect }: { blockRect: DOMRect }) {\n const outlineRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n const el = outlineRef.current;\n if (!el) return;\n\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // Attempt progressively smaller offsets until the border is fully visible.\n // offset=4 → normal outline-offset feel\n // offset=0 → border flush with block edge\n // offset=-2 → border drawn inside the block (inset)\n const offsets = [4, 0, -2];\n\n for (const offset of offsets) {\n const pad = offset + 2; // 2px border + offset\n el.style.top = `${blockRect.top - pad}px`;\n el.style.left = `${blockRect.left - pad}px`;\n el.style.width = `${blockRect.width + pad * 2}px`;\n el.style.height = `${blockRect.height + pad * 2}px`;\n\n const rect = el.getBoundingClientRect();\n if (rect.top >= 0 && rect.left >= 0 && rect.right <= vw && rect.bottom <= vh) break;\n }\n }, [blockRect]);\n\n return (\n <div\n ref={outlineRef}\n data-cms-outline=\"true\"\n style={{\n position: 'fixed',\n border: '2px solid #3b82f6',\n borderRadius: '2px',\n pointerEvents: 'none',\n zIndex: 99998,\n boxSizing: 'border-box',\n }}\n />\n );\n}\n","'use client';\n\nimport { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react';\nimport type React from 'react';\nimport { useLayoutEffect, useRef } from 'react';\n\n/**\n * Block Toolbar Component\n *\n * Provides move up/down and delete controls for blocks in edit mode.\n * This is a Client Component because it requires onClick handlers.\n */\n\nexport function BlockToolbar({ blockId, style }: { blockId: string; style?: React.CSSProperties }) {\n const toolbarRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n const el = toolbarRef.current;\n if (!el) return;\n\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const MAX_ATTEMPTS = 3;\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n const rect = el.getBoundingClientRect();\n\n const offRight = rect.right > vw;\n const offLeft = rect.left < 0;\n const offTop = rect.top < 0;\n const offBottom = rect.bottom > vh;\n\n if (!offRight && !offLeft && !offTop && !offBottom) break;\n\n // Horizontal: remove the centering transform and pin to the edge that's clipped\n if (offRight) {\n el.style.left = `${vw - rect.width}px`;\n el.style.transform = 'none';\n } else if (offLeft) {\n el.style.left = '0px';\n el.style.transform = 'none';\n }\n\n // Vertical: toolbar uses `bottom` (distance from viewport bottom), so\n // top < 0 means bottom is too large — clamp it so top aligns to viewport edge\n if (offTop) {\n el.style.bottom = `${vh - rect.height}px`;\n } else if (offBottom) {\n el.style.bottom = '0px';\n }\n }\n }, []);\n\n const handleAction = (action: string) => {\n if (typeof window !== 'undefined' && window.parent && window.parent !== window) {\n window.parent.postMessage({ type: 'cms-block-action', action, blockId }, '*');\n }\n };\n\n return (\n <div ref={toolbarRef} className=\"cms-block-toolbar\" data-cms-toolbar=\"true\" style={style}>\n <button\n type=\"button\"\n title=\"Move up\"\n aria-label=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <ChevronUp />\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n aria-label=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <ChevronDown />\n </button>\n <button\n type=\"button\"\n title=\"Add block\"\n aria-label=\"Add block\"\n data-action=\"add-block\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('add-block');\n }}\n >\n <Plus />\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n aria-label=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <Trash2 />\n </button>\n </div>\n );\n}\n"],"mappings":"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","names":["useLayoutEffect","useRef","useLayoutEffect","useRef","jsx","jsx","jsxs","useRef","useLayoutEffect"]}
1
+ {"version":3,"sources":["../../lib/client-editable-block.tsx","../../lib/block-outline.tsx","../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useLayoutEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { BlockOutline } from './block-outline';\nimport { BlockToolbar } from './block-toolbar';\n\n// ---------------------------------------------------------------------------\n// Global styles + click-routing — injected once into <head> on first mount.\n// We do this programmatically because Next.js RSC marks body-level <style> and\n// <script> tags as inactive (they appear greyed-out in DevTools and are not\n// applied by the browser).\n// ---------------------------------------------------------------------------\n\nconst CMS_STYLES = `\n [data-cms-editable] { cursor: pointer; border-radius: 2px; }\n [data-cms-editable]:hover { outline: 2px solid #3b82f6; outline-offset: 2px; }\n .cms-block-toolbar {\n position: fixed;\n display: flex; gap: 4px; background: #1f2937; border-radius: 6px; padding: 4px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n transition: opacity 0.15s ease; z-index: 99999;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex; align-items: center; justify-content: center;\n width: 28px; height: 28px; border: none; background: transparent;\n color: #9ca3af; border-radius: 4px; cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover { background: #374151; color: #fff; }\n .cms-block-toolbar button.delete:hover { background: #dc2626; color: #fff; }\n .cms-block-toolbar button:disabled { opacity: 0.4; cursor: not-allowed; }\n .cms-block-toolbar button:disabled:hover { background: transparent; color: #9ca3af; }\n .cms-block-toolbar svg { width: 16px; height: 16px; }\n`;\n\nlet cmsGlobalInjected = false;\n\nfunction ensureCmsGlobals() {\n if (cmsGlobalInjected) return;\n cmsGlobalInjected = true;\n\n const style = document.createElement('style');\n style.setAttribute('data-cms', '');\n style.textContent = CMS_STYLES;\n document.head.appendChild(style);\n\n if (!(window as Window & { __cmsEditableInitialized?: boolean }).__cmsEditableInitialized) {\n (window as Window & { __cmsEditableInitialized?: boolean }).__cmsEditableInitialized = true;\n document.addEventListener('click', (e) => {\n const target = e.target as Element;\n if (target.closest('.cms-block-toolbar')) return;\n\n const editable = target.closest('[data-cms-editable]');\n if (editable) {\n const msg = {\n type: 'cms-editable-click',\n blockId: editable.getAttribute('data-block-id'),\n blockType: editable.getAttribute('data-block-type'),\n contentPath: editable.getAttribute('data-content-path'),\n };\n if (window.parent && window.parent !== window) window.parent.postMessage(msg, '*');\n return;\n }\n\n const block = target.closest('[data-cms-block]');\n if (block) {\n const msg = {\n type: 'cms-editable-click',\n blockId: block.getAttribute('data-block-id'),\n blockType: block.getAttribute('data-block-type'),\n contentPath: null,\n };\n if (window.parent && window.parent !== window) window.parent.postMessage(msg, '*');\n }\n });\n }\n}\n\ninterface ContentEntry {\n v: string;\n p: string;\n}\n\ninterface ClientEditableBlockProps {\n blockId: string;\n blockType: string;\n contentEntries: ContentEntry[];\n children: React.ReactNode;\n}\n\n/**\n * Client-side block editable wrapper.\n *\n * Renders a hidden sentinel span followed by the block's children (no wrapper div).\n * On mount, finds the component's root element via nextElementSibling and:\n * - Stamps data-cms-block, data-block-id, data-block-type directly on it\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into document.body with position:fixed so it is\n * never clipped by overflow:hidden or stacking contexts on the block itself\n *\n * Uses a MutationObserver to re-inject spans after every DOM mutation\n * (e.g. animated state changes that swap visible elements).\n */\nexport function ClientEditableBlock({\n blockId,\n blockType,\n contentEntries,\n children,\n}: ClientEditableBlockProps) {\n const sentinelRef = useRef<HTMLSpanElement>(null);\n const observerRef = useRef<MutationObserver | null>(null);\n const isInjectingRef = useRef(false);\n const [toolbarStyle, setToolbarStyle] = useState<React.CSSProperties | null>(null);\n const [outlineRect, setOutlineRect] = useState<DOMRect | null>(null);\n\n const getBlockRoot = useCallback((): HTMLElement | null => {\n return (sentinelRef.current?.nextElementSibling as HTMLElement) ?? null;\n }, []);\n\n const injectSpans = useCallback(() => {\n if (isInjectingRef.current) return;\n const blockRoot = getBlockRoot();\n if (!blockRoot) return;\n\n isInjectingRef.current = true;\n // Disconnect observer while we mutate the DOM to avoid re-entrant calls.\n observerRef.current?.disconnect();\n\n try {\n // Remove previously injected text-level spans, restoring the original text nodes.\n const existing = Array.from(\n blockRoot.querySelectorAll<HTMLElement>('span[data-cms-editable][data-content-path]')\n );\n for (const span of existing) {\n const parent = span.parentNode;\n if (!parent) continue;\n while (span.firstChild) parent.insertBefore(span.firstChild, span);\n parent.removeChild(span);\n }\n\n // Walk all visible text nodes and wrap the ones that match content values.\n const used = new Set<string>();\n const walker = document.createTreeWalker(blockRoot, NodeFilter.SHOW_TEXT, null);\n const textNodes: Text[] = [];\n let n = walker.nextNode();\n while (n !== null) {\n const textNode = n as Text;\n if (\n textNode.parentElement?.closest('.cms-block-toolbar') == null &&\n textNode.nodeValue?.trim()\n ) {\n textNodes.push(textNode);\n }\n n = walker.nextNode();\n }\n\n for (const textNode of textNodes) {\n // Never inject spans inside SVG — <span> is not valid SVG content\n if (textNode.parentElement?.closest('svg') != null) continue;\n\n const text = textNode.nodeValue;\n if (!text) continue;\n for (const entry of contentEntries) {\n if (used.has(entry.p)) continue;\n if (text.indexOf(entry.v) !== -1 && text.trim() === entry.v.trim()) {\n used.add(entry.p);\n const span = document.createElement('span');\n span.setAttribute('data-cms-editable', '');\n span.setAttribute('data-block-id', blockId);\n span.setAttribute('data-block-type', blockType);\n span.setAttribute('data-content-path', entry.p);\n textNode.parentNode?.insertBefore(span, textNode);\n span.appendChild(textNode);\n break;\n }\n }\n }\n } finally {\n isInjectingRef.current = false;\n // Reconnect after our mutations are done.\n const root = getBlockRoot();\n if (root && observerRef.current) {\n observerRef.current.observe(root, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n }, [blockId, blockType, contentEntries, getBlockRoot]);\n\n useLayoutEffect(() => {\n ensureCmsGlobals();\n\n const blockRoot = getBlockRoot();\n if (!blockRoot) return;\n\n // Stamp CMS attributes directly on the component's root element.\n blockRoot.setAttribute('data-cms-block', '');\n blockRoot.setAttribute('data-block-id', blockId);\n blockRoot.setAttribute('data-block-type', blockType);\n\n // Toolbar: portal to document.body with position:fixed so it escapes\n // any overflow:hidden or stacking context on the block or its ancestors.\n const showToolbar = () => {\n const rect = blockRoot.getBoundingClientRect();\n setToolbarStyle({\n bottom: window.innerHeight - rect.bottom + 8,\n left: rect.left + rect.width / 2,\n transform: 'translateX(-50%)',\n });\n setOutlineRect(rect);\n };\n const hideToolbar = () => {\n setToolbarStyle(null);\n setOutlineRect(null);\n };\n\n blockRoot.addEventListener('mouseenter', showToolbar);\n blockRoot.addEventListener('mouseleave', hideToolbar);\n\n // Initial span injection after React's first commit.\n injectSpans();\n\n // Re-inject whenever the child component mutates the DOM.\n const observer = new MutationObserver(injectSpans);\n observerRef.current = observer;\n observer.observe(blockRoot, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n\n return () => {\n observer.disconnect();\n observerRef.current = null;\n blockRoot.removeEventListener('mouseenter', showToolbar);\n blockRoot.removeEventListener('mouseleave', hideToolbar);\n };\n }, [injectSpans, blockId, blockType, getBlockRoot]);\n\n return (\n <>\n <span ref={sentinelRef} style={{ display: 'none' }} aria-hidden />\n {children}\n {outlineRect && createPortal(<BlockOutline blockRect={outlineRect} />, document.body)}\n {toolbarStyle &&\n createPortal(<BlockToolbar blockId={blockId} style={toolbarStyle} />, document.body)}\n </>\n );\n}\n","'use client';\n\nimport { useLayoutEffect, useRef } from 'react';\n\n/**\n * Block Outline Component\n *\n * Renders a position:fixed border overlay around a hovered block.\n * Tries up to 3 offset values (4px → 0px → -2px inset) to keep every\n * edge of the border visible inside the viewport before giving up.\n */\nexport function BlockOutline({ blockRect }: { blockRect: DOMRect }) {\n const outlineRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n const el = outlineRef.current;\n if (!el) return;\n\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // Attempt progressively smaller offsets until the border is fully visible.\n // offset=4 → normal outline-offset feel\n // offset=0 → border flush with block edge\n // offset=-2 → border drawn inside the block (inset)\n const offsets = [4, 0, -2];\n\n for (const offset of offsets) {\n const pad = offset + 2; // 2px border + offset\n el.style.top = `${blockRect.top - pad}px`;\n el.style.left = `${blockRect.left - pad}px`;\n el.style.width = `${blockRect.width + pad * 2}px`;\n el.style.height = `${blockRect.height + pad * 2}px`;\n\n const rect = el.getBoundingClientRect();\n if (rect.top >= 0 && rect.left >= 0 && rect.right <= vw && rect.bottom <= vh) break;\n }\n }, [blockRect]);\n\n return (\n <div\n ref={outlineRef}\n data-cms-outline=\"true\"\n style={{\n position: 'fixed',\n border: '2px solid #3b82f6',\n borderRadius: '2px',\n pointerEvents: 'none',\n zIndex: 99998,\n boxSizing: 'border-box',\n }}\n />\n );\n}\n","'use client';\n\nimport { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react';\nimport type React from 'react';\nimport { useLayoutEffect, useRef } from 'react';\n\n/**\n * Block Toolbar Component\n *\n * Provides move up/down and delete controls for blocks in edit mode.\n * This is a Client Component because it requires onClick handlers.\n */\n\nexport function BlockToolbar({ blockId, style }: { blockId: string; style?: React.CSSProperties }) {\n const toolbarRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n const el = toolbarRef.current;\n if (!el) return;\n\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const MAX_ATTEMPTS = 3;\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n const rect = el.getBoundingClientRect();\n\n const offRight = rect.right > vw;\n const offLeft = rect.left < 0;\n const offTop = rect.top < 0;\n const offBottom = rect.bottom > vh;\n\n if (!offRight && !offLeft && !offTop && !offBottom) break;\n\n // Horizontal: remove the centering transform and pin to the edge that's clipped\n if (offRight) {\n el.style.left = `${vw - rect.width}px`;\n el.style.transform = 'none';\n } else if (offLeft) {\n el.style.left = '0px';\n el.style.transform = 'none';\n }\n\n // Vertical: toolbar uses `bottom` (distance from viewport bottom), so\n // top < 0 means bottom is too large — clamp it so top aligns to viewport edge\n if (offTop) {\n el.style.bottom = `${vh - rect.height}px`;\n } else if (offBottom) {\n el.style.bottom = '0px';\n }\n }\n }, []);\n\n const handleAction = (action: string) => {\n if (typeof window !== 'undefined' && window.parent && window.parent !== window) {\n window.parent.postMessage({ type: 'cms-block-action', action, blockId }, '*');\n }\n };\n\n return (\n <div ref={toolbarRef} className=\"cms-block-toolbar\" data-cms-toolbar=\"true\" style={style}>\n <button\n type=\"button\"\n title=\"Move up\"\n aria-label=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <ChevronUp />\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n aria-label=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <ChevronDown />\n </button>\n <button\n type=\"button\"\n title=\"Add block\"\n aria-label=\"Add block\"\n data-action=\"add-block\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('add-block');\n }}\n >\n <Plus />\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n aria-label=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <Trash2 />\n </button>\n </div>\n );\n}\n"],"mappings":"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","names":["useLayoutEffect","useRef","useLayoutEffect","useRef","jsx","jsx","jsxs","useRef","useLayoutEffect"]}
@@ -269,10 +269,6 @@ function extractContentValues(content, basePath = []) {
269
269
  function CmsEditableInit() {
270
270
  return /* @__PURE__ */ jsxs(Fragment, { children: [
271
271
  /* @__PURE__ */ jsx("style", { children: `
272
- [data-cms-block]:hover {
273
- outline: 2px solid #3b82f6;
274
- outline-offset: 4px;
275
- }
276
272
  [data-cms-editable] {
277
273
  cursor: pointer;
278
274
  border-radius: 2px;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../packages/cms-schema/src/blocks/article.ts","../../../../packages/cms-schema/src/validation/image.ts","../../../../packages/cms-schema/src/blocks/schemas/article-block.ts","../../../../packages/cms-schema/src/blocks/schemas/cta-block.ts","../../../../packages/cms-schema/src/blocks/schemas/features-block.ts","../../../../packages/cms-schema/src/blocks/schemas/hero-block.ts","../../../../packages/cms-schema/src/fields/complex/media.ts","../../../../packages/cms-schema/src/blocks/schemas/logo-trust-block.ts","../../../../packages/cms-schema/src/blocks/registry.ts","../../lib/renderer.tsx","../../lib/block-renderer.tsx","../../lib/cms-api.ts"],"sourcesContent":["/**\n * Article block normalization helpers shared across CMS and website apps.\n */\n\nexport interface ArticleContent {\n headline: string;\n author?: string;\n publishedAt?: string;\n body: string;\n tags?: readonly string[];\n status?: string;\n}\n\nexport interface ArticleRow {\n id: string;\n published_content: unknown;\n}\n\nexport interface NormalizedArticleRow extends ArticleContent {\n id: string;\n tags: string[];\n status: string;\n}\n\n/**\n * Normalize article content coming from Supabase blocks.\n * Returns null when required fields are missing or invalid.\n */\nexport function normalizeArticleContent(payload: unknown): ArticleContent | null {\n if (!payload || typeof payload !== 'object') {\n return null;\n }\n\n const record = payload as Record<string, unknown>;\n const headline = typeof record.headline === 'string' ? record.headline : null;\n const body = typeof record.body === 'string' ? record.body : null;\n\n if (!headline || !body) {\n return null;\n }\n\n const author = typeof record.author === 'string' ? record.author : undefined;\n const publishedAt = typeof record.publishedAt === 'string' ? record.publishedAt : undefined;\n const tags = Array.isArray(record.tags) ? record.tags.map((tag) => String(tag)) : undefined;\n const statusRaw = typeof record.status === 'string' ? record.status.trim() : undefined;\n\n return {\n headline,\n body,\n author,\n publishedAt,\n tags,\n status: statusRaw || undefined,\n };\n}\n\n/**\n * Normalize an article block row, applying defaults for tags/status.\n */\nexport function normalizeArticleRow(row: ArticleRow): NormalizedArticleRow | null {\n const article = normalizeArticleContent(row.published_content);\n if (!article) {\n return null;\n }\n\n return {\n ...article,\n id: row.id,\n tags: article.tags ? [...article.tags] : [],\n status: article.status ?? 'published',\n };\n}\n\n/**\n * Ensure the article is published.\n */\nexport function isArticlePublished(article: ArticleContent): boolean {\n const now = new Date();\n const publishedAt = article.publishedAt ? new Date(article.publishedAt) : null;\n\n return (\n article.status === 'published' &&\n publishedAt !== null &&\n !Number.isNaN(publishedAt.getTime()) &&\n publishedAt <= now\n );\n}\n","/**\n * Image Validation Utilities\n *\n * Provides validation for:\n * - File type (MIME type checking)\n * - File size (configurable limits)\n * - Image dimensions (width/height constraints)\n */\n\nimport { z } from 'zod';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nexport const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'] as const;\nexport type ImageMimeType = (typeof ALLOWED_MIME_TYPES)[number];\n\nexport const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB\nexport const MIN_FILE_SIZE = 1024; // 1KB (avoid empty/corrupt files)\n\nexport const MAX_DIMENSION = 8192; // 8K resolution\nexport const MIN_DIMENSION = 10; // Minimum useful size\n\n// =============================================================================\n// Zod Schemas\n// =============================================================================\n\nexport const MimeTypeSchema = z.enum(ALLOWED_MIME_TYPES);\n\nexport const FileSizeSchema = z\n .number()\n .int()\n .min(MIN_FILE_SIZE, `File too small. Minimum: ${MIN_FILE_SIZE} bytes`)\n .max(MAX_FILE_SIZE, `File too large. Maximum: ${MAX_FILE_SIZE / 1024 / 1024}MB`);\n\nexport const DimensionSchema = z\n .number()\n .int()\n .min(MIN_DIMENSION, `Dimension too small. Minimum: ${MIN_DIMENSION}px`)\n .max(MAX_DIMENSION, `Dimension too large. Maximum: ${MAX_DIMENSION}px`);\n\n/**\n * Schema for upload request validation.\n */\nexport const UploadRequestSchema = z.object({\n filename: z\n .string()\n .min(1, 'Filename is required')\n .max(255, 'Filename too long')\n .regex(/^[^<>:\"/\\\\|?*]+$/, 'Filename contains invalid characters'),\n mimeType: MimeTypeSchema,\n fileSize: FileSizeSchema,\n});\n\nexport type UploadRequest = z.infer<typeof UploadRequestSchema>;\n\n/**\n * Schema for confirming upload with dimensions.\n * assetId is optional - if not provided, the server will generate a UUID.\n */\nexport const ConfirmUploadSchema = z.object({\n assetId: z.uuid().optional(),\n width: DimensionSchema,\n height: DimensionSchema,\n});\n\nexport type ConfirmUpload = z.infer<typeof ConfirmUploadSchema>;\n\n// =============================================================================\n// Validation Functions\n// =============================================================================\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Validate file type by MIME type.\n */\nexport function validateMimeType(mimeType: string): ValidationResult {\n const result = MimeTypeSchema.safeParse(mimeType);\n if (result.success) {\n return { valid: true };\n }\n return {\n valid: false,\n error: `File type not supported. Use JPEG, PNG, or WebP.`,\n };\n}\n\n/**\n * Validate file size.\n */\nexport function validateFileSize(size: number): ValidationResult {\n if (size < MIN_FILE_SIZE) {\n return {\n valid: false,\n error: `File appears to be empty or corrupt.`,\n };\n }\n if (size > MAX_FILE_SIZE) {\n const sizeMB = (size / 1024 / 1024).toFixed(1);\n return {\n valid: false,\n error: `File too large (${sizeMB}MB). Maximum: 10 MB.`,\n };\n }\n return { valid: true };\n}\n\n/**\n * Validate image dimensions.\n */\nexport function validateDimensions(width: number, height: number): ValidationResult {\n if (width < MIN_DIMENSION || height < MIN_DIMENSION) {\n return {\n valid: false,\n error: `Image too small. Minimum: ${MIN_DIMENSION}x${MIN_DIMENSION}px.`,\n };\n }\n if (width > MAX_DIMENSION || height > MAX_DIMENSION) {\n return {\n valid: false,\n error: `Image too large. Maximum: ${MAX_DIMENSION}x${MAX_DIMENSION}px.`,\n };\n }\n return { valid: true };\n}\n\n/**\n * Validate complete upload request.\n */\nexport function validateUploadRequest(\n request: unknown\n): { valid: true; data: UploadRequest } | { valid: false; error: string } {\n const result = UploadRequestSchema.safeParse(request);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n const firstIssue = result.error.issues[0];\n return {\n valid: false,\n error: firstIssue?.message ?? 'Invalid upload request',\n };\n}\n\n// =============================================================================\n// File Extension Utilities\n// =============================================================================\n\nconst MIME_TO_EXT: Record<ImageMimeType, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n};\n\nconst EXT_TO_MIME: Record<string, ImageMimeType> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n webp: 'image/webp',\n};\n\n/**\n * Get file extension from MIME type.\n */\nexport function getExtensionFromMime(mimeType: ImageMimeType | string): string {\n return MIME_TO_EXT[mimeType as ImageMimeType] ?? 'bin';\n}\n\n/**\n * Get MIME type from file extension.\n */\nexport function getMimeFromExtension(ext: string): ImageMimeType | undefined {\n return EXT_TO_MIME[ext.toLowerCase()];\n}\n\n/**\n * Extract extension from filename.\n */\nexport function getExtensionFromFilename(filename: string): string {\n const parts = filename.split('.');\n return parts.pop()?.toLowerCase?.() ?? '';\n}\n\n/**\n * Sanitize filename for storage.\n * Removes special characters, preserves extension.\n */\nexport function sanitizeFilename(filename: string): string {\n // Get extension\n const ext = getExtensionFromFilename(filename);\n\n // Get base name without extension\n const base = filename.slice(0, filename.length - ext.length - 1);\n\n // Sanitize: lowercase, replace spaces and special chars\n const sanitized = base\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 50); // Limit length\n\n return ext ? `${sanitized}.${ext}` : sanitized;\n}\n","/**\n * ArticleBlock Schema.\n *\n * Defines the structure and validation for ArticleBlock content.\n *\n * ArticleBlock represents a block of content focused on articles,\n * including fields such as headline, author, publication date, body content, tags, and status.\n *\n * Includes:\n * - Zod schema for validation\n * - TypeScript type inference\n * - Default values\n * - Validation function\n * - Function to create default content\n */\nimport { z } from 'zod';\n\n// Article block content schema with validation rules\nexport const ArticleBlockContentSchema = z.object({\n headline: z.string().min(1, 'Article headline is required').max(300, 'Headline too long').trim(),\n author: z.string().max(100, 'Article author too long').trim().optional(),\n publishedAt: z.iso\n .datetime({ message: 'Article publishedAt must be a valid ISO 8601 datetime string.' })\n .optional(),\n body: z.string().min(1, 'Article body content is required'),\n tags: z.array(z.string()).optional(),\n status: z.enum(['draft', 'review', 'published']),\n});\n\n// Inferred type for components\nexport type ArticleBlockContent = z.infer<typeof ArticleBlockContentSchema>;\n\n// Schema name constant\nexport const ARTICLE_BLOCK_SCHEMA_NAME = 'article';\n\n// Default values for a new ArticleBlock\nexport const ArticleBlockDefaults: Partial<ArticleBlockContent> = {\n status: 'draft',\n};\n\n// Validate ArticleBlock content\nexport function validateArticleBlockContent(content: unknown): {\n valid: boolean;\n data?: ArticleBlockContent;\n errors?: z.ZodError;\n} {\n const result = ArticleBlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default ArticleBlock content\nexport function createDefaultArticleContent(): ArticleBlockContent {\n return {\n headline: 'untitled article',\n author: '',\n publishedAt: new Date().toISOString(),\n body: '',\n tags: [],\n status: 'draft',\n };\n}\n","/**\n * CTABlock Schema.\n *\n * A call-to-action section with headline, description, and action buttons.\n * Supports primary and optional secondary CTA buttons.\n */\n\nimport { z } from 'zod';\n\n// CTA button schema\n// URL can be either a full URL (http://...) or a relative path (/path)\nconst CTAButtonSchema = z.object({\n text: z.string().min(1, 'Button text is required').max(50, 'Button text too long'),\n url: z.string().refine(\n (val) => {\n // Accept full URLs\n if (val.startsWith('http://') || val.startsWith('https://')) {\n try {\n new URL(val);\n return true;\n } catch {\n return false;\n }\n }\n // Accept relative paths starting with /\n if (val.startsWith('/')) {\n return true;\n }\n // Accept hash/anchor links\n if (val.startsWith('#')) {\n return true;\n }\n return false;\n },\n {\n message:\n 'URL must be a valid full URL (http://... or https://...), relative path (/path), or anchor (#anchor)',\n }\n ),\n});\n\nexport type CTAButton = z.infer<typeof CTAButtonSchema>;\n\n// CTABlock content schema with validation rules\nexport const CTABlockContentSchema = z.object({\n headline: z.string().min(1, 'Headline is required').max(100, 'Headline too long'),\n description: z.string().max(500, 'Description too long').optional(),\n primaryButton: CTAButtonSchema,\n secondaryButton: CTAButtonSchema.optional(),\n});\n\n// Inferred type for components\nexport type CTABlockContent = z.infer<typeof CTABlockContentSchema>;\n\n// Schema name constant\nexport const CTA_BLOCK_SCHEMA_NAME = 'cta-block';\n\n// Default values for a new CTABlock\nexport const CTABlockDefaults: Partial<CTABlockContent> = {\n primaryButton: {\n text: 'Get Started',\n url: '#',\n },\n};\n\n// Validate CTABlock content\nexport function validateCTABlockContent(content: unknown): {\n valid: boolean;\n data?: CTABlockContent;\n errors?: z.ZodError;\n} {\n const result = CTABlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default CTABlock content\nexport function createDefaultCTAContent(): CTABlockContent {\n return {\n headline: '',\n description: undefined,\n primaryButton: {\n text: 'Get Started',\n url: '#',\n },\n secondaryButton: undefined,\n };\n}\n","/**\n * FeaturesBlock Schema.\n *\n * A section displaying a list of features with icons, titles, and descriptions.\n * Supports different layouts (grid, list, carousel).\n */\n\nimport { z } from 'zod';\n\n// Layout options for features display\nexport const FeaturesLayout = ['grid', 'list', 'carousel'] as const;\nexport type FeaturesLayoutType = (typeof FeaturesLayout)[number];\n\n// Single feature item schema\nexport const FeatureItemSchema = z.object({\n icon: z.string().max(50, 'Icon name too long').optional(),\n title: z.string().min(1, 'Title is required').max(100, 'Title too long'),\n description: z.string().max(500, 'Description too long').optional(),\n});\n\nexport type FeatureItem = z.infer<typeof FeatureItemSchema>;\n\n// FeaturesBlock content schema with array validation\nexport const FeaturesBlockContentSchema = z.object({\n title: z.string().min(1, 'Section title is required').max(100, 'Title too long'),\n subtitle: z.string().max(200, 'Subtitle too long').optional(),\n features: z\n .array(FeatureItemSchema)\n .min(1, 'At least one feature is required')\n .max(6, 'Maximum 6 features allowed'),\n layout: z.enum(FeaturesLayout).default('grid'),\n});\n\n// Inferred type for components\nexport type FeaturesBlockContent = z.infer<typeof FeaturesBlockContentSchema>;\n\n// Schema name constant\nexport const FEATURES_BLOCK_SCHEMA_NAME = 'features-block';\n\n// Default values for a new FeaturesBlock\nexport const FeaturesBlockDefaults: Partial<FeaturesBlockContent> = {\n layout: 'grid',\n features: [\n {\n icon: 'star',\n title: 'Feature Title',\n description: 'Describe this feature',\n },\n ],\n};\n\n// Validate FeaturesBlock content\nexport function validateFeaturesBlockContent(content: unknown): {\n valid: boolean;\n data?: FeaturesBlockContent;\n errors?: z.ZodError;\n} {\n const result = FeaturesBlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default FeaturesBlock content\nexport function createDefaultFeaturesContent(): FeaturesBlockContent {\n return {\n title: '',\n subtitle: undefined,\n features: [],\n layout: 'grid',\n };\n}\n\n// Add a feature to the content\nexport function addFeature(\n content: FeaturesBlockContent,\n feature: FeatureItem\n): FeaturesBlockContent {\n if (content.features.length >= 6) {\n throw new Error('Maximum 6 features allowed');\n }\n return {\n ...content,\n features: [...content.features, feature],\n };\n}\n\n// Remove a feature by index\nexport function removeFeature(content: FeaturesBlockContent, index: number): FeaturesBlockContent {\n if (index < 0 || index >= content.features.length) {\n throw new Error('Invalid feature index');\n }\n if (content.features.length <= 1) {\n throw new Error('At least one feature is required');\n }\n return {\n ...content,\n features: content.features.filter((_, i) => i !== index),\n };\n}\n\n// Update a feature by index\nexport function updateFeature(\n content: FeaturesBlockContent,\n index: number,\n updates: Partial<FeatureItem>\n): FeaturesBlockContent {\n if (index < 0 || index >= content.features.length) {\n throw new Error('Invalid feature index');\n }\n return {\n ...content,\n features: content.features.map((feature, i) =>\n i === index ? { ...feature, ...updates } : feature\n ),\n };\n}\n","/**\n * HeroBlock Schema.\n *\n * A full-width hero section typically used at the top of pages.\n * Supports headline, subheadline, CTA button, background image, and alignment.\n */\n\nimport { z } from 'zod';\nimport { ImageReferenceSchema } from '../../fields/complex/media';\n\n// Alignment options for the hero content\nexport const HeroAlignment = ['left', 'center', 'right'] as const;\nexport type HeroAlignmentType = (typeof HeroAlignment)[number];\n\n// HeroBlock content schema with validation rules\nexport const HeroBlockContentSchema = z.object({\n headline: z.string().min(1, 'Headline is required').max(100, 'Headline too long'),\n subheadline: z.string().max(200, 'Subheadline too long').optional(),\n ctaText: z.string().max(50, 'CTA text too long').optional(),\n ctaUrl: z\n .string()\n .refine(\n (val) => {\n // Empty string is valid\n if (val === '') return true;\n // Accept full URLs\n if (val.startsWith('http://') || val.startsWith('https://')) {\n try {\n new URL(val);\n return true;\n } catch {\n return false;\n }\n }\n // Accept relative paths starting with /\n if (val.startsWith('/')) {\n return true;\n }\n // Accept hash/anchor links\n if (val.startsWith('#')) {\n return true;\n }\n return false;\n },\n {\n message:\n 'URL must be a valid full URL (http://... or https://...), relative path (/path), anchor (#anchor), or empty string',\n }\n )\n .optional()\n .or(z.literal('')),\n backgroundImage: ImageReferenceSchema.nullable().optional(),\n alignment: z.enum(HeroAlignment).default('center'),\n});\n\n// Inferred type for components\nexport type HeroBlockContent = z.infer<typeof HeroBlockContentSchema>;\n\n// Schema name constant\nexport const HERO_BLOCK_SCHEMA_NAME = 'hero-block';\n\n// Default values for a new HeroBlock\nexport const HeroBlockDefaults: Partial<HeroBlockContent> = {\n alignment: 'center',\n};\n\n// Validate HeroBlock content\nexport function validateHeroBlockContent(content: unknown): {\n valid: boolean;\n data?: HeroBlockContent;\n errors?: z.ZodError;\n} {\n const result = HeroBlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default HeroBlock content\nexport function createDefaultHeroContent(): HeroBlockContent {\n return {\n headline: '',\n subheadline: undefined,\n ctaText: undefined,\n ctaUrl: undefined,\n backgroundImage: undefined,\n alignment: 'center',\n };\n}\n","/**\n * Image and file field factories.\n *\n * The image() factory creates an ImageReference schema that:\n * - References an ImageAsset by ID\n * - Stores context-specific alt text (required for accessibility)\n * - Supports optional hotspot (focal point) and crop settings\n */\n\nimport { z } from 'zod';\n\nimport type { FieldMeta } from '../../types';\nimport {\n DimensionSchema,\n FileSizeSchema,\n MAX_DIMENSION,\n MAX_FILE_SIZE,\n MIN_DIMENSION,\n MimeTypeSchema,\n} from '../../validation/image';\n\n// =============================================================================\n// ImageAsset Schema (Public API type)\n// =============================================================================\n\n/**\n * Public API schema for image assets.\n *\n * This is the camelCase API representation. The database uses snake_case\n * (ImageAssetRow in db/image-asset-types.ts). Conversion between formats\n * should happen at the API boundary.\n *\n * Reuses validation schemas from validation/image.ts to ensure consistency.\n */\nexport const ImageAssetSchema = z.object({\n /** UUID primary key */\n id: z.uuid(),\n /** R2/S3 storage URL for the original file */\n url: z.url(),\n /** Image width in pixels */\n width: DimensionSchema,\n /** Image height in pixels */\n height: DimensionSchema,\n /** Original filename from upload */\n originalFilename: z.string().min(1).max(255),\n /** MIME type (only web-safe formats allowed) */\n mimeType: MimeTypeSchema,\n /** File size in bytes */\n fileSize: FileSizeSchema,\n /** Base64-encoded tiny preview for blur-up loading */\n lqip: z.string().optional(),\n});\n\nexport type ImageAsset = z.infer<typeof ImageAssetSchema>;\n\n// =============================================================================\n// ImageReference Schema (Asset-reference model)\n// =============================================================================\n\n/**\n * Hotspot defines the focal point of an image.\n * Coordinates are fractions (0-1) from top-left.\n */\nexport const HotspotSchema = z.object({\n x: z.number().min(0).max(1),\n y: z.number().min(0).max(1),\n});\n\nexport type Hotspot = z.infer<typeof HotspotSchema>;\n\n/**\n * Crop defines the region to extract from the image.\n * - x, y: Top-left coordinate of the crop region in pixels\n * - width, height: Size of the crop region in pixels\n * All values are positive integers representing pixel coordinates/dimensions.\n */\nexport const CropSchema = z.object({\n /** X coordinate of top-left corner in pixels */\n x: z.number().int().nonnegative(),\n /** Y coordinate of top-left corner in pixels */\n y: z.number().int().nonnegative(),\n /** Width of crop region in pixels (must be > 0) */\n width: z.number().int().positive(),\n /** Height of crop region in pixels (must be > 0) */\n height: z.number().int().positive(),\n});\n\nexport type Crop = z.infer<typeof CropSchema>;\n\n/**\n * ImageReference is what blocks store.\n * It points to an ImageAsset and adds context-specific metadata.\n */\nexport const ImageReferenceSchema = z.object({\n // Alt text is REQUIRED for accessibility\n alt: z.string().min(1, 'Alt text is required for accessibility'),\n\n // Optional metadata\n caption: z.string().max(500).optional(),\n attribution: z.string().max(255).optional(),\n\n // Reference to the ImageAsset with stored transformation\n _asset: z.object({\n id: z.uuid(),\n transformation: z.string().nullable().optional(),\n }),\n});\n\nexport type ImageReference = z.infer<typeof ImageReferenceSchema>;\n\n// =============================================================================\n// Image Field Factory\n// =============================================================================\n\nexport interface ImageFieldOptions {\n label: string;\n description?: string;\n required?: boolean;\n accept?: string;\n maxSize?: number;\n minWidth?: number;\n minHeight?: number;\n maxWidth?: number;\n maxHeight?: number;\n aspectRatio?: number;\n}\n\n/**\n * Create an image field with metadata.\n *\n * @example\n * const featuredImage = image({\n * label: 'Featured Image',\n * required: true,\n * accept: 'image/jpeg, image/png',\n * maxSize: 5 * 1024 * 1024,\n * });\n *\n * // Type of the value:\n * // {\n * // alt: string;\n * // caption?: string;\n * // attribution?: string;\n * // _asset: { id: string; transformation?: string | null };\n * // }\n */\nexport function image(options: ImageFieldOptions) {\n const meta: FieldMeta = {\n label: options.label,\n component: 'image-uploader',\n required: options.required,\n description: options.description,\n options: {\n accept: options.accept ?? 'image/jpeg, image/png, image/webp',\n maxSize: options.maxSize ?? MAX_FILE_SIZE, // 10MB default\n minWidth: options.minWidth ?? MIN_DIMENSION,\n minHeight: options.minHeight ?? MIN_DIMENSION,\n maxWidth: options.maxWidth ?? MAX_DIMENSION,\n maxHeight: options.maxHeight ?? MAX_DIMENSION,\n aspectRatio: options.aspectRatio,\n },\n };\n\n // Use the new ImageReferenceSchema instead of URL-only\n const result = options.required ? ImageReferenceSchema : ImageReferenceSchema.nullish();\n\n return result.meta(meta);\n}\n\n// =============================================================================\n// File Field\n// =============================================================================\n\nexport interface FileFieldOptions {\n label: string;\n description?: string;\n required?: boolean;\n accept?: string;\n maxSize?: number;\n}\n\nconst fileSchema = z.object({\n url: z.url(),\n name: z.string(),\n size: z.number().int().positive(),\n type: z.string(),\n});\n\n/**\n * Create a file field with metadata.\n */\nexport function file(options: FileFieldOptions) {\n const meta: FieldMeta = {\n label: options.label,\n component: 'file-uploader',\n required: options.required,\n description: options.description,\n options: {\n accept: options.accept ?? '*/*',\n maxSize: options.maxSize,\n },\n };\n\n const result = options.required ? fileSchema : fileSchema.nullish();\n return result.meta(meta);\n}\n","/**\n * LogoTrustBlock Schema.\n *\n * A section displaying customer/partner logos for trust and credibility.\n * Supports an optional title and an array of logo images.\n *\n * Each logo uses ImageReference to integrate with the image manager.\n */\n\nimport { z } from 'zod';\n\nimport { ImageReferenceSchema } from '../../fields/complex/media';\n\n// Logo item schema - uses ImageReference for image manager integration\nexport const LogoItemSchema = z.object({\n /** Unique ID for this logo item */\n id: z.uuid(),\n /** Image reference (alt + _asset with transformation) */\n image: ImageReferenceSchema,\n /** Optional company/brand name to display */\n name: z.string().max(100, 'Name too long').optional(),\n});\n\nexport type LogoItem = z.infer<typeof LogoItemSchema>;\n\n/**\n * Legacy logo format schema for backwards compatibility.\n * Used for logos stored with direct URL before the ImageReference migration.\n */\nexport const LegacyLogoItemSchema = z.object({\n /** Direct URL to the logo image */\n url: z.string(),\n /** Alt text for the image */\n alt: z.string(),\n /** Optional company/brand name */\n name: z.string().optional(),\n});\n\nexport type LegacyLogoItem = z.infer<typeof LegacyLogoItemSchema>;\n\n// LogoTrustBlock content schema with validation rules\nexport const LogoTrustBlockContentSchema = z.object({\n title: z.string().max(100, 'Title too long').optional(),\n logos: z.array(LogoItemSchema).max(20, 'Maximum 20 logos allowed'),\n});\n\n// Inferred type for components\nexport type LogoTrustBlockContent = z.infer<typeof LogoTrustBlockContentSchema>;\n\n// Schema name constant\nexport const LOGO_TRUST_BLOCK_SCHEMA_NAME = 'logo-trust-block';\n\n// Default values for a new LogoTrustBlock\nexport const LogoTrustBlockDefaults: Partial<LogoTrustBlockContent> = {\n logos: [],\n};\n\n// Validate LogoTrustBlock content\nexport function validateLogoTrustBlockContent(content: unknown): {\n valid: boolean;\n data?: LogoTrustBlockContent;\n errors?: z.ZodError;\n} {\n const result = LogoTrustBlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default LogoTrustBlock content\nexport function createDefaultLogoTrustContent(): LogoTrustBlockContent {\n return {\n title: undefined,\n logos: [],\n };\n}\n","/**\n * Block Schema Registry.\n *\n * Centralizes registration for all block schemas.\n * Call registerAllBlockSchemas() during application initialization.\n */\n\nimport { clearSchemaRegistry, registerBlockSchema } from '../validation';\nimport { ARTICLE_BLOCK_SCHEMA_NAME, ArticleBlockContentSchema } from './schemas/article-block';\nimport { CTA_BLOCK_SCHEMA_NAME, CTABlockContentSchema } from './schemas/cta-block';\nimport { FEATURES_BLOCK_SCHEMA_NAME, FeaturesBlockContentSchema } from './schemas/features-block';\nimport { HERO_BLOCK_SCHEMA_NAME, HeroBlockContentSchema } from './schemas/hero-block';\nimport {\n LOGO_TRUST_BLOCK_SCHEMA_NAME,\n LogoTrustBlockContentSchema,\n} from './schemas/logo-trust-block';\n\n// All registered block schema names\nexport const BLOCK_SCHEMA_NAMES = [\n ARTICLE_BLOCK_SCHEMA_NAME,\n HERO_BLOCK_SCHEMA_NAME,\n FEATURES_BLOCK_SCHEMA_NAME,\n CTA_BLOCK_SCHEMA_NAME,\n LOGO_TRUST_BLOCK_SCHEMA_NAME,\n] as const;\n\n// Union type for type-safe schema name usage\nexport type BlockSchemaName = (typeof BLOCK_SCHEMA_NAMES)[number];\n\n// Register all block schemas\nexport function registerAllBlockSchemas(): void {\n registerBlockSchema(ARTICLE_BLOCK_SCHEMA_NAME, ArticleBlockContentSchema);\n registerBlockSchema(HERO_BLOCK_SCHEMA_NAME, HeroBlockContentSchema);\n registerBlockSchema(FEATURES_BLOCK_SCHEMA_NAME, FeaturesBlockContentSchema);\n registerBlockSchema(CTA_BLOCK_SCHEMA_NAME, CTABlockContentSchema);\n registerBlockSchema(LOGO_TRUST_BLOCK_SCHEMA_NAME, LogoTrustBlockContentSchema);\n}\n\n// Clear and re-register all block schemas (for testing)\nexport function resetBlockSchemas(): void {\n clearSchemaRegistry();\n registerAllBlockSchemas();\n}\n\n// Type guard for schema names\nexport function isValidBlockSchemaName(name: string): name is BlockSchemaName {\n return BLOCK_SCHEMA_NAMES.includes(name as BlockSchemaName);\n}\n","/**\n * Catch-all Route Handler for Parametric Routes\n *\n * Handles routes with multiple segments like /us/en/products.\n * Uses the CMS API to fetch and render blocks.\n */\n\nimport {\n isArticlePublished,\n isValidBlockSchemaName,\n normalizeArticleContent,\n} from '@repo/cms-schema/blocks';\nimport type { GeneratedBlockRow } from '@repo/cms-schema/db';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { BlockRenderer, CmsEditableInit } from './block-renderer';\nimport { type CmsConfig, getCmsClient } from './cms-api';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype PageProps = CmsConfig & {\n params: Promise<{ slug: string[] }>;\n searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;\n registry?: Partial<BlockComponentRegistry>;\n};\n\nfunction getWebsiteId(providedWebsiteId?: string): string {\n const websiteId =\n providedWebsiteId ??\n process.env.NEXT_PUBLIC_WEBSITE_ID ??\n process.env.WEBSITE_ID ??\n process.env.CMS_WEBSITE_ID;\n\n if (!websiteId) {\n throw new Error(\n 'Missing websiteId for website renderer. Either pass websiteId prop or set NEXT_PUBLIC_WEBSITE_ID (or WEBSITE_ID/CMS_WEBSITE_ID) to a valid UUID.'\n );\n }\n\n const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n if (!uuidRegex.test(websiteId)) {\n throw new Error(\n `Invalid websiteId \"${websiteId}\". Provide a valid UUID via prop or set NEXT_PUBLIC_WEBSITE_ID (or WEBSITE_ID/CMS_WEBSITE_ID).`\n );\n }\n\n return websiteId;\n}\n\n/**\n * Catch-all route handler for parametric routes.\n *\n * Handles paths like:\n * - /us/en/products -> slug = ['us', 'en', 'products']\n * - /about -> slug = ['about']\n *\n * Reconstructs the full path and fetches route via tRPC.\n */\nexport default async function ParametricRoutePage({\n params,\n searchParams,\n registry,\n apiKey,\n cmsUrl,\n websiteId: providedWebsiteId,\n}: PageProps) {\n const websiteId = getWebsiteId(providedWebsiteId);\n const { slug } = await params;\n const resolvedSearchParams = await searchParams;\n\n // Check for ai_preview query param to determine if we should show generated blocks\n let aiPreviewIndex: number | null = null;\n const aiPreviewParam = resolvedSearchParams?.ai_preview;\n if (aiPreviewParam) {\n const paramValue = Array.isArray(aiPreviewParam) ? aiPreviewParam[0] : aiPreviewParam;\n if (paramValue) {\n const parsed = parseInt(paramValue, 10);\n if (!Number.isNaN(parsed)) {\n aiPreviewIndex = parsed;\n }\n }\n }\n\n // Check for edit_mode query param to enable editable wrappers\n const editModeParam = resolvedSearchParams?.edit_mode;\n const editMode = editModeParam === 'true' || editModeParam === '1';\n\n // Reconstruct full path from slug segments and normalize it\n const rawPath = `/${slug.join('/')}`;\n const path = normalizePath(rawPath);\n\n // Get CMS API client with optional API key and custom URL\n const client = getCmsClient({ apiKey, cmsUrl });\n\n try {\n // Fetch route by path via CMS API\n const { route, resolvedParams } = await client.route.getByPath.query({\n websiteId,\n path,\n });\n\n // Only show Live routes on public website\n if (route.state !== 'Live') {\n console.error(`Route found but not Live. Path: ${path}, State: ${route.state}`);\n notFound();\n }\n\n // Fetch all blocks by ID via CMS API (skip any that fail)\n const blockPromises = route.block_ids.map(async (blockId) => {\n try {\n const result = await client.block.getById.query({ websiteId, id: blockId });\n return result.block;\n } catch (error) {\n // Log error but don't fail the entire page\n console.error(`Failed to fetch block ${blockId}:`, error);\n return null;\n }\n });\n\n // Fetch generated blocks for preview only if ai_preview param is set\n // generated_content is an array of variants: [{ description, content }, ...]\n type GeneratedBlockData = Pick<\n GeneratedBlockRow,\n 'id' | 'generated_content' | 'generated_layout' | 'schema_name' | 'status'\n >;\n const generatedBlocksPromise =\n aiPreviewIndex !== null\n ? client.block.getGeneratedByBlockIds\n .query({ websiteId, blockIds: route.block_ids })\n .catch((error) => {\n console.error('Failed to fetch generated blocks:', error);\n return { generatedBlocks: {} as Record<string, GeneratedBlockData> };\n })\n : Promise.resolve({ generatedBlocks: {} as Record<string, GeneratedBlockData> });\n\n const [blockResults, { generatedBlocks }] = await Promise.all([\n Promise.all(blockPromises),\n generatedBlocksPromise,\n ]);\n\n // Transform blocks to BlockData format for BlockRenderer\n // Filter out any blocks that failed to load\n // Use generated content only when ai_preview param is set\n const blocks: BlockData[] = [];\n\n for (const block of blockResults) {\n if (!block || block.published_content === null) continue;\n\n // Use generated content only if ai_preview is set and generated block exists\n // ai_preview is 1-indexed, so subtract 1 to get array index\n let content: Record<string, unknown> | null = null;\n if (aiPreviewIndex !== null) {\n const generatedBlock = generatedBlocks[block.id] as GeneratedBlockData | undefined;\n const variantIndex = aiPreviewIndex - 1; // Convert to 0-indexed\n const variants = generatedBlock?.generated_content;\n if (variants && Array.isArray(variants) && variants[variantIndex]) {\n content = variants[variantIndex].content;\n }\n }\n // Fall back to published content if no variant found\n content = content ?? (block.published_content as Record<string, unknown> | null);\n if (!content) continue;\n\n // Handle 'article' blocks separately before checking schema type\n if (block.schema_name === 'article') {\n const article = normalizeArticleContent(content);\n const isPublished = article ? isArticlePublished(article) : null;\n if (article && isPublished) {\n blocks.push({ id: block.id, type: 'article', content: article });\n }\n continue;\n }\n\n // Custom blocks have a schema_id (UUID); use schema_name as the type\n // so consumers register components by schema_name in the registry\n if (block.schema_id) {\n blocks.push({ id: block.id, type: block.schema_name, content });\n continue;\n }\n\n // Skip predefined blocks with invalid schema names\n if (!isValidBlockSchemaName(block.schema_name)) {\n continue;\n }\n\n // For all predefined block types, map schema_name to type and include content\n // Image references are automatically resolved by block.getById\n blocks.push({\n id: block.id,\n type: block.schema_name,\n content,\n } as BlockData);\n }\n\n // Map resolved params to the slimmed-down shape for block components\n const routeParams: ResolvedRouteParams | undefined = resolvedParams\n ? Object.fromEntries(\n Object.entries(resolvedParams).map(([key, param]) => [\n key,\n {\n value: param.value,\n schemaName: param.schemaName,\n document: {\n id: param.document.id,\n title: param.document.title,\n content: param.document.content,\n schema_name: param.document.schema_name,\n },\n },\n ])\n )\n : undefined;\n\n return (\n <main>\n {editMode && <CmsEditableInit />}\n {blocks.map((block) => (\n <BlockRenderer\n key={block.id}\n block={block}\n registry={registry ?? {}}\n disableEditable={!editMode}\n routeParams={routeParams}\n path={path}\n />\n ))}\n </main>\n );\n } catch (error) {\n // Log error for debugging\n console.error(`Route fetch error for path: ${path}`, error);\n\n // If route not found or param validation fails, show 404\n // TRPCClientError has data.code for the error code\n const errorCode =\n error instanceof Error && 'data' in error\n ? (error as { data?: { code?: string } }).data?.code\n : error instanceof Error && 'code' in error\n ? (error as { code: string }).code\n : undefined;\n\n if (errorCode === 'NOT_FOUND' || errorCode === 'P0002') {\n notFound();\n }\n\n // Re-throw other errors\n throw error;\n }\n}\n\n// -----------------------------------------------------------------------------\n// Metadata\n// -----------------------------------------------------------------------------\n\n/**\n * Generate metadata for the page.\n * Uses Next.js 15+ async params pattern.\n */\nexport async function generateMetadata({\n params,\n apiKey,\n cmsUrl,\n websiteId: providedWebsiteId,\n}: PageProps): Promise<Metadata> {\n const websiteId = getWebsiteId(providedWebsiteId);\n const { slug } = await params;\n const rawPath = `/${slug.join('/')}`;\n const path = normalizePath(rawPath);\n const client = getCmsClient({ apiKey, cmsUrl });\n\n try {\n const { route } = await client.route.getByPath.query({ websiteId, path });\n return {\n title: `${route.path} | Website`,\n description: `Content page: ${route.path}`,\n };\n } catch {\n return {\n title: 'Page Not Found | Website',\n description: 'The requested page could not be found.',\n };\n }\n}\n\nexport function normalizePath(path: string): string {\n if (!path || path === '/') {\n return '/';\n }\n\n // Remove trailing slashes, ensure leading slash\n let normalized = path.trim();\n\n // Remove trailing slashes (but keep root \"/\")\n normalized = normalized.replace(/\\/+$/, '');\n\n // Ensure leading slash\n if (!normalized.startsWith('/')) {\n normalized = `/${normalized}`;\n }\n\n // Collapse multiple consecutive slashes to single slash\n normalized = normalized.replace(/\\/+/g, '/');\n\n return normalized;\n}\n","/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { ClientEditableBlock } from './client-editable-block';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\n// Elements where injecting a <span> would break layout or produce invalid HTML\nconst SKIP_SPAN_PARENTS = new Set([\n // SVG text-bearing elements — spans are not valid SVG children\n 'text',\n 'tspan',\n 'textPath',\n 'desc',\n // Form elements whose text content must not be interrupted\n 'option',\n 'optgroup',\n // Non-rendered elements\n 'script',\n 'style',\n 'noscript',\n]);\n\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n inSvg: ctx.inSvg,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Track SVG context so we never inject <span> inside SVG subtrees\n const nextInSvg = ctx.inSvg || el.type === 'svg';\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n inSvg: nextInSvg,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// CmsEditableInit — render once per page in edit mode\n// -----------------------------------------------------------------------------\n\n/**\n * Renders the shared CMS edit-mode styles and click-routing script.\n * Place this once at the top of your page when edit_mode is active.\n */\nexport function CmsEditableInit() {\n return (\n <>\n <style>{`\n [data-cms-block]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 4px;\n }\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n `,\n }}\n />\n </>\n );\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n /**\n * The current URL path (e.g. \"/en/test\").\n * When provided, enables path-namespaced component lookup.\n * Registry keys like \"/{lang}/test Article\" will be matched against this path,\n * where `{x}` and `(x)` are treated as single-segment wildcards.\n */\n path?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Path-namespaced component resolution\n// -----------------------------------------------------------------------------\n\n/**\n * Returns true if each segment of `path` matches the corresponding segment in\n * `pattern`, where a pattern segment wrapped in `{…}` or `(…)` is a wildcard.\n */\nfunction pathMatchesPattern(path: string, pattern: string): boolean {\n const pathSegs = path.split('/').filter(Boolean);\n const patternSegs = pattern.split('/').filter(Boolean);\n if (pathSegs.length !== patternSegs.length) return false;\n for (let i = 0; i < patternSegs.length; i++) {\n const seg = patternSegs[i];\n if (!seg) return false;\n if ((seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('(') && seg.endsWith(')'))) {\n continue;\n }\n if (seg !== pathSegs[i]) return false;\n }\n return true;\n}\n\n/**\n * Resolves the component for `blockType` from the registry.\n *\n * When `path` is provided, keys of the form `\"/{pattern} BlockType\"` are\n * checked first. The first key whose path pattern matches the current path\n * and whose block-type suffix matches `blockType` wins. Falls back to a\n * direct `registry[blockType]` lookup.\n */\nfunction resolveComponent(\n registry: Partial<BlockComponentRegistry>,\n blockType: string,\n path?: string\n): BlockComponentRegistry[string] | undefined {\n if (path) {\n for (const key of Object.keys(registry)) {\n if (!key.startsWith('/')) continue;\n const spaceIdx = key.indexOf(' ');\n if (spaceIdx === -1) continue;\n const pathPattern = key.slice(0, spaceIdx);\n const registeredType = key.slice(spaceIdx + 1);\n if (registeredType !== blockType) continue;\n if (pathMatchesPattern(path, pathPattern)) {\n return registry[key];\n }\n }\n }\n return registry[blockType];\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * In editable mode, wraps the block in a ClientEditableBlock that:\n * - Stamps data-cms-block attributes directly on the component's root element\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into the component's root element\n *\n * Render CmsEditableInit once at the page level to include the shared styles\n * and click-routing script.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n path,\n}: BlockRendererProps) {\n const Component = resolveComponent(registry, block.type, path);\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Build content entries for client-side text matching\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n const contentEntries = Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p);\n\n return (\n <ClientEditableBlock blockId={block.id} blockType={block.type} contentEntries={contentEntries}>\n {component}\n </ClientEditableBlock>\n );\n}\n","/**\n * CMS API Client\n *\n * Creates an HTTP-based tRPC client for calling the CMS API.\n * Used in Server Components to fetch routes and blocks from the CMS.\n */\n\nimport type { AppRouter } from '@repo/cms-schema/trpc';\nimport { type CreateTRPCClient, createTRPCClient, httpBatchLink } from '@trpc/client';\nimport superjson from 'superjson';\n\n/** Type alias for the CMS API client */\ntype CmsClient = CreateTRPCClient<AppRouter>;\n\n/**\n * Get the CMS API URL from the provided base URL.\n */\nfunction getCmsApiUrl(cmsUrl: string): string {\n return new URL('/api/trpc', cmsUrl).toString();\n}\n\n/** Unified configuration for all CMS API access */\nexport interface CmsConfig {\n /** CMS API base URL (e.g., 'http://localhost:3000') */\n cmsUrl: string;\n /** API key for authentication */\n apiKey?: string;\n /** Website ID to scope API requests */\n websiteId?: string;\n}\n\n/**\n * Create a custom fetch function that appends API key as query parameter.\n */\nfunction createFetchWithApiKey(apiKey?: string, websiteId?: string) {\n return async (url: URL | RequestInfo, options?: RequestInit): Promise<Response> => {\n let finalUrl = url;\n\n const urlObj = new URL(url.toString());\n if (apiKey) {\n urlObj.searchParams.set('api_key', apiKey);\n }\n if (websiteId) {\n urlObj.searchParams.set('website_id', websiteId);\n }\n if (apiKey || websiteId) {\n finalUrl = urlObj.toString();\n }\n\n const response = await fetch(finalUrl, options);\n\n return response;\n };\n}\n\n/**\n * Create a tRPC client for the CMS API.\n */\nfunction createCmsClient(options: CmsConfig): CmsClient {\n const url = getCmsApiUrl(options.cmsUrl);\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: createFetchWithApiKey(options.apiKey, options.websiteId),\n }),\n ],\n });\n}\n\n/**\n * Get a CMS client for the specified CMS URL.\n */\nexport function getCmsClient(options: CmsConfig): CmsClient {\n return createCmsClient(options);\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z","jsx","jsxs"]}
1
+ {"version":3,"sources":["../../../../packages/cms-schema/src/blocks/article.ts","../../../../packages/cms-schema/src/validation/image.ts","../../../../packages/cms-schema/src/blocks/schemas/article-block.ts","../../../../packages/cms-schema/src/blocks/schemas/cta-block.ts","../../../../packages/cms-schema/src/blocks/schemas/features-block.ts","../../../../packages/cms-schema/src/blocks/schemas/hero-block.ts","../../../../packages/cms-schema/src/fields/complex/media.ts","../../../../packages/cms-schema/src/blocks/schemas/logo-trust-block.ts","../../../../packages/cms-schema/src/blocks/registry.ts","../../lib/renderer.tsx","../../lib/block-renderer.tsx","../../lib/cms-api.ts"],"sourcesContent":["/**\n * Article block normalization helpers shared across CMS and website apps.\n */\n\nexport interface ArticleContent {\n headline: string;\n author?: string;\n publishedAt?: string;\n body: string;\n tags?: readonly string[];\n status?: string;\n}\n\nexport interface ArticleRow {\n id: string;\n published_content: unknown;\n}\n\nexport interface NormalizedArticleRow extends ArticleContent {\n id: string;\n tags: string[];\n status: string;\n}\n\n/**\n * Normalize article content coming from Supabase blocks.\n * Returns null when required fields are missing or invalid.\n */\nexport function normalizeArticleContent(payload: unknown): ArticleContent | null {\n if (!payload || typeof payload !== 'object') {\n return null;\n }\n\n const record = payload as Record<string, unknown>;\n const headline = typeof record.headline === 'string' ? record.headline : null;\n const body = typeof record.body === 'string' ? record.body : null;\n\n if (!headline || !body) {\n return null;\n }\n\n const author = typeof record.author === 'string' ? record.author : undefined;\n const publishedAt = typeof record.publishedAt === 'string' ? record.publishedAt : undefined;\n const tags = Array.isArray(record.tags) ? record.tags.map((tag) => String(tag)) : undefined;\n const statusRaw = typeof record.status === 'string' ? record.status.trim() : undefined;\n\n return {\n headline,\n body,\n author,\n publishedAt,\n tags,\n status: statusRaw || undefined,\n };\n}\n\n/**\n * Normalize an article block row, applying defaults for tags/status.\n */\nexport function normalizeArticleRow(row: ArticleRow): NormalizedArticleRow | null {\n const article = normalizeArticleContent(row.published_content);\n if (!article) {\n return null;\n }\n\n return {\n ...article,\n id: row.id,\n tags: article.tags ? [...article.tags] : [],\n status: article.status ?? 'published',\n };\n}\n\n/**\n * Ensure the article is published.\n */\nexport function isArticlePublished(article: ArticleContent): boolean {\n const now = new Date();\n const publishedAt = article.publishedAt ? new Date(article.publishedAt) : null;\n\n return (\n article.status === 'published' &&\n publishedAt !== null &&\n !Number.isNaN(publishedAt.getTime()) &&\n publishedAt <= now\n );\n}\n","/**\n * Image Validation Utilities\n *\n * Provides validation for:\n * - File type (MIME type checking)\n * - File size (configurable limits)\n * - Image dimensions (width/height constraints)\n */\n\nimport { z } from 'zod';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nexport const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'] as const;\nexport type ImageMimeType = (typeof ALLOWED_MIME_TYPES)[number];\n\nexport const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB\nexport const MIN_FILE_SIZE = 1024; // 1KB (avoid empty/corrupt files)\n\nexport const MAX_DIMENSION = 8192; // 8K resolution\nexport const MIN_DIMENSION = 10; // Minimum useful size\n\n// =============================================================================\n// Zod Schemas\n// =============================================================================\n\nexport const MimeTypeSchema = z.enum(ALLOWED_MIME_TYPES);\n\nexport const FileSizeSchema = z\n .number()\n .int()\n .min(MIN_FILE_SIZE, `File too small. Minimum: ${MIN_FILE_SIZE} bytes`)\n .max(MAX_FILE_SIZE, `File too large. Maximum: ${MAX_FILE_SIZE / 1024 / 1024}MB`);\n\nexport const DimensionSchema = z\n .number()\n .int()\n .min(MIN_DIMENSION, `Dimension too small. Minimum: ${MIN_DIMENSION}px`)\n .max(MAX_DIMENSION, `Dimension too large. Maximum: ${MAX_DIMENSION}px`);\n\n/**\n * Schema for upload request validation.\n */\nexport const UploadRequestSchema = z.object({\n filename: z\n .string()\n .min(1, 'Filename is required')\n .max(255, 'Filename too long')\n .regex(/^[^<>:\"/\\\\|?*]+$/, 'Filename contains invalid characters'),\n mimeType: MimeTypeSchema,\n fileSize: FileSizeSchema,\n});\n\nexport type UploadRequest = z.infer<typeof UploadRequestSchema>;\n\n/**\n * Schema for confirming upload with dimensions.\n * assetId is optional - if not provided, the server will generate a UUID.\n */\nexport const ConfirmUploadSchema = z.object({\n assetId: z.uuid().optional(),\n width: DimensionSchema,\n height: DimensionSchema,\n});\n\nexport type ConfirmUpload = z.infer<typeof ConfirmUploadSchema>;\n\n// =============================================================================\n// Validation Functions\n// =============================================================================\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Validate file type by MIME type.\n */\nexport function validateMimeType(mimeType: string): ValidationResult {\n const result = MimeTypeSchema.safeParse(mimeType);\n if (result.success) {\n return { valid: true };\n }\n return {\n valid: false,\n error: `File type not supported. Use JPEG, PNG, or WebP.`,\n };\n}\n\n/**\n * Validate file size.\n */\nexport function validateFileSize(size: number): ValidationResult {\n if (size < MIN_FILE_SIZE) {\n return {\n valid: false,\n error: `File appears to be empty or corrupt.`,\n };\n }\n if (size > MAX_FILE_SIZE) {\n const sizeMB = (size / 1024 / 1024).toFixed(1);\n return {\n valid: false,\n error: `File too large (${sizeMB}MB). Maximum: 10 MB.`,\n };\n }\n return { valid: true };\n}\n\n/**\n * Validate image dimensions.\n */\nexport function validateDimensions(width: number, height: number): ValidationResult {\n if (width < MIN_DIMENSION || height < MIN_DIMENSION) {\n return {\n valid: false,\n error: `Image too small. Minimum: ${MIN_DIMENSION}x${MIN_DIMENSION}px.`,\n };\n }\n if (width > MAX_DIMENSION || height > MAX_DIMENSION) {\n return {\n valid: false,\n error: `Image too large. Maximum: ${MAX_DIMENSION}x${MAX_DIMENSION}px.`,\n };\n }\n return { valid: true };\n}\n\n/**\n * Validate complete upload request.\n */\nexport function validateUploadRequest(\n request: unknown\n): { valid: true; data: UploadRequest } | { valid: false; error: string } {\n const result = UploadRequestSchema.safeParse(request);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n const firstIssue = result.error.issues[0];\n return {\n valid: false,\n error: firstIssue?.message ?? 'Invalid upload request',\n };\n}\n\n// =============================================================================\n// File Extension Utilities\n// =============================================================================\n\nconst MIME_TO_EXT: Record<ImageMimeType, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n};\n\nconst EXT_TO_MIME: Record<string, ImageMimeType> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n webp: 'image/webp',\n};\n\n/**\n * Get file extension from MIME type.\n */\nexport function getExtensionFromMime(mimeType: ImageMimeType | string): string {\n return MIME_TO_EXT[mimeType as ImageMimeType] ?? 'bin';\n}\n\n/**\n * Get MIME type from file extension.\n */\nexport function getMimeFromExtension(ext: string): ImageMimeType | undefined {\n return EXT_TO_MIME[ext.toLowerCase()];\n}\n\n/**\n * Extract extension from filename.\n */\nexport function getExtensionFromFilename(filename: string): string {\n const parts = filename.split('.');\n return parts.pop()?.toLowerCase?.() ?? '';\n}\n\n/**\n * Sanitize filename for storage.\n * Removes special characters, preserves extension.\n */\nexport function sanitizeFilename(filename: string): string {\n // Get extension\n const ext = getExtensionFromFilename(filename);\n\n // Get base name without extension\n const base = filename.slice(0, filename.length - ext.length - 1);\n\n // Sanitize: lowercase, replace spaces and special chars\n const sanitized = base\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 50); // Limit length\n\n return ext ? `${sanitized}.${ext}` : sanitized;\n}\n","/**\n * ArticleBlock Schema.\n *\n * Defines the structure and validation for ArticleBlock content.\n *\n * ArticleBlock represents a block of content focused on articles,\n * including fields such as headline, author, publication date, body content, tags, and status.\n *\n * Includes:\n * - Zod schema for validation\n * - TypeScript type inference\n * - Default values\n * - Validation function\n * - Function to create default content\n */\nimport { z } from 'zod';\n\n// Article block content schema with validation rules\nexport const ArticleBlockContentSchema = z.object({\n headline: z.string().min(1, 'Article headline is required').max(300, 'Headline too long').trim(),\n author: z.string().max(100, 'Article author too long').trim().optional(),\n publishedAt: z.iso\n .datetime({ message: 'Article publishedAt must be a valid ISO 8601 datetime string.' })\n .optional(),\n body: z.string().min(1, 'Article body content is required'),\n tags: z.array(z.string()).optional(),\n status: z.enum(['draft', 'review', 'published']),\n});\n\n// Inferred type for components\nexport type ArticleBlockContent = z.infer<typeof ArticleBlockContentSchema>;\n\n// Schema name constant\nexport const ARTICLE_BLOCK_SCHEMA_NAME = 'article';\n\n// Default values for a new ArticleBlock\nexport const ArticleBlockDefaults: Partial<ArticleBlockContent> = {\n status: 'draft',\n};\n\n// Validate ArticleBlock content\nexport function validateArticleBlockContent(content: unknown): {\n valid: boolean;\n data?: ArticleBlockContent;\n errors?: z.ZodError;\n} {\n const result = ArticleBlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default ArticleBlock content\nexport function createDefaultArticleContent(): ArticleBlockContent {\n return {\n headline: 'untitled article',\n author: '',\n publishedAt: new Date().toISOString(),\n body: '',\n tags: [],\n status: 'draft',\n };\n}\n","/**\n * CTABlock Schema.\n *\n * A call-to-action section with headline, description, and action buttons.\n * Supports primary and optional secondary CTA buttons.\n */\n\nimport { z } from 'zod';\n\n// CTA button schema\n// URL can be either a full URL (http://...) or a relative path (/path)\nconst CTAButtonSchema = z.object({\n text: z.string().min(1, 'Button text is required').max(50, 'Button text too long'),\n url: z.string().refine(\n (val) => {\n // Accept full URLs\n if (val.startsWith('http://') || val.startsWith('https://')) {\n try {\n new URL(val);\n return true;\n } catch {\n return false;\n }\n }\n // Accept relative paths starting with /\n if (val.startsWith('/')) {\n return true;\n }\n // Accept hash/anchor links\n if (val.startsWith('#')) {\n return true;\n }\n return false;\n },\n {\n message:\n 'URL must be a valid full URL (http://... or https://...), relative path (/path), or anchor (#anchor)',\n }\n ),\n});\n\nexport type CTAButton = z.infer<typeof CTAButtonSchema>;\n\n// CTABlock content schema with validation rules\nexport const CTABlockContentSchema = z.object({\n headline: z.string().min(1, 'Headline is required').max(100, 'Headline too long'),\n description: z.string().max(500, 'Description too long').optional(),\n primaryButton: CTAButtonSchema,\n secondaryButton: CTAButtonSchema.optional(),\n});\n\n// Inferred type for components\nexport type CTABlockContent = z.infer<typeof CTABlockContentSchema>;\n\n// Schema name constant\nexport const CTA_BLOCK_SCHEMA_NAME = 'cta-block';\n\n// Default values for a new CTABlock\nexport const CTABlockDefaults: Partial<CTABlockContent> = {\n primaryButton: {\n text: 'Get Started',\n url: '#',\n },\n};\n\n// Validate CTABlock content\nexport function validateCTABlockContent(content: unknown): {\n valid: boolean;\n data?: CTABlockContent;\n errors?: z.ZodError;\n} {\n const result = CTABlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default CTABlock content\nexport function createDefaultCTAContent(): CTABlockContent {\n return {\n headline: '',\n description: undefined,\n primaryButton: {\n text: 'Get Started',\n url: '#',\n },\n secondaryButton: undefined,\n };\n}\n","/**\n * FeaturesBlock Schema.\n *\n * A section displaying a list of features with icons, titles, and descriptions.\n * Supports different layouts (grid, list, carousel).\n */\n\nimport { z } from 'zod';\n\n// Layout options for features display\nexport const FeaturesLayout = ['grid', 'list', 'carousel'] as const;\nexport type FeaturesLayoutType = (typeof FeaturesLayout)[number];\n\n// Single feature item schema\nexport const FeatureItemSchema = z.object({\n icon: z.string().max(50, 'Icon name too long').optional(),\n title: z.string().min(1, 'Title is required').max(100, 'Title too long'),\n description: z.string().max(500, 'Description too long').optional(),\n});\n\nexport type FeatureItem = z.infer<typeof FeatureItemSchema>;\n\n// FeaturesBlock content schema with array validation\nexport const FeaturesBlockContentSchema = z.object({\n title: z.string().min(1, 'Section title is required').max(100, 'Title too long'),\n subtitle: z.string().max(200, 'Subtitle too long').optional(),\n features: z\n .array(FeatureItemSchema)\n .min(1, 'At least one feature is required')\n .max(6, 'Maximum 6 features allowed'),\n layout: z.enum(FeaturesLayout).default('grid'),\n});\n\n// Inferred type for components\nexport type FeaturesBlockContent = z.infer<typeof FeaturesBlockContentSchema>;\n\n// Schema name constant\nexport const FEATURES_BLOCK_SCHEMA_NAME = 'features-block';\n\n// Default values for a new FeaturesBlock\nexport const FeaturesBlockDefaults: Partial<FeaturesBlockContent> = {\n layout: 'grid',\n features: [\n {\n icon: 'star',\n title: 'Feature Title',\n description: 'Describe this feature',\n },\n ],\n};\n\n// Validate FeaturesBlock content\nexport function validateFeaturesBlockContent(content: unknown): {\n valid: boolean;\n data?: FeaturesBlockContent;\n errors?: z.ZodError;\n} {\n const result = FeaturesBlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default FeaturesBlock content\nexport function createDefaultFeaturesContent(): FeaturesBlockContent {\n return {\n title: '',\n subtitle: undefined,\n features: [],\n layout: 'grid',\n };\n}\n\n// Add a feature to the content\nexport function addFeature(\n content: FeaturesBlockContent,\n feature: FeatureItem\n): FeaturesBlockContent {\n if (content.features.length >= 6) {\n throw new Error('Maximum 6 features allowed');\n }\n return {\n ...content,\n features: [...content.features, feature],\n };\n}\n\n// Remove a feature by index\nexport function removeFeature(content: FeaturesBlockContent, index: number): FeaturesBlockContent {\n if (index < 0 || index >= content.features.length) {\n throw new Error('Invalid feature index');\n }\n if (content.features.length <= 1) {\n throw new Error('At least one feature is required');\n }\n return {\n ...content,\n features: content.features.filter((_, i) => i !== index),\n };\n}\n\n// Update a feature by index\nexport function updateFeature(\n content: FeaturesBlockContent,\n index: number,\n updates: Partial<FeatureItem>\n): FeaturesBlockContent {\n if (index < 0 || index >= content.features.length) {\n throw new Error('Invalid feature index');\n }\n return {\n ...content,\n features: content.features.map((feature, i) =>\n i === index ? { ...feature, ...updates } : feature\n ),\n };\n}\n","/**\n * HeroBlock Schema.\n *\n * A full-width hero section typically used at the top of pages.\n * Supports headline, subheadline, CTA button, background image, and alignment.\n */\n\nimport { z } from 'zod';\nimport { ImageReferenceSchema } from '../../fields/complex/media';\n\n// Alignment options for the hero content\nexport const HeroAlignment = ['left', 'center', 'right'] as const;\nexport type HeroAlignmentType = (typeof HeroAlignment)[number];\n\n// HeroBlock content schema with validation rules\nexport const HeroBlockContentSchema = z.object({\n headline: z.string().min(1, 'Headline is required').max(100, 'Headline too long'),\n subheadline: z.string().max(200, 'Subheadline too long').optional(),\n ctaText: z.string().max(50, 'CTA text too long').optional(),\n ctaUrl: z\n .string()\n .refine(\n (val) => {\n // Empty string is valid\n if (val === '') return true;\n // Accept full URLs\n if (val.startsWith('http://') || val.startsWith('https://')) {\n try {\n new URL(val);\n return true;\n } catch {\n return false;\n }\n }\n // Accept relative paths starting with /\n if (val.startsWith('/')) {\n return true;\n }\n // Accept hash/anchor links\n if (val.startsWith('#')) {\n return true;\n }\n return false;\n },\n {\n message:\n 'URL must be a valid full URL (http://... or https://...), relative path (/path), anchor (#anchor), or empty string',\n }\n )\n .optional()\n .or(z.literal('')),\n backgroundImage: ImageReferenceSchema.nullable().optional(),\n alignment: z.enum(HeroAlignment).default('center'),\n});\n\n// Inferred type for components\nexport type HeroBlockContent = z.infer<typeof HeroBlockContentSchema>;\n\n// Schema name constant\nexport const HERO_BLOCK_SCHEMA_NAME = 'hero-block';\n\n// Default values for a new HeroBlock\nexport const HeroBlockDefaults: Partial<HeroBlockContent> = {\n alignment: 'center',\n};\n\n// Validate HeroBlock content\nexport function validateHeroBlockContent(content: unknown): {\n valid: boolean;\n data?: HeroBlockContent;\n errors?: z.ZodError;\n} {\n const result = HeroBlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default HeroBlock content\nexport function createDefaultHeroContent(): HeroBlockContent {\n return {\n headline: '',\n subheadline: undefined,\n ctaText: undefined,\n ctaUrl: undefined,\n backgroundImage: undefined,\n alignment: 'center',\n };\n}\n","/**\n * Image and file field factories.\n *\n * The image() factory creates an ImageReference schema that:\n * - References an ImageAsset by ID\n * - Stores context-specific alt text (required for accessibility)\n * - Supports optional hotspot (focal point) and crop settings\n */\n\nimport { z } from 'zod';\n\nimport type { FieldMeta } from '../../types';\nimport {\n DimensionSchema,\n FileSizeSchema,\n MAX_DIMENSION,\n MAX_FILE_SIZE,\n MIN_DIMENSION,\n MimeTypeSchema,\n} from '../../validation/image';\n\n// =============================================================================\n// ImageAsset Schema (Public API type)\n// =============================================================================\n\n/**\n * Public API schema for image assets.\n *\n * This is the camelCase API representation. The database uses snake_case\n * (ImageAssetRow in db/image-asset-types.ts). Conversion between formats\n * should happen at the API boundary.\n *\n * Reuses validation schemas from validation/image.ts to ensure consistency.\n */\nexport const ImageAssetSchema = z.object({\n /** UUID primary key */\n id: z.uuid(),\n /** R2/S3 storage URL for the original file */\n url: z.url(),\n /** Image width in pixels */\n width: DimensionSchema,\n /** Image height in pixels */\n height: DimensionSchema,\n /** Original filename from upload */\n originalFilename: z.string().min(1).max(255),\n /** MIME type (only web-safe formats allowed) */\n mimeType: MimeTypeSchema,\n /** File size in bytes */\n fileSize: FileSizeSchema,\n /** Base64-encoded tiny preview for blur-up loading */\n lqip: z.string().optional(),\n});\n\nexport type ImageAsset = z.infer<typeof ImageAssetSchema>;\n\n// =============================================================================\n// ImageReference Schema (Asset-reference model)\n// =============================================================================\n\n/**\n * Hotspot defines the focal point of an image.\n * Coordinates are fractions (0-1) from top-left.\n */\nexport const HotspotSchema = z.object({\n x: z.number().min(0).max(1),\n y: z.number().min(0).max(1),\n});\n\nexport type Hotspot = z.infer<typeof HotspotSchema>;\n\n/**\n * Crop defines the region to extract from the image.\n * - x, y: Top-left coordinate of the crop region in pixels\n * - width, height: Size of the crop region in pixels\n * All values are positive integers representing pixel coordinates/dimensions.\n */\nexport const CropSchema = z.object({\n /** X coordinate of top-left corner in pixels */\n x: z.number().int().nonnegative(),\n /** Y coordinate of top-left corner in pixels */\n y: z.number().int().nonnegative(),\n /** Width of crop region in pixels (must be > 0) */\n width: z.number().int().positive(),\n /** Height of crop region in pixels (must be > 0) */\n height: z.number().int().positive(),\n});\n\nexport type Crop = z.infer<typeof CropSchema>;\n\n/**\n * ImageReference is what blocks store.\n * It points to an ImageAsset and adds context-specific metadata.\n */\nexport const ImageReferenceSchema = z.object({\n // Alt text is REQUIRED for accessibility\n alt: z.string().min(1, 'Alt text is required for accessibility'),\n\n // Optional metadata\n caption: z.string().max(500).optional(),\n attribution: z.string().max(255).optional(),\n\n // Reference to the ImageAsset with stored transformation\n _asset: z.object({\n id: z.uuid(),\n transformation: z.string().nullable().optional(),\n }),\n});\n\nexport type ImageReference = z.infer<typeof ImageReferenceSchema>;\n\n// =============================================================================\n// Image Field Factory\n// =============================================================================\n\nexport interface ImageFieldOptions {\n label: string;\n description?: string;\n required?: boolean;\n accept?: string;\n maxSize?: number;\n minWidth?: number;\n minHeight?: number;\n maxWidth?: number;\n maxHeight?: number;\n aspectRatio?: number;\n}\n\n/**\n * Create an image field with metadata.\n *\n * @example\n * const featuredImage = image({\n * label: 'Featured Image',\n * required: true,\n * accept: 'image/jpeg, image/png',\n * maxSize: 5 * 1024 * 1024,\n * });\n *\n * // Type of the value:\n * // {\n * // alt: string;\n * // caption?: string;\n * // attribution?: string;\n * // _asset: { id: string; transformation?: string | null };\n * // }\n */\nexport function image(options: ImageFieldOptions) {\n const meta: FieldMeta = {\n label: options.label,\n component: 'image-uploader',\n required: options.required,\n description: options.description,\n options: {\n accept: options.accept ?? 'image/jpeg, image/png, image/webp',\n maxSize: options.maxSize ?? MAX_FILE_SIZE, // 10MB default\n minWidth: options.minWidth ?? MIN_DIMENSION,\n minHeight: options.minHeight ?? MIN_DIMENSION,\n maxWidth: options.maxWidth ?? MAX_DIMENSION,\n maxHeight: options.maxHeight ?? MAX_DIMENSION,\n aspectRatio: options.aspectRatio,\n },\n };\n\n // Use the new ImageReferenceSchema instead of URL-only\n const result = options.required ? ImageReferenceSchema : ImageReferenceSchema.nullish();\n\n return result.meta(meta);\n}\n\n// =============================================================================\n// File Field\n// =============================================================================\n\nexport interface FileFieldOptions {\n label: string;\n description?: string;\n required?: boolean;\n accept?: string;\n maxSize?: number;\n}\n\nconst fileSchema = z.object({\n url: z.url(),\n name: z.string(),\n size: z.number().int().positive(),\n type: z.string(),\n});\n\n/**\n * Create a file field with metadata.\n */\nexport function file(options: FileFieldOptions) {\n const meta: FieldMeta = {\n label: options.label,\n component: 'file-uploader',\n required: options.required,\n description: options.description,\n options: {\n accept: options.accept ?? '*/*',\n maxSize: options.maxSize,\n },\n };\n\n const result = options.required ? fileSchema : fileSchema.nullish();\n return result.meta(meta);\n}\n","/**\n * LogoTrustBlock Schema.\n *\n * A section displaying customer/partner logos for trust and credibility.\n * Supports an optional title and an array of logo images.\n *\n * Each logo uses ImageReference to integrate with the image manager.\n */\n\nimport { z } from 'zod';\n\nimport { ImageReferenceSchema } from '../../fields/complex/media';\n\n// Logo item schema - uses ImageReference for image manager integration\nexport const LogoItemSchema = z.object({\n /** Unique ID for this logo item */\n id: z.uuid(),\n /** Image reference (alt + _asset with transformation) */\n image: ImageReferenceSchema,\n /** Optional company/brand name to display */\n name: z.string().max(100, 'Name too long').optional(),\n});\n\nexport type LogoItem = z.infer<typeof LogoItemSchema>;\n\n/**\n * Legacy logo format schema for backwards compatibility.\n * Used for logos stored with direct URL before the ImageReference migration.\n */\nexport const LegacyLogoItemSchema = z.object({\n /** Direct URL to the logo image */\n url: z.string(),\n /** Alt text for the image */\n alt: z.string(),\n /** Optional company/brand name */\n name: z.string().optional(),\n});\n\nexport type LegacyLogoItem = z.infer<typeof LegacyLogoItemSchema>;\n\n// LogoTrustBlock content schema with validation rules\nexport const LogoTrustBlockContentSchema = z.object({\n title: z.string().max(100, 'Title too long').optional(),\n logos: z.array(LogoItemSchema).max(20, 'Maximum 20 logos allowed'),\n});\n\n// Inferred type for components\nexport type LogoTrustBlockContent = z.infer<typeof LogoTrustBlockContentSchema>;\n\n// Schema name constant\nexport const LOGO_TRUST_BLOCK_SCHEMA_NAME = 'logo-trust-block';\n\n// Default values for a new LogoTrustBlock\nexport const LogoTrustBlockDefaults: Partial<LogoTrustBlockContent> = {\n logos: [],\n};\n\n// Validate LogoTrustBlock content\nexport function validateLogoTrustBlockContent(content: unknown): {\n valid: boolean;\n data?: LogoTrustBlockContent;\n errors?: z.ZodError;\n} {\n const result = LogoTrustBlockContentSchema.safeParse(content);\n if (result.success) {\n return { valid: true, data: result.data };\n }\n return { valid: false, errors: result.error };\n}\n\n// Create default LogoTrustBlock content\nexport function createDefaultLogoTrustContent(): LogoTrustBlockContent {\n return {\n title: undefined,\n logos: [],\n };\n}\n","/**\n * Block Schema Registry.\n *\n * Centralizes registration for all block schemas.\n * Call registerAllBlockSchemas() during application initialization.\n */\n\nimport { clearSchemaRegistry, registerBlockSchema } from '../validation';\nimport { ARTICLE_BLOCK_SCHEMA_NAME, ArticleBlockContentSchema } from './schemas/article-block';\nimport { CTA_BLOCK_SCHEMA_NAME, CTABlockContentSchema } from './schemas/cta-block';\nimport { FEATURES_BLOCK_SCHEMA_NAME, FeaturesBlockContentSchema } from './schemas/features-block';\nimport { HERO_BLOCK_SCHEMA_NAME, HeroBlockContentSchema } from './schemas/hero-block';\nimport {\n LOGO_TRUST_BLOCK_SCHEMA_NAME,\n LogoTrustBlockContentSchema,\n} from './schemas/logo-trust-block';\n\n// All registered block schema names\nexport const BLOCK_SCHEMA_NAMES = [\n ARTICLE_BLOCK_SCHEMA_NAME,\n HERO_BLOCK_SCHEMA_NAME,\n FEATURES_BLOCK_SCHEMA_NAME,\n CTA_BLOCK_SCHEMA_NAME,\n LOGO_TRUST_BLOCK_SCHEMA_NAME,\n] as const;\n\n// Union type for type-safe schema name usage\nexport type BlockSchemaName = (typeof BLOCK_SCHEMA_NAMES)[number];\n\n// Register all block schemas\nexport function registerAllBlockSchemas(): void {\n registerBlockSchema(ARTICLE_BLOCK_SCHEMA_NAME, ArticleBlockContentSchema);\n registerBlockSchema(HERO_BLOCK_SCHEMA_NAME, HeroBlockContentSchema);\n registerBlockSchema(FEATURES_BLOCK_SCHEMA_NAME, FeaturesBlockContentSchema);\n registerBlockSchema(CTA_BLOCK_SCHEMA_NAME, CTABlockContentSchema);\n registerBlockSchema(LOGO_TRUST_BLOCK_SCHEMA_NAME, LogoTrustBlockContentSchema);\n}\n\n// Clear and re-register all block schemas (for testing)\nexport function resetBlockSchemas(): void {\n clearSchemaRegistry();\n registerAllBlockSchemas();\n}\n\n// Type guard for schema names\nexport function isValidBlockSchemaName(name: string): name is BlockSchemaName {\n return BLOCK_SCHEMA_NAMES.includes(name as BlockSchemaName);\n}\n","/**\n * Catch-all Route Handler for Parametric Routes\n *\n * Handles routes with multiple segments like /us/en/products.\n * Uses the CMS API to fetch and render blocks.\n */\n\nimport {\n isArticlePublished,\n isValidBlockSchemaName,\n normalizeArticleContent,\n} from '@repo/cms-schema/blocks';\nimport type { GeneratedBlockRow } from '@repo/cms-schema/db';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { BlockRenderer, CmsEditableInit } from './block-renderer';\nimport { type CmsConfig, getCmsClient } from './cms-api';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype PageProps = CmsConfig & {\n params: Promise<{ slug: string[] }>;\n searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;\n registry?: Partial<BlockComponentRegistry>;\n};\n\nfunction getWebsiteId(providedWebsiteId?: string): string {\n const websiteId =\n providedWebsiteId ??\n process.env.NEXT_PUBLIC_WEBSITE_ID ??\n process.env.WEBSITE_ID ??\n process.env.CMS_WEBSITE_ID;\n\n if (!websiteId) {\n throw new Error(\n 'Missing websiteId for website renderer. Either pass websiteId prop or set NEXT_PUBLIC_WEBSITE_ID (or WEBSITE_ID/CMS_WEBSITE_ID) to a valid UUID.'\n );\n }\n\n const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n if (!uuidRegex.test(websiteId)) {\n throw new Error(\n `Invalid websiteId \"${websiteId}\". Provide a valid UUID via prop or set NEXT_PUBLIC_WEBSITE_ID (or WEBSITE_ID/CMS_WEBSITE_ID).`\n );\n }\n\n return websiteId;\n}\n\n/**\n * Catch-all route handler for parametric routes.\n *\n * Handles paths like:\n * - /us/en/products -> slug = ['us', 'en', 'products']\n * - /about -> slug = ['about']\n *\n * Reconstructs the full path and fetches route via tRPC.\n */\nexport default async function ParametricRoutePage({\n params,\n searchParams,\n registry,\n apiKey,\n cmsUrl,\n websiteId: providedWebsiteId,\n}: PageProps) {\n const websiteId = getWebsiteId(providedWebsiteId);\n const { slug } = await params;\n const resolvedSearchParams = await searchParams;\n\n // Check for ai_preview query param to determine if we should show generated blocks\n let aiPreviewIndex: number | null = null;\n const aiPreviewParam = resolvedSearchParams?.ai_preview;\n if (aiPreviewParam) {\n const paramValue = Array.isArray(aiPreviewParam) ? aiPreviewParam[0] : aiPreviewParam;\n if (paramValue) {\n const parsed = parseInt(paramValue, 10);\n if (!Number.isNaN(parsed)) {\n aiPreviewIndex = parsed;\n }\n }\n }\n\n // Check for edit_mode query param to enable editable wrappers\n const editModeParam = resolvedSearchParams?.edit_mode;\n const editMode = editModeParam === 'true' || editModeParam === '1';\n\n // Reconstruct full path from slug segments and normalize it\n const rawPath = `/${slug.join('/')}`;\n const path = normalizePath(rawPath);\n\n // Get CMS API client with optional API key and custom URL\n const client = getCmsClient({ apiKey, cmsUrl });\n\n try {\n // Fetch route by path via CMS API\n const { route, resolvedParams } = await client.route.getByPath.query({\n websiteId,\n path,\n });\n\n // Only show Live routes on public website\n if (route.state !== 'Live') {\n console.error(`Route found but not Live. Path: ${path}, State: ${route.state}`);\n notFound();\n }\n\n // Fetch all blocks by ID via CMS API (skip any that fail)\n const blockPromises = route.block_ids.map(async (blockId) => {\n try {\n const result = await client.block.getById.query({ websiteId, id: blockId });\n return result.block;\n } catch (error) {\n // Log error but don't fail the entire page\n console.error(`Failed to fetch block ${blockId}:`, error);\n return null;\n }\n });\n\n // Fetch generated blocks for preview only if ai_preview param is set\n // generated_content is an array of variants: [{ description, content }, ...]\n type GeneratedBlockData = Pick<\n GeneratedBlockRow,\n 'id' | 'generated_content' | 'generated_layout' | 'schema_name' | 'status'\n >;\n const generatedBlocksPromise =\n aiPreviewIndex !== null\n ? client.block.getGeneratedByBlockIds\n .query({ websiteId, blockIds: route.block_ids })\n .catch((error) => {\n console.error('Failed to fetch generated blocks:', error);\n return { generatedBlocks: {} as Record<string, GeneratedBlockData> };\n })\n : Promise.resolve({ generatedBlocks: {} as Record<string, GeneratedBlockData> });\n\n const [blockResults, { generatedBlocks }] = await Promise.all([\n Promise.all(blockPromises),\n generatedBlocksPromise,\n ]);\n\n // Transform blocks to BlockData format for BlockRenderer\n // Filter out any blocks that failed to load\n // Use generated content only when ai_preview param is set\n const blocks: BlockData[] = [];\n\n for (const block of blockResults) {\n if (!block || block.published_content === null) continue;\n\n // Use generated content only if ai_preview is set and generated block exists\n // ai_preview is 1-indexed, so subtract 1 to get array index\n let content: Record<string, unknown> | null = null;\n if (aiPreviewIndex !== null) {\n const generatedBlock = generatedBlocks[block.id] as GeneratedBlockData | undefined;\n const variantIndex = aiPreviewIndex - 1; // Convert to 0-indexed\n const variants = generatedBlock?.generated_content;\n if (variants && Array.isArray(variants) && variants[variantIndex]) {\n content = variants[variantIndex].content;\n }\n }\n // Fall back to published content if no variant found\n content = content ?? (block.published_content as Record<string, unknown> | null);\n if (!content) continue;\n\n // Handle 'article' blocks separately before checking schema type\n if (block.schema_name === 'article') {\n const article = normalizeArticleContent(content);\n const isPublished = article ? isArticlePublished(article) : null;\n if (article && isPublished) {\n blocks.push({ id: block.id, type: 'article', content: article });\n }\n continue;\n }\n\n // Custom blocks have a schema_id (UUID); use schema_name as the type\n // so consumers register components by schema_name in the registry\n if (block.schema_id) {\n blocks.push({ id: block.id, type: block.schema_name, content });\n continue;\n }\n\n // Skip predefined blocks with invalid schema names\n if (!isValidBlockSchemaName(block.schema_name)) {\n continue;\n }\n\n // For all predefined block types, map schema_name to type and include content\n // Image references are automatically resolved by block.getById\n blocks.push({\n id: block.id,\n type: block.schema_name,\n content,\n } as BlockData);\n }\n\n // Map resolved params to the slimmed-down shape for block components\n const routeParams: ResolvedRouteParams | undefined = resolvedParams\n ? Object.fromEntries(\n Object.entries(resolvedParams).map(([key, param]) => [\n key,\n {\n value: param.value,\n schemaName: param.schemaName,\n document: {\n id: param.document.id,\n title: param.document.title,\n content: param.document.content,\n schema_name: param.document.schema_name,\n },\n },\n ])\n )\n : undefined;\n\n return (\n <main>\n {editMode && <CmsEditableInit />}\n {blocks.map((block) => (\n <BlockRenderer\n key={block.id}\n block={block}\n registry={registry ?? {}}\n disableEditable={!editMode}\n routeParams={routeParams}\n path={path}\n />\n ))}\n </main>\n );\n } catch (error) {\n // Log error for debugging\n console.error(`Route fetch error for path: ${path}`, error);\n\n // If route not found or param validation fails, show 404\n // TRPCClientError has data.code for the error code\n const errorCode =\n error instanceof Error && 'data' in error\n ? (error as { data?: { code?: string } }).data?.code\n : error instanceof Error && 'code' in error\n ? (error as { code: string }).code\n : undefined;\n\n if (errorCode === 'NOT_FOUND' || errorCode === 'P0002') {\n notFound();\n }\n\n // Re-throw other errors\n throw error;\n }\n}\n\n// -----------------------------------------------------------------------------\n// Metadata\n// -----------------------------------------------------------------------------\n\n/**\n * Generate metadata for the page.\n * Uses Next.js 15+ async params pattern.\n */\nexport async function generateMetadata({\n params,\n apiKey,\n cmsUrl,\n websiteId: providedWebsiteId,\n}: PageProps): Promise<Metadata> {\n const websiteId = getWebsiteId(providedWebsiteId);\n const { slug } = await params;\n const rawPath = `/${slug.join('/')}`;\n const path = normalizePath(rawPath);\n const client = getCmsClient({ apiKey, cmsUrl });\n\n try {\n const { route } = await client.route.getByPath.query({ websiteId, path });\n return {\n title: `${route.path} | Website`,\n description: `Content page: ${route.path}`,\n };\n } catch {\n return {\n title: 'Page Not Found | Website',\n description: 'The requested page could not be found.',\n };\n }\n}\n\nexport function normalizePath(path: string): string {\n if (!path || path === '/') {\n return '/';\n }\n\n // Remove trailing slashes, ensure leading slash\n let normalized = path.trim();\n\n // Remove trailing slashes (but keep root \"/\")\n normalized = normalized.replace(/\\/+$/, '');\n\n // Ensure leading slash\n if (!normalized.startsWith('/')) {\n normalized = `/${normalized}`;\n }\n\n // Collapse multiple consecutive slashes to single slash\n normalized = normalized.replace(/\\/+/g, '/');\n\n return normalized;\n}\n","/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { ClientEditableBlock } from './client-editable-block';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n inSvg: ctx.inSvg,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Track SVG context so we never inject <span> inside SVG subtrees\n const nextInSvg = ctx.inSvg || el.type === 'svg';\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n inSvg: nextInSvg,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// CmsEditableInit — render once per page in edit mode\n// -----------------------------------------------------------------------------\n\n/**\n * Renders the shared CMS edit-mode styles and click-routing script.\n * Place this once at the top of your page when edit_mode is active.\n */\nexport function CmsEditableInit() {\n return (\n <>\n <style>{`\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n `,\n }}\n />\n </>\n );\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n /**\n * The current URL path (e.g. \"/en/test\").\n * When provided, enables path-namespaced component lookup.\n * Registry keys like \"/{lang}/test Article\" will be matched against this path,\n * where `{x}` and `(x)` are treated as single-segment wildcards.\n */\n path?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Path-namespaced component resolution\n// -----------------------------------------------------------------------------\n\n/**\n * Returns true if each segment of `path` matches the corresponding segment in\n * `pattern`, where a pattern segment wrapped in `{…}` or `(…)` is a wildcard.\n */\nfunction pathMatchesPattern(path: string, pattern: string): boolean {\n const pathSegs = path.split('/').filter(Boolean);\n const patternSegs = pattern.split('/').filter(Boolean);\n if (pathSegs.length !== patternSegs.length) return false;\n for (let i = 0; i < patternSegs.length; i++) {\n const seg = patternSegs[i];\n if (!seg) return false;\n if ((seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('(') && seg.endsWith(')'))) {\n continue;\n }\n if (seg !== pathSegs[i]) return false;\n }\n return true;\n}\n\n/**\n * Resolves the component for `blockType` from the registry.\n *\n * When `path` is provided, keys of the form `\"/{pattern} BlockType\"` are\n * checked first. The first key whose path pattern matches the current path\n * and whose block-type suffix matches `blockType` wins. Falls back to a\n * direct `registry[blockType]` lookup.\n */\nfunction resolveComponent(\n registry: Partial<BlockComponentRegistry>,\n blockType: string,\n path?: string\n): BlockComponentRegistry[string] | undefined {\n if (path) {\n for (const key of Object.keys(registry)) {\n if (!key.startsWith('/')) continue;\n const spaceIdx = key.indexOf(' ');\n if (spaceIdx === -1) continue;\n const pathPattern = key.slice(0, spaceIdx);\n const registeredType = key.slice(spaceIdx + 1);\n if (registeredType !== blockType) continue;\n if (pathMatchesPattern(path, pathPattern)) {\n return registry[key];\n }\n }\n }\n return registry[blockType];\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * In editable mode, wraps the block in a ClientEditableBlock that:\n * - Stamps data-cms-block attributes directly on the component's root element\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into the component's root element\n *\n * Render CmsEditableInit once at the page level to include the shared styles\n * and click-routing script.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n path,\n}: BlockRendererProps) {\n const Component = resolveComponent(registry, block.type, path);\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Build content entries for client-side text matching\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n const contentEntries = Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p);\n\n return (\n <ClientEditableBlock blockId={block.id} blockType={block.type} contentEntries={contentEntries}>\n {component}\n </ClientEditableBlock>\n );\n}\n","/**\n * CMS API Client\n *\n * Creates an HTTP-based tRPC client for calling the CMS API.\n * Used in Server Components to fetch routes and blocks from the CMS.\n */\n\nimport type { AppRouter } from '@repo/cms-schema/trpc';\nimport { type CreateTRPCClient, createTRPCClient, httpBatchLink } from '@trpc/client';\nimport superjson from 'superjson';\n\n/** Type alias for the CMS API client */\ntype CmsClient = CreateTRPCClient<AppRouter>;\n\n/**\n * Get the CMS API URL from the provided base URL.\n */\nfunction getCmsApiUrl(cmsUrl: string): string {\n return new URL('/api/trpc', cmsUrl).toString();\n}\n\n/** Unified configuration for all CMS API access */\nexport interface CmsConfig {\n /** CMS API base URL (e.g., 'http://localhost:3000') */\n cmsUrl: string;\n /** API key for authentication */\n apiKey?: string;\n /** Website ID to scope API requests */\n websiteId?: string;\n}\n\n/**\n * Create a custom fetch function that appends API key as query parameter.\n */\nfunction createFetchWithApiKey(apiKey?: string, websiteId?: string) {\n return async (url: URL | RequestInfo, options?: RequestInit): Promise<Response> => {\n let finalUrl = url;\n\n const urlObj = new URL(url.toString());\n if (apiKey) {\n urlObj.searchParams.set('api_key', apiKey);\n }\n if (websiteId) {\n urlObj.searchParams.set('website_id', websiteId);\n }\n if (apiKey || websiteId) {\n finalUrl = urlObj.toString();\n }\n\n const response = await fetch(finalUrl, options);\n\n return response;\n };\n}\n\n/**\n * Create a tRPC client for the CMS API.\n */\nfunction createCmsClient(options: CmsConfig): CmsClient {\n const url = getCmsApiUrl(options.cmsUrl);\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: createFetchWithApiKey(options.apiKey, options.websiteId),\n }),\n ],\n });\n}\n\n/**\n * Get a CMS client for the specified CMS URL.\n */\nexport function getCmsClient(options: CmsConfig): CmsClient {\n return createCmsClient(options);\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z","jsx","jsxs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cms-renderer",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {