cms-renderer 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/block-renderer.js +281 -5
- package/dist/lib/block-renderer.js.map +1 -1
- package/dist/lib/block-toolbar.d.ts +2 -2
- package/dist/lib/block-toolbar.js +95 -3
- package/dist/lib/block-toolbar.js.map +1 -1
- package/dist/lib/cms-api.js +34 -3
- package/dist/lib/cms-api.js.map +1 -1
- package/dist/lib/data-utils.js +7 -4
- package/dist/lib/data-utils.js.map +1 -1
- package/dist/lib/image/lazy-load.js +1 -0
- package/dist/lib/image/lazy-load.js.map +1 -1
- package/dist/lib/markdown-utils.js +10 -5
- package/dist/lib/markdown-utils.js.map +1 -1
- package/dist/lib/renderer.js +321 -10
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/result.js +96 -18
- package/dist/lib/result.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-HVKFEZBT.js +0 -116
- package/dist/chunk-HVKFEZBT.js.map +0 -1
- package/dist/chunk-JHKDRASN.js +0 -39
- package/dist/chunk-JHKDRASN.js.map +0 -1
- package/dist/chunk-SJZTIW2I.js +0 -290
- package/dist/chunk-SJZTIW2I.js.map +0 -1
- package/dist/chunk-TIR3RJMY.js +0 -98
- package/dist/chunk-TIR3RJMY.js.map +0 -1
|
@@ -1,8 +1,284 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "
|
|
5
|
-
|
|
1
|
+
// lib/block-renderer.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { BlockToolbar } from "./block-toolbar.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
function walkReactNode(node, visitors, ctx = {}) {
|
|
6
|
+
const path = ctx.path ?? [];
|
|
7
|
+
if (node == null || typeof node === "boolean") return node;
|
|
8
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
9
|
+
const value = String(node);
|
|
10
|
+
return visitors.onText ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key }) : node;
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(node)) {
|
|
13
|
+
return node.map((child, i) => {
|
|
14
|
+
const childKey = child?.key ?? null;
|
|
15
|
+
const result = walkReactNode(child, visitors, {
|
|
16
|
+
path: [...path, i],
|
|
17
|
+
parentType: ctx.parentType,
|
|
18
|
+
key: childKey
|
|
19
|
+
});
|
|
20
|
+
if (React.isValidElement(result) && result.key == null) {
|
|
21
|
+
return React.cloneElement(result, { key: childKey ?? `arr-${path.join("-")}-${i}` });
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (React.isValidElement(node)) {
|
|
27
|
+
const el = node;
|
|
28
|
+
const elProps = el.props;
|
|
29
|
+
const hasChildren = elProps && "children" in elProps;
|
|
30
|
+
const nextChildren = hasChildren ? React.Children.map(elProps.children, (child, i) => {
|
|
31
|
+
const childKey = child?.key ?? null;
|
|
32
|
+
const result = walkReactNode(child, visitors, {
|
|
33
|
+
path: [...path, "children", i],
|
|
34
|
+
parentType: el.type,
|
|
35
|
+
key: childKey
|
|
36
|
+
});
|
|
37
|
+
if (React.isValidElement(result) && result.key == null) {
|
|
38
|
+
return React.cloneElement(result, { key: childKey ?? `child-${path.join("-")}-${i}` });
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}) : elProps?.children;
|
|
42
|
+
const cloned = hasChildren ? React.cloneElement(el, void 0, nextChildren) : el;
|
|
43
|
+
return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;
|
|
44
|
+
}
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
function extractContentValues(content, basePath = []) {
|
|
48
|
+
const map = /* @__PURE__ */ new Map();
|
|
49
|
+
function walk(obj, path) {
|
|
50
|
+
if (typeof obj === "string" && obj.trim() !== "") {
|
|
51
|
+
const contentPath = path.join(".");
|
|
52
|
+
const existing = map.get(obj) || [];
|
|
53
|
+
existing.push({ contentPath, value: obj });
|
|
54
|
+
map.set(obj, existing);
|
|
55
|
+
} else if (Array.isArray(obj)) {
|
|
56
|
+
for (let index = 0; index < obj.length; index++) {
|
|
57
|
+
walk(obj[index], [...path, String(index)]);
|
|
58
|
+
}
|
|
59
|
+
} else if (obj && typeof obj === "object") {
|
|
60
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
61
|
+
walk(value, [...path, key]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
walk(content, basePath);
|
|
66
|
+
return map;
|
|
67
|
+
}
|
|
68
|
+
function renderToWalkableTree(node, keyPrefix = "") {
|
|
69
|
+
if (node == null || typeof node === "boolean") return node;
|
|
70
|
+
if (typeof node === "string" || typeof node === "number") return node;
|
|
71
|
+
if (Array.isArray(node)) {
|
|
72
|
+
return node.map((child, i) => {
|
|
73
|
+
const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);
|
|
74
|
+
if (React.isValidElement(result) && result.key == null) {
|
|
75
|
+
const existingKey = child?.key;
|
|
76
|
+
return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (React.isValidElement(node)) {
|
|
82
|
+
const el = node;
|
|
83
|
+
const elProps = el.props;
|
|
84
|
+
if (typeof el.type === "function") {
|
|
85
|
+
try {
|
|
86
|
+
const rendered = el.type(el.props);
|
|
87
|
+
return renderToWalkableTree(rendered, keyPrefix);
|
|
88
|
+
} catch {
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (elProps && "children" in elProps) {
|
|
93
|
+
const newChildren = renderToWalkableTree(elProps.children, keyPrefix);
|
|
94
|
+
return React.cloneElement(el, void 0, newChildren);
|
|
95
|
+
}
|
|
96
|
+
return node;
|
|
97
|
+
}
|
|
98
|
+
return node;
|
|
99
|
+
}
|
|
100
|
+
function BlockRenderer({ block, registry, disableEditable }) {
|
|
101
|
+
const Component = registry[block.type];
|
|
102
|
+
if (!Component) {
|
|
103
|
+
if (process.env.NODE_ENV === "development") {
|
|
104
|
+
console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const component = /* @__PURE__ */ jsx(Component, { content: block.content });
|
|
109
|
+
if (disableEditable) {
|
|
110
|
+
return component;
|
|
111
|
+
}
|
|
112
|
+
const renderedTree = renderToWalkableTree(component);
|
|
113
|
+
const contentValueMap = extractContentValues(block.content);
|
|
114
|
+
const usedPaths = /* @__PURE__ */ new Set();
|
|
115
|
+
let isRoot = true;
|
|
116
|
+
const wrappedComponent = walkReactNode(renderedTree, {
|
|
117
|
+
onText: ({ value, key, path }) => {
|
|
118
|
+
const matches = contentValueMap.get(value);
|
|
119
|
+
if (!matches || matches.length === 0) {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];
|
|
123
|
+
if (!match) return value;
|
|
124
|
+
usedPaths.add(match.contentPath);
|
|
125
|
+
const spanKey = key ?? `${block.id}-${match.contentPath}-${path.join("-")}`;
|
|
126
|
+
return /* @__PURE__ */ jsx(
|
|
127
|
+
"span",
|
|
128
|
+
{
|
|
129
|
+
"data-cms-editable": true,
|
|
130
|
+
"data-block-id": block.id,
|
|
131
|
+
"data-block-type": block.type,
|
|
132
|
+
"data-content-path": match.contentPath,
|
|
133
|
+
children: value
|
|
134
|
+
},
|
|
135
|
+
spanKey
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
onElement: ({ element, path }) => {
|
|
139
|
+
if (isRoot && path.length === 0) {
|
|
140
|
+
isRoot = false;
|
|
141
|
+
const elProps = element.props;
|
|
142
|
+
const existingChildren = elProps?.children;
|
|
143
|
+
return React.cloneElement(
|
|
144
|
+
element,
|
|
145
|
+
{
|
|
146
|
+
"data-cms-block": true,
|
|
147
|
+
"data-block-id": block.id,
|
|
148
|
+
"data-block-type": block.type
|
|
149
|
+
},
|
|
150
|
+
existingChildren,
|
|
151
|
+
/* @__PURE__ */ jsx(BlockToolbar, { blockId: block.id }, "cms-toolbar")
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return element;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "contents" }, children: [
|
|
158
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
159
|
+
[data-cms-block] {
|
|
160
|
+
position: relative;
|
|
161
|
+
}
|
|
162
|
+
[data-cms-block]:hover {
|
|
163
|
+
outline: 2px solid #3b82f6;
|
|
164
|
+
outline-offset: 4px;
|
|
165
|
+
}
|
|
166
|
+
[data-cms-editable] {
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
border-radius: 2px;
|
|
169
|
+
}
|
|
170
|
+
[data-cms-editable]:hover {
|
|
171
|
+
outline: 2px solid #3b82f6;
|
|
172
|
+
outline-offset: 2px;
|
|
173
|
+
}
|
|
174
|
+
.cms-block-toolbar {
|
|
175
|
+
position: absolute;
|
|
176
|
+
bottom: -16px;
|
|
177
|
+
left: 50%;
|
|
178
|
+
transform: translateX(-50%);
|
|
179
|
+
display: flex;
|
|
180
|
+
gap: 4px;
|
|
181
|
+
background: #1f2937;
|
|
182
|
+
border-radius: 6px;
|
|
183
|
+
padding: 4px;
|
|
184
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
185
|
+
opacity: 0;
|
|
186
|
+
pointer-events: none;
|
|
187
|
+
transition: opacity 0.15s ease;
|
|
188
|
+
z-index: 1000;
|
|
189
|
+
}
|
|
190
|
+
[data-cms-block]:hover .cms-block-toolbar {
|
|
191
|
+
opacity: 1;
|
|
192
|
+
pointer-events: auto;
|
|
193
|
+
}
|
|
194
|
+
.cms-block-toolbar button {
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
width: 28px;
|
|
199
|
+
height: 28px;
|
|
200
|
+
border: none;
|
|
201
|
+
background: transparent;
|
|
202
|
+
color: #9ca3af;
|
|
203
|
+
border-radius: 4px;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
206
|
+
}
|
|
207
|
+
.cms-block-toolbar button:hover {
|
|
208
|
+
background: #374151;
|
|
209
|
+
color: #fff;
|
|
210
|
+
}
|
|
211
|
+
.cms-block-toolbar button.delete:hover {
|
|
212
|
+
background: #dc2626;
|
|
213
|
+
color: #fff;
|
|
214
|
+
}
|
|
215
|
+
.cms-block-toolbar button:disabled {
|
|
216
|
+
opacity: 0.4;
|
|
217
|
+
cursor: not-allowed;
|
|
218
|
+
}
|
|
219
|
+
.cms-block-toolbar button:disabled:hover {
|
|
220
|
+
background: transparent;
|
|
221
|
+
color: #9ca3af;
|
|
222
|
+
}
|
|
223
|
+
.cms-block-toolbar svg {
|
|
224
|
+
width: 16px;
|
|
225
|
+
height: 16px;
|
|
226
|
+
}
|
|
227
|
+
` }),
|
|
228
|
+
/* @__PURE__ */ jsx(
|
|
229
|
+
"script",
|
|
230
|
+
{
|
|
231
|
+
dangerouslySetInnerHTML: {
|
|
232
|
+
__html: `
|
|
233
|
+
(function() {
|
|
234
|
+
if (window.__cmsEditableInitialized) return;
|
|
235
|
+
window.__cmsEditableInitialized = true;
|
|
236
|
+
|
|
237
|
+
document.addEventListener('click', function(e) {
|
|
238
|
+
// Ignore toolbar clicks
|
|
239
|
+
if (e.target.closest('.cms-block-toolbar')) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check for editable text click first (more specific)
|
|
244
|
+
var editableTarget = e.target.closest('[data-cms-editable]');
|
|
245
|
+
if (editableTarget) {
|
|
246
|
+
var message = {
|
|
247
|
+
type: 'cms-editable-click',
|
|
248
|
+
blockId: editableTarget.getAttribute('data-block-id'),
|
|
249
|
+
blockType: editableTarget.getAttribute('data-block-type'),
|
|
250
|
+
contentPath: editableTarget.getAttribute('data-content-path')
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (window.parent && window.parent !== window) {
|
|
254
|
+
window.parent.postMessage(message, '*');
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check for block-level click
|
|
260
|
+
var blockTarget = e.target.closest('[data-cms-block]');
|
|
261
|
+
if (blockTarget) {
|
|
262
|
+
var message = {
|
|
263
|
+
type: 'cms-editable-click',
|
|
264
|
+
blockId: blockTarget.getAttribute('data-block-id'),
|
|
265
|
+
blockType: blockTarget.getAttribute('data-block-type'),
|
|
266
|
+
contentPath: null
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (window.parent && window.parent !== window) {
|
|
270
|
+
window.parent.postMessage(message, '*');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
})();
|
|
275
|
+
`
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
),
|
|
279
|
+
wrappedComponent
|
|
280
|
+
] }, block.id);
|
|
281
|
+
}
|
|
6
282
|
export {
|
|
7
283
|
BlockRenderer,
|
|
8
284
|
walkReactNode
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","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 { BlockToolbar } from './block-toolbar';\nimport type { BlockComponentRegistry, BlockData } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\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 } = {}\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 })\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 });\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 // 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 });\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// 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\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Recursively renders a React node, invoking function components to get their output.\n * This allows us to walk the full rendered tree, not just the element wrappers.\n */\nfunction renderToWalkableTree(node: React.ReactNode, keyPrefix = ''): React.ReactNode {\n if (node == null || typeof node === 'boolean') return node;\n if (typeof node === 'string' || typeof node === 'number') return node;\n\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n // biome-ignore lint/suspicious/noExplicitAny: Adding key to element\n const existingKey = (child as any)?.key;\n return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });\n }\n return result;\n });\n }\n\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 // If it's a function component, invoke it to get the rendered output\n if (typeof el.type === 'function') {\n try {\n // biome-ignore lint/complexity/noBannedTypes: Need to invoke React function component\n const rendered = (el.type as Function)(el.props);\n return renderToWalkableTree(rendered, keyPrefix);\n } catch {\n // If component throws (e.g., uses hooks), return as-is\n return node;\n }\n }\n\n // For host elements (div, span, etc.), recurse into children\n if (elProps && 'children' in elProps) {\n const newChildren = renderToWalkableTree(elProps.children as React.ReactNode, keyPrefix);\n return React.cloneElement(el, undefined, newChildren);\n }\n\n return node;\n }\n\n return node;\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 * Internally, it:\n * 1. Renders the component tree by invoking function components\n * 2. Extracts all string values from block.content\n * 3. Walks the rendered tree and wraps matching text nodes with spans\n */\nexport function BlockRenderer({ block, registry, disableEditable }: BlockRendererProps) {\n const Component = registry[block.type];\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} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Render the component tree to get the actual DOM structure with all children\n const renderedTree = renderToWalkableTree(component);\n\n // Extract all string values from content with their paths\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n\n // Track which content paths have been used to handle duplicates\n const usedPaths = new Set<string>();\n\n // Track if we've processed the root element\n let isRoot = true;\n\n // Walk the tree: wrap matching text nodes and add attributes to root element\n const wrappedComponent = walkReactNode(renderedTree, {\n onText: ({ value, key, path }) => {\n const matches = contentValueMap.get(value);\n\n if (!matches || matches.length === 0) {\n return value;\n }\n\n // Find the first unused match, or use the first one if all are used\n const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];\n if (!match) return value;\n\n usedPaths.add(match.contentPath);\n\n // Generate a unique key using the original key, content path, or path\n const spanKey = key ?? `${block.id}-${match.contentPath}-${path.join('-')}`;\n\n return (\n <span\n key={spanKey}\n data-cms-editable\n data-block-id={block.id}\n data-block-type={block.type}\n data-content-path={match.contentPath}\n >\n {value}\n </span>\n );\n },\n onElement: ({ element, path }) => {\n // Add wrapper attributes and toolbar to the root element\n if (isRoot && path.length === 0) {\n isRoot = false;\n // Get existing children\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const elProps = (element as React.ReactElement<any>).props as Record<\n string,\n unknown\n > | null;\n const existingChildren = elProps?.children;\n // Clone with new attributes and inject toolbar as last child\n return React.cloneElement(\n element,\n {\n 'data-cms-block': true,\n 'data-block-id': block.id,\n 'data-block-type': block.type,\n } as React.Attributes & Record<string, unknown>,\n existingChildren as React.ReactNode,\n <BlockToolbar key=\"cms-toolbar\" blockId={block.id} />\n );\n }\n return element;\n },\n });\n\n return (\n <div key={block.id} style={{ display: 'contents' }}>\n <style>{`\n [data-cms-block] {\n position: relative;\n }\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: -16px;\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\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (window.__cmsEditableInitialized) return;\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n // Ignore toolbar clicks\n if (e.target.closest('.cms-block-toolbar')) {\n return;\n }\n\n // Check for editable text click first (more specific)\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\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n // Check for block-level click\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\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n })();\n `,\n }}\n />\n {wrappedComponent}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Block Toolbar Component
|
|
@@ -8,6 +8,6 @@ import * as React from 'react';
|
|
|
8
8
|
*/
|
|
9
9
|
declare function BlockToolbar({ blockId }: {
|
|
10
10
|
blockId: string;
|
|
11
|
-
}):
|
|
11
|
+
}): react.JSX.Element;
|
|
12
12
|
|
|
13
13
|
export { BlockToolbar };
|
|
@@ -1,7 +1,99 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// lib/block-toolbar.tsx
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function BlockToolbar({ blockId }) {
|
|
7
|
+
const handleAction = (action) => {
|
|
8
|
+
if (typeof window !== "undefined" && window.parent && window.parent !== window) {
|
|
9
|
+
window.parent.postMessage({ type: "cms-block-action", action, blockId }, "*");
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
return /* @__PURE__ */ jsxs("div", { className: "cms-block-toolbar", "data-cms-toolbar": "true", children: [
|
|
13
|
+
/* @__PURE__ */ jsx(
|
|
14
|
+
"button",
|
|
15
|
+
{
|
|
16
|
+
type: "button",
|
|
17
|
+
title: "Move up",
|
|
18
|
+
"data-action": "move-up",
|
|
19
|
+
onClick: (e) => {
|
|
20
|
+
e.stopPropagation();
|
|
21
|
+
handleAction("move-up");
|
|
22
|
+
},
|
|
23
|
+
children: /* @__PURE__ */ jsx(
|
|
24
|
+
"svg",
|
|
25
|
+
{
|
|
26
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
27
|
+
viewBox: "0 0 24 24",
|
|
28
|
+
fill: "none",
|
|
29
|
+
stroke: "currentColor",
|
|
30
|
+
strokeWidth: "2",
|
|
31
|
+
strokeLinecap: "round",
|
|
32
|
+
strokeLinejoin: "round",
|
|
33
|
+
children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" })
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
),
|
|
38
|
+
/* @__PURE__ */ jsx(
|
|
39
|
+
"button",
|
|
40
|
+
{
|
|
41
|
+
type: "button",
|
|
42
|
+
title: "Move down",
|
|
43
|
+
"data-action": "move-down",
|
|
44
|
+
onClick: (e) => {
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
handleAction("move-down");
|
|
47
|
+
},
|
|
48
|
+
children: /* @__PURE__ */ jsx(
|
|
49
|
+
"svg",
|
|
50
|
+
{
|
|
51
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
52
|
+
viewBox: "0 0 24 24",
|
|
53
|
+
fill: "none",
|
|
54
|
+
stroke: "currentColor",
|
|
55
|
+
strokeWidth: "2",
|
|
56
|
+
strokeLinecap: "round",
|
|
57
|
+
strokeLinejoin: "round",
|
|
58
|
+
children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
"button",
|
|
65
|
+
{
|
|
66
|
+
type: "button",
|
|
67
|
+
className: "delete",
|
|
68
|
+
title: "Delete block",
|
|
69
|
+
"data-action": "delete",
|
|
70
|
+
onClick: (e) => {
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
handleAction("delete");
|
|
73
|
+
},
|
|
74
|
+
children: /* @__PURE__ */ jsxs(
|
|
75
|
+
"svg",
|
|
76
|
+
{
|
|
77
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
78
|
+
viewBox: "0 0 24 24",
|
|
79
|
+
fill: "none",
|
|
80
|
+
stroke: "currentColor",
|
|
81
|
+
strokeWidth: "2",
|
|
82
|
+
strokeLinecap: "round",
|
|
83
|
+
strokeLinejoin: "round",
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
|
|
86
|
+
/* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
|
|
87
|
+
/* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }),
|
|
88
|
+
/* @__PURE__ */ jsx("line", { x1: "10", x2: "10", y1: "11", y2: "17" }),
|
|
89
|
+
/* @__PURE__ */ jsx("line", { x1: "14", x2: "14", y1: "11", y2: "17" })
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
] });
|
|
96
|
+
}
|
|
5
97
|
export {
|
|
6
98
|
BlockToolbar
|
|
7
99
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\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 }: { blockId: string }) {\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 className=\"cms-block-toolbar\" data-cms-toolbar=\"true\">\n <button\n type=\"button\"\n title=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m18 15-6-6-6 6\" />\n </svg>\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 6h18\" />\n <path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\" />\n <path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\" />\n <line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\" />\n <line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\" />\n </svg>\n </button>\n </div>\n );\n}\n"],"mappings":";;;;AAoCU,cAkCF,YAlCE;AA3BH,SAAS,aAAa,EAAE,QAAQ,GAAwB;AAC7D,QAAM,eAAe,CAAC,WAAmB;AACvC,QAAI,OAAO,WAAW,eAAe,OAAO,UAAU,OAAO,WAAW,QAAQ;AAC9E,aAAO,OAAO,YAAY,EAAE,MAAM,oBAAoB,QAAQ,QAAQ,GAAG,GAAG;AAAA,IAC9E;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAU,qBAAoB,oBAAiB,QAClD;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,SAAS;AAAA,QACxB;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,QAAO;AAAA,YACP,aAAY;AAAA,YACZ,eAAc;AAAA,YACd,gBAAe;AAAA,YAEf,8BAAC,UAAK,GAAE,kBAAiB;AAAA;AAAA,QAC3B;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,WAAW;AAAA,QAC1B;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,QAAO;AAAA,YACP,aAAY;AAAA,YACZ,eAAc;AAAA,YACd,gBAAe;AAAA,YAEf,8BAAC,UAAK,GAAE,gBAAe;AAAA;AAAA,QACzB;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAM;AAAA,QACN,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,QAAQ;AAAA,QACvB;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,QAAO;AAAA,YACP,aAAY;AAAA,YACZ,eAAc;AAAA,YACd,gBAAe;AAAA,YAEf;AAAA,kCAAC,UAAK,GAAE,WAAU;AAAA,cAClB,oBAAC,UAAK,GAAE,yCAAwC;AAAA,cAChD,oBAAC,UAAK,GAAE,sCAAqC;AAAA,cAC7C,oBAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,cACtC,oBAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA;AAAA;AAAA,QACxC;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
|
package/dist/lib/cms-api.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// lib/cms-api.ts
|
|
2
|
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
3
|
+
import superjson from "superjson";
|
|
4
|
+
function getCmsApiUrl(cmsUrl) {
|
|
5
|
+
return `${cmsUrl}/api/trpc`;
|
|
6
|
+
}
|
|
7
|
+
function createFetchWithApiKey(apiKey) {
|
|
8
|
+
return async (url, options) => {
|
|
9
|
+
let finalUrl = url;
|
|
10
|
+
if (apiKey) {
|
|
11
|
+
const urlObj = new URL(url.toString());
|
|
12
|
+
urlObj.searchParams.set("api_key", apiKey);
|
|
13
|
+
finalUrl = urlObj.toString();
|
|
14
|
+
}
|
|
15
|
+
const response = await fetch(finalUrl, options);
|
|
16
|
+
return response;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function createCmsClient(options) {
|
|
20
|
+
const url = getCmsApiUrl(options.cmsUrl);
|
|
21
|
+
console.log("[CMS API] Creating client with URL:", url);
|
|
22
|
+
return createTRPCClient({
|
|
23
|
+
links: [
|
|
24
|
+
httpBatchLink({
|
|
25
|
+
url,
|
|
26
|
+
transformer: superjson,
|
|
27
|
+
fetch: createFetchWithApiKey(options.apiKey)
|
|
28
|
+
})
|
|
29
|
+
]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function getCmsClient(options) {
|
|
33
|
+
return createCmsClient(options);
|
|
34
|
+
}
|
|
4
35
|
export {
|
|
5
36
|
getCmsClient
|
|
6
37
|
};
|
package/dist/lib/cms-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../lib/cms-api.ts"],"sourcesContent":["/**\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 `${cmsUrl}/api/trpc`;\n}\n\n/** Options for creating a CMS client */\nexport interface CmsClientOptions {\n /** CMS API base URL (e.g., 'http://localhost:3000') */\n cmsUrl: string;\n /** API key for authentication (passed as query parameter) */\n apiKey?: string;\n}\n\n/**\n * Create a custom fetch function that appends API key as query parameter.\n */\nfunction createFetchWithApiKey(apiKey?: string) {\n return async (url: URL | RequestInfo, options?: RequestInit): Promise<Response> => {\n let finalUrl = url;\n\n // Append api_key to URL if provided\n if (apiKey) {\n const urlObj = new URL(url.toString());\n urlObj.searchParams.set('api_key', apiKey);\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: CmsClientOptions): CmsClient {\n const url = getCmsApiUrl(options.cmsUrl);\n console.log('[CMS API] Creating client with URL:', url);\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: createFetchWithApiKey(options.apiKey),\n }),\n ],\n });\n}\n\n/**\n * Get a CMS client for the specified CMS URL.\n */\nexport function getCmsClient(options: CmsClientOptions): CmsClient {\n return createCmsClient(options);\n}\n"],"mappings":";AAQA,SAAgC,kBAAkB,qBAAqB;AACvE,OAAO,eAAe;AAQtB,SAAS,aAAa,QAAwB;AAC5C,SAAO,GAAG,MAAM;AAClB;AAaA,SAAS,sBAAsB,QAAiB;AAC9C,SAAO,OAAO,KAAwB,YAA6C;AACjF,QAAI,WAAW;AAGf,QAAI,QAAQ;AACV,YAAM,SAAS,IAAI,IAAI,IAAI,SAAS,CAAC;AACrC,aAAO,aAAa,IAAI,WAAW,MAAM;AACzC,iBAAW,OAAO,SAAS;AAAA,IAC7B;AAEA,UAAM,WAAW,MAAM,MAAM,UAAU,OAAO;AAE9C,WAAO;AAAA,EACT;AACF;AAKA,SAAS,gBAAgB,SAAsC;AAC7D,QAAM,MAAM,aAAa,QAAQ,MAAM;AACvC,UAAQ,IAAI,uCAAuC,GAAG;AAEtD,SAAO,iBAA4B;AAAA,IACjC,OAAO;AAAA,MACL,cAAc;AAAA,QACZ;AAAA,QACA,aAAa;AAAA,QACb,OAAO,sBAAsB,QAAQ,MAAM;AAAA,MAC7C,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAKO,SAAS,aAAa,SAAsC;AACjE,SAAO,gBAAgB,OAAO;AAChC;","names":[]}
|
package/dist/lib/data-utils.js
CHANGED