@cmssy/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js ADDED
@@ -0,0 +1,690 @@
1
+ "use client";
2
+ import { useState, useRef, useEffect, useMemo, createElement } from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/registry.ts
6
+ function buildBlockMap(blocks) {
7
+ const map = /* @__PURE__ */ Object.create(null);
8
+ for (const block of blocks) map[block.type] = block.component;
9
+ return map;
10
+ }
11
+ function blocksToSchemas(blocks) {
12
+ const out = /* @__PURE__ */ Object.create(null);
13
+ for (const block of blocks) {
14
+ const schema = {};
15
+ for (const [key, def] of Object.entries(block.props)) {
16
+ schema[key] = { ...def, label: def.label || key };
17
+ }
18
+ out[block.type] = schema;
19
+ }
20
+ return out;
21
+ }
22
+ function blocksToMeta(blocks, defaults = {}) {
23
+ const out = /* @__PURE__ */ Object.create(null);
24
+ for (const block of blocks) {
25
+ const category = block.category ?? defaults.category;
26
+ out[block.type] = {
27
+ label: block.label ?? block.type,
28
+ ...category ? { category } : {},
29
+ ...block.icon ? { icon: block.icon } : {},
30
+ ...block.layoutPositions ? { layoutPositions: block.layoutPositions } : {}
31
+ };
32
+ }
33
+ return out;
34
+ }
35
+
36
+ // src/bridge/protocol.ts
37
+ var PROTOCOL_VERSION = 1;
38
+
39
+ // src/bridge/messages.ts
40
+ function normalizeOrigin(origin) {
41
+ if (origin === "*") return "*";
42
+ try {
43
+ return new URL(origin).origin;
44
+ } catch {
45
+ return origin;
46
+ }
47
+ }
48
+ function postToEditor(target, editorOrigin, message) {
49
+ target.postMessage(message, normalizeOrigin(editorOrigin));
50
+ }
51
+ function isObject(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function parseEditorMessage(data, origin, expectedOrigin) {
55
+ const expected = normalizeOrigin(expectedOrigin);
56
+ if (expected !== "*" && origin !== expected) return null;
57
+ if (!isObject(data)) return null;
58
+ switch (data.type) {
59
+ case "cmssy:select":
60
+ return typeof data.blockId === "string" && data.protocolVersion === PROTOCOL_VERSION ? {
61
+ type: "cmssy:select",
62
+ protocolVersion: PROTOCOL_VERSION,
63
+ blockId: data.blockId
64
+ } : null;
65
+ case "cmssy:patch":
66
+ return typeof data.blockId === "string" && isObject(data.content) && data.protocolVersion === PROTOCOL_VERSION ? {
67
+ type: "cmssy:patch",
68
+ blockId: data.blockId,
69
+ content: data.content,
70
+ protocolVersion: PROTOCOL_VERSION,
71
+ ...typeof data.layoutPosition === "string" ? { layoutPosition: data.layoutPosition } : {}
72
+ } : null;
73
+ case "cmssy:parent-ready":
74
+ return data.protocolVersion === PROTOCOL_VERSION ? { type: "cmssy:parent-ready", protocolVersion: PROTOCOL_VERSION } : null;
75
+ case "cmssy:insert":
76
+ return typeof data.blockId === "string" && typeof data.blockType === "string" && isObject(data.content) && typeof data.index === "number" && data.protocolVersion === PROTOCOL_VERSION ? {
77
+ type: "cmssy:insert",
78
+ protocolVersion: PROTOCOL_VERSION,
79
+ blockId: data.blockId,
80
+ blockType: data.blockType,
81
+ content: data.content,
82
+ index: data.index
83
+ } : null;
84
+ case "cmssy:reorder":
85
+ return Array.isArray(data.blockIds) && data.blockIds.every((id) => typeof id === "string") && data.protocolVersion === PROTOCOL_VERSION ? {
86
+ type: "cmssy:reorder",
87
+ protocolVersion: PROTOCOL_VERSION,
88
+ blockIds: data.blockIds
89
+ } : null;
90
+ case "cmssy:remove":
91
+ return typeof data.blockId === "string" && data.protocolVersion === PROTOCOL_VERSION ? {
92
+ type: "cmssy:remove",
93
+ protocolVersion: PROTOCOL_VERSION,
94
+ blockId: data.blockId
95
+ } : null;
96
+ case "cmssy:drag-over":
97
+ return typeof data.y === "number" && data.protocolVersion === PROTOCOL_VERSION ? {
98
+ type: "cmssy:drag-over",
99
+ protocolVersion: PROTOCOL_VERSION,
100
+ y: data.y
101
+ } : null;
102
+ case "cmssy:drag-end":
103
+ return data.protocolVersion === PROTOCOL_VERSION ? { type: "cmssy:drag-end", protocolVersion: PROTOCOL_VERSION } : null;
104
+ default:
105
+ return null;
106
+ }
107
+ }
108
+
109
+ // src/bridge/use-edit-bridge.tsx
110
+ var ZERO_RECT = { x: 0, y: 0, width: 0, height: 0 };
111
+ function collectRects() {
112
+ const rects = /* @__PURE__ */ new Map();
113
+ if (typeof document === "undefined") return rects;
114
+ for (const el of document.querySelectorAll("[data-block-id]")) {
115
+ const id = el.getAttribute("data-block-id");
116
+ if (id && !rects.has(id)) {
117
+ const r = el.getBoundingClientRect();
118
+ rects.set(id, { x: r.x, y: r.y, width: r.width, height: r.height });
119
+ }
120
+ }
121
+ return rects;
122
+ }
123
+ function collectLayoutBlocks(rects, pageIds) {
124
+ const out = [];
125
+ if (typeof document === "undefined") return out;
126
+ const seen = /* @__PURE__ */ new Set();
127
+ for (const el of document.querySelectorAll("[data-layout-position]")) {
128
+ const id = el.getAttribute("data-block-id");
129
+ const type = el.getAttribute("data-block-type");
130
+ const layoutPosition = el.getAttribute("data-layout-position");
131
+ if (id && type && layoutPosition !== null && !pageIds.has(id) && !seen.has(id)) {
132
+ seen.add(id);
133
+ out.push({
134
+ id,
135
+ type,
136
+ layoutPosition,
137
+ bounds: rects.get(id) ?? ZERO_RECT
138
+ });
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+ function useEditBridge(page, config) {
144
+ const [patches, setPatches] = useState({});
145
+ const [selected, setSelected] = useState(null);
146
+ const [inserted, setInserted] = useState([]);
147
+ const [order, setOrder] = useState(null);
148
+ const [removed, setRemoved] = useState([]);
149
+ const selectedIdRef = useRef(null);
150
+ const { id: pageId, blocks } = page;
151
+ const blocksKey = blocks.map((b) => `${b.id}:${b.type}`).join("|");
152
+ useEffect(() => {
153
+ setPatches({});
154
+ setSelected(null);
155
+ setInserted([]);
156
+ setOrder(null);
157
+ setRemoved([]);
158
+ selectedIdRef.current = null;
159
+ }, [pageId, blocksKey]);
160
+ useEffect(() => {
161
+ if (typeof window === "undefined" || window.parent === window) return;
162
+ const { editorOrigin } = config;
163
+ if (editorOrigin === "*" && typeof console !== "undefined") {
164
+ console.warn(
165
+ "[cmssy] editorOrigin '*' disables origin checks - dev only, do not use in production"
166
+ );
167
+ }
168
+ const sendReady = () => {
169
+ try {
170
+ const rects = collectRects();
171
+ const pageIds = new Set(blocks.map((b) => b.id));
172
+ postToEditor(window.parent, editorOrigin, {
173
+ type: "cmssy:ready",
174
+ protocolVersion: PROTOCOL_VERSION,
175
+ blocks: [
176
+ ...blocks.map((b) => ({
177
+ id: b.id,
178
+ type: b.type,
179
+ bounds: rects.get(b.id) ?? ZERO_RECT
180
+ })),
181
+ ...collectLayoutBlocks(rects, pageIds)
182
+ ],
183
+ schemas: config.schemas ?? /* @__PURE__ */ Object.create(null),
184
+ blockMeta: config.blockMeta ?? /* @__PURE__ */ Object.create(null)
185
+ });
186
+ } catch (error) {
187
+ if (typeof console !== "undefined") {
188
+ console.warn("[cmssy] failed to post to editor", error);
189
+ }
190
+ }
191
+ };
192
+ const handler = (event) => {
193
+ if (event.source && event.source !== window.parent) return;
194
+ const message = parseEditorMessage(
195
+ event.data,
196
+ event.origin,
197
+ editorOrigin
198
+ );
199
+ if (!message) return;
200
+ if (message.type === "cmssy:patch") {
201
+ if (message.layoutPosition !== void 0) return;
202
+ setPatches((prev) => ({
203
+ ...prev,
204
+ [message.blockId]: { ...prev[message.blockId], ...message.content }
205
+ }));
206
+ } else if (message.type === "cmssy:select") {
207
+ setSelected(message.blockId);
208
+ selectedIdRef.current = message.blockId;
209
+ } else if (message.type === "cmssy:insert") {
210
+ setInserted((prev) => {
211
+ const next = prev.filter((b) => b.blockId !== message.blockId);
212
+ next.push({
213
+ blockId: message.blockId,
214
+ blockType: message.blockType,
215
+ content: message.content,
216
+ index: message.index
217
+ });
218
+ return next;
219
+ });
220
+ } else if (message.type === "cmssy:reorder") {
221
+ setOrder(message.blockIds);
222
+ } else if (message.type === "cmssy:remove") {
223
+ setRemoved(
224
+ (prev) => prev.includes(message.blockId) ? prev : [...prev, message.blockId]
225
+ );
226
+ } else if (message.type === "cmssy:parent-ready") {
227
+ sendReady();
228
+ }
229
+ };
230
+ const onClick = (event) => {
231
+ const target = event.target;
232
+ const el = target?.closest?.("[data-block-id]");
233
+ const id = el?.getAttribute("data-block-id");
234
+ if (!id || !el) return;
235
+ selectedIdRef.current = id;
236
+ if (target?.closest?.("a[href]")) event.preventDefault();
237
+ const r = el.getBoundingClientRect();
238
+ const layoutPosition = el.getAttribute("data-layout-position");
239
+ try {
240
+ postToEditor(window.parent, editorOrigin, {
241
+ type: "cmssy:click",
242
+ blockId: id,
243
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height },
244
+ ...layoutPosition !== null ? { layoutPosition } : {}
245
+ });
246
+ } catch {
247
+ }
248
+ };
249
+ let boundsRaf = 0;
250
+ let boundsPending = false;
251
+ const emitSelectedBounds = () => {
252
+ if (boundsPending || !selectedIdRef.current) return;
253
+ boundsPending = true;
254
+ boundsRaf = requestAnimationFrame(() => {
255
+ boundsPending = false;
256
+ boundsRaf = 0;
257
+ const id = selectedIdRef.current;
258
+ if (!id) return;
259
+ const el = document.querySelector(
260
+ `[data-block-id="${id.replace(/["\\]/g, "\\$&")}"]`
261
+ );
262
+ if (!el) return;
263
+ const r = el.getBoundingClientRect();
264
+ try {
265
+ postToEditor(window.parent, editorOrigin, {
266
+ type: "cmssy:bounds",
267
+ blockId: id,
268
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height }
269
+ });
270
+ } catch {
271
+ }
272
+ });
273
+ };
274
+ window.addEventListener("message", handler);
275
+ document.addEventListener("click", onClick);
276
+ window.addEventListener("scroll", emitSelectedBounds, {
277
+ capture: true,
278
+ passive: true
279
+ });
280
+ window.addEventListener("resize", emitSelectedBounds);
281
+ sendReady();
282
+ return () => {
283
+ if (boundsRaf) cancelAnimationFrame(boundsRaf);
284
+ window.removeEventListener("message", handler);
285
+ document.removeEventListener("click", onClick);
286
+ window.removeEventListener("scroll", emitSelectedBounds, {
287
+ capture: true
288
+ });
289
+ window.removeEventListener("resize", emitSelectedBounds);
290
+ };
291
+ }, [config.editorOrigin, pageId, blocksKey]);
292
+ return { patches, selected, inserted, order, removed };
293
+ }
294
+ var MOVE_MIME = "application/x-cmssy-move";
295
+ function visible(el) {
296
+ return el.offsetParent !== null || el.getClientRects().length > 0;
297
+ }
298
+ function blockElements() {
299
+ return Array.from(
300
+ document.querySelectorAll(
301
+ "[data-block-id]:not([data-layout-position])"
302
+ )
303
+ ).filter(visible);
304
+ }
305
+ function computeDropTarget(clientY) {
306
+ const els = blockElements();
307
+ for (let i = 0; i < els.length; i++) {
308
+ const r = els[i].getBoundingClientRect();
309
+ if (clientY < r.top + r.height / 2) {
310
+ return { index: i, y: r.top };
311
+ }
312
+ }
313
+ const last = els[els.length - 1];
314
+ return {
315
+ index: els.length,
316
+ y: last ? last.getBoundingClientRect().bottom : 0
317
+ };
318
+ }
319
+ function useDragAgent(config) {
320
+ const [dropY, setDropY] = useState(null);
321
+ useEffect(() => {
322
+ if (typeof window === "undefined" || window.parent === window) return;
323
+ const { editorOrigin } = config;
324
+ if (editorOrigin === "*" && typeof console !== "undefined") {
325
+ console.warn(
326
+ "[cmssy] editorOrigin '*' disables origin checks - dev only, do not use in production"
327
+ );
328
+ }
329
+ let movingId = null;
330
+ let lastDropY = null;
331
+ const updateDropY = (y) => {
332
+ if (y === lastDropY) return;
333
+ lastDropY = y;
334
+ setDropY(y);
335
+ };
336
+ const onDragStart = (event) => {
337
+ const blockEl = event.target?.closest("[data-block-id]");
338
+ const id = blockEl?.getAttribute("data-block-id");
339
+ if (!id || !event.dataTransfer) return;
340
+ movingId = id;
341
+ event.dataTransfer.setData(MOVE_MIME, id);
342
+ event.dataTransfer.effectAllowed = "move";
343
+ };
344
+ const onDragOver = (event) => {
345
+ if (!movingId) return;
346
+ event.preventDefault();
347
+ updateDropY(computeDropTarget(event.clientY).y);
348
+ };
349
+ const onDrop = (event) => {
350
+ if (!movingId) return;
351
+ event.preventDefault();
352
+ const { index } = computeDropTarget(event.clientY);
353
+ const blockId = movingId;
354
+ movingId = null;
355
+ updateDropY(null);
356
+ try {
357
+ postToEditor(window.parent, editorOrigin, {
358
+ type: "cmssy:move",
359
+ protocolVersion: PROTOCOL_VERSION,
360
+ blockId,
361
+ index
362
+ });
363
+ } catch {
364
+ }
365
+ };
366
+ const onDragEnd = () => {
367
+ movingId = null;
368
+ updateDropY(null);
369
+ };
370
+ const onMessage = (event) => {
371
+ if (event.source && event.source !== window.parent) return;
372
+ const message = parseEditorMessage(
373
+ event.data,
374
+ event.origin,
375
+ editorOrigin
376
+ );
377
+ if (!message) return;
378
+ if (message.type === "cmssy:drag-over") {
379
+ const edge = 64;
380
+ const step = 20;
381
+ if (message.y < edge) {
382
+ window.scrollBy(0, -step);
383
+ } else if (message.y > window.innerHeight - edge) {
384
+ window.scrollBy(0, step);
385
+ }
386
+ const { index, y } = computeDropTarget(message.y);
387
+ updateDropY(y);
388
+ try {
389
+ postToEditor(window.parent, editorOrigin, {
390
+ type: "cmssy:drag-index",
391
+ protocolVersion: PROTOCOL_VERSION,
392
+ index
393
+ });
394
+ } catch {
395
+ }
396
+ } else if (message.type === "cmssy:drag-end") {
397
+ updateDropY(null);
398
+ }
399
+ };
400
+ document.addEventListener("dragstart", onDragStart);
401
+ document.addEventListener("dragover", onDragOver);
402
+ document.addEventListener("drop", onDrop);
403
+ document.addEventListener("dragend", onDragEnd);
404
+ window.addEventListener("message", onMessage);
405
+ return () => {
406
+ document.removeEventListener("dragstart", onDragStart);
407
+ document.removeEventListener("dragover", onDragOver);
408
+ document.removeEventListener("drop", onDrop);
409
+ document.removeEventListener("dragend", onDragEnd);
410
+ window.removeEventListener("message", onMessage);
411
+ };
412
+ }, [config.editorOrigin]);
413
+ return { dropY };
414
+ }
415
+
416
+ // src/content/get-block-content.ts
417
+ function isPlainObject(value) {
418
+ return typeof value === "object" && value !== null && !Array.isArray(value);
419
+ }
420
+ function looksLikeLocaleKey(key) {
421
+ return /^[a-z]{2}(-[A-Za-z]{2})?$/.test(key);
422
+ }
423
+ function getBlockContentForLanguage(content, locale, defaultLocale = "en", availableLocales) {
424
+ if (!isPlainObject(content)) return {};
425
+ const isLocale = looksLikeLocaleKey;
426
+ const localeEntries = Object.entries(content).filter(
427
+ ([key, value]) => isLocale(key) && isPlainObject(value)
428
+ );
429
+ if (localeEntries.length === 0) return { ...content };
430
+ const localeMap = Object.fromEntries(localeEntries);
431
+ const nonTranslatable = {};
432
+ for (const [key, value] of Object.entries(content)) {
433
+ if (!(isLocale(key) && isPlainObject(value))) nonTranslatable[key] = value;
434
+ }
435
+ const fallbackKey = Object.keys(localeMap)[0];
436
+ const chosen = localeMap[locale] ?? localeMap[defaultLocale] ?? localeMap[fallbackKey];
437
+ return { ...nonTranslatable, ...chosen };
438
+ }
439
+ var WARN_CAP = 256;
440
+ var warned = /* @__PURE__ */ new Set();
441
+ function UnknownBlock({ type }) {
442
+ if (typeof window !== "undefined" && !warned.has(type)) {
443
+ if (warned.size >= WARN_CAP) warned.clear();
444
+ warned.add(type);
445
+ console.warn(`[cmssy] no component registered for block type "${type}"`);
446
+ }
447
+ return /* @__PURE__ */ jsx("div", { "data-cmssy-unknown-block": type });
448
+ }
449
+ function CmssyBlock({
450
+ block,
451
+ locale,
452
+ defaultLocale,
453
+ blockMap,
454
+ patchedContent,
455
+ editable,
456
+ layoutPosition
457
+ }) {
458
+ const Component = Object.hasOwn(blockMap, block.type) ? blockMap[block.type] : void 0;
459
+ const base = getBlockContentForLanguage(block.content, locale, defaultLocale);
460
+ const content = patchedContent ? { ...base, ...patchedContent } : base;
461
+ return /* @__PURE__ */ jsx(
462
+ "div",
463
+ {
464
+ "data-block-id": block.id,
465
+ "data-block-type": block.type,
466
+ "data-layout-position": layoutPosition,
467
+ draggable: editable || void 0,
468
+ style: Component ? void 0 : { display: "none" },
469
+ children: Component ? createElement(Component, { content }) : /* @__PURE__ */ jsx(UnknownBlock, { type: block.type })
470
+ }
471
+ );
472
+ }
473
+ function CmssyEditablePage({
474
+ page,
475
+ blocks,
476
+ locale = "en",
477
+ defaultLocale = "en",
478
+ edit,
479
+ category
480
+ }) {
481
+ if (!Array.isArray(blocks)) {
482
+ throw new Error(
483
+ "cmssy: CmssyEditablePage requires a blocks array \u2014 pass your defineBlock(...) array"
484
+ );
485
+ }
486
+ if (!page) return null;
487
+ return /* @__PURE__ */ jsx(
488
+ EditableBlocks,
489
+ {
490
+ page,
491
+ blocks,
492
+ locale,
493
+ defaultLocale,
494
+ edit,
495
+ category
496
+ }
497
+ );
498
+ }
499
+ function EditableBlocks({
500
+ page,
501
+ blocks,
502
+ locale,
503
+ defaultLocale,
504
+ edit,
505
+ category
506
+ }) {
507
+ const blockMap = useMemo(() => buildBlockMap(blocks), [blocks]);
508
+ const bridgeConfig = useMemo(
509
+ () => ({
510
+ ...edit,
511
+ schemas: edit.schemas ?? blocksToSchemas(blocks),
512
+ blockMeta: edit.blockMeta ?? blocksToMeta(blocks, { category })
513
+ }),
514
+ [edit, blocks, category]
515
+ );
516
+ const { patches, inserted, order, removed } = useEditBridge(
517
+ page,
518
+ bridgeConfig
519
+ );
520
+ const { dropY } = useDragAgent(bridgeConfig);
521
+ const renderBlocks = useMemo(() => {
522
+ const removedSet = new Set(removed);
523
+ const merged = page.blocks.filter((b) => !removedSet.has(b.id));
524
+ const sorted = [...inserted].filter((ins) => !removedSet.has(ins.blockId)).sort((a, b) => a.index - b.index);
525
+ for (const ins of sorted) {
526
+ const at = Math.max(0, Math.min(ins.index, merged.length));
527
+ merged.splice(at, 0, {
528
+ id: ins.blockId,
529
+ type: ins.blockType,
530
+ content: ins.content
531
+ });
532
+ }
533
+ if (order) {
534
+ const rank = new Map(order.map((id, i) => [id, i]));
535
+ const fallback = order.length;
536
+ merged.sort(
537
+ (a, b) => (rank.get(a.id) ?? fallback) - (rank.get(b.id) ?? fallback)
538
+ );
539
+ }
540
+ return merged;
541
+ }, [page.blocks, inserted, order, removed]);
542
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
543
+ renderBlocks.map((block) => /* @__PURE__ */ jsx(
544
+ CmssyBlock,
545
+ {
546
+ block,
547
+ locale,
548
+ defaultLocale,
549
+ patchedContent: patches[block.id],
550
+ blockMap,
551
+ editable: true
552
+ },
553
+ block.id
554
+ )),
555
+ dropY !== null && /* @__PURE__ */ jsx(
556
+ "div",
557
+ {
558
+ style: {
559
+ position: "fixed",
560
+ left: 0,
561
+ right: 0,
562
+ top: dropY,
563
+ height: 2,
564
+ background: "#3b82f6",
565
+ zIndex: 2147483647,
566
+ pointerEvents: "none"
567
+ }
568
+ }
569
+ )
570
+ ] });
571
+ }
572
+ function CmssyLazyEditor({ load, ...props }) {
573
+ const [loaded, setLoaded] = useState(null);
574
+ useEffect(() => {
575
+ let active = true;
576
+ setLoaded(null);
577
+ (async () => {
578
+ try {
579
+ const m = await load();
580
+ if (!active) return;
581
+ if (!Array.isArray(m.blocks)) {
582
+ throw new Error(
583
+ "cmssy: CmssyLazyEditor load() must resolve to { blocks: BlockDefinition[] }"
584
+ );
585
+ }
586
+ setLoaded({ blocks: m.blocks, category: m.category });
587
+ } catch (err) {
588
+ if (typeof console !== "undefined") {
589
+ console.error("[cmssy] CmssyLazyEditor failed to load blocks", err);
590
+ }
591
+ }
592
+ })();
593
+ return () => {
594
+ active = false;
595
+ };
596
+ }, [load]);
597
+ if (!loaded) return null;
598
+ return /* @__PURE__ */ jsx(
599
+ CmssyEditablePage,
600
+ {
601
+ ...props,
602
+ blocks: loaded.blocks,
603
+ category: loaded.category
604
+ }
605
+ );
606
+ }
607
+ function useLayoutPatchBridge(position, config) {
608
+ const [patches, setPatches] = useState({});
609
+ useEffect(() => {
610
+ setPatches({});
611
+ if (typeof window === "undefined" || window.parent === window) return;
612
+ const { editorOrigin } = config;
613
+ const handler = (event) => {
614
+ if (event.source && event.source !== window.parent) return;
615
+ const message = parseEditorMessage(
616
+ event.data,
617
+ event.origin,
618
+ editorOrigin
619
+ );
620
+ if (!message) return;
621
+ if (message.type === "cmssy:patch" && message.layoutPosition === position) {
622
+ setPatches((prev) => ({
623
+ ...prev,
624
+ [message.blockId]: { ...prev[message.blockId], ...message.content }
625
+ }));
626
+ }
627
+ };
628
+ window.addEventListener("message", handler);
629
+ return () => window.removeEventListener("message", handler);
630
+ }, [config.editorOrigin, position]);
631
+ return patches;
632
+ }
633
+ function CmssyEditableLayout({
634
+ groups,
635
+ blocks,
636
+ position,
637
+ locale = "en",
638
+ defaultLocale = "en",
639
+ edit
640
+ }) {
641
+ const blockMap = useMemo(() => buildBlockMap(blocks), [blocks]);
642
+ const layoutBlocks = useMemo(() => {
643
+ const group = groups.find((g) => g.position === position);
644
+ return group ? group.blocks.filter((b) => b.isActive).slice().sort((a, b) => a.order - b.order) : [];
645
+ }, [groups, position]);
646
+ const patches = useLayoutPatchBridge(position, edit);
647
+ if (layoutBlocks.length === 0) return null;
648
+ return /* @__PURE__ */ jsx(Fragment, { children: layoutBlocks.map((block) => /* @__PURE__ */ jsx(
649
+ CmssyBlock,
650
+ {
651
+ block,
652
+ locale,
653
+ defaultLocale,
654
+ blockMap,
655
+ patchedContent: patches[block.id],
656
+ layoutPosition: position
657
+ },
658
+ block.id
659
+ )) });
660
+ }
661
+ function CmssyLazyLayout({ load, ...props }) {
662
+ const [blocks, setBlocks] = useState(null);
663
+ useEffect(() => {
664
+ let active = true;
665
+ setBlocks(null);
666
+ (async () => {
667
+ try {
668
+ const m = await load();
669
+ if (!active) return;
670
+ if (!Array.isArray(m.blocks)) {
671
+ throw new Error(
672
+ "cmssy: CmssyLazyLayout load() must resolve to { blocks: BlockDefinition[] }"
673
+ );
674
+ }
675
+ setBlocks(m.blocks);
676
+ } catch (err) {
677
+ if (typeof console !== "undefined") {
678
+ console.error("[cmssy] CmssyLazyLayout failed to load blocks", err);
679
+ }
680
+ }
681
+ })();
682
+ return () => {
683
+ active = false;
684
+ };
685
+ }, [load]);
686
+ if (!blocks) return null;
687
+ return /* @__PURE__ */ jsx(CmssyEditableLayout, { ...props, blocks });
688
+ }
689
+
690
+ export { CmssyEditableLayout, CmssyEditablePage, CmssyLazyEditor, CmssyLazyLayout, useEditBridge };