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