create-next-imagicma 0.1.6 → 0.1.9

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,1994 @@
1
+ import {
2
+ bindPreviewParentOrigin,
3
+ getBoundPreviewParentOrigin,
4
+ isAllowedPreviewParentOrigin,
5
+ } from "@/lib/imagicma-preview-bridge";
6
+
7
+ const PREVIEW_PICKER_CHANNEL = "imagicma.preview-picker";
8
+ const PREVIEW_PICKER_VERSION = 2;
9
+ const MAX_SIBLING_HIGHLIGHTERS = 1000;
10
+ const DEBUG_ATTR_ID = "data-imagicma-id";
11
+ const DEBUG_ATTR_PATH = "data-imagicma-path";
12
+ const DEBUG_ATTR_LINE = "data-imagicma-line";
13
+ const DEBUG_ATTR_FILE = "data-imagicma-file";
14
+ const DEBUG_ATTR_COMPONENT = "data-imagicma-component";
15
+ const SORT_DRAG_THRESHOLD_PX = 4;
16
+
17
+ type PreviewPickerMode = "single" | "design";
18
+ type PreviewPickerSyncMode = "idle" | "picking";
19
+ type PreviewDraftTargetScope = "single" | "group";
20
+
21
+ type PreviewDraftPatch = {
22
+ textContent?: string;
23
+ fontSize?: string;
24
+ fontWeight?: string;
25
+ textAlign?: string;
26
+ color?: string;
27
+ backgroundColor?: string;
28
+ borderRadius?: string;
29
+ margin?: string;
30
+ padding?: string;
31
+ };
32
+
33
+ type PreviewSourceStyleField =
34
+ | "fontSize"
35
+ | "fontWeight"
36
+ | "textAlign"
37
+ | "color"
38
+ | "backgroundColor"
39
+ | "borderRadius"
40
+ | "margin"
41
+ | "padding";
42
+
43
+ type PreviewSourceTextBinding = {
44
+ kind: "jsx-text";
45
+ source: PreviewSourceRef;
46
+ };
47
+
48
+ type PreviewSourceRemoveBinding = {
49
+ kind: "jsx-element";
50
+ source: PreviewSourceRef;
51
+ };
52
+
53
+ type PreviewSourceStyleBinding = {
54
+ kind: "inline-style-property";
55
+ field: PreviewSourceStyleField;
56
+ source: PreviewSourceRef;
57
+ };
58
+
59
+ type PreviewSourceSortBinding =
60
+ | {
61
+ kind: "jsx-sibling-order";
62
+ source: PreviewSourceRef;
63
+ }
64
+ | {
65
+ kind: "array-literal-order";
66
+ itemKeyField: string;
67
+ source: PreviewSourceRef;
68
+ };
69
+
70
+ type PreviewSourceBindings = {
71
+ textContent?: PreviewSourceTextBinding;
72
+ remove?: PreviewSourceRemoveBinding;
73
+ fontSize?: PreviewSourceStyleBinding;
74
+ fontWeight?: PreviewSourceStyleBinding;
75
+ textAlign?: PreviewSourceStyleBinding;
76
+ color?: PreviewSourceStyleBinding;
77
+ backgroundColor?: PreviewSourceStyleBinding;
78
+ borderRadius?: PreviewSourceStyleBinding;
79
+ margin?: PreviewSourceStyleBinding;
80
+ padding?: PreviewSourceStyleBinding;
81
+ sort?: PreviewSourceSortBinding;
82
+ };
83
+
84
+ type PreviewSourceRef = {
85
+ file: string;
86
+ line: number;
87
+ column: number;
88
+ nodeKey: string;
89
+ templateKey: string;
90
+ componentName?: string;
91
+ ownerName?: string;
92
+ };
93
+
94
+ type PreviewDraftApplyPayload = {
95
+ revision?: number;
96
+ target?: {
97
+ scope: PreviewDraftTargetScope;
98
+ nodeKey?: string;
99
+ groupKey?: string;
100
+ itemKey?: string;
101
+ };
102
+ patch: PreviewDraftPatch;
103
+ };
104
+
105
+ type PreviewOverridePageEntry = {
106
+ nodes?: Record<string, { textContent?: string; style?: Omit<PreviewDraftPatch, "textContent">; removed?: boolean }>;
107
+ groups?: Record<string, { style?: Omit<PreviewDraftPatch, "textContent"> }>;
108
+ sorts?: Record<string, string[]>;
109
+ };
110
+
111
+ type PreviewSortPreviewPayload = {
112
+ sortableParentId: string;
113
+ orderedSortKeys: readonly string[];
114
+ };
115
+
116
+ type PreviewLinkedSourceSelection = {
117
+ nodeId?: string;
118
+ sourceFile?: string;
119
+ sortKey?: string;
120
+ source?: PreviewSourceRef;
121
+ repeat?: {
122
+ groupKey: string;
123
+ sortableParentId?: string;
124
+ itemKey?: string;
125
+ index?: number;
126
+ size?: number;
127
+ sortable: boolean;
128
+ };
129
+ sourceBindings?: PreviewSourceBindings;
130
+ };
131
+
132
+ type PreviewPickerStateSyncPayload = {
133
+ mode: PreviewPickerSyncMode;
134
+ pageKey: string;
135
+ overrides: PreviewOverridePageEntry | null;
136
+ selectedNodeId: string | null;
137
+ draft: PreviewDraftApplyPayload | null;
138
+ drafts: PreviewDraftApplyPayload[];
139
+ pendingSort: PreviewSortPreviewPayload | null;
140
+ };
141
+
142
+ type PickerMessage =
143
+ | {
144
+ channel: typeof PREVIEW_PICKER_CHANNEL;
145
+ version: typeof PREVIEW_PICKER_VERSION;
146
+ type: "IMAGICMA_PICKER_FRAME_HELLO";
147
+ frameInstanceId: string;
148
+ payload?: {
149
+ pageUrl?: string;
150
+ protocolVersion?: number;
151
+ reason?: string;
152
+ };
153
+ }
154
+ | {
155
+ channel: typeof PREVIEW_PICKER_CHANNEL;
156
+ version: typeof PREVIEW_PICKER_VERSION;
157
+ type: "IMAGICMA_PICKER_STATE_SYNC";
158
+ frameInstanceId: string;
159
+ sessionId: string | null;
160
+ payload: PreviewPickerStateSyncPayload;
161
+ }
162
+ | {
163
+ channel: typeof PREVIEW_PICKER_CHANNEL;
164
+ version: typeof PREVIEW_PICKER_VERSION;
165
+ type: "IMAGICMA_PICKER_STOP";
166
+ frameInstanceId: string;
167
+ sessionId: string | null;
168
+ };
169
+
170
+ type RuntimeState = {
171
+ frameInstanceId: string;
172
+ parentOrigin: string | null;
173
+ activeSessionId: string | null;
174
+ enabled: boolean;
175
+ selectedElement: HTMLElement | null;
176
+ hoveredElement: HTMLElement | null;
177
+ overlayRoot: HTMLDivElement | null;
178
+ hoverLabelEl: HTMLDivElement | null;
179
+ selectedLabelEl: HTMLDivElement | null;
180
+ hoverBoxEl: HTMLDivElement | null;
181
+ selectedBoxEl: HTMLDivElement | null;
182
+ sortIndicatorEl: HTMLDivElement | null;
183
+ hoverSiblingBoxEls: HTMLDivElement[];
184
+ selectedSiblingBoxEls: HTMLDivElement[];
185
+ draftStyleEl: HTMLStyleElement | null;
186
+ persistedOverrides: PreviewOverridePageEntry | null;
187
+ draftPayload: PreviewDraftApplyPayload | null;
188
+ draftPayloads: PreviewDraftApplyPayload[];
189
+ pendingSort: { groupKey: string; orderedSortKeys: readonly string[] } | null;
190
+ draggingSortKey: string | null;
191
+ sortPointerId: number | null;
192
+ sortPointerStartX: number;
193
+ sortPointerStartY: number;
194
+ sortDragging: boolean;
195
+ sortSourceElement: HTMLElement | null;
196
+ sortIndicatorTarget: HTMLElement | null;
197
+ sortIndicatorPlacement: "before" | "after" | null;
198
+ ignoreClickUntilTs: number;
199
+ originalText: Map<HTMLElement, { mode: "full"; value: string } | { mode: "owned"; nodes: Text[]; value: string[] }>;
200
+ originalDraftStyles: Map<HTMLElement, Record<PreviewSourceStyleField, { value: string; priority: string }>>;
201
+ originalSortStyles: Map<HTMLElement, {
202
+ transform: string;
203
+ transition: string;
204
+ zIndex: string;
205
+ }>;
206
+ throttledRecalculate: (() => void) | null;
207
+ mutationObserver: MutationObserver | null;
208
+ suppressMutationObserver: number;
209
+ };
210
+
211
+ type SortableMetadata = {
212
+ sortableParentId: string;
213
+ itemKey: string;
214
+ index: number;
215
+ size: number;
216
+ sortable: true;
217
+ itemElements: HTMLElement[];
218
+ mode: "explicit" | "sibling";
219
+ };
220
+
221
+ function logPreviewPickerRuntime(event: string, details?: Record<string, unknown>) {
222
+ console.debug("[PreviewPicker][frame]", event, details ?? {});
223
+ }
224
+
225
+ declare global {
226
+ interface Window {
227
+ __IMAGICMA_PREVIEW_PICKER__?: boolean;
228
+ }
229
+ }
230
+
231
+ function isRecord(value: unknown): value is Record<string, unknown> {
232
+ return typeof value === "object" && value !== null;
233
+ }
234
+
235
+ function trimText(value: unknown): string {
236
+ return typeof value === "string" ? value.trim() : "";
237
+ }
238
+
239
+ function createFrameInstanceId(): string {
240
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
241
+ return `frame_${crypto.randomUUID()}`;
242
+ }
243
+ return `frame_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
244
+ }
245
+
246
+ function isPickerMessage(value: unknown): value is PickerMessage {
247
+ if (!isRecord(value)) return false;
248
+ if (value.channel !== PREVIEW_PICKER_CHANNEL) return false;
249
+ if (value.version !== PREVIEW_PICKER_VERSION) return false;
250
+ if (typeof value.type !== "string") return false;
251
+ if (typeof value.frameInstanceId !== "string" || value.frameInstanceId.length === 0) return false;
252
+ return true;
253
+ }
254
+
255
+ function escapeAttributeValue(value: string): string {
256
+ return typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(value) : value.replace(/"/g, '\\"');
257
+ }
258
+
259
+ function parseLineAndColumnFromId(id: string): { line: number; column: number } | null {
260
+ const match = /:(\d+):(\d+)$/.exec(id);
261
+ if (!match) return null;
262
+ const line = Number(match[1]);
263
+ const column = Number(match[2]);
264
+ if (!Number.isFinite(line) || !Number.isFinite(column)) return null;
265
+ return { line, column };
266
+ }
267
+
268
+ function buildSelectorForElement(element: HTMLElement): string {
269
+ const nodeId = trimText(element.getAttribute("data-imagicma-node-id"));
270
+ if (nodeId) {
271
+ return `[data-imagicma-node-id="${escapeAttributeValue(nodeId)}"]`;
272
+ }
273
+
274
+ if (element.id) {
275
+ return `#${escapeAttributeValue(element.id)}`;
276
+ }
277
+
278
+ return element.tagName.toLowerCase();
279
+ }
280
+
281
+ function getSourceId(element: HTMLElement): string {
282
+ return trimText(element.getAttribute(DEBUG_ATTR_ID));
283
+ }
284
+
285
+ function getSourceFile(element: HTMLElement): string {
286
+ return trimText(element.getAttribute(DEBUG_ATTR_PATH))
287
+ || trimText(element.getAttribute(DEBUG_ATTR_FILE))
288
+ || trimText(element.getAttribute("data-imagicma-source-file"));
289
+ }
290
+
291
+ function getComponentName(element: HTMLElement): string {
292
+ return trimText(element.getAttribute(DEBUG_ATTR_COMPONENT))
293
+ || trimText(element.getAttribute("data-imagicma-component"))
294
+ || trimText(element.getAttribute("data-component-name"))
295
+ || element.tagName.toLowerCase();
296
+ }
297
+
298
+ function getSourceMetadata(element: HTMLElement) {
299
+ const sourceId = getSourceId(element);
300
+ const sourceFile = getSourceFile(element);
301
+ const parsed = sourceId ? parseLineAndColumnFromId(sourceId) : null;
302
+ if (!sourceId || !sourceFile || !parsed) return null;
303
+
304
+ return {
305
+ file: sourceFile,
306
+ line: parsed.line,
307
+ column: parsed.column,
308
+ nodeKey: sourceId,
309
+ templateKey: sourceId,
310
+ componentName: getComponentName(element),
311
+ };
312
+ }
313
+
314
+ function setSemanticAttribute(element: HTMLElement, name: string, value: string | undefined, force = false) {
315
+ const nextValue = trimText(value);
316
+ if (!nextValue) return;
317
+ if (!force && trimText(element.getAttribute(name))) return;
318
+ element.setAttribute(name, nextValue);
319
+ }
320
+
321
+ function toSyntheticNodeId(element: HTMLElement): string | null {
322
+ const debugId = getSourceId(element);
323
+ if (debugId) return `runtime:${debugId}`;
324
+
325
+ const sourcePath = trimText(element.getAttribute(DEBUG_ATTR_PATH));
326
+ const line = trimText(element.getAttribute(DEBUG_ATTR_LINE));
327
+ const componentName = getComponentName(element);
328
+ if (!sourcePath) return null;
329
+
330
+ return [
331
+ "runtime",
332
+ sourcePath,
333
+ line || "0",
334
+ componentName || element.tagName.toLowerCase(),
335
+ ].join(":");
336
+ }
337
+
338
+ function inferSemanticKind(element: HTMLElement): "text" | "box" {
339
+ return isSimpleTextElement(element) ? "text" : "box";
340
+ }
341
+
342
+ function annotateBaseSemanticNode(element: HTMLElement) {
343
+ const syntheticNodeId = toSyntheticNodeId(element);
344
+ if (!syntheticNodeId) return;
345
+
346
+ setSemanticAttribute(element, "data-imagicma-node-id", syntheticNodeId);
347
+ setSemanticAttribute(element, "data-imagicma-kind", inferSemanticKind(element));
348
+ setSemanticAttribute(element, "data-imagicma-source-file", getSourceFile(element));
349
+ }
350
+
351
+ function hydrateRuntimeSemantics() {
352
+ document.querySelectorAll<HTMLElement>(`[${DEBUG_ATTR_ID}]`).forEach(annotateBaseSemanticNode);
353
+ }
354
+
355
+ function getRepeatItemRoot(element: HTMLElement): HTMLElement | null {
356
+ return element.closest<HTMLElement>('[data-imagicma-kind="repeat-item"]');
357
+ }
358
+
359
+ function queryRepeatItemRoots(groupKey: string): HTMLElement[] {
360
+ if (!groupKey) return [];
361
+
362
+ return Array.from(
363
+ document.querySelectorAll<HTMLElement>(
364
+ `[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"]`,
365
+ ),
366
+ );
367
+ }
368
+
369
+ function findNodeWithinRepeatItem(root: HTMLElement, nodeId: string): HTMLElement | null {
370
+ if (trimText(root.getAttribute("data-imagicma-node-id")) === nodeId) {
371
+ return root;
372
+ }
373
+
374
+ return root.querySelector<HTMLElement>(
375
+ `[data-imagicma-node-id="${escapeAttributeValue(nodeId)}"]`,
376
+ );
377
+ }
378
+
379
+ function getRepeatMetadata(element: HTMLElement) {
380
+ const repeatNode = getRepeatItemRoot(element);
381
+ const groupKey = trimText(repeatNode?.getAttribute("data-imagicma-repeat-group"));
382
+ const itemKey = trimText(repeatNode?.getAttribute("data-imagicma-sort-key"));
383
+ const sortableParentId = trimText(repeatNode?.getAttribute("data-imagicma-sort-parent"));
384
+
385
+ if (!groupKey && !sortableParentId) return null;
386
+
387
+ const siblings = sortableParentId
388
+ ? Array.from(
389
+ document.querySelectorAll<HTMLElement>(
390
+ `[data-imagicma-kind="repeat-item"][data-imagicma-sort-parent="${escapeAttributeValue(sortableParentId)}"]`,
391
+ ),
392
+ )
393
+ : [];
394
+
395
+ return {
396
+ groupKey: groupKey || sortableParentId,
397
+ sortableParentId: sortableParentId || undefined,
398
+ itemKey: itemKey || undefined,
399
+ index: itemKey ? siblings.findIndex((item) => trimText(item.getAttribute("data-imagicma-sort-key")) === itemKey) : undefined,
400
+ size: siblings.length || undefined,
401
+ sortable: Boolean(sortableParentId && itemKey),
402
+ };
403
+ }
404
+
405
+ function getElementNodeId(element: HTMLElement | null | undefined): string {
406
+ return trimText(element?.getAttribute("data-imagicma-node-id"));
407
+ }
408
+
409
+ function getDirectSemanticSiblingItems(parent: HTMLElement | null): HTMLElement[] {
410
+ if (!parent) return [];
411
+
412
+ const items = Array.from(parent.children).filter((child): child is HTMLElement => {
413
+ return child instanceof HTMLElement && Boolean(getElementNodeId(child));
414
+ });
415
+
416
+ if (items.length < 2) return [];
417
+
418
+ const keys = items.map((item) => getElementNodeId(item));
419
+ if (keys.some((key) => !key)) return [];
420
+ if (new Set(keys).size !== keys.length) return [];
421
+
422
+ return items;
423
+ }
424
+
425
+ function getSyntheticSortableParentId(parent: HTMLElement | null): string | null {
426
+ if (!parent) return null;
427
+ return getElementNodeId(parent) || toSyntheticNodeId(parent);
428
+ }
429
+
430
+ function getSiblingSortableMetadata(element: HTMLElement): SortableMetadata | null {
431
+ const nodeId = getElementNodeId(element);
432
+ if (!nodeId) return null;
433
+
434
+ const parent = element.parentElement;
435
+ const itemElements = getDirectSemanticSiblingItems(parent);
436
+ if (itemElements.length < 2) return null;
437
+
438
+ const sortableParentId = getSyntheticSortableParentId(parent);
439
+ if (!sortableParentId) return null;
440
+
441
+ const index = itemElements.findIndex((item) => item === element);
442
+ if (index < 0) return null;
443
+
444
+ return {
445
+ sortableParentId,
446
+ itemKey: nodeId,
447
+ index,
448
+ size: itemElements.length,
449
+ sortable: true,
450
+ itemElements,
451
+ mode: "sibling",
452
+ };
453
+ }
454
+
455
+ function getExplicitSortableMetadata(element: HTMLElement): SortableMetadata | null {
456
+ const sortableParentId = trimText(element.getAttribute("data-imagicma-sort-parent"));
457
+ const itemKey = trimText(element.getAttribute("data-imagicma-sort-key"));
458
+ if (!sortableParentId || !itemKey) return null;
459
+
460
+ const itemElements = Array.from(
461
+ document.querySelectorAll<HTMLElement>(
462
+ `[data-imagicma-kind="repeat-item"][data-imagicma-sort-parent="${escapeAttributeValue(sortableParentId)}"][data-imagicma-sort-key]`,
463
+ ),
464
+ );
465
+ if (itemElements.length < 2) return null;
466
+
467
+ const index = itemElements.findIndex((item) => trimText(item.getAttribute("data-imagicma-sort-key")) === itemKey);
468
+ if (index < 0) return null;
469
+
470
+ return {
471
+ sortableParentId,
472
+ itemKey,
473
+ index,
474
+ size: itemElements.length,
475
+ sortable: true,
476
+ itemElements,
477
+ mode: "explicit",
478
+ };
479
+ }
480
+
481
+ function getSortableMetadata(element: HTMLElement): SortableMetadata | null {
482
+ return getExplicitSortableMetadata(element) || getSiblingSortableMetadata(element);
483
+ }
484
+
485
+ function queryPeerElements(selector: string, element: HTMLElement): HTMLElement[] {
486
+ return Array.from(document.querySelectorAll<HTMLElement>(selector))
487
+ .filter((item) => item !== element)
488
+ .slice(0, MAX_SIBLING_HIGHLIGHTERS);
489
+ }
490
+
491
+ function getPeerElements(element: HTMLElement): HTMLElement[] {
492
+ const repeatMetadata = getRepeatMetadata(element);
493
+ const repeatGroupId = repeatMetadata?.groupKey || "";
494
+ const nodeId = trimText(element.getAttribute("data-imagicma-node-id"));
495
+
496
+ if (repeatGroupId && nodeId) {
497
+ return queryRepeatItemRoots(repeatGroupId)
498
+ .map((root) => findNodeWithinRepeatItem(root, nodeId))
499
+ .filter((item): item is HTMLElement => Boolean(item) && item !== element)
500
+ .slice(0, MAX_SIBLING_HIGHLIGHTERS);
501
+ }
502
+
503
+ if (nodeId) {
504
+ return queryPeerElements(
505
+ `[data-imagicma-node-id="${escapeAttributeValue(nodeId)}"]`,
506
+ element,
507
+ );
508
+ }
509
+
510
+ return [];
511
+ }
512
+
513
+ function isElementVisible(element: HTMLElement): boolean {
514
+ const rect = element.getBoundingClientRect();
515
+ return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
516
+ }
517
+
518
+ function throttleRAF(callback: () => void): () => void {
519
+ let frame: number | null = null;
520
+ return () => {
521
+ if (frame !== null) return;
522
+ frame = window.requestAnimationFrame(() => {
523
+ frame = null;
524
+ callback();
525
+ });
526
+ };
527
+ }
528
+
529
+ function findSortableItems(sortableParentId: string): HTMLElement[] {
530
+ const explicitItems = Array.from(
531
+ document.querySelectorAll<HTMLElement>(
532
+ `[data-imagicma-kind="repeat-item"][data-imagicma-sort-parent="${escapeAttributeValue(sortableParentId)}"][data-imagicma-sort-key]`,
533
+ ),
534
+ );
535
+ if (explicitItems.length > 0) return explicitItems;
536
+
537
+ const candidateParents = new Set<HTMLElement>();
538
+ document.querySelectorAll<HTMLElement>("[data-imagicma-node-id]").forEach((element) => {
539
+ if (element.parentElement) {
540
+ candidateParents.add(element.parentElement);
541
+ }
542
+ });
543
+
544
+ for (const parent of candidateParents) {
545
+ const parentId = getSyntheticSortableParentId(parent);
546
+ if (parentId !== sortableParentId) continue;
547
+ const items = getDirectSemanticSiblingItems(parent);
548
+ if (items.length >= 2) return items;
549
+ }
550
+
551
+ return [];
552
+ }
553
+
554
+ function getSortableItemKey(element: HTMLElement): string | null {
555
+ return getSortableMetadata(element)?.itemKey || null;
556
+ }
557
+
558
+ function getSortableAxis(items: HTMLElement[]): "horizontal" | "vertical" {
559
+ if (items.length < 2) return "vertical";
560
+ const firstRect = items[0].getBoundingClientRect();
561
+ const secondRect = items[1].getBoundingClientRect();
562
+ const dx = Math.abs(secondRect.left - firstRect.left);
563
+ const dy = Math.abs(secondRect.top - firstRect.top);
564
+ return dx > dy ? "horizontal" : "vertical";
565
+ }
566
+
567
+ function beginInternalMutation(state: RuntimeState) {
568
+ state.suppressMutationObserver += 1;
569
+ }
570
+
571
+ function endInternalMutation(state: RuntimeState) {
572
+ queueMicrotask(() => {
573
+ state.suppressMutationObserver = Math.max(0, state.suppressMutationObserver - 1);
574
+ });
575
+ }
576
+
577
+ function runWithInternalMutation<T>(state: RuntimeState, fn: () => T): T {
578
+ beginInternalMutation(state);
579
+ try {
580
+ return fn();
581
+ } finally {
582
+ endInternalMutation(state);
583
+ }
584
+ }
585
+
586
+ function applySortableAffordance(state: RuntimeState, enabled: boolean, sortableParentId?: string | null) {
587
+ runWithInternalMutation(state, () => {
588
+ document.querySelectorAll<HTMLElement>("[data-imagicma-node-id]").forEach((item) => {
589
+ if (item.dataset.imagicmaSortCursor === "grab") {
590
+ item.style.cursor = "";
591
+ delete item.dataset.imagicmaSortCursor;
592
+ }
593
+ });
594
+
595
+ if (!enabled || !sortableParentId) return;
596
+
597
+ findSortableItems(sortableParentId).forEach((item) => {
598
+ item.style.cursor = "grab";
599
+ item.dataset.imagicmaSortCursor = "grab";
600
+ });
601
+ });
602
+ }
603
+
604
+ function isSimpleTextElement(element: HTMLElement): boolean {
605
+ if (element.children.length > 0) return false;
606
+ return trimText(element.textContent).length > 0;
607
+ }
608
+
609
+ const INLINE_STYLE_BINDINGS: Array<{ field: PreviewSourceStyleField; cssName: string }> = [
610
+ { field: "fontSize", cssName: "font-size" },
611
+ { field: "fontWeight", cssName: "font-weight" },
612
+ { field: "textAlign", cssName: "text-align" },
613
+ { field: "color", cssName: "color" },
614
+ { field: "backgroundColor", cssName: "background-color" },
615
+ { field: "borderRadius", cssName: "border-radius" },
616
+ { field: "margin", cssName: "margin" },
617
+ { field: "padding", cssName: "padding" },
618
+ ];
619
+
620
+ function getExplicitArraySortBinding(element: HTMLElement): PreviewSourceSortBinding | undefined {
621
+ const root = getRepeatItemRoot(element) || element;
622
+ const bindingSourceFile = trimText(root.getAttribute("data-imagicma-sort-source-file"));
623
+ const bindingLine = Number(trimText(root.getAttribute("data-imagicma-sort-source-line")));
624
+ const bindingColumn = Number(trimText(root.getAttribute("data-imagicma-sort-source-column")));
625
+ const itemKeyField = trimText(root.getAttribute("data-imagicma-sort-item-key-field"));
626
+ if (!bindingSourceFile || !Number.isFinite(bindingLine) || !Number.isFinite(bindingColumn) || !itemKeyField) {
627
+ return undefined;
628
+ }
629
+
630
+ return {
631
+ kind: "array-literal-order",
632
+ itemKeyField,
633
+ source: {
634
+ file: bindingSourceFile,
635
+ line: bindingLine,
636
+ column: bindingColumn,
637
+ nodeKey: `${bindingSourceFile}:${bindingLine}:${bindingColumn}`,
638
+ templateKey: `${bindingSourceFile}:${bindingLine}:${bindingColumn}`,
639
+ componentName: "array",
640
+ },
641
+ };
642
+ }
643
+
644
+ function buildSourceBindings(element: HTMLElement, source: ReturnType<typeof getSourceMetadata>): PreviewSourceBindings | undefined {
645
+ if (!source) return undefined;
646
+
647
+ const bindings: PreviewSourceBindings = {
648
+ remove: {
649
+ kind: "jsx-element",
650
+ source,
651
+ },
652
+ };
653
+
654
+ if (isSimpleTextElement(element)) {
655
+ bindings.textContent = {
656
+ kind: "jsx-text",
657
+ source,
658
+ };
659
+ }
660
+
661
+ for (const entry of INLINE_STYLE_BINDINGS) {
662
+ const value = trimText(element.style.getPropertyValue(entry.cssName)) || trimText((element.style as CSSStyleDeclaration & Record<string, string>)[entry.field]);
663
+ if (!value) continue;
664
+ bindings[entry.field] = {
665
+ kind: "inline-style-property",
666
+ field: entry.field,
667
+ source,
668
+ };
669
+ }
670
+
671
+ const sortable = getSortableMetadata(element);
672
+ const explicitSortBinding = getExplicitArraySortBinding(element);
673
+ if (explicitSortBinding) {
674
+ bindings.sort = explicitSortBinding;
675
+ } else if (sortable?.sortable && sortable.mode === "sibling") {
676
+ bindings.sort = {
677
+ kind: "jsx-sibling-order",
678
+ source,
679
+ };
680
+ }
681
+
682
+ return Object.keys(bindings).length > 0 ? bindings : undefined;
683
+ }
684
+
685
+ function getEditableFields(bindings: PreviewSourceBindings | undefined, sortable: SortableMetadata | null) {
686
+ return {
687
+ textContent: Boolean(bindings?.textContent),
688
+ fontSize: Boolean(bindings?.fontSize),
689
+ fontWeight: Boolean(bindings?.fontWeight),
690
+ textAlign: Boolean(bindings?.textAlign),
691
+ color: Boolean(bindings?.color),
692
+ backgroundColor: Boolean(bindings?.backgroundColor),
693
+ borderRadius: Boolean(bindings?.borderRadius),
694
+ margin: Boolean(bindings?.margin),
695
+ padding: Boolean(bindings?.padding),
696
+ sortable: Boolean(bindings?.sort || sortable?.sortable),
697
+ };
698
+ }
699
+
700
+ function getComputedStyleSnapshot(element: HTMLElement) {
701
+ const style = window.getComputedStyle(element);
702
+ return {
703
+ textContent: trimText(element.textContent),
704
+ fontSize: trimText(style.fontSize),
705
+ fontWeight: trimText(style.fontWeight),
706
+ textAlign: trimText(style.textAlign),
707
+ color: trimText(style.color),
708
+ backgroundColor: trimText(style.backgroundColor),
709
+ borderRadius: trimText(style.borderRadius),
710
+ margin: trimText(style.margin),
711
+ padding: trimText(style.padding),
712
+ };
713
+ }
714
+
715
+ function buildLinkedSourceSelection(element: HTMLElement): PreviewLinkedSourceSelection {
716
+ const source = getSourceMetadata(element);
717
+ const repeat = getRepeatMetadata(element);
718
+ const sourceBindings = buildSourceBindings(element, source);
719
+
720
+ return {
721
+ nodeId: trimText(element.getAttribute("data-imagicma-node-id")) || undefined,
722
+ sourceFile: trimText(element.getAttribute("data-imagicma-source-file")) || source?.file || undefined,
723
+ sortKey: getSortableMetadata(element)?.itemKey || repeat?.itemKey || undefined,
724
+ source: source || undefined,
725
+ repeat: repeat || undefined,
726
+ sourceBindings,
727
+ };
728
+ }
729
+
730
+ function getSortablePeerSelections(element: HTMLElement): PreviewLinkedSourceSelection[] | undefined {
731
+ const sortable = getSortableMetadata(element);
732
+ if (!sortable?.sortable) return undefined;
733
+
734
+ const items = sortable.itemElements
735
+ .map((item) => buildLinkedSourceSelection(item))
736
+ .filter((item) => item.sortKey || item.source || item.sourceFile);
737
+
738
+ return items.length > 0 ? items : undefined;
739
+ }
740
+
741
+ function getSelectionPayload(element: HTMLElement) {
742
+ const rect = element.getBoundingClientRect();
743
+ const source = getSourceMetadata(element);
744
+ const repeat = getRepeatMetadata(element);
745
+ const sortable = getSortableMetadata(element);
746
+ const peers = getPeerElements(element);
747
+ const sourceBindings = buildSourceBindings(element, source);
748
+ const peerSelections = peers.map((peer) => buildLinkedSourceSelection(peer));
749
+ const sortablePeers = getSortablePeerSelections(element);
750
+ const sourceId = source?.templateKey || trimText(element.getAttribute("data-imagicma-node-id"));
751
+ const persistCapability = {
752
+ textContent: sourceBindings?.textContent ? "source" : (isSimpleTextElement(element) ? "preview-only" : "unsupported"),
753
+ style: INLINE_STYLE_BINDINGS.some((entry) => Boolean(sourceBindings?.[entry.field])) ? "source" : "preview-only",
754
+ sort: sourceBindings?.sort ? "source" : (sortable?.sortable ? "preview-only" : "unsupported"),
755
+ };
756
+
757
+ return {
758
+ pageUrl: window.location.href,
759
+ selector: buildSelectorForElement(element),
760
+ tagName: element.tagName.toLowerCase(),
761
+ textSnippet: trimText(element.textContent).slice(0, 240),
762
+ rect: {
763
+ x: rect.left,
764
+ y: rect.top,
765
+ width: rect.width,
766
+ height: rect.height,
767
+ },
768
+ attributes: {
769
+ class: trimText(element.getAttribute("class")),
770
+ id: trimText(element.getAttribute("id")),
771
+ href: trimText(element.getAttribute("href")),
772
+ "data-testid": trimText(element.getAttribute("data-testid")),
773
+ },
774
+ templateKey: sourceId || undefined,
775
+ nodeId: trimText(element.getAttribute("data-imagicma-node-id")) || undefined,
776
+ kind: trimText(element.getAttribute("data-imagicma-kind")) || undefined,
777
+ sourceFile: trimText(element.getAttribute("data-imagicma-source-file")) || source?.file || undefined,
778
+ propPath: trimText(element.getAttribute("data-imagicma-prop-path")) || undefined,
779
+ repeatGroupId: trimText(element.getAttribute("data-imagicma-repeat-group")) || repeat?.groupKey || undefined,
780
+ repeatTemplate: trimText(element.getAttribute("data-imagicma-repeat-template")) || undefined,
781
+ sortableParentId: sortable?.sortableParentId || undefined,
782
+ sortKey: sortable?.itemKey || undefined,
783
+ sortIndex: sortable?.index,
784
+ editableFields: getEditableFields(sourceBindings, sortable),
785
+ computedStyleSnapshot: getComputedStyleSnapshot(element),
786
+ peerRects: peers.map((peer) => {
787
+ const peerRect = peer.getBoundingClientRect();
788
+ return {
789
+ nodeId: trimText(peer.getAttribute("data-imagicma-node-id")) || getSourceId(peer) || buildSelectorForElement(peer),
790
+ rect: {
791
+ x: peerRect.left,
792
+ y: peerRect.top,
793
+ width: peerRect.width,
794
+ height: peerRect.height,
795
+ },
796
+ };
797
+ }),
798
+ source: source || undefined,
799
+ repeat: repeat || undefined,
800
+ persistCapability,
801
+ sourceBindings,
802
+ peerSelections: peerSelections.length > 0 ? peerSelections : undefined,
803
+ sortablePeers,
804
+ };
805
+ }
806
+
807
+ function createOverlayBox(): HTMLDivElement {
808
+ const box = document.createElement("div");
809
+ Object.assign(box.style, {
810
+ position: "fixed",
811
+ display: "none",
812
+ pointerEvents: "none",
813
+ boxSizing: "border-box",
814
+ borderRadius: "2px",
815
+ });
816
+ return box;
817
+ }
818
+
819
+ function createOverlayLabel(): HTMLDivElement {
820
+ const label = document.createElement("div");
821
+ Object.assign(label.style, {
822
+ position: "fixed",
823
+ display: "none",
824
+ padding: "6px 10px",
825
+ borderRadius: "8px",
826
+ background: "#1d7cf2",
827
+ color: "#fff",
828
+ fontSize: "12px",
829
+ fontWeight: "600",
830
+ lineHeight: "1",
831
+ boxShadow: "0 8px 24px rgba(0,0,0,0.2)",
832
+ maxWidth: "240px",
833
+ whiteSpace: "nowrap",
834
+ });
835
+ return label;
836
+ }
837
+
838
+ function createOverlayRoot(state: RuntimeState): HTMLDivElement {
839
+ if (state.overlayRoot) return state.overlayRoot;
840
+
841
+ const root = document.createElement("div");
842
+ root.setAttribute("data-imagicma-picker-overlay", "true");
843
+ Object.assign(root.style, {
844
+ position: "fixed",
845
+ inset: "0",
846
+ pointerEvents: "none",
847
+ zIndex: "2147483646",
848
+ });
849
+
850
+ state.hoverBoxEl = createOverlayBox();
851
+ state.selectedBoxEl = createOverlayBox();
852
+ state.hoverLabelEl = createOverlayLabel();
853
+ state.selectedLabelEl = createOverlayLabel();
854
+ state.sortIndicatorEl = document.createElement("div");
855
+ state.sortIndicatorEl.setAttribute("data-imagicma-picker-sort-indicator", "true");
856
+ Object.assign(state.sortIndicatorEl.style, {
857
+ position: "fixed",
858
+ display: "none",
859
+ pointerEvents: "none",
860
+ zIndex: "2147483647",
861
+ boxSizing: "border-box",
862
+ borderRadius: "0",
863
+ background: "#1d7cf2",
864
+ boxShadow: "none",
865
+ });
866
+
867
+ root.appendChild(state.hoverBoxEl);
868
+ root.appendChild(state.selectedBoxEl);
869
+ root.appendChild(state.hoverLabelEl);
870
+ root.appendChild(state.selectedLabelEl);
871
+ root.appendChild(state.sortIndicatorEl);
872
+
873
+ document.documentElement.appendChild(root);
874
+ state.overlayRoot = root;
875
+ return root;
876
+ }
877
+
878
+ function hideOverlayBox(box: HTMLDivElement | null) {
879
+ if (!box) return;
880
+ box.style.display = "none";
881
+ }
882
+
883
+ function hideOverlayLabel(label: HTMLDivElement | null) {
884
+ if (!label) return;
885
+ label.style.display = "none";
886
+ label.textContent = "";
887
+ }
888
+
889
+ function clearSiblingBoxes(boxes: HTMLDivElement[]) {
890
+ boxes.forEach((box) => box.remove());
891
+ boxes.length = 0;
892
+ }
893
+
894
+ function clearOverlay(state: RuntimeState) {
895
+ hideOverlayBox(state.hoverBoxEl);
896
+ hideOverlayBox(state.selectedBoxEl);
897
+ hideOverlayBox(state.sortIndicatorEl);
898
+ hideOverlayLabel(state.hoverLabelEl);
899
+ hideOverlayLabel(state.selectedLabelEl);
900
+ clearSiblingBoxes(state.hoverSiblingBoxEls);
901
+ clearSiblingBoxes(state.selectedSiblingBoxEls);
902
+ }
903
+
904
+ function getVisibleOverlayRect(element: HTMLElement) {
905
+ const rect = element.getBoundingClientRect();
906
+ if (rect.width <= 0 || rect.height <= 0) return null;
907
+ if (rect.bottom <= 0 || rect.top >= window.innerHeight || rect.right <= 0 || rect.left >= window.innerWidth) {
908
+ return null;
909
+ }
910
+
911
+ const top = Math.max(0, rect.top);
912
+ const bottom = Math.min(window.innerHeight, rect.bottom);
913
+ const height = Math.max(0, bottom - top);
914
+ if (height <= 0) return null;
915
+
916
+ return {
917
+ top,
918
+ left: rect.left,
919
+ width: rect.width,
920
+ height,
921
+ rawTop: rect.top,
922
+ };
923
+ }
924
+
925
+ function syncOverlayBox(
926
+ box: HTMLDivElement | null,
927
+ element: HTMLElement | null,
928
+ style: { border: string; background: string; outlineOffset?: string },
929
+ ) {
930
+ if (!box || !element) {
931
+ hideOverlayBox(box);
932
+ return;
933
+ }
934
+
935
+ const rect = getVisibleOverlayRect(element);
936
+ if (!rect) {
937
+ hideOverlayBox(box);
938
+ return;
939
+ }
940
+
941
+ Object.assign(box.style, {
942
+ display: "block",
943
+ top: `${rect.top}px`,
944
+ left: `${rect.left}px`,
945
+ width: `${rect.width}px`,
946
+ height: `${rect.height}px`,
947
+ border: style.border,
948
+ background: style.background,
949
+ outlineOffset: style.outlineOffset ?? "0",
950
+ });
951
+ }
952
+
953
+ function syncOverlayLabel(label: HTMLDivElement | null, element: HTMLElement | null, peerCount: number) {
954
+ if (!label || !element) {
955
+ hideOverlayLabel(label);
956
+ return;
957
+ }
958
+
959
+ const rect = getVisibleOverlayRect(element);
960
+ if (!rect) {
961
+ hideOverlayLabel(label);
962
+ return;
963
+ }
964
+
965
+ label.textContent = `${getComponentName(element)}${peerCount > 0 ? ` ×${peerCount + 1}` : ""}`;
966
+ Object.assign(label.style, {
967
+ display: "block",
968
+ top: `${Math.max(8, rect.rawTop - 30)}px`,
969
+ left: `${Math.max(8, rect.left)}px`,
970
+ });
971
+ }
972
+
973
+ function syncSiblingOverlayBoxes(
974
+ root: HTMLDivElement,
975
+ boxes: HTMLDivElement[],
976
+ elements: HTMLElement[],
977
+ style: { border: string; background: string },
978
+ ) {
979
+ clearSiblingBoxes(boxes);
980
+
981
+ for (const element of elements.filter(isElementVisible)) {
982
+ const rect = getVisibleOverlayRect(element);
983
+ if (!rect) continue;
984
+ const box = createOverlayBox();
985
+ Object.assign(box.style, {
986
+ display: "block",
987
+ top: `${rect.top}px`,
988
+ left: `${rect.left}px`,
989
+ width: `${rect.width}px`,
990
+ height: `${rect.height}px`,
991
+ border: style.border,
992
+ background: style.background,
993
+ });
994
+ root.appendChild(box);
995
+ boxes.push(box);
996
+ }
997
+ }
998
+
999
+ function renderOverlayLayer(
1000
+ state: RuntimeState,
1001
+ element: HTMLElement | null,
1002
+ peers: HTMLElement[],
1003
+ variant: "hover" | "selected",
1004
+ ) {
1005
+ const root = createOverlayRoot(state);
1006
+ const visiblePeers = peers.filter((peer) => peer !== element && isElementVisible(peer));
1007
+
1008
+ if (variant === "selected") {
1009
+ syncOverlayBox(state.selectedBoxEl, element, {
1010
+ border: "2px solid #1d7cf2",
1011
+ background: "transparent",
1012
+ outlineOffset: "3px",
1013
+ });
1014
+ syncOverlayLabel(state.selectedLabelEl, element, visiblePeers.length);
1015
+ syncSiblingOverlayBoxes(root, state.selectedSiblingBoxEls, visiblePeers, {
1016
+ border: "2px dashed #1d7cf2",
1017
+ background: "rgba(29,124,242,0.08)",
1018
+ });
1019
+ return;
1020
+ }
1021
+
1022
+ syncOverlayBox(state.hoverBoxEl, element, {
1023
+ border: "2px dashed rgba(29,124,242,0.85)",
1024
+ background: "rgba(29,124,242,0.04)",
1025
+ });
1026
+ syncOverlayLabel(state.hoverLabelEl, element, visiblePeers.length);
1027
+ syncSiblingOverlayBoxes(root, state.hoverSiblingBoxEls, visiblePeers, {
1028
+ border: "2px dashed rgba(29,124,242,0.6)",
1029
+ background: "rgba(29,124,242,0.04)",
1030
+ });
1031
+ }
1032
+
1033
+ function syncSortIndicator(
1034
+ state: RuntimeState,
1035
+ target: HTMLElement | null,
1036
+ placement: "before" | "after" | null,
1037
+ items: HTMLElement[] = [],
1038
+ ) {
1039
+ const indicator = state.sortIndicatorEl;
1040
+ if (!indicator || !target || !placement) {
1041
+ state.sortIndicatorTarget = null;
1042
+ state.sortIndicatorPlacement = null;
1043
+ hideOverlayBox(indicator);
1044
+ return;
1045
+ }
1046
+
1047
+ state.sortIndicatorTarget = target;
1048
+ state.sortIndicatorPlacement = placement;
1049
+
1050
+ const rect = target.getBoundingClientRect();
1051
+ if (rect.width <= 0 || rect.height <= 0) {
1052
+ state.sortIndicatorTarget = null;
1053
+ state.sortIndicatorPlacement = null;
1054
+ hideOverlayBox(indicator);
1055
+ return;
1056
+ }
1057
+
1058
+ const axis = getSortableAxis(items);
1059
+ if (axis === "horizontal") {
1060
+ const x = placement === "before" ? rect.left : rect.right;
1061
+ Object.assign(indicator.style, {
1062
+ display: "block",
1063
+ top: `${Math.max(0, rect.top)}px`,
1064
+ left: `${Math.max(0, x - 2)}px`,
1065
+ width: "4px",
1066
+ height: `${rect.height}px`,
1067
+ });
1068
+ return;
1069
+ }
1070
+
1071
+ const y = placement === "before" ? rect.top : rect.bottom;
1072
+ Object.assign(indicator.style, {
1073
+ display: "block",
1074
+ top: `${Math.max(0, y - 2)}px`,
1075
+ left: `${Math.max(0, rect.left)}px`,
1076
+ width: `${rect.width}px`,
1077
+ height: "4px",
1078
+ });
1079
+ }
1080
+
1081
+ function clearSortGesture(state: RuntimeState) {
1082
+ if (state.sortSourceElement) {
1083
+ runWithInternalMutation(state, () => {
1084
+ state.sortSourceElement!.style.opacity = "";
1085
+ });
1086
+ }
1087
+ hideOverlayBox(state.sortIndicatorEl);
1088
+ state.draggingSortKey = null;
1089
+ state.sortPointerId = null;
1090
+ state.sortPointerStartX = 0;
1091
+ state.sortPointerStartY = 0;
1092
+ state.sortDragging = false;
1093
+ state.sortSourceElement = null;
1094
+ state.sortIndicatorTarget = null;
1095
+ state.sortIndicatorPlacement = null;
1096
+ }
1097
+
1098
+ function buildOrderedSortableItems(items: HTMLElement[], orderedKeys: readonly string[]): HTMLElement[] {
1099
+ if (items.length === 0 || orderedKeys.length === 0) return items;
1100
+
1101
+ const keyedItems = new Map<string, HTMLElement>();
1102
+ items.forEach((item) => {
1103
+ const key = getSortableItemKey(item);
1104
+ if (key) {
1105
+ keyedItems.set(key, item);
1106
+ }
1107
+ });
1108
+
1109
+ const ordered: HTMLElement[] = [];
1110
+ orderedKeys.forEach((key) => {
1111
+ const item = keyedItems.get(key);
1112
+ if (!item) return;
1113
+ ordered.push(item);
1114
+ keyedItems.delete(key);
1115
+ });
1116
+
1117
+ items.forEach((item) => {
1118
+ if (!ordered.includes(item)) {
1119
+ ordered.push(item);
1120
+ }
1121
+ });
1122
+
1123
+ return ordered;
1124
+ }
1125
+
1126
+ function commitSort(state: RuntimeState, target: HTMLElement, placement: "before" | "after"): boolean {
1127
+ const sortable = getSortableMetadata(target);
1128
+ const draggingKey = state.draggingSortKey;
1129
+ if (!sortable || !draggingKey) return false;
1130
+
1131
+ const groupKey = sortable.sortableParentId;
1132
+ const items = buildOrderedSortableItems(
1133
+ findSortableItems(groupKey),
1134
+ state.pendingSort?.groupKey === groupKey ? state.pendingSort.orderedSortKeys : [],
1135
+ );
1136
+ const draggingItem = items.find((item) => {
1137
+ const sortableItem = getSortableMetadata(item);
1138
+ return sortableItem?.itemKey === draggingKey;
1139
+ });
1140
+ if (!draggingItem || draggingItem === target || draggingItem.parentElement !== target.parentElement) return false;
1141
+
1142
+ const remainingItems = items.filter((item) => item !== draggingItem);
1143
+ const targetIndex = remainingItems.findIndex((item) => item === target);
1144
+ if (targetIndex < 0) return false;
1145
+
1146
+ const insertIndex = placement === "after" ? targetIndex + 1 : targetIndex;
1147
+ remainingItems.splice(insertIndex, 0, draggingItem);
1148
+
1149
+ const orderedSortKeys = remainingItems
1150
+ .map((item) => getSortableItemKey(item))
1151
+ .filter((key): key is string => Boolean(key));
1152
+ state.pendingSort = {
1153
+ groupKey,
1154
+ orderedSortKeys,
1155
+ };
1156
+
1157
+ postToParent(state, {
1158
+ channel: PREVIEW_PICKER_CHANNEL,
1159
+ version: PREVIEW_PICKER_VERSION,
1160
+ type: "IMAGICMA_PICKER_SORT_COMMITTED",
1161
+ frameInstanceId: state.frameInstanceId,
1162
+ sessionId: state.activeSessionId,
1163
+ payload: {
1164
+ sortableParentId: groupKey,
1165
+ orderedSortKeys,
1166
+ },
1167
+ });
1168
+
1169
+ reapplyVisualState(state);
1170
+ return true;
1171
+ }
1172
+
1173
+ function queryElementsByNodeKey(nodeKey: string): HTMLElement[] {
1174
+ return Array.from(
1175
+ document.querySelectorAll<HTMLElement>(
1176
+ `[data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`,
1177
+ ),
1178
+ );
1179
+ }
1180
+
1181
+ function queryElementsByDraftTarget(target?: {
1182
+ scope?: PreviewDraftTargetScope;
1183
+ nodeKey?: string;
1184
+ groupKey?: string;
1185
+ itemKey?: string;
1186
+ }): HTMLElement[] {
1187
+ const nodeKey = trimText(target?.nodeKey);
1188
+ if (!nodeKey) return [];
1189
+
1190
+ const groupKey = trimText(target?.groupKey);
1191
+ const itemKey = trimText(target?.itemKey);
1192
+ if (groupKey && itemKey) {
1193
+ return Array.from(
1194
+ document.querySelectorAll<HTMLElement>(
1195
+ `[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"][data-imagicma-sort-key="${escapeAttributeValue(itemKey)}"][data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"],[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"][data-imagicma-sort-key="${escapeAttributeValue(itemKey)}"] [data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`,
1196
+ ),
1197
+ );
1198
+ }
1199
+
1200
+ if (groupKey && target?.scope === "group") {
1201
+ return queryElementsByGroupKey(groupKey, nodeKey);
1202
+ }
1203
+
1204
+ return queryElementsByNodeKey(nodeKey);
1205
+ }
1206
+
1207
+ function doesElementMatchDraftTarget(
1208
+ element: HTMLElement | null,
1209
+ target?: {
1210
+ scope?: PreviewDraftTargetScope;
1211
+ nodeKey?: string;
1212
+ groupKey?: string;
1213
+ itemKey?: string;
1214
+ },
1215
+ ): boolean {
1216
+ if (!element) return false;
1217
+ const nodeKey = trimText(target?.nodeKey);
1218
+ if (!nodeKey) return false;
1219
+ if (trimText(element.getAttribute("data-imagicma-node-id")) !== nodeKey) return false;
1220
+
1221
+ const groupKey = trimText(target?.groupKey);
1222
+ if (groupKey) {
1223
+ const elementGroupKey = trimText(element.getAttribute("data-imagicma-repeat-group"));
1224
+ if (elementGroupKey !== groupKey) return false;
1225
+ }
1226
+
1227
+ const itemKey = trimText(target?.itemKey);
1228
+ if (itemKey) {
1229
+ const repeatRoot = element.closest<HTMLElement>("[data-imagicma-kind='repeat-item']");
1230
+ const elementItemKey = trimText(repeatRoot?.getAttribute("data-imagicma-sort-key"));
1231
+ if (elementItemKey !== itemKey) return false;
1232
+ }
1233
+
1234
+ return true;
1235
+ }
1236
+
1237
+ function queryElementsByGroupKey(groupKey: string, nodeKey?: string): HTMLElement[] {
1238
+ if (!groupKey || !nodeKey) return [];
1239
+
1240
+ return queryRepeatItemRoots(groupKey)
1241
+ .map((root) => findNodeWithinRepeatItem(root, nodeKey))
1242
+ .filter((item): item is HTMLElement => Boolean(item));
1243
+ }
1244
+
1245
+ function parseNodeOverrideKey(nodeOverrideKey: string): {
1246
+ scope: PreviewDraftTargetScope;
1247
+ nodeKey?: string;
1248
+ groupKey?: string;
1249
+ itemKey?: string;
1250
+ } {
1251
+ const match = /^item::([^:]+)::([^:]+)::(.+)$/.exec(nodeOverrideKey);
1252
+ if (!match) {
1253
+ return {
1254
+ scope: "single",
1255
+ nodeKey: nodeOverrideKey || undefined,
1256
+ };
1257
+ }
1258
+
1259
+ return {
1260
+ scope: "single",
1261
+ groupKey: match[1] || undefined,
1262
+ itemKey: match[2] || undefined,
1263
+ nodeKey: match[3] || undefined,
1264
+ };
1265
+ }
1266
+
1267
+ function buildSelectorForDraftTarget(target?: {
1268
+ scope?: PreviewDraftTargetScope;
1269
+ nodeKey?: string;
1270
+ groupKey?: string;
1271
+ itemKey?: string;
1272
+ }): string {
1273
+ const nodeKey = trimText(target?.nodeKey);
1274
+ if (!nodeKey) return "";
1275
+
1276
+ const groupKey = trimText(target?.groupKey);
1277
+ const itemKey = trimText(target?.itemKey);
1278
+ if (groupKey && itemKey) {
1279
+ return `[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"][data-imagicma-sort-key="${escapeAttributeValue(itemKey)}"][data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"],[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"][data-imagicma-sort-key="${escapeAttributeValue(itemKey)}"] [data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`;
1280
+ }
1281
+
1282
+ if (groupKey && target?.scope === "group") {
1283
+ return `[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"][data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"],[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"] [data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`;
1284
+ }
1285
+
1286
+ return `[data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`;
1287
+ }
1288
+
1289
+ function parseGroupStyleKey(groupStyleKey: string): { groupKey: string; nodeKey?: string } {
1290
+ const [groupKey, nodeKey] = groupStyleKey.split("::");
1291
+ return {
1292
+ groupKey,
1293
+ nodeKey: nodeKey || undefined,
1294
+ };
1295
+ }
1296
+
1297
+ function ensureDraftStyleEl(state: RuntimeState): HTMLStyleElement {
1298
+ if (state.draftStyleEl) return state.draftStyleEl;
1299
+ const styleEl = document.createElement("style");
1300
+ styleEl.setAttribute("data-imagicma-preview-draft-style", "true");
1301
+ document.head.appendChild(styleEl);
1302
+ state.draftStyleEl = styleEl;
1303
+ return styleEl;
1304
+ }
1305
+
1306
+ function resetDraftDom(state: RuntimeState) {
1307
+ runWithInternalMutation(state, () => {
1308
+ state.originalText.forEach((entry, element) => {
1309
+ if (!element.isConnected) return;
1310
+ if (entry.mode === "full") {
1311
+ if (element.textContent !== entry.value) {
1312
+ element.textContent = entry.value;
1313
+ }
1314
+ return;
1315
+ }
1316
+
1317
+ entry.nodes.forEach((node, index) => {
1318
+ if (!node.isConnected) return;
1319
+ node.textContent = entry.value[index] ?? "";
1320
+ });
1321
+ });
1322
+ state.originalText.clear();
1323
+
1324
+ state.originalDraftStyles.forEach((snapshot, element) => {
1325
+ if (!element.isConnected) return;
1326
+ INLINE_STYLE_BINDINGS.forEach(({ field, cssName }) => {
1327
+ const original = snapshot[field];
1328
+ if (original.value) {
1329
+ element.style.setProperty(cssName, original.value, original.priority);
1330
+ } else {
1331
+ element.style.removeProperty(cssName);
1332
+ }
1333
+ });
1334
+ });
1335
+ state.originalDraftStyles.clear();
1336
+
1337
+ state.originalSortStyles.forEach((snapshot, element) => {
1338
+ if (!element.isConnected) return;
1339
+ element.style.transform = snapshot.transform;
1340
+ element.style.transition = snapshot.transition;
1341
+ element.style.zIndex = snapshot.zIndex;
1342
+ });
1343
+ state.originalSortStyles.clear();
1344
+ });
1345
+ }
1346
+
1347
+ function buildStyleRules(entry: PreviewOverridePageEntry | null): string {
1348
+ const rules: string[] = [];
1349
+ const appendRule = (
1350
+ selector: string,
1351
+ style?: Omit<PreviewDraftPatch, "textContent">,
1352
+ options?: { removed?: boolean },
1353
+ ) => {
1354
+ const declarations = [];
1355
+ if (style) {
1356
+ declarations.push(
1357
+ ...Object.entries(style)
1358
+ .filter(([, value]) => typeof value === "string" && value.trim().length > 0)
1359
+ .map(([key, value]) => `${key.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)}: ${value} !important;`),
1360
+ );
1361
+ }
1362
+ if (options?.removed) {
1363
+ declarations.push("display: none !important;");
1364
+ }
1365
+ if (declarations.length === 0) return;
1366
+ rules.push(`${selector} { ${declarations.join(" ")} }`);
1367
+ };
1368
+
1369
+ for (const [nodeOverrideKey, nodeEntry] of Object.entries(entry?.nodes ?? {})) {
1370
+ const selector = buildSelectorForDraftTarget(parseNodeOverrideKey(nodeOverrideKey));
1371
+ if (!selector) continue;
1372
+ appendRule(
1373
+ selector,
1374
+ nodeEntry.style,
1375
+ { removed: nodeEntry.removed === true },
1376
+ );
1377
+ }
1378
+
1379
+ for (const [groupStyleKey, groupEntry] of Object.entries(entry?.groups ?? {})) {
1380
+ const { groupKey, nodeKey } = parseGroupStyleKey(groupStyleKey);
1381
+ if (!nodeKey) continue;
1382
+ appendRule(
1383
+ `[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"][data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"],[data-imagicma-kind="repeat-item"][data-imagicma-repeat-group="${escapeAttributeValue(groupKey)}"] [data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`,
1384
+ groupEntry.style,
1385
+ );
1386
+ }
1387
+
1388
+ return rules.join("\n");
1389
+ }
1390
+
1391
+ function applyDraftStyleOverrides(state: RuntimeState, draftPayloads: PreviewDraftApplyPayload[]) {
1392
+ const applyStylePatch = (
1393
+ elements: HTMLElement[],
1394
+ patch: Partial<Record<PreviewSourceStyleField, string>>,
1395
+ ) => {
1396
+ if (elements.length === 0) return;
1397
+ const hasStylePatch = Object.values(patch).some((value) => typeof value === "string" && value.trim().length > 0);
1398
+ if (!hasStylePatch) return;
1399
+
1400
+ runWithInternalMutation(state, () => {
1401
+ for (const element of elements) {
1402
+ if (!state.originalDraftStyles.has(element)) {
1403
+ const snapshot = {} as Record<PreviewSourceStyleField, { value: string; priority: string }>;
1404
+ INLINE_STYLE_BINDINGS.forEach(({ field, cssName }) => {
1405
+ snapshot[field] = {
1406
+ value: element.style.getPropertyValue(cssName),
1407
+ priority: element.style.getPropertyPriority(cssName),
1408
+ };
1409
+ });
1410
+ state.originalDraftStyles.set(element, snapshot);
1411
+ }
1412
+
1413
+ INLINE_STYLE_BINDINGS.forEach(({ field, cssName }) => {
1414
+ const nextValue = patch[field];
1415
+ if (typeof nextValue !== "string") return;
1416
+ const normalized = nextValue.trim();
1417
+ if (normalized) {
1418
+ element.style.setProperty(cssName, normalized);
1419
+ } else {
1420
+ element.style.removeProperty(cssName);
1421
+ }
1422
+ });
1423
+ }
1424
+ });
1425
+ };
1426
+
1427
+ for (const draftPayload of draftPayloads) {
1428
+ const patch: Partial<Record<PreviewSourceStyleField, string>> = {
1429
+ fontSize: draftPayload.patch.fontSize,
1430
+ fontWeight: draftPayload.patch.fontWeight,
1431
+ textAlign: draftPayload.patch.textAlign,
1432
+ color: draftPayload.patch.color,
1433
+ backgroundColor: draftPayload.patch.backgroundColor,
1434
+ borderRadius: draftPayload.patch.borderRadius,
1435
+ margin: draftPayload.patch.margin,
1436
+ padding: draftPayload.patch.padding,
1437
+ };
1438
+
1439
+ const queriedTargets = queryElementsByDraftTarget(draftPayload.target);
1440
+ const targets = doesElementMatchDraftTarget(state.selectedElement, draftPayload.target)
1441
+ ? [state.selectedElement!, ...queriedTargets.filter((element) => element !== state.selectedElement)]
1442
+ : queriedTargets;
1443
+
1444
+ applyStylePatch(targets, patch);
1445
+ }
1446
+ }
1447
+
1448
+ function applyTextOverrides(state: RuntimeState, entry: PreviewOverridePageEntry | null, draftPayloads: PreviewDraftApplyPayload[]) {
1449
+ const collectOwnedTextNodes = (element: HTMLElement): Text[] => {
1450
+ const nodes: Text[] = [];
1451
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
1452
+ let current = walker.nextNode();
1453
+ while (current) {
1454
+ if (current instanceof Text) {
1455
+ const parentElement = current.parentElement;
1456
+ const owner = parentElement?.closest<HTMLElement>("[data-imagicma-node-id]");
1457
+ if (owner === element) {
1458
+ nodes.push(current);
1459
+ }
1460
+ }
1461
+ current = walker.nextNode();
1462
+ }
1463
+ return nodes;
1464
+ };
1465
+
1466
+ const applyText = (elements: HTMLElement[], text: string | undefined, enabled = true) => {
1467
+ if (!enabled) return;
1468
+ runWithInternalMutation(state, () => {
1469
+ for (const element of elements) {
1470
+ const nextText = text ?? "";
1471
+ const ownedTextNodes = collectOwnedTextNodes(element);
1472
+ const hasSemanticDescendants = Boolean(
1473
+ element.querySelector("[data-imagicma-node-id]"),
1474
+ );
1475
+
1476
+ if (!state.originalText.has(element)) {
1477
+ if (ownedTextNodes.length > 0 && (hasSemanticDescendants || element.children.length > 0)) {
1478
+ state.originalText.set(element, {
1479
+ mode: "owned",
1480
+ nodes: ownedTextNodes,
1481
+ value: ownedTextNodes.map((node) => node.textContent || ""),
1482
+ });
1483
+ } else {
1484
+ state.originalText.set(element, {
1485
+ mode: "full",
1486
+ value: element.textContent || "",
1487
+ });
1488
+ }
1489
+ }
1490
+
1491
+ if (ownedTextNodes.length > 0 && (hasSemanticDescendants || element.children.length > 0)) {
1492
+ ownedTextNodes.forEach((node, index) => {
1493
+ node.textContent = index === 0 ? nextText : "";
1494
+ });
1495
+ continue;
1496
+ }
1497
+
1498
+ if (element.textContent !== nextText) {
1499
+ element.textContent = nextText;
1500
+ }
1501
+ }
1502
+ });
1503
+ };
1504
+
1505
+ for (const [nodeOverrideKey, nodeEntry] of Object.entries(entry?.nodes ?? {})) {
1506
+ const targets = queryElementsByDraftTarget(parseNodeOverrideKey(nodeOverrideKey));
1507
+ applyText(
1508
+ targets,
1509
+ nodeEntry.textContent,
1510
+ nodeEntry.removed !== true && Object.prototype.hasOwnProperty.call(nodeEntry, "textContent"),
1511
+ );
1512
+ }
1513
+
1514
+ for (const activeDraftPayload of draftPayloads) {
1515
+ const hasDraftText = Object.prototype.hasOwnProperty.call(activeDraftPayload.patch, "textContent");
1516
+ const canApplyDraftText = hasDraftText && activeDraftPayload.target?.scope !== "group";
1517
+ if (!canApplyDraftText) continue;
1518
+ const queriedTargets = queryElementsByDraftTarget(activeDraftPayload.target);
1519
+ const targets = doesElementMatchDraftTarget(state.selectedElement, activeDraftPayload.target)
1520
+ ? [state.selectedElement!, ...queriedTargets.filter((element) => element !== state.selectedElement)]
1521
+ : queriedTargets;
1522
+
1523
+ applyText(targets, activeDraftPayload.patch.textContent, true);
1524
+ }
1525
+ }
1526
+
1527
+ function applyCurrentDraftToSelectedElement(state: RuntimeState) {
1528
+ const activeDraftPayload = state.draftPayload;
1529
+ const selectedElement = state.selectedElement;
1530
+ if (!activeDraftPayload || !selectedElement) return;
1531
+ if (!Object.prototype.hasOwnProperty.call(activeDraftPayload.patch, "textContent")) return;
1532
+ if (activeDraftPayload.target?.scope === "group") return;
1533
+ if (!doesElementMatchDraftTarget(selectedElement, activeDraftPayload.target)) return;
1534
+
1535
+ const nextText = activeDraftPayload.patch.textContent ?? "";
1536
+ runWithInternalMutation(state, () => {
1537
+ if (!state.originalText.has(selectedElement)) {
1538
+ state.originalText.set(selectedElement, {
1539
+ mode: "full",
1540
+ value: selectedElement.textContent || "",
1541
+ });
1542
+ }
1543
+ if (selectedElement.textContent !== nextText) {
1544
+ selectedElement.textContent = nextText;
1545
+ }
1546
+ });
1547
+ }
1548
+
1549
+ function applySortPreview(state: RuntimeState, items: HTMLElement[], orderedKeys: readonly string[]) {
1550
+ if (items.length < 2 || orderedKeys.length === 0) return;
1551
+
1552
+ const orderedItems = buildOrderedSortableItems(items, orderedKeys);
1553
+ if (orderedItems.length !== items.length) return;
1554
+
1555
+ runWithInternalMutation(state, () => {
1556
+ items.forEach((item) => {
1557
+ if (!state.originalSortStyles.has(item)) {
1558
+ state.originalSortStyles.set(item, {
1559
+ transform: item.style.transform,
1560
+ transition: item.style.transition,
1561
+ zIndex: item.style.zIndex,
1562
+ });
1563
+ }
1564
+ const snapshot = state.originalSortStyles.get(item);
1565
+ if (!snapshot) return;
1566
+ item.style.transform = snapshot.transform;
1567
+ item.style.transition = snapshot.transition;
1568
+ item.style.zIndex = snapshot.zIndex;
1569
+ });
1570
+ });
1571
+
1572
+ const baseRects = items.map((item) => item.getBoundingClientRect());
1573
+ const targetRectByItem = new Map<HTMLElement, DOMRect>();
1574
+ orderedItems.forEach((item, index) => {
1575
+ const targetRect = baseRects[index];
1576
+ if (targetRect) {
1577
+ targetRectByItem.set(item, targetRect);
1578
+ }
1579
+ });
1580
+
1581
+ // Preview sort order with transforms so React-managed DOM order stays untouched.
1582
+ runWithInternalMutation(state, () => {
1583
+ items.forEach((item, index) => {
1584
+ const snapshot = state.originalSortStyles.get(item);
1585
+ const currentRect = baseRects[index];
1586
+ const targetRect = targetRectByItem.get(item);
1587
+ if (!snapshot || !currentRect || !targetRect) return;
1588
+
1589
+ const dx = targetRect.left - currentRect.left;
1590
+ const dy = targetRect.top - currentRect.top;
1591
+ const baseTransform = snapshot.transform.trim();
1592
+ const translate = Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5
1593
+ ? `translate(${dx}px, ${dy}px)`
1594
+ : "";
1595
+ item.style.transform = [baseTransform, translate].filter(Boolean).join(" ");
1596
+ item.style.transition = snapshot.transition;
1597
+ item.style.zIndex = translate ? "1" : snapshot.zIndex;
1598
+ });
1599
+ });
1600
+ }
1601
+
1602
+ function applySortOverrides(state: RuntimeState, entry: PreviewOverridePageEntry | null, pendingSort: RuntimeState["pendingSort"]) {
1603
+ const sortRecords = new Map<string, readonly string[]>();
1604
+
1605
+ for (const [groupKey, orderedKeys] of Object.entries(entry?.sorts ?? {})) {
1606
+ sortRecords.set(groupKey, orderedKeys);
1607
+ }
1608
+
1609
+ if (pendingSort) {
1610
+ sortRecords.set(pendingSort.groupKey, pendingSort.orderedSortKeys);
1611
+ }
1612
+
1613
+ sortRecords.forEach((orderedKeys, groupKey) => {
1614
+ const items = findSortableItems(groupKey);
1615
+ applySortPreview(state, items, orderedKeys);
1616
+ });
1617
+ }
1618
+
1619
+ function reapplyVisualState(state: RuntimeState) {
1620
+ hydrateRuntimeSemantics();
1621
+ resetDraftDom(state);
1622
+
1623
+ const styleEl = ensureDraftStyleEl(state);
1624
+ styleEl.textContent = buildStyleRules(state.persistedOverrides);
1625
+
1626
+ applyDraftStyleOverrides(state, state.draftPayloads);
1627
+ applyTextOverrides(state, state.persistedOverrides, state.draftPayloads);
1628
+ applyCurrentDraftToSelectedElement(state);
1629
+ applySortOverrides(state, state.persistedOverrides, state.pendingSort);
1630
+
1631
+ const hoverTarget = state.hoveredElement && state.hoveredElement !== state.selectedElement
1632
+ ? state.hoveredElement
1633
+ : null;
1634
+ if (state.selectedElement) {
1635
+ renderOverlayLayer(state, state.selectedElement, getPeerElements(state.selectedElement), "selected");
1636
+ } else {
1637
+ hideOverlayBox(state.selectedBoxEl);
1638
+ hideOverlayLabel(state.selectedLabelEl);
1639
+ clearSiblingBoxes(state.selectedSiblingBoxEls);
1640
+ }
1641
+
1642
+ if (hoverTarget) {
1643
+ renderOverlayLayer(state, hoverTarget, getPeerElements(hoverTarget), "hover");
1644
+ } else {
1645
+ hideOverlayBox(state.hoverBoxEl);
1646
+ hideOverlayLabel(state.hoverLabelEl);
1647
+ clearSiblingBoxes(state.hoverSiblingBoxEls);
1648
+ }
1649
+ }
1650
+
1651
+ function isNodeSelectableElement(element: HTMLElement): boolean {
1652
+ return element.hasAttribute("data-imagicma-node-id");
1653
+ }
1654
+
1655
+ function isOverlayElement(state: RuntimeState, element: HTMLElement): boolean {
1656
+ return Boolean(state.overlayRoot && state.overlayRoot.contains(element));
1657
+ }
1658
+
1659
+ function findElementByNodeKey(nodeKey: string): HTMLElement | null {
1660
+ return document.querySelector<HTMLElement>(
1661
+ `[data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`,
1662
+ );
1663
+ }
1664
+
1665
+ function buildSemanticPath(target: HTMLElement, event: MouseEvent): HTMLElement[] {
1666
+ const fromPath = typeof event.composedPath === "function"
1667
+ ? event.composedPath().filter((item): item is HTMLElement => item instanceof HTMLElement)
1668
+ : [];
1669
+
1670
+ if (fromPath.length > 0) {
1671
+ return fromPath;
1672
+ }
1673
+
1674
+ const path: HTMLElement[] = [];
1675
+ let current: HTMLElement | null = target;
1676
+ while (current) {
1677
+ path.push(current);
1678
+ current = current.parentElement;
1679
+ }
1680
+ return path;
1681
+ }
1682
+
1683
+ function findSelectableElement(state: RuntimeState, event: MouseEvent): HTMLElement | null {
1684
+ const hit = document.elementFromPoint(event.clientX, event.clientY);
1685
+ if (!(hit instanceof HTMLElement) || isOverlayElement(state, hit)) {
1686
+ return null;
1687
+ }
1688
+
1689
+ return buildSemanticPath(hit, event).find((candidate) => !isOverlayElement(state, candidate) && isNodeSelectableElement(candidate)) ?? null;
1690
+ }
1691
+
1692
+ function postToParent(state: RuntimeState, message: Record<string, unknown>) {
1693
+ const parentOrigin = state.parentOrigin || getBoundPreviewParentOrigin();
1694
+ if (!parentOrigin || window.parent === window) return;
1695
+ logPreviewPickerRuntime("post-message", {
1696
+ type: typeof message.type === "string" ? message.type : "unknown",
1697
+ sessionId: typeof message.sessionId === "string" ? message.sessionId : null,
1698
+ frameInstanceId: typeof message.frameInstanceId === "string" ? message.frameInstanceId : state.frameInstanceId,
1699
+ targetOrigin: parentOrigin,
1700
+ });
1701
+ window.parent.postMessage(message, parentOrigin);
1702
+ }
1703
+
1704
+ function createRuntimeState(): RuntimeState {
1705
+ return {
1706
+ frameInstanceId: createFrameInstanceId(),
1707
+ parentOrigin: getBoundPreviewParentOrigin(),
1708
+ activeSessionId: null,
1709
+ enabled: false,
1710
+ selectedElement: null,
1711
+ hoveredElement: null,
1712
+ overlayRoot: null,
1713
+ hoverLabelEl: null,
1714
+ selectedLabelEl: null,
1715
+ hoverBoxEl: null,
1716
+ selectedBoxEl: null,
1717
+ sortIndicatorEl: null,
1718
+ hoverSiblingBoxEls: [],
1719
+ selectedSiblingBoxEls: [],
1720
+ draftStyleEl: null,
1721
+ persistedOverrides: null,
1722
+ draftPayload: null,
1723
+ draftPayloads: [],
1724
+ pendingSort: null,
1725
+ draggingSortKey: null,
1726
+ sortPointerId: null,
1727
+ sortPointerStartX: 0,
1728
+ sortPointerStartY: 0,
1729
+ sortDragging: false,
1730
+ sortSourceElement: null,
1731
+ sortIndicatorTarget: null,
1732
+ sortIndicatorPlacement: null,
1733
+ ignoreClickUntilTs: 0,
1734
+ originalText: new Map(),
1735
+ originalDraftStyles: new Map(),
1736
+ originalSortStyles: new Map(),
1737
+ throttledRecalculate: null,
1738
+ mutationObserver: null,
1739
+ suppressMutationObserver: 0,
1740
+ };
1741
+ }
1742
+
1743
+ function sendFrameHello(state: RuntimeState, reason: string) {
1744
+ if (window.parent === window) return;
1745
+ const targetOrigin = state.parentOrigin || getBoundPreviewParentOrigin() || "*";
1746
+ window.parent.postMessage({
1747
+ channel: PREVIEW_PICKER_CHANNEL,
1748
+ version: PREVIEW_PICKER_VERSION,
1749
+ type: "IMAGICMA_PICKER_FRAME_HELLO",
1750
+ frameInstanceId: state.frameInstanceId,
1751
+ sessionId: state.activeSessionId,
1752
+ payload: {
1753
+ pageUrl: window.location.href,
1754
+ protocolVersion: PREVIEW_PICKER_VERSION,
1755
+ reason,
1756
+ },
1757
+ }, targetOrigin);
1758
+ }
1759
+
1760
+ export function installPreviewPickerRuntime() {
1761
+ if (typeof window === "undefined" || window.parent === window) return;
1762
+ if (window.__IMAGICMA_PREVIEW_PICKER__) return;
1763
+ window.__IMAGICMA_PREVIEW_PICKER__ = true;
1764
+
1765
+ const state = createRuntimeState();
1766
+ logPreviewPickerRuntime("install", {
1767
+ pageUrl: window.location.href,
1768
+ parentOrigin: state.parentOrigin,
1769
+ frameInstanceId: state.frameInstanceId,
1770
+ });
1771
+ hydrateRuntimeSemantics();
1772
+ state.throttledRecalculate = throttleRAF(() => {
1773
+ reapplyVisualState(state);
1774
+ });
1775
+
1776
+ const handlePointerMove = (event: MouseEvent) => {
1777
+ if (!state.enabled) return;
1778
+ if (state.sortPointerId !== null) return;
1779
+ const element = findSelectableElement(state, event);
1780
+ const nextHoveredElement = element && element !== state.selectedElement ? element : null;
1781
+ if (nextHoveredElement === state.hoveredElement) return;
1782
+ state.hoveredElement = nextHoveredElement;
1783
+ reapplyVisualState(state);
1784
+ };
1785
+
1786
+ const handleClick = (event: MouseEvent) => {
1787
+ if (!state.enabled) return;
1788
+ if (Date.now() < state.ignoreClickUntilTs) return;
1789
+ const element = findSelectableElement(state, event);
1790
+ if (!element) return;
1791
+
1792
+ event.preventDefault();
1793
+ event.stopPropagation();
1794
+ if (typeof event.stopImmediatePropagation === "function") {
1795
+ event.stopImmediatePropagation();
1796
+ }
1797
+
1798
+ state.selectedElement = element;
1799
+ state.hoveredElement = null;
1800
+ reapplyVisualState(state);
1801
+ const sortableMetadata = getSortableMetadata(element);
1802
+ applySortableAffordance(state, Boolean(sortableMetadata?.sortable), sortableMetadata?.sortableParentId ?? null);
1803
+ logPreviewPickerRuntime("select-element", {
1804
+ nodeId: trimText(element.getAttribute("data-imagicma-node-id")) || null,
1805
+ selector: buildSelectorForElement(element),
1806
+ sessionId: state.activeSessionId,
1807
+ frameInstanceId: state.frameInstanceId,
1808
+ });
1809
+ postToParent(state, {
1810
+ channel: PREVIEW_PICKER_CHANNEL,
1811
+ version: PREVIEW_PICKER_VERSION,
1812
+ type: "IMAGICMA_PICKER_SELECTED",
1813
+ frameInstanceId: state.frameInstanceId,
1814
+ sessionId: state.activeSessionId,
1815
+ payload: getSelectionPayload(element),
1816
+ });
1817
+ };
1818
+
1819
+ window.addEventListener("mousemove", handlePointerMove, true);
1820
+ window.addEventListener("click", handleClick, true);
1821
+ window.addEventListener("scroll", () => state.throttledRecalculate?.(), true);
1822
+ window.addEventListener("resize", () => state.throttledRecalculate?.());
1823
+ state.mutationObserver = new MutationObserver(() => {
1824
+ if (state.suppressMutationObserver > 0) return;
1825
+ hydrateRuntimeSemantics();
1826
+ state.throttledRecalculate?.();
1827
+ });
1828
+ state.mutationObserver.observe(document.body, {
1829
+ subtree: true,
1830
+ childList: true,
1831
+ characterData: true,
1832
+ attributes: true,
1833
+ attributeFilter: ["style", "class"],
1834
+ });
1835
+ window.addEventListener("pointerdown", (event) => {
1836
+ if (!state.enabled || event.button !== 0) return;
1837
+ const target = event.target instanceof HTMLElement ? event.target.closest<HTMLElement>("[data-imagicma-node-id]") : null;
1838
+ if (!target || !state.selectedElement || target !== state.selectedElement) return;
1839
+ const sortable = getSortableMetadata(target);
1840
+ if (!sortable) return;
1841
+
1842
+ event.preventDefault();
1843
+ event.stopPropagation();
1844
+
1845
+ state.sortPointerId = event.pointerId;
1846
+ state.sortPointerStartX = event.clientX;
1847
+ state.sortPointerStartY = event.clientY;
1848
+ state.sortDragging = false;
1849
+ state.sortSourceElement = target;
1850
+ state.draggingSortKey = sortable.itemKey;
1851
+ state.sortIndicatorTarget = null;
1852
+ state.sortIndicatorPlacement = null;
1853
+ }, true);
1854
+
1855
+ window.addEventListener("pointermove", (event) => {
1856
+ if (!state.enabled || state.sortPointerId !== event.pointerId || !state.sortSourceElement || !state.draggingSortKey) return;
1857
+
1858
+ const distance = Math.hypot(event.clientX - state.sortPointerStartX, event.clientY - state.sortPointerStartY);
1859
+ if (!state.sortDragging && distance < SORT_DRAG_THRESHOLD_PX) return;
1860
+
1861
+ if (!state.sortDragging) {
1862
+ state.sortDragging = true;
1863
+ runWithInternalMutation(state, () => {
1864
+ state.sortSourceElement!.style.opacity = "0.5";
1865
+ });
1866
+ }
1867
+
1868
+ const hit = document.elementFromPoint(event.clientX, event.clientY);
1869
+ const target = hit instanceof HTMLElement ? hit.closest<HTMLElement>("[data-imagicma-node-id]") : null;
1870
+ if (!target) {
1871
+ syncSortIndicator(state, null, null);
1872
+ return;
1873
+ }
1874
+
1875
+ const sortable = getSortableMetadata(target);
1876
+ const sourceSortable = getSortableMetadata(state.sortSourceElement);
1877
+ if (!sortable || !sourceSortable || sortable.sortableParentId !== sourceSortable.sortableParentId || target === state.sortSourceElement) {
1878
+ syncSortIndicator(state, null, null);
1879
+ return;
1880
+ }
1881
+
1882
+ event.preventDefault();
1883
+ const rect = target.getBoundingClientRect();
1884
+ const axis = getSortableAxis(sortable.itemElements);
1885
+ const placement = axis === "horizontal"
1886
+ ? event.clientX > rect.left + rect.width / 2 ? "after" : "before"
1887
+ : event.clientY > rect.top + rect.height / 2 ? "after" : "before";
1888
+ syncSortIndicator(state, target, placement, sortable.itemElements);
1889
+ }, true);
1890
+
1891
+ window.addEventListener("pointerup", (event) => {
1892
+ if (state.sortPointerId !== event.pointerId) return;
1893
+ if (state.sortDragging && state.sortIndicatorTarget && state.sortIndicatorPlacement) {
1894
+ commitSort(state, state.sortIndicatorTarget, state.sortIndicatorPlacement);
1895
+ state.ignoreClickUntilTs = Date.now() + 120;
1896
+ }
1897
+ clearSortGesture(state);
1898
+ reapplyVisualState(state);
1899
+ }, true);
1900
+
1901
+ window.addEventListener("pointercancel", (event) => {
1902
+ if (state.sortPointerId !== event.pointerId) return;
1903
+ clearSortGesture(state);
1904
+ reapplyVisualState(state);
1905
+ }, true);
1906
+
1907
+ const originalPushState = window.history.pushState.bind(window.history);
1908
+ const originalReplaceState = window.history.replaceState.bind(window.history);
1909
+ window.history.pushState = (...args) => {
1910
+ originalPushState(...args);
1911
+ window.queueMicrotask(() => sendFrameHello(state, "pushState"));
1912
+ };
1913
+ window.history.replaceState = (...args) => {
1914
+ originalReplaceState(...args);
1915
+ window.queueMicrotask(() => sendFrameHello(state, "replaceState"));
1916
+ };
1917
+ window.addEventListener("popstate", () => {
1918
+ sendFrameHello(state, "popstate");
1919
+ });
1920
+ window.addEventListener("load", () => {
1921
+ sendFrameHello(state, "load");
1922
+ });
1923
+
1924
+ window.addEventListener("message", (event) => {
1925
+ console.log('on message', event);
1926
+ if (event.source !== window.parent) return;
1927
+ if (!isAllowedPreviewParentOrigin(event.origin)) return;
1928
+ if (!isPickerMessage(event.data)) return;
1929
+ if (state.parentOrigin && event.origin !== state.parentOrigin) return;
1930
+ logPreviewPickerRuntime("receive-message", {
1931
+ type: event.data.type,
1932
+ sessionId: "sessionId" in event.data ? event.data.sessionId : null,
1933
+ frameInstanceId: event.data.frameInstanceId,
1934
+ enabled: state.enabled,
1935
+ activeSessionId: state.activeSessionId,
1936
+ pageUrl: window.location.href,
1937
+ });
1938
+
1939
+ state.parentOrigin = bindPreviewParentOrigin(event.origin);
1940
+ if (event.data.frameInstanceId !== state.frameInstanceId) return;
1941
+
1942
+ if (event.data.type === "IMAGICMA_PICKER_STATE_SYNC") {
1943
+ hydrateRuntimeSemantics();
1944
+ state.activeSessionId = event.data.sessionId;
1945
+ state.enabled = event.data.payload.mode === "picking";
1946
+ state.persistedOverrides = event.data.payload.overrides;
1947
+ state.draftPayload = event.data.payload.draft;
1948
+ state.draftPayloads = event.data.payload.drafts ?? (event.data.payload.draft ? [event.data.payload.draft] : []);
1949
+ state.pendingSort = event.data.payload.pendingSort
1950
+ ? {
1951
+ groupKey: event.data.payload.pendingSort.sortableParentId,
1952
+ orderedSortKeys: event.data.payload.pendingSort.orderedSortKeys,
1953
+ }
1954
+ : null;
1955
+ state.selectedElement = event.data.payload.selectedNodeId
1956
+ ? findElementByNodeKey(event.data.payload.selectedNodeId)
1957
+ : null;
1958
+ state.hoveredElement = null;
1959
+ clearOverlay(state);
1960
+ const sortableMetadata = state.selectedElement ? getSortableMetadata(state.selectedElement) : null;
1961
+ applySortableAffordance(state, Boolean(sortableMetadata?.sortable), sortableMetadata?.sortableParentId ?? null);
1962
+ logPreviewPickerRuntime("state-sync", {
1963
+ sessionId: state.activeSessionId,
1964
+ mode: event.data.payload.mode,
1965
+ selectedNodeId: event.data.payload.selectedNodeId,
1966
+ hasDraft: Boolean(event.data.payload.draft),
1967
+ hasPendingSort: Boolean(event.data.payload.pendingSort),
1968
+ pageKey: event.data.payload.pageKey,
1969
+ });
1970
+ reapplyVisualState(state);
1971
+ return;
1972
+ }
1973
+
1974
+ if (event.data.type === "IMAGICMA_PICKER_STOP") {
1975
+ state.enabled = false;
1976
+ state.activeSessionId = null;
1977
+ state.selectedElement = null;
1978
+ state.hoveredElement = null;
1979
+ state.draftPayload = null;
1980
+ state.draftPayloads = [];
1981
+ state.pendingSort = null;
1982
+ clearOverlay(state);
1983
+ applySortableAffordance(state, false);
1984
+ logPreviewPickerRuntime("stop-session", {
1985
+ sessionId: event.data.sessionId,
1986
+ frameInstanceId: event.data.frameInstanceId,
1987
+ });
1988
+ reapplyVisualState(state);
1989
+ return;
1990
+ }
1991
+ });
1992
+
1993
+ sendFrameHello(state, "install");
1994
+ }