cms-renderer 0.6.13 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/block-renderer.tsx","../../lib/cms-overlay-script.ts","../../lib/cms-post-message.ts"],"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 { generateCmsOverlayScript } from './cms-overlay-script';\nimport { getCmsParentTargetOrigin } from './cms-post-message';\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 */\nexport function 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 cmsUrl,\n cmsParentOrigin,\n}: {\n cmsUrl?: string;\n /** Admin window origin when proxied (from ?cms_parent_origin=). */\n cmsParentOrigin?: string;\n}) {\n const targetOrigin = getCmsParentTargetOrigin(cmsUrl, cmsParentOrigin) ?? '';\n\n return (\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for CMS overlay functionality\n dangerouslySetInnerHTML={{\n __html: generateCmsOverlayScript(targetOrigin),\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 * If true, wraps matched text nodes in contentEditable CMS spans.\n */\n enableContentEditable?: 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 */\nexport function 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 */\nexport function 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, renders a hidden sentinel before the component. The shared\n * CMS overlay script uses that sentinel to stamp attributes on the component's\n * root element without adding a layout-affecting wrapper.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n enableContentEditable,\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 // Extract language code from route params if any param is bound to the language schema.\n const language = routeParams\n ? Object.values(routeParams).find((p) => p.schemaName === 'language')?.value\n : undefined;\n\n const component = (\n <Component content={block.content} routeParams={routeParams} language={language} />\n );\n\n if (disableEditable) {\n return component;\n }\n\n const contentEntries = enableContentEditable\n ? [...extractContentValues(block.content as Record<string, unknown>).values()]\n .flat()\n .map(({ value, contentPath }) => ({ v: value, p: contentPath }))\n : undefined;\n\n return (\n <>\n <span\n data-cms-sentinel=\"\"\n data-block-id={block.id}\n data-block-type={block.type}\n data-content-entries={contentEntries ? JSON.stringify(contentEntries) : undefined}\n style={{ display: 'none' }}\n aria-hidden=\"true\"\n />\n {component}\n </>\n );\n}\n","/**\n * CMS Overlay Script\n *\n * Framework-agnostic vanilla JS script that handles all CMS edit mode overlays:\n * - Block hover outlines\n * - Cursor indicator\n * - Block toolbar (move up/down, delete)\n *\n * Uses a sentinel-based approach: hidden <span> elements mark block positions,\n * and this script stamps data attributes on the actual block elements (next sibling).\n * This avoids wrapper elements that could break layouts.\n *\n * Compatible with Next.js, TanStack Start, Remix, and other frameworks.\n */\n\nexport function generateCmsOverlayScript(cmsParentOrigin: string): string {\n return `\n(function() {\n if (window.__cmsOverlayInitialized) return;\n window.__cmsOverlayInitialized = true;\n\n var CMS_PARENT_ORIGIN = ${JSON.stringify(cmsParentOrigin)};\n\n var style = document.createElement('style');\n style.setAttribute('data-cms-overlay', '');\n style.textContent = \\`\n [data-cms-block],\n [data-cms-editable] {\n cursor: pointer;\n }\n [data-cms-editable] {\n border-radius: 2px;\n }\n [data-cms-editable]:not([data-cms-block]):hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n #cms-overlay-root {\n position: fixed;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n pointer-events: none;\n z-index: 99998;\n }\n #cms-overlay-root > *:not(.cms-block-toolbar) {\n pointer-events: none;\n }\n .cms-block-outline {\n position: fixed;\n border: 2px solid #3b82f6;\n border-radius: 4px;\n pointer-events: none;\n z-index: 99997;\n transition: all 0.15s ease;\n }\n .cms-block-outline.cms-outline-selected {\n border-color: #2563eb;\n border-width: 3px;\n }\n .cms-block-cursor {\n position: fixed;\n width: 22px;\n height: 22px;\n background: radial-gradient(circle, #fff 5px, #000 5px);\n border-radius: 50%;\n pointer-events: none;\n z-index: 99999;\n transform: translate(-50%, -50%);\n opacity: 0;\n transition: opacity 0.1s ease;\n }\n .cms-block-cursor.cms-cursor-visible {\n opacity: 1;\n }\n .cms-block-toolbar {\n position: fixed;\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 4px 12px rgba(0,0,0,0.25);\n z-index: 99999;\n pointer-events: none;\n opacity: 0;\n transform: scale(0.9) translateY(4px);\n transition: opacity 0.15s ease, transform 0.15s ease;\n }\n .cms-block-toolbar.cms-toolbar-visible {\n opacity: 1;\n transform: scale(1) translateY(0);\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 \\`;\n document.head.appendChild(style);\n\n function stampBlockElements() {\n var sentinels = document.querySelectorAll('[data-cms-sentinel]');\n sentinels.forEach(function(sentinel) {\n var blockEl = sentinel.nextElementSibling;\n if (!blockEl) return;\n\n var blockId = sentinel.getAttribute('data-block-id');\n var blockType = sentinel.getAttribute('data-block-type');\n\n blockEl.setAttribute('data-cms-block', '');\n blockEl.setAttribute('data-block-id', blockId);\n blockEl.setAttribute('data-block-type', blockType);\n\n injectEditableSpans(\n blockEl,\n blockId,\n blockType,\n sentinel.getAttribute('data-content-entries')\n );\n });\n }\n\n function injectEditableSpans(blockEl, blockId, blockType, rawEntries) {\n if (!rawEntries || blockEl.querySelector('[data-cms-editable][data-content-path]')) return;\n\n var entries;\n try {\n entries = JSON.parse(rawEntries);\n } catch (_) {\n return;\n }\n if (!Array.isArray(entries) || entries.length === 0) return;\n\n var used = {};\n var walker = document.createTreeWalker(blockEl, NodeFilter.SHOW_TEXT, null);\n var textNodes = [];\n var node = walker.nextNode();\n while (node !== null) {\n if (node.nodeValue && node.nodeValue.trim()) {\n textNodes.push(node);\n }\n node = walker.nextNode();\n }\n\n for (var i = 0; i < textNodes.length; i++) {\n var textNode = textNodes[i];\n var parentEl = textNode.parentElement;\n if (!parentEl || parentEl.closest('svg') || parentEl.closest('[data-cms-editable]')) {\n continue;\n }\n\n var text = textNode.nodeValue;\n if (!text) continue;\n\n for (var j = 0; j < entries.length; j++) {\n var entry = entries[j];\n if (!entry || typeof entry.v !== 'string' || typeof entry.p !== 'string' || used[entry.p]) {\n continue;\n }\n if (text.trim() !== entry.v.trim()) continue;\n\n used[entry.p] = true;\n var 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 span.setAttribute('contenteditable', 'true');\n parentEl.insertBefore(span, textNode);\n span.appendChild(textNode);\n break;\n }\n }\n }\n\n stampBlockElements();\n\n var stampObserver = new MutationObserver(function(mutations) {\n var needsStamp = mutations.some(function(m) {\n return m.addedNodes.length > 0;\n });\n if (needsStamp) stampBlockElements();\n });\n stampObserver.observe(document.body, { childList: true, subtree: true });\n\n var overlayRoot = document.createElement('div');\n overlayRoot.id = 'cms-overlay-root';\n document.body.appendChild(overlayRoot);\n\n var cursor = document.createElement('div');\n cursor.className = 'cms-block-cursor';\n overlayRoot.appendChild(cursor);\n\n var outline = document.createElement('div');\n outline.className = 'cms-block-outline';\n outline.style.display = 'none';\n overlayRoot.appendChild(outline);\n\n var selectedOutline = document.createElement('div');\n selectedOutline.className = 'cms-block-outline cms-outline-selected';\n selectedOutline.style.display = 'none';\n overlayRoot.appendChild(selectedOutline);\n\n var toolbar = document.createElement('div');\n toolbar.className = 'cms-block-toolbar';\n toolbar.setAttribute('data-cms-toolbar', '');\n toolbar.innerHTML = \\`\n <button class=\"move-up\" title=\"Move up\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M18 15l-6-6-6 6\"/>\n </svg>\n </button>\n <button class=\"move-down\" title=\"Move down\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M6 9l6 6 6-6\"/>\n </svg>\n </button>\n <button class=\"delete\" title=\"Delete\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\"/>\n </svg>\n </button>\n \\`;\n overlayRoot.appendChild(toolbar);\n\n var currentBlockId = null;\n var toolbarVisible = false;\n var selectedBlockId = null;\n\n function decoratePreviewUrl(rawHref) {\n if (!rawHref) return rawHref;\n try {\n var url = new URL(rawHref, window.location.href);\n if (url.origin !== window.location.origin) return rawHref;\n url.searchParams.set('edit_mode', 'true');\n if (CMS_PARENT_ORIGIN) {\n url.searchParams.set('cms_parent_origin', CMS_PARENT_ORIGIN);\n }\n return url.toString();\n } catch (_) {\n return rawHref;\n }\n }\n\n function preserveEditParamsOnNavigation(e) {\n if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return;\n }\n\n var anchor = e.target.closest('a[href]');\n if (!anchor || (anchor.target && anchor.target !== '_self') || anchor.hasAttribute('download')) {\n return;\n }\n\n var href = anchor.getAttribute('href');\n var decorated = decoratePreviewUrl(href);\n if (decorated && decorated !== href) {\n anchor.setAttribute('href', decorated);\n }\n }\n\n function postToParent(message) {\n if (!CMS_PARENT_ORIGIN || !window.parent || window.parent === window) {\n return;\n }\n window.parent.postMessage(message, CMS_PARENT_ORIGIN);\n }\n\n function sendReadySignal() {\n postToParent({ type: 'cms-preview-ready' });\n }\n sendReadySignal();\n // The preview iframe can execute before the admin listener is attached.\n setTimeout(sendReadySignal, 500);\n setTimeout(sendReadySignal, 1500);\n\n function getBlockElement(blockId) {\n return document.querySelector('[data-block-id=\"' + blockId + '\"]');\n }\n\n function getAllBlocks() {\n return Array.from(document.querySelectorAll('[data-cms-block]'));\n }\n\n function getBlockIndex(blockId) {\n var blocks = getAllBlocks();\n for (var i = 0; i < blocks.length; i++) {\n if (blocks[i].getAttribute('data-block-id') === blockId) return i;\n }\n return -1;\n }\n\n function updateOutline(el, outlineEl) {\n if (!el) {\n outlineEl.style.display = 'none';\n return;\n }\n var rect = el.getBoundingClientRect();\n outlineEl.style.display = 'block';\n outlineEl.style.top = rect.top + 'px';\n outlineEl.style.left = rect.left + 'px';\n outlineEl.style.width = rect.width + 'px';\n outlineEl.style.height = rect.height + 'px';\n }\n\n function positionToolbar(x, y) {\n var rect = toolbar.getBoundingClientRect();\n var top = Math.max(4, y - rect.height - 12);\n var left = Math.max(4, Math.min(x - rect.width / 2, window.innerWidth - rect.width - 4));\n toolbar.style.top = top + 'px';\n toolbar.style.left = left + 'px';\n }\n\n function showToolbar(x, y, blockId) {\n currentBlockId = blockId;\n toolbarVisible = true;\n positionToolbar(x, y);\n toolbar.classList.add('cms-toolbar-visible');\n\n var index = getBlockIndex(blockId);\n var total = getAllBlocks().length;\n toolbar.querySelector('.move-up').disabled = index <= 0;\n toolbar.querySelector('.move-down').disabled = index >= total - 1;\n }\n\n function hideToolbar() {\n toolbarVisible = false;\n toolbar.classList.remove('cms-toolbar-visible');\n }\n\n function showCursor(x, y) {\n cursor.style.top = y + 'px';\n cursor.style.left = x + 'px';\n cursor.classList.add('cms-cursor-visible');\n }\n\n function hideCursor() {\n cursor.classList.remove('cms-cursor-visible');\n }\n\n document.addEventListener('mouseover', function(e) {\n if (toolbarVisible) return;\n var block = e.target.closest('[data-cms-block]');\n if (block) {\n updateOutline(block, outline);\n }\n });\n\n document.addEventListener('mouseout', function(e) {\n var block = e.target.closest('[data-cms-block]');\n var related = e.relatedTarget ? e.relatedTarget.closest('[data-cms-block]') : null;\n if (block && block !== related) {\n outline.style.display = 'none';\n hideCursor();\n }\n });\n\n document.addEventListener('mousemove', function(e) {\n if (toolbarVisible) return;\n var block = e.target.closest('[data-cms-block]');\n if (block) {\n showCursor(e.clientX, e.clientY);\n }\n });\n\n document.addEventListener('contextmenu', function(e) {\n if (e.target.closest('[data-cms-toolbar]')) return;\n var block = e.target.closest('[data-cms-block]');\n if (block) {\n if (toolbarVisible) return;\n e.preventDefault();\n hideCursor();\n outline.style.display = 'none';\n var blockId = block.getAttribute('data-block-id');\n showToolbar(e.clientX, e.clientY, blockId);\n }\n });\n\n document.addEventListener('click', function(e) {\n preserveEditParamsOnNavigation(e);\n\n if (toolbarVisible && !e.target.closest('[data-cms-toolbar]')) {\n var block = e.target.closest('[data-cms-block]');\n if (!block || block.getAttribute('data-block-id') !== currentBlockId) {\n hideToolbar();\n }\n }\n\n if (e.target.closest('[data-cms-toolbar]')) return;\n\n var editable = e.target.closest('[data-cms-editable]');\n if (editable) {\n postToParent({\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 return;\n }\n\n var block = e.target.closest('[data-cms-block]');\n if (block) {\n postToParent({\n type: 'cms-editable-click',\n blockId: block.getAttribute('data-block-id'),\n blockType: block.getAttribute('data-block-type'),\n contentPath: null\n });\n }\n }, true);\n\n toolbar.querySelector('.move-up').addEventListener('click', function() {\n if (!currentBlockId) return;\n postToParent({ type: 'cms-block-action', action: 'move-up', blockId: currentBlockId });\n hideToolbar();\n });\n\n toolbar.querySelector('.move-down').addEventListener('click', function() {\n if (!currentBlockId) return;\n postToParent({ type: 'cms-block-action', action: 'move-down', blockId: currentBlockId });\n hideToolbar();\n });\n\n toolbar.querySelector('.delete').addEventListener('click', function() {\n if (!currentBlockId) return;\n postToParent({ type: 'cms-block-action', action: 'delete', blockId: currentBlockId });\n hideToolbar();\n });\n\n window.addEventListener('scroll', function() {\n hideCursor();\n hideToolbar();\n if (selectedBlockId) {\n var el = getBlockElement(selectedBlockId);\n updateOutline(el, selectedOutline);\n }\n }, { passive: true, capture: true });\n\n window.addEventListener('resize', function() {\n if (selectedBlockId) {\n var el = getBlockElement(selectedBlockId);\n updateOutline(el, selectedOutline);\n }\n }, { passive: true });\n\n window.addEventListener('message', function(e) {\n if (CMS_PARENT_ORIGIN && e.origin !== CMS_PARENT_ORIGIN) return;\n if (e.source !== window.parent) return;\n\n if (e.data && e.data.type === 'cms-select-block') {\n selectedBlockId = e.data.blockId || null;\n if (selectedBlockId) {\n var el = getBlockElement(selectedBlockId);\n updateOutline(el, selectedOutline);\n } else {\n selectedOutline.style.display = 'none';\n }\n }\n });\n})();\n`;\n}\n","/**\n * Trusted-origin helpers for CMS template-builder postMessage traffic\n * between the admin UI (parent) and the site preview iframe (child).\n */\n\nexport const CMS_PARENT_ORIGIN_PARAM = 'cms_parent_origin';\n\nexport function parseOrigin(url: string | undefined): string | null {\n if (!url) return null;\n try {\n return new URL(url).origin;\n } catch {\n return null;\n }\n}\n\nfunction getParentOriginFromQueryParam(): string | null {\n if (typeof window === 'undefined') return null;\n const value = new URLSearchParams(window.location.search).get(CMS_PARENT_ORIGIN_PARAM);\n return parseOrigin(value ?? undefined);\n}\n\nfunction getReferrerOrigin(): string | null {\n if (typeof document === 'undefined' || !document.referrer) return null;\n return parseOrigin(document.referrer);\n}\n\n/** Same-origin embed (e.g. proxied admin + preview on the customer domain). */\nfunction getSameOriginParentOrigin(): string | null {\n if (typeof window === 'undefined' || !window.parent || window.parent === window) {\n return null;\n }\n try {\n return window.parent.location.origin;\n } catch {\n return null;\n }\n}\n\n/** Origins allowed to send messages to the preview iframe (CMS admin hosts). */\nexport function getAllowedCmsParentOrigins(explicitParentOrigin?: string): string[] {\n const origins = new Set<string>();\n const apiOrigin = parseOrigin(process.env.NEXT_PUBLIC_CMS_API_URL);\n if (apiOrigin) origins.add(apiOrigin);\n\n const fromQuery = parseOrigin(explicitParentOrigin) ?? getParentOriginFromQueryParam();\n if (fromQuery) origins.add(fromQuery);\n\n const referrerOrigin = getReferrerOrigin();\n if (referrerOrigin) origins.add(referrerOrigin);\n\n const sameOriginParent = getSameOriginParentOrigin();\n if (sameOriginParent) origins.add(sameOriginParent);\n\n return [...origins];\n}\n\n/**\n * Target origin when posting from the preview iframe to the CMS parent.\n *\n * When the admin is proxied on a customer domain, the parent window origin is\n * the customer site — not NEXT_PUBLIC_CMS_API_URL. Prefer explicit/referrer/\n * same-origin parent detection over the CMS API URL.\n */\nexport function getCmsParentTargetOrigin(\n preferredCmsUrl?: string,\n explicitParentOrigin?: string\n): string | null {\n const fromExplicit = parseOrigin(explicitParentOrigin) ?? getParentOriginFromQueryParam();\n if (fromExplicit) return fromExplicit;\n\n const referrerOrigin = getReferrerOrigin();\n if (referrerOrigin) return referrerOrigin;\n\n const sameOriginParent = getSameOriginParentOrigin();\n if (sameOriginParent) return sameOriginParent;\n\n const preferredOrigin = parseOrigin(preferredCmsUrl);\n if (preferredOrigin) return preferredOrigin;\n\n const allowed = getAllowedCmsParentOrigins(explicitParentOrigin);\n return allowed[0] ?? null;\n}\n\nexport function isTrustedCmsParentMessage(\n event: MessageEvent,\n explicitParentOrigin?: string\n): boolean {\n const allowed = getAllowedCmsParentOrigins(explicitParentOrigin);\n if (allowed.length === 0) return false;\n if (!allowed.includes(event.origin)) return false;\n return event.source === window.parent;\n}\n\nexport function postMessageToCmsParent(\n message: unknown,\n options?: { preferredCmsUrl?: string; explicitParentOrigin?: string }\n): void {\n if (typeof window === 'undefined' || !window.parent || window.parent === window) return;\n const targetOrigin = getCmsParentTargetOrigin(\n options?.preferredCmsUrl,\n options?.explicitParentOrigin\n );\n if (!targetOrigin) return;\n window.parent.postMessage(message, targetOrigin);\n}\n"],"mappings":";AAOA,OAAO,WAAW;;;ACQX,SAAS,yBAAyB,iBAAiC;AACxE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,4BAKmB,KAAK,UAAU,eAAe,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
1
+ {"version":3,"sources":["../../lib/block-renderer.tsx","../../lib/cms-overlay-script.ts","../../lib/cms-post-message.ts"],"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 { generateCmsOverlayScript } from './cms-overlay-script';\nimport { getCmsParentTargetOrigin } from './cms-post-message';\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 */\nexport function 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 cmsUrl,\n cmsParentOrigin,\n}: {\n cmsUrl?: string;\n /** Admin window origin when proxied (from ?cms_parent_origin=). */\n cmsParentOrigin?: string;\n}) {\n const targetOrigin = getCmsParentTargetOrigin(cmsUrl, cmsParentOrigin) ?? '';\n\n return (\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for CMS overlay functionality\n dangerouslySetInnerHTML={{\n __html: generateCmsOverlayScript(targetOrigin),\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 * If true, wraps matched text nodes in contentEditable CMS spans.\n */\n enableContentEditable?: 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 */\nexport function 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 */\nexport function 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, renders a hidden sentinel before the component. The shared\n * CMS overlay script uses that sentinel to stamp attributes on the component's\n * root element without adding a layout-affecting wrapper.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n enableContentEditable,\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 // Extract language code from route params if any param is bound to the language schema.\n const language = routeParams\n ? Object.values(routeParams).find((p) => p.schemaName === 'language')?.value\n : undefined;\n\n const component = (\n <Component content={block.content} routeParams={routeParams} language={language} path={path} />\n );\n\n if (disableEditable) {\n return component;\n }\n\n const contentEntries = enableContentEditable\n ? [...extractContentValues(block.content as Record<string, unknown>).values()]\n .flat()\n .map(({ value, contentPath }) => ({ v: value, p: contentPath }))\n : undefined;\n\n return (\n <>\n <span\n data-cms-sentinel=\"\"\n data-block-id={block.id}\n data-block-type={block.type}\n data-content-entries={contentEntries ? JSON.stringify(contentEntries) : undefined}\n style={{ display: 'none' }}\n aria-hidden=\"true\"\n />\n {component}\n </>\n );\n}\n","/**\n * CMS Overlay Script\n *\n * Framework-agnostic vanilla JS script that handles all CMS edit mode overlays:\n * - Block hover outlines\n * - Cursor indicator\n * - Block toolbar (move up/down, delete)\n *\n * Uses a sentinel-based approach: hidden <span> elements mark block positions,\n * and this script stamps data attributes on the actual block elements (next sibling).\n * This avoids wrapper elements that could break layouts.\n *\n * Compatible with Next.js, TanStack Start, Remix, and other frameworks.\n */\n\nexport function generateCmsOverlayScript(cmsParentOrigin: string): string {\n return `\n(function() {\n if (window.__cmsOverlayInitialized) return;\n window.__cmsOverlayInitialized = true;\n\n var CMS_PARENT_ORIGIN = ${JSON.stringify(cmsParentOrigin)};\n\n var style = document.createElement('style');\n style.setAttribute('data-cms-overlay', '');\n style.textContent = \\`\n [data-cms-block],\n [data-cms-editable] {\n cursor: pointer;\n }\n [data-cms-editable] {\n border-radius: 2px;\n }\n [data-cms-editable]:not([data-cms-block]):hover {\n outline: 2px solid var(--component-accent, #A78BFA);\n outline-offset: 2px;\n }\n #cms-overlay-root {\n position: fixed;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n pointer-events: none;\n z-index: 99998;\n }\n #cms-overlay-root > *:not(.cms-block-toolbar) {\n pointer-events: none;\n }\n .cms-block-outline {\n position: fixed;\n box-sizing: border-box;\n border: 4px solid var(--component-accent, #A78BFA);\n border-radius: 4px;\n pointer-events: none;\n z-index: 99997;\n transition: all 0.15s ease;\n }\n .cms-block-outline.cms-outline-selected {\n border-color: var(--component-accent, #A78BFA);\n border-width: 4px;\n }\n .cms-block-label {\n position: absolute;\n top: 0;\n left: 0;\n display: none;\n max-width: 240px;\n padding: 2px 8px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 12px;\n font-weight: 600;\n line-height: 1.4;\n color: #fff;\n background: var(--component-accent, #A78BFA);\n border-radius: 4px 4px 4px 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n pointer-events: none;\n }\n .cms-block-label.cms-label-visible {\n display: block;\n }\n .cms-block-cursor {\n position: fixed;\n width: 22px;\n height: 22px;\n background: radial-gradient(circle, #fff 5px, #000 5px);\n border-radius: 50%;\n pointer-events: none;\n z-index: 99999;\n transform: translate(-50%, -50%);\n opacity: 0;\n transition: opacity 0.1s ease;\n }\n .cms-block-cursor.cms-cursor-visible {\n opacity: 1;\n }\n .cms-block-toolbar {\n position: fixed;\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 4px 12px rgba(0,0,0,0.25);\n z-index: 99999;\n pointer-events: none;\n opacity: 0;\n transform: scale(0.9) translateY(4px);\n transition: opacity 0.15s ease, transform 0.15s ease;\n }\n .cms-block-toolbar.cms-toolbar-visible {\n opacity: 1;\n transform: scale(1) translateY(0);\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 \\`;\n document.head.appendChild(style);\n\n function stampBlockElements() {\n var sentinels = document.querySelectorAll('[data-cms-sentinel]');\n sentinels.forEach(function(sentinel) {\n var blockEl = sentinel.nextElementSibling;\n if (!blockEl) return;\n\n var blockId = sentinel.getAttribute('data-block-id');\n var blockType = sentinel.getAttribute('data-block-type');\n\n blockEl.setAttribute('data-cms-block', '');\n blockEl.setAttribute('data-block-id', blockId);\n blockEl.setAttribute('data-block-type', blockType);\n\n injectEditableSpans(\n blockEl,\n blockId,\n blockType,\n sentinel.getAttribute('data-content-entries')\n );\n });\n }\n\n function injectEditableSpans(blockEl, blockId, blockType, rawEntries) {\n if (!rawEntries || blockEl.querySelector('[data-cms-editable][data-content-path]')) return;\n\n var entries;\n try {\n entries = JSON.parse(rawEntries);\n } catch (_) {\n return;\n }\n if (!Array.isArray(entries) || entries.length === 0) return;\n\n var used = {};\n var walker = document.createTreeWalker(blockEl, NodeFilter.SHOW_TEXT, null);\n var textNodes = [];\n var node = walker.nextNode();\n while (node !== null) {\n if (node.nodeValue && node.nodeValue.trim()) {\n textNodes.push(node);\n }\n node = walker.nextNode();\n }\n\n for (var i = 0; i < textNodes.length; i++) {\n var textNode = textNodes[i];\n var parentEl = textNode.parentElement;\n if (!parentEl || parentEl.closest('svg') || parentEl.closest('[data-cms-editable]')) {\n continue;\n }\n\n var text = textNode.nodeValue;\n if (!text) continue;\n\n for (var j = 0; j < entries.length; j++) {\n var entry = entries[j];\n if (!entry || typeof entry.v !== 'string' || typeof entry.p !== 'string' || used[entry.p]) {\n continue;\n }\n if (text.trim() !== entry.v.trim()) continue;\n\n used[entry.p] = true;\n var 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 span.setAttribute('contenteditable', 'true');\n parentEl.insertBefore(span, textNode);\n span.appendChild(textNode);\n break;\n }\n }\n }\n\n stampBlockElements();\n\n var stampObserver = new MutationObserver(function(mutations) {\n var needsStamp = mutations.some(function(m) {\n return m.addedNodes.length > 0;\n });\n if (needsStamp) stampBlockElements();\n });\n stampObserver.observe(document.body, { childList: true, subtree: true });\n\n var overlayRoot = document.createElement('div');\n overlayRoot.id = 'cms-overlay-root';\n document.body.appendChild(overlayRoot);\n\n var cursor = document.createElement('div');\n cursor.className = 'cms-block-cursor';\n overlayRoot.appendChild(cursor);\n\n function createOutline(extraClass) {\n var el = document.createElement('div');\n el.className = 'cms-block-outline' + (extraClass ? ' ' + extraClass : '');\n el.style.display = 'none';\n var label = document.createElement('span');\n label.className = 'cms-block-label';\n el.appendChild(label);\n overlayRoot.appendChild(el);\n return el;\n }\n\n var outline = createOutline();\n var selectedOutline = createOutline('cms-outline-selected');\n\n var toolbar = document.createElement('div');\n toolbar.className = 'cms-block-toolbar';\n toolbar.setAttribute('data-cms-toolbar', '');\n toolbar.innerHTML = \\`\n <button class=\"move-up\" title=\"Move up\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M18 15l-6-6-6 6\"/>\n </svg>\n </button>\n <button class=\"move-down\" title=\"Move down\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M6 9l6 6 6-6\"/>\n </svg>\n </button>\n <button class=\"delete\" title=\"Delete\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\"/>\n </svg>\n </button>\n \\`;\n overlayRoot.appendChild(toolbar);\n\n var currentBlockId = null;\n var toolbarVisible = false;\n var selectedBlockId = null;\n\n // In edit mode we hijack every link/button activation. Following a link or\n // firing a button's own handler makes inline editing on that element\n // impossible (the click navigates away or triggers an action instead of\n // placing the caret). We block the default action and stop the event from\n // reaching the element's own listeners, while still letting our selection /\n // edit routing run below.\n function blockInteractiveActivation(e) {\n var interactive = e.target.closest(\n 'a[href], button, [role=\"button\"], input[type=\"submit\"], input[type=\"button\"], input[type=\"reset\"]'\n );\n if (interactive) {\n e.preventDefault();\n e.stopPropagation();\n }\n }\n\n function postToParent(message) {\n if (!CMS_PARENT_ORIGIN || !window.parent || window.parent === window) {\n return;\n }\n window.parent.postMessage(message, CMS_PARENT_ORIGIN);\n }\n\n function sendReadySignal() {\n postToParent({ type: 'cms-preview-ready' });\n }\n sendReadySignal();\n // The preview iframe can execute before the admin listener is attached.\n setTimeout(sendReadySignal, 500);\n setTimeout(sendReadySignal, 1500);\n\n function getBlockElement(blockId) {\n // Match the real block element, not the hidden sentinel <span> that shares\n // the same data-block-id and precedes it in the DOM. The sentinel is\n // display:none (a 0x0 rect), so selecting it would paint the outline at the\n // top-left corner. Only the stamped block carries data-cms-block.\n return document.querySelector('[data-cms-block][data-block-id=\"' + blockId + '\"]');\n }\n\n function getAllBlocks() {\n return Array.from(document.querySelectorAll('[data-cms-block]'));\n }\n\n function getBlockIndex(blockId) {\n var blocks = getAllBlocks();\n for (var i = 0; i < blocks.length; i++) {\n if (blocks[i].getAttribute('data-block-id') === blockId) return i;\n }\n return -1;\n }\n\n function formatBlockType(type) {\n if (!type) return 'Block';\n return type\n .replace(/[-_]+/g, ' ')\n .replace(/\\\\b\\\\w/g, function(c) { return c.toUpperCase(); });\n }\n\n function updateOutline(el, outlineEl) {\n var label = outlineEl.querySelector('.cms-block-label');\n if (!el) {\n outlineEl.style.display = 'none';\n if (label) label.classList.remove('cms-label-visible');\n return;\n }\n var rect = el.getBoundingClientRect();\n // A 0x0 rect means the element isn't laid out yet (or is hidden). Hide the\n // outline instead of drawing a zero-size box with a floating label at the\n // top-left corner.\n if (rect.width === 0 && rect.height === 0) {\n outlineEl.style.display = 'none';\n if (label) label.classList.remove('cms-label-visible');\n return;\n }\n outlineEl.style.display = 'block';\n outlineEl.style.top = rect.top + 'px';\n outlineEl.style.left = rect.left + 'px';\n outlineEl.style.width = rect.width + 'px';\n outlineEl.style.height = rect.height + 'px';\n if (label) {\n label.textContent = formatBlockType(el.getAttribute('data-block-type'));\n label.classList.add('cms-label-visible');\n }\n }\n\n function positionToolbar(x, y) {\n var rect = toolbar.getBoundingClientRect();\n var top = Math.max(4, y - rect.height - 12);\n var left = Math.max(4, Math.min(x - rect.width / 2, window.innerWidth - rect.width - 4));\n toolbar.style.top = top + 'px';\n toolbar.style.left = left + 'px';\n }\n\n function showToolbar(x, y, blockId) {\n currentBlockId = blockId;\n toolbarVisible = true;\n positionToolbar(x, y);\n toolbar.classList.add('cms-toolbar-visible');\n\n var index = getBlockIndex(blockId);\n var total = getAllBlocks().length;\n toolbar.querySelector('.move-up').disabled = index <= 0;\n toolbar.querySelector('.move-down').disabled = index >= total - 1;\n }\n\n function hideToolbar() {\n toolbarVisible = false;\n toolbar.classList.remove('cms-toolbar-visible');\n }\n\n function showCursor(x, y) {\n cursor.style.top = y + 'px';\n cursor.style.left = x + 'px';\n cursor.classList.add('cms-cursor-visible');\n }\n\n function hideCursor() {\n cursor.classList.remove('cms-cursor-visible');\n }\n\n document.addEventListener('mouseover', function(e) {\n if (toolbarVisible) return;\n var block = e.target.closest('[data-cms-block]');\n if (block) {\n updateOutline(block, outline);\n }\n });\n\n document.addEventListener('mouseout', function(e) {\n var block = e.target.closest('[data-cms-block]');\n var related = e.relatedTarget ? e.relatedTarget.closest('[data-cms-block]') : null;\n if (block && block !== related) {\n outline.style.display = 'none';\n hideCursor();\n }\n });\n\n document.addEventListener('mousemove', function(e) {\n if (toolbarVisible) return;\n var block = e.target.closest('[data-cms-block]');\n if (block) {\n showCursor(e.clientX, e.clientY);\n }\n });\n\n document.addEventListener('contextmenu', function(e) {\n if (e.target.closest('[data-cms-toolbar]')) return;\n var block = e.target.closest('[data-cms-block]');\n if (block) {\n if (toolbarVisible) return;\n e.preventDefault();\n hideCursor();\n outline.style.display = 'none';\n var blockId = block.getAttribute('data-block-id');\n showToolbar(e.clientX, e.clientY, blockId);\n }\n });\n\n document.addEventListener('click', function(e) {\n // The floating toolbar is interactive (and its buttons are real <button>s);\n // bail out before any blocking so its own handlers run.\n if (e.target.closest('[data-cms-toolbar]')) return;\n\n blockInteractiveActivation(e);\n\n if (toolbarVisible) {\n var activeBlock = e.target.closest('[data-cms-block]');\n if (!activeBlock || activeBlock.getAttribute('data-block-id') !== currentBlockId) {\n hideToolbar();\n }\n }\n\n var editable = e.target.closest('[data-cms-editable]');\n if (editable) {\n postToParent({\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 return;\n }\n\n var block = e.target.closest('[data-cms-block]');\n if (block) {\n postToParent({\n type: 'cms-editable-click',\n blockId: block.getAttribute('data-block-id'),\n blockType: block.getAttribute('data-block-type'),\n contentPath: null\n });\n }\n }, true);\n\n toolbar.querySelector('.move-up').addEventListener('click', function() {\n if (!currentBlockId) return;\n postToParent({ type: 'cms-block-action', action: 'move-up', blockId: currentBlockId });\n hideToolbar();\n });\n\n toolbar.querySelector('.move-down').addEventListener('click', function() {\n if (!currentBlockId) return;\n postToParent({ type: 'cms-block-action', action: 'move-down', blockId: currentBlockId });\n hideToolbar();\n });\n\n toolbar.querySelector('.delete').addEventListener('click', function() {\n if (!currentBlockId) return;\n postToParent({ type: 'cms-block-action', action: 'delete', blockId: currentBlockId });\n hideToolbar();\n });\n\n window.addEventListener('scroll', function() {\n hideCursor();\n hideToolbar();\n if (selectedBlockId) {\n var el = getBlockElement(selectedBlockId);\n updateOutline(el, selectedOutline);\n }\n }, { passive: true, capture: true });\n\n window.addEventListener('resize', function() {\n if (selectedBlockId) {\n var el = getBlockElement(selectedBlockId);\n updateOutline(el, selectedOutline);\n }\n }, { passive: true });\n\n window.addEventListener('message', function(e) {\n if (CMS_PARENT_ORIGIN && e.origin !== CMS_PARENT_ORIGIN) return;\n if (e.source !== window.parent) return;\n\n if (e.data && e.data.type === 'cms-select-block') {\n selectedBlockId = e.data.blockId || null;\n if (selectedBlockId) {\n var el = getBlockElement(selectedBlockId);\n updateOutline(el, selectedOutline);\n } else {\n selectedOutline.style.display = 'none';\n }\n }\n });\n})();\n`;\n}\n","/**\n * Trusted-origin helpers for CMS template-builder postMessage traffic\n * between the admin UI (parent) and the site preview iframe (child).\n */\n\nexport const CMS_PARENT_ORIGIN_PARAM = 'cms_parent_origin';\n\nexport function parseOrigin(url: string | undefined): string | null {\n if (!url) return null;\n try {\n return new URL(url).origin;\n } catch {\n return null;\n }\n}\n\nfunction getParentOriginFromQueryParam(): string | null {\n if (typeof window === 'undefined') return null;\n const value = new URLSearchParams(window.location.search).get(CMS_PARENT_ORIGIN_PARAM);\n return parseOrigin(value ?? undefined);\n}\n\nfunction getReferrerOrigin(): string | null {\n if (typeof document === 'undefined' || !document.referrer) return null;\n return parseOrigin(document.referrer);\n}\n\n/** Same-origin embed (e.g. proxied admin + preview on the customer domain). */\nfunction getSameOriginParentOrigin(): string | null {\n if (typeof window === 'undefined' || !window.parent || window.parent === window) {\n return null;\n }\n try {\n return window.parent.location.origin;\n } catch {\n return null;\n }\n}\n\n/** Origins allowed to send messages to the preview iframe (CMS admin hosts). */\nexport function getAllowedCmsParentOrigins(explicitParentOrigin?: string): string[] {\n const origins = new Set<string>();\n const apiOrigin = parseOrigin(process.env.NEXT_PUBLIC_CMS_API_URL);\n if (apiOrigin) origins.add(apiOrigin);\n\n const fromQuery = parseOrigin(explicitParentOrigin) ?? getParentOriginFromQueryParam();\n if (fromQuery) origins.add(fromQuery);\n\n const referrerOrigin = getReferrerOrigin();\n if (referrerOrigin) origins.add(referrerOrigin);\n\n const sameOriginParent = getSameOriginParentOrigin();\n if (sameOriginParent) origins.add(sameOriginParent);\n\n return [...origins];\n}\n\n/**\n * Target origin when posting from the preview iframe to the CMS parent.\n *\n * When the admin is proxied on a customer domain, the parent window origin is\n * the customer site — not NEXT_PUBLIC_CMS_API_URL. Prefer explicit/referrer/\n * same-origin parent detection over the CMS API URL.\n */\nexport function getCmsParentTargetOrigin(\n preferredCmsUrl?: string,\n explicitParentOrigin?: string\n): string | null {\n const fromExplicit = parseOrigin(explicitParentOrigin) ?? getParentOriginFromQueryParam();\n if (fromExplicit) return fromExplicit;\n\n const referrerOrigin = getReferrerOrigin();\n if (referrerOrigin) return referrerOrigin;\n\n const sameOriginParent = getSameOriginParentOrigin();\n if (sameOriginParent) return sameOriginParent;\n\n const preferredOrigin = parseOrigin(preferredCmsUrl);\n if (preferredOrigin) return preferredOrigin;\n\n const allowed = getAllowedCmsParentOrigins(explicitParentOrigin);\n return allowed[0] ?? null;\n}\n\nexport function isTrustedCmsParentMessage(\n event: MessageEvent,\n explicitParentOrigin?: string\n): boolean {\n const allowed = getAllowedCmsParentOrigins(explicitParentOrigin);\n if (allowed.length === 0) return false;\n if (!allowed.includes(event.origin)) return false;\n return event.source === window.parent;\n}\n\nexport function postMessageToCmsParent(\n message: unknown,\n options?: { preferredCmsUrl?: string; explicitParentOrigin?: string }\n): void {\n if (typeof window === 'undefined' || !window.parent || window.parent === window) return;\n const targetOrigin = getCmsParentTargetOrigin(\n options?.preferredCmsUrl,\n options?.explicitParentOrigin\n );\n if (!targetOrigin) return;\n window.parent.postMessage(message, targetOrigin);\n}\n"],"mappings":";AAOA,OAAO,WAAW;;;ACQX,SAAS,yBAAyB,iBAAiC;AACxE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,4BAKmB,KAAK,UAAU,eAAe,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
@@ -2584,14 +2584,14 @@ var require_buffer_from = __commonJS({
2584
2584
  }
2585
2585
  return isModern ? Buffer.from(obj.slice(byteOffset, byteOffset + length)) : new Buffer(new Uint8Array(obj.slice(byteOffset, byteOffset + length)));
2586
2586
  }
2587
- function fromString(string, encoding) {
2587
+ function fromString(string2, encoding) {
2588
2588
  if (typeof encoding !== "string" || encoding === "") {
2589
2589
  encoding = "utf8";
2590
2590
  }
2591
2591
  if (!Buffer.isEncoding(encoding)) {
2592
2592
  throw new TypeError('"encoding" must be a valid string encoding');
2593
2593
  }
2594
- return isModern ? Buffer.from(string, encoding) : new Buffer(string, encoding);
2594
+ return isModern ? Buffer.from(string2, encoding) : new Buffer(string2, encoding);
2595
2595
  }
2596
2596
  function bufferFrom(value, encodingOrOffset, length) {
2597
2597
  if (typeof value === "number") {
@@ -213877,10 +213877,10 @@ var LanguageSchema = z3.object({
213877
213877
  });
213878
213878
 
213879
213879
  // ../../packages/cms-schema/src/documents/unified-registry.ts
213880
- import { z as z8 } from "zod";
213880
+ import { z as z10 } from "zod";
213881
213881
 
213882
213882
  // ../../packages/cms-schema/src/documents/rehydration.ts
213883
- import { z as z7 } from "zod";
213883
+ import { z as z9 } from "zod";
213884
213884
 
213885
213885
  // ../../packages/cms-schema/src/fields/complex/media.ts
213886
213886
  import { z as z5 } from "zod";
@@ -213990,6 +213990,66 @@ function richtext(options) {
213990
213990
  return result.meta(meta);
213991
213991
  }
213992
213992
 
213993
+ // ../../packages/cms-schema/src/fields/primitives/select.ts
213994
+ import { z as z7 } from "zod";
213995
+ function normalizeChoices(choices) {
213996
+ return choices.map(
213997
+ (choice) => typeof choice === "string" ? { value: choice, label: choice } : choice
213998
+ );
213999
+ }
214000
+ function select(options) {
214001
+ const normalizedChoices = normalizeChoices(options.choices);
214002
+ const values = normalizedChoices.map((c) => c.value);
214003
+ const schema = z7.enum(values);
214004
+ const meta = {
214005
+ label: options.label,
214006
+ component: "select",
214007
+ required: options.required,
214008
+ description: options.description,
214009
+ options: {
214010
+ choices: normalizedChoices
214011
+ }
214012
+ };
214013
+ const result = options.required ? schema : schema.optional();
214014
+ return result.meta(meta);
214015
+ }
214016
+
214017
+ // ../../packages/cms-schema/src/fields/primitives/string.ts
214018
+ import { z as z8 } from "zod";
214019
+ function string(options) {
214020
+ let schema = z8.string();
214021
+ if (options.minLength) {
214022
+ schema = schema.min(options.minLength);
214023
+ }
214024
+ if (options.maxLength) {
214025
+ schema = schema.max(options.maxLength);
214026
+ }
214027
+ if (options.pattern) {
214028
+ schema = schema.regex(options.pattern, options.patternMessage);
214029
+ }
214030
+ if (options.format === "email") {
214031
+ schema = schema.email();
214032
+ }
214033
+ if (options.format === "url") {
214034
+ schema = schema.url();
214035
+ }
214036
+ if (options.format === "uuid") {
214037
+ schema = schema.uuid();
214038
+ }
214039
+ const meta = {
214040
+ label: options.label,
214041
+ component: options.multiline ? "textarea" : "input",
214042
+ required: options.required,
214043
+ description: options.description,
214044
+ placeholder: options.placeholder,
214045
+ options: {
214046
+ rows: options.rows
214047
+ }
214048
+ };
214049
+ const result = options.required ? schema : schema.optional();
214050
+ return result.meta(meta);
214051
+ }
214052
+
213993
214053
  // ../../packages/cms-schema/src/utils/safe-regex.ts
213994
214054
  var import_safe_regex2 = __toESM(require_safe_regex2(), 1);
213995
214055
  function validateSafeRegex(pattern) {
@@ -214008,6 +214068,25 @@ function validateSafeRegex(pattern) {
214008
214068
  }
214009
214069
 
214010
214070
  // ../../packages/cms-schema/src/documents/rehydration.ts
214071
+ var LAYOUT_VALUES = ["middle", "left", "right"];
214072
+ var HEX_COLOR_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
214073
+ var DEFAULT_COMPONENT_FIELDS = {
214074
+ layout: select({
214075
+ label: "Layout",
214076
+ description: "Horizontal alignment of the component.",
214077
+ choices: LAYOUT_VALUES.map((value) => ({
214078
+ value,
214079
+ label: value.charAt(0).toUpperCase() + value.slice(1)
214080
+ }))
214081
+ }),
214082
+ background: string({
214083
+ label: "Background",
214084
+ description: "Background color as a hex value, e.g. #1a2b3c.",
214085
+ placeholder: "#000000",
214086
+ pattern: HEX_COLOR_PATTERN,
214087
+ patternMessage: "Must be a hex color like #fff or #1a2b3c"
214088
+ })
214089
+ };
214011
214090
  var RehydrationError = class extends Error {
214012
214091
  constructor(message, fieldName, fieldType) {
214013
214092
  super(message);
@@ -214036,41 +214115,41 @@ function rehydrateField(field) {
214036
214115
  function createBaseSchema(type, constraints) {
214037
214116
  switch (type) {
214038
214117
  case "string":
214039
- return applyStringConstraints(z7.string(), constraints);
214118
+ return applyStringConstraints(z9.string(), constraints);
214040
214119
  case "richtext":
214041
214120
  return richtext({ label: "Rich Text", required: true });
214042
214121
  case "number":
214043
- return applyNumberConstraints(z7.number(), constraints);
214122
+ return applyNumberConstraints(z9.number(), constraints);
214044
214123
  case "boolean":
214045
- return z7.boolean();
214124
+ return z9.boolean();
214046
214125
  case "image":
214047
214126
  return ImageReferenceSchema;
214048
214127
  case "icon":
214049
- return z7.string().max(50);
214128
+ return z9.string().max(50);
214050
214129
  case "date":
214051
- return z7.iso.date("Invalid date format. Expected YYYY-MM-DD");
214130
+ return z9.iso.date("Invalid date format. Expected YYYY-MM-DD");
214052
214131
  case "datetime":
214053
- return z7.iso.datetime({
214132
+ return z9.iso.datetime({
214054
214133
  offset: true,
214055
214134
  message: "Invalid datetime format. Expected ISO 8601"
214056
214135
  });
214057
214136
  case "url":
214058
- return applyStringConstraints(z7.string(), constraints).pipe(z7.url("Invalid URL format"));
214137
+ return applyStringConstraints(z9.string(), constraints).pipe(z9.url("Invalid URL format"));
214059
214138
  case "email":
214060
- return applyStringConstraints(z7.string(), constraints).pipe(z7.email("Invalid email format"));
214139
+ return applyStringConstraints(z9.string(), constraints).pipe(z9.email("Invalid email format"));
214061
214140
  case "enum": {
214062
214141
  const values = constraints?.enumValues;
214063
214142
  if (!values || values.length === 0) {
214064
214143
  throw new Error("Enum field requires at least one enumValues in constraints");
214065
214144
  }
214066
- return z7.enum(values);
214145
+ return z9.enum(values);
214067
214146
  }
214068
214147
  case "reference":
214069
- return z7.record(z7.string(), z7.unknown());
214148
+ return z9.record(z9.string(), z9.unknown());
214070
214149
  case "array": {
214071
214150
  const itemType = constraints?.arrayItemType ?? "string";
214072
- const itemSchema = itemType === "reference" ? z7.record(z7.string(), z7.unknown()) : createPrimitiveSchema(itemType);
214073
- return applyArrayConstraints(z7.array(itemSchema), constraints);
214151
+ const itemSchema = itemType === "reference" ? z9.record(z9.string(), z9.unknown()) : createPrimitiveSchema(itemType);
214152
+ return applyArrayConstraints(z9.array(itemSchema), constraints);
214074
214153
  }
214075
214154
  default:
214076
214155
  throw new Error(`Unknown field type: ${type}`);
@@ -214079,11 +214158,11 @@ function createBaseSchema(type, constraints) {
214079
214158
  function createPrimitiveSchema(type) {
214080
214159
  switch (type) {
214081
214160
  case "string":
214082
- return z7.string();
214161
+ return z9.string();
214083
214162
  case "number":
214084
- return z7.number();
214163
+ return z9.number();
214085
214164
  case "boolean":
214086
- return z7.boolean();
214165
+ return z9.boolean();
214087
214166
  default:
214088
214167
  throw new Error(`Unsupported array item type: ${type}`);
214089
214168
  }
@@ -214175,7 +214254,12 @@ function rehydrateSchema(fields) {
214175
214254
  }
214176
214255
  shape[field.name] = rehydrateField(field);
214177
214256
  }
214178
- return z7.object(shape);
214257
+ for (const [name, fieldSchema] of Object.entries(DEFAULT_COMPONENT_FIELDS)) {
214258
+ if (!seenNames.has(name)) {
214259
+ shape[name] = fieldSchema;
214260
+ }
214261
+ }
214262
+ return z9.object(shape);
214179
214263
  }
214180
214264
 
214181
214265
  // ../../packages/cms-schema/src/documents/schema-hash.ts
@@ -214185,24 +214269,24 @@ import hash from "object-hash";
214185
214269
  import { TRPCError } from "@trpc/server";
214186
214270
 
214187
214271
  // ../../packages/cms-schema/src/documents/parse-field-definitions-json.ts
214188
- import { z as z9 } from "zod";
214272
+ import { z as z11 } from "zod";
214189
214273
  var SNAKE_CASE = /^[a-z][a-z0-9_]*$/;
214190
- var fieldConstraintsSchema = z9.object({
214191
- minLength: z9.number().int().min(0).optional(),
214192
- maxLength: z9.number().int().min(0).optional(),
214193
- pattern: z9.string().max(500).optional(),
214194
- min: z9.number().optional(),
214195
- max: z9.number().optional(),
214196
- integer: z9.boolean().optional(),
214197
- enumValues: z9.array(z9.string()).optional(),
214198
- arrayItemType: z9.enum(["string", "number", "boolean", "reference"]).optional(),
214199
- minItems: z9.number().int().min(0).optional(),
214200
- maxItems: z9.number().int().min(0).optional(),
214201
- refSchema: z9.string().regex(SNAKE_CASE, "refSchema must be snake_case").optional()
214274
+ var fieldConstraintsSchema = z11.object({
214275
+ minLength: z11.number().int().min(0).optional(),
214276
+ maxLength: z11.number().int().min(0).optional(),
214277
+ pattern: z11.string().max(500).optional(),
214278
+ min: z11.number().optional(),
214279
+ max: z11.number().optional(),
214280
+ integer: z11.boolean().optional(),
214281
+ enumValues: z11.array(z11.string()).optional(),
214282
+ arrayItemType: z11.enum(["string", "number", "boolean", "reference"]).optional(),
214283
+ minItems: z11.number().int().min(0).optional(),
214284
+ maxItems: z11.number().int().min(0).optional(),
214285
+ refSchema: z11.string().regex(SNAKE_CASE, "refSchema must be snake_case").optional()
214202
214286
  });
214203
- var fieldDefinitionSchema = z9.object({
214204
- name: z9.string().regex(SNAKE_CASE, "must be snake_case (lowercase letters, digits, underscores only)"),
214205
- type: z9.enum([
214287
+ var fieldDefinitionSchema = z11.object({
214288
+ name: z11.string().regex(SNAKE_CASE, "must be snake_case (lowercase letters, digits, underscores only)"),
214289
+ type: z11.enum([
214206
214290
  "string",
214207
214291
  "richtext",
214208
214292
  "number",
@@ -214217,14 +214301,14 @@ var fieldDefinitionSchema = z9.object({
214217
214301
  "image",
214218
214302
  "icon"
214219
214303
  ]),
214220
- displayName: z9.string().optional(),
214221
- description: z9.string().optional(),
214222
- required: z9.boolean(),
214304
+ displayName: z11.string().optional(),
214305
+ description: z11.string().optional(),
214306
+ required: z11.boolean(),
214223
214307
  constraints: fieldConstraintsSchema.optional()
214224
214308
  });
214225
- var fieldArraySchema = z9.array(fieldDefinitionSchema).min(1, "Schema must have at least one field");
214226
- var multiSchemaFieldDefSchema = z9.record(
214227
- z9.string().regex(SNAKE_CASE, "schema name must be snake_case"),
214309
+ var fieldArraySchema = z11.array(fieldDefinitionSchema).min(1, "Schema must have at least one field");
214310
+ var multiSchemaFieldDefSchema = z11.record(
214311
+ z11.string().regex(SNAKE_CASE, "schema name must be snake_case"),
214228
214312
  fieldArraySchema
214229
214313
  );
214230
214314