@jxsuite/studio 0.33.0 → 0.34.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.
Files changed (85) hide show
  1. package/dist/iframe-entry.js +6230 -0
  2. package/dist/iframe-entry.js.map +35 -0
  3. package/dist/studio.js +10754 -11060
  4. package/dist/studio.js.map +83 -72
  5. package/package.json +7 -7
  6. package/src/browse/browse.ts +11 -4
  7. package/src/canvas/canvas-helpers.ts +2 -56
  8. package/src/canvas/canvas-live-render.ts +102 -435
  9. package/src/canvas/canvas-origin.ts +66 -0
  10. package/src/canvas/canvas-patcher.ts +63 -403
  11. package/src/canvas/canvas-render.ts +70 -212
  12. package/src/canvas/canvas-utils.ts +37 -65
  13. package/src/canvas/iframe-channel.ts +154 -0
  14. package/src/canvas/iframe-drop.ts +484 -0
  15. package/src/canvas/iframe-entry.ts +600 -0
  16. package/src/canvas/iframe-host.ts +1373 -0
  17. package/src/canvas/iframe-inline-edit.ts +367 -0
  18. package/src/canvas/iframe-insert.ts +164 -0
  19. package/src/canvas/iframe-interaction.ts +176 -0
  20. package/src/canvas/iframe-keys.ts +85 -0
  21. package/src/canvas/iframe-overlay.ts +218 -0
  22. package/src/canvas/iframe-patch.ts +363 -0
  23. package/src/canvas/iframe-protocol.ts +361 -0
  24. package/src/canvas/iframe-render.ts +458 -0
  25. package/src/canvas/iframe-slash.ts +114 -0
  26. package/src/canvas/iframe-subtree.ts +113 -0
  27. package/src/canvas/path-mapping.ts +86 -0
  28. package/src/canvas/serialize-scope.ts +65 -0
  29. package/src/editor/canvas-context-menu.ts +40 -0
  30. package/src/editor/canvas-slash-bridge.ts +21 -0
  31. package/src/editor/context-menu.ts +2 -1
  32. package/src/editor/inline-edit-apply.ts +183 -0
  33. package/src/editor/inline-edit.ts +99 -21
  34. package/src/editor/inline-link.ts +89 -0
  35. package/src/editor/insert-zone-action.ts +35 -0
  36. package/src/editor/merge-tags.ts +26 -2
  37. package/src/editor/repeater-scope.ts +144 -0
  38. package/src/editor/shortcuts.ts +14 -28
  39. package/src/editor/slash-menu.ts +73 -42
  40. package/src/files/files.ts +2 -1
  41. package/src/page-params.ts +383 -0
  42. package/src/panels/ai-panel.ts +5 -7
  43. package/src/panels/block-action-bar.ts +296 -138
  44. package/src/panels/canvas-dnd-bridge.ts +397 -0
  45. package/src/panels/component-preview.ts +56 -0
  46. package/src/panels/dnd.ts +41 -17
  47. package/src/panels/drag-ghost.ts +62 -0
  48. package/src/panels/editors.ts +1 -1
  49. package/src/panels/overlays.ts +10 -125
  50. package/src/panels/properties-panel.ts +210 -0
  51. package/src/panels/right-panel.ts +0 -2
  52. package/src/panels/signals-panel.ts +136 -22
  53. package/src/panels/stylebook-doc.ts +373 -0
  54. package/src/panels/stylebook-panel.ts +46 -689
  55. package/src/panels/tab-bar.ts +159 -13
  56. package/src/panels/toolbar.ts +3 -2
  57. package/src/platforms/devserver.ts +15 -0
  58. package/src/services/monaco-setup.ts +12 -0
  59. package/src/services/render-critic.ts +9 -9
  60. package/src/settings/css-vars-editor.ts +2 -2
  61. package/src/store.ts +4 -62
  62. package/src/studio.ts +90 -40
  63. package/src/tabs/doc-op-apply.ts +89 -0
  64. package/src/tabs/patch-ops.ts +6 -2
  65. package/src/tabs/tab.ts +23 -4
  66. package/src/tabs/transact.ts +2 -74
  67. package/src/types.ts +14 -18
  68. package/src/ui/jx-theme.ts +63 -0
  69. package/src/ui/media-picker.ts +6 -4
  70. package/src/ui/spectrum.ts +5 -0
  71. package/src/utils/canvas-media.ts +0 -137
  72. package/src/utils/edit-display.ts +23 -3
  73. package/src/utils/geometry.ts +43 -0
  74. package/src/utils/link-target.ts +93 -0
  75. package/src/utils/strip-events.ts +54 -0
  76. package/src/view.ts +0 -23
  77. package/src/workspace/workspace.ts +14 -1
  78. package/src/canvas/canvas-subtree-render.ts +0 -113
  79. package/src/editor/component-inline-edit.ts +0 -349
  80. package/src/editor/content-inline-edit.ts +0 -207
  81. package/src/editor/insertion-helper.ts +0 -308
  82. package/src/panels/canvas-dnd.ts +0 -329
  83. package/src/panels/panel-events.ts +0 -306
  84. package/src/panels/preview-render.ts +0 -132
  85. package/src/panels/pseudo-preview.ts +0 -75
@@ -0,0 +1,397 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Cross-frame canvas DnD coordinator (Phase 4c spike). The canvas is an iframe, and pragmatic-dnd
4
+ * is per-realm, so the parent keeps ONE `monitorForElements` for its drag SOURCES and drives the
5
+ * cross-frame leg as a pointer/message stream: it resolves the target iframe host by hit-testing
6
+ * the pointer against each live host's rect, posts `dragStart`/`dragMove`/`drop`, and the iframe
7
+ * replies with `dragOver`/`dropResult` (handled in iframe-host). The applied mutation runs through
8
+ * the realm-agnostic `applyDropInstruction` with PARENT-retained source data (never crossing the
9
+ * wire).
10
+ *
11
+ * Sources: palette/element/component cards ({type:'block'}, flow 1) and layer rows + the
12
+ * block-action-bar ⠿ handle ({type:'tree-node', path}, flows 2 & 4). Flow 3 (grab-anywhere) enters
13
+ * the SAME coordinator via the iframe's `dragOriginate` ({@link startIframeOriginatedDrag}). The
14
+ * iframe's drop math rejects a tree-node dropped onto its own ancestor/self (canDrop), so no
15
+ * coordinator-side guard is needed.
16
+ *
17
+ * The coordinator is PARENT-authoritative for boundary + host selection + cancel (D-4): each move
18
+ * re-resolves the host by cursor hit-test ({@link liveDragHostAt}) and migrates the session across
19
+ * panels (dragEnd old + dragStart new); a cancel ({@link isCancelDrop}) tears down without
20
+ * applying; a drop with no reply within {@link DROP_RESULT_TIMEOUT} clears the affordances. The
21
+ * drag ghost (D-3) follows the raw pointer 1:1; the drop indicator is drawn host-side from the
22
+ * iframe's `dragOver` preview.
23
+ *
24
+ * The coordinate math is factored into the PURE {@link buildDragMessages} (injected scale + iframe
25
+ * rect) so the cursor conversion is unit-tested without a live iframe or transform.
26
+ */
27
+
28
+ import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
29
+ import {
30
+ adoptDragSession,
31
+ beginDragSession,
32
+ clearDropIndicator,
33
+ currentDragSession,
34
+ endDragSession,
35
+ hostDragGeometry,
36
+ liveDragHostAt,
37
+ postDragMessage,
38
+ sawIframeDragOver,
39
+ setIframeOriginateHandler,
40
+ setNativeDragEnterHandler,
41
+ } from "../canvas/iframe-host";
42
+ import { parentCursorToIframe } from "../canvas/iframe-overlay";
43
+ import { clearDragGhost, moveDragGhost, setDragGhost } from "./drag-ghost";
44
+ import { getNodeAtPath } from "../store";
45
+ import { activeTab } from "../workspace/workspace";
46
+ import type { DragHost } from "../canvas/iframe-host";
47
+ import type { DragSrcKind, ParentToIframe } from "../canvas/iframe-protocol";
48
+ import type { JxMutableNode } from "@jxsuite/schema/types";
49
+
50
+ /** A parent-viewport pointer position (pragmatic's `location.current.input`). */
51
+ interface Cursor {
52
+ x: number;
53
+ y: number;
54
+ }
55
+
56
+ interface MonitorDragArgs {
57
+ source: { data: Record<string, unknown> };
58
+ location: {
59
+ current: { input: { clientX: number; clientY: number } };
60
+ // Pragmatic resets `current` back to `initial` when a drag is CANCELLED (Escape / dropped on no
61
+ // Valid target). The canvas isn't a parent pragmatic drop target, so we detect cancel by that
62
+ // Snap-back rather than an explicit flag (single-sourced through pragmatic, per the spec).
63
+ initial?: { input: { clientX: number; clientY: number } };
64
+ };
65
+ }
66
+
67
+ /**
68
+ * PURE: build the `dragMove` + `drop` parent→iframe messages for a cursor, given the session id and
69
+ * the target iframe's EMPIRICAL `scale` + `rect` (both injected so this is testable without a DOM).
70
+ * The cursor is converted to iframe-viewport coords via {@link parentCursorToIframe} (divide by
71
+ * scale; the iframe rect's left/top cancel the pan). Both leg messages share the one conversion so
72
+ * the move-preview and the authoritative drop never use a divergent coordinate transform.
73
+ */
74
+ export function buildDragMessages(
75
+ cursor: Cursor,
76
+ dragSeq: number,
77
+ scale: number,
78
+ iframeRect: { left: number; top: number },
79
+ ): { move: ParentToIframe; drop: ParentToIframe } {
80
+ const local = parentCursorToIframe(cursor, iframeRect, scale);
81
+ return {
82
+ drop: { cursor: local, dragSeq, kind: "drop" },
83
+ move: { cursor: local, dragSeq, kind: "dragMove" },
84
+ };
85
+ }
86
+
87
+ /** The cursor of a monitor event in parent-viewport coords. */
88
+ function cursorOf(location: MonitorDragArgs["location"]): Cursor {
89
+ return { x: location.current.input.clientX, y: location.current.input.clientY };
90
+ }
91
+
92
+ /**
93
+ * Map a pragmatic source's `data` to a realm-agnostic {@link DragSrcKind}, or null when this source
94
+ * doesn't drive the canvas coordinator. Palette/element/component cards are `{type:'block'}` (the
95
+ * fragment is retained parent-side); layer rows + the block-action-bar ⠿ handle are
96
+ * `{type:'tree-node', path}` (a move — the source's existing document path crosses as the kind, and
97
+ * the iframe's `canDrop` rejects ancestor/self targets). Anything else (e.g. a row missing a path)
98
+ * is ignored.
99
+ */
100
+ function dragSrcOf(data: Record<string, unknown>): DragSrcKind | null {
101
+ if (data.type === "block") {
102
+ return { type: "block" };
103
+ }
104
+ if (data.type === "tree-node" && Array.isArray(data.path)) {
105
+ return { path: data.path as (string | number)[], type: "tree-node" };
106
+ }
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * The ghost label for a drag (Phase 4c). A block source uses its fragment's tag (the card label); a
112
+ * tree-node source uses the tag of the node at its path in the live doc (a node chip). Falls back
113
+ * to a generic label when the tag is unknown.
114
+ */
115
+ export function ghostLabel(src: DragSrcKind, data: Record<string, unknown>): string {
116
+ if (src.type === "block") {
117
+ const fragment = data.fragment as JxMutableNode | undefined;
118
+ return fragment?.tagName ?? "block";
119
+ }
120
+ const doc = activeTab.value?.doc.document as JxMutableNode | undefined;
121
+ const node = doc ? (getNodeAtPath(doc, src.path) as JxMutableNode | undefined) : undefined;
122
+ return node?.tagName ?? "node";
123
+ }
124
+
125
+ /**
126
+ * Adopt a flow-3 (iframe-ORIGINATED + iframe-DRIVEN) drag session. A drag that begins inside the
127
+ * canvas iframe gets its held-button pointermoves in the IFRAME document (implicit pointer
128
+ * capture), so the parent would never receive them — the iframe therefore drives the whole gesture
129
+ * itself, computing the preview/drop locally and posting `dragOver`/`dropResult` directly.
130
+ *
131
+ * The parent's only job here is to ADOPT the iframe's `seq` (so those replies pass the host's seq
132
+ * gate, see {@link file://../canvas/iframe-host.ts}'s `adoptDragSession`) and SHOW the ghost; the
133
+ * host's `dragOver` handler then positions the ghost from the `cursor` the iframe posts. The parent
134
+ * attaches NO document pointer listeners — it has no pointer of its own during this drag. `path` is
135
+ * the grabbed node's document path; `seq` is the iframe's pre-allocated session id.
136
+ */
137
+ export function startIframeOriginatedDrag(
138
+ host: DragHost,
139
+ path: (string | number)[],
140
+ seq: number,
141
+ ): void {
142
+ const src: DragSrcKind = { path: [...path], type: "tree-node" };
143
+ const srcData = { path: [...path], type: "tree-node" };
144
+ // Adopt the iframe's seq + retain the (path-only) source data; do NOT post dragStart (the iframe
145
+ // Is driving) and do NOT attach parent-document listeners (the iframe owns the pointer it started).
146
+ adoptDragSession(host, src, srcData, seq);
147
+ // Show the ghost; the host's dragOver handler moves it from the iframe-posted cursor each move.
148
+ setDragGhost(ghostLabel(src, srcData), 0, 0);
149
+ }
150
+
151
+ /**
152
+ * Timeout (ms) after a `drop` post before the parent gives up waiting for a `dropResult` and clears
153
+ * the ghost/indicator (the iframe reloaded mid-drag, so no reply will come — treat as cancel).
154
+ */
155
+ const DROP_RESULT_TIMEOUT = 250;
156
+
157
+ /**
158
+ * A live coordinator session. PARENT-authoritative for boundary + host selection (D-4): `host` is
159
+ * the panel the session is currently bound to (its iframe got the latest `dragStart`); `null` while
160
+ * the pointer is off every canvas. `seq` is the session id the bound host's iframe replies with.
161
+ */
162
+ interface CoordSession {
163
+ src: DragSrcKind;
164
+ srcData: Record<string, unknown>;
165
+ /** The host whose iframe currently holds the session, or null while off-canvas. */
166
+ host: DragHost | null;
167
+ /** The session id the bound host was started with (for move/drop tagging). */
168
+ seq: number;
169
+ /** The drag ghost's label (computed once at start; the dragged thing doesn't change). */
170
+ label: string;
171
+ }
172
+
173
+ /** Begin a session bound to `host`: bump+post dragStart, show the ghost, record the seq. */
174
+ function startSession(
175
+ host: DragHost,
176
+ src: DragSrcKind,
177
+ srcData: Record<string, unknown>,
178
+ ): CoordSession {
179
+ const seq = beginDragSession(host, src, srcData);
180
+ return { host, label: ghostLabel(src, srcData), seq, src, srcData };
181
+ }
182
+
183
+ /**
184
+ * Advance a session for a new cursor (parent-authoritative boundary, D-4). Moves the ghost 1:1,
185
+ * then resolves the host under the cursor and reconciles:
186
+ *
187
+ * - Same host → post dragMove.
188
+ * - Different host (cross-panel) → MIGRATE: dragEnd the old, dragStart the new (a fresh seq), so the
189
+ * new panel's iframe drives the rest.
190
+ * - No host (inside→outside) → dragEnd the old + unbind; the ghost keeps following off-canvas.
191
+ */
192
+ function sessionDrag(session: CoordSession, cursor: Cursor): void {
193
+ moveDragGhost(cursor.x, cursor.y);
194
+ const target = liveDragHostAt(cursor);
195
+ if (target === session.host) {
196
+ if (target) {
197
+ const { rect, scale } = hostDragGeometry(target);
198
+ postDragMessage(target, buildDragMessages(cursor, session.seq, scale, rect).move);
199
+ }
200
+ return;
201
+ }
202
+ // The bound host changed (migrate) or the pointer left every canvas (unbind).
203
+ if (session.host) {
204
+ postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
205
+ }
206
+ if (!target) {
207
+ session.host = null;
208
+ return;
209
+ }
210
+ // Migrate the session to the new panel: a fresh dragStart (new seq) the new iframe replies with.
211
+ session.host = target;
212
+ session.seq = beginDragSession(target, session.src, session.srcData);
213
+ const { rect, scale } = hostDragGeometry(target);
214
+ postDragMessage(target, buildDragMessages(cursor, session.seq, scale, rect).move);
215
+ }
216
+
217
+ /**
218
+ * Finish a session at the drop cursor. Posts `drop` to the host under the cursor (the pointer's
219
+ * panel owns the drop, not the active panel); the iframe computes the drop FRESH and posts
220
+ * `dropResult`, which iframe-host applies + clears the ghost/indicator. A timeout fallback clears
221
+ * the affordances if no reply arrives (iframe reloaded mid-drag). Drops off-canvas release retained
222
+ * data.
223
+ */
224
+ function sessionDrop(session: CoordSession, cursor: Cursor): void {
225
+ const target = liveDragHostAt(cursor);
226
+ // Migrate first if the drop landed on a different panel than the last move bound.
227
+ if (target && target !== session.host) {
228
+ if (session.host) {
229
+ postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
230
+ }
231
+ session.host = target;
232
+ session.seq = beginDragSession(target, session.src, session.srcData);
233
+ }
234
+ if (!target) {
235
+ if (session.host) {
236
+ postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
237
+ }
238
+ endDragSession(session.seq);
239
+ clearDragGhost();
240
+ return;
241
+ }
242
+ const { rect, scale } = hostDragGeometry(target);
243
+ postDragMessage(target, buildDragMessages(cursor, session.seq, scale, rect).drop);
244
+ // The iframe-host's dropResult handler clears the ghost/indicator on a reply; guard against a
245
+ // Reload that never replies by clearing after a short timeout (treated as a cancel).
246
+ const { seq } = session;
247
+ setTimeout(() => {
248
+ if (currentDragSession() === seq) {
249
+ clearDragGhost();
250
+ clearDropIndicator(target);
251
+ }
252
+ }, DROP_RESULT_TIMEOUT);
253
+ }
254
+
255
+ /**
256
+ * Tear down a session WITHOUT applying a drop (Escape/abort): dragEnd the bound host so it clears
257
+ * its indicator + stops auto-scroll, release the retained source data, hide the ghost.
258
+ */
259
+ function sessionCancel(session: CoordSession): void {
260
+ if (session.host) {
261
+ postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
262
+ clearDropIndicator(session.host);
263
+ }
264
+ endDragSession(session.seq);
265
+ clearDragGhost();
266
+ }
267
+
268
+ /**
269
+ * Whether a pragmatic `onDrop` represents a CANCEL: pragmatic snaps `current` back to `initial` on
270
+ * Escape/abort, so equal input coords (with an `initial` present) signal a cancel rather than a
271
+ * real release. PURE.
272
+ */
273
+ export function isCancelDrop(location: MonitorDragArgs["location"]): boolean {
274
+ const { initial } = location;
275
+ if (!initial) {
276
+ return false;
277
+ }
278
+ return (
279
+ initial.input.clientX === location.current.input.clientX &&
280
+ initial.input.clientY === location.current.input.clientY
281
+ );
282
+ }
283
+
284
+ /**
285
+ * Register the single coordinator monitor. Returns a cleanup. Wired once from studio init; the
286
+ * monitor stays for the app's lifetime (pragmatic monitors are global, not per-render).
287
+ */
288
+ export function registerCanvasDndBridge(): () => void {
289
+ // The active parent-source (pragmatic) session, or null between drags / before it binds a host.
290
+ let session: CoordSession | null = null;
291
+ // The in-flight pragmatic source (set in onDragStart, cleared in onDrop) so a nativeDragEnter can
292
+ // Bind a session for a drag whose cursor entered an iframe before the parent ever bound a host.
293
+ let pending: { src: DragSrcKind; srcData: Record<string, unknown> } | null = null;
294
+
295
+ // Install the flow-3 handler so the iframe-host's `dragOriginate` case enters this coordinator.
296
+ // The iframe drives the gesture; the parent only adopts the seq + shows the ghost (no listeners).
297
+ setIframeOriginateHandler((host, path, seq) => startIframeOriginatedDrag(host, path, seq));
298
+
299
+ // Bind/migrate the live pragmatic session when its NATIVE stream enters an unclaimed iframe:
300
+ // Chromium delivers dragover to the frame under the cursor, so a drag from the palette/layers
301
+ // Crosses onto the canvas without this monitor ever seeing a cursor inside the iframe rect — the
302
+ // Iframe announces the crossing instead (the `nativeDragEnter` message).
303
+ setNativeDragEnterHandler((host) => {
304
+ if (!pending || session?.host === host) {
305
+ // No parent drag in flight (e.g. an OS file drag), or already bound here — nothing to do.
306
+ return;
307
+ }
308
+ if (session) {
309
+ // Migrate: the session was bound elsewhere (or unbound after leaving a canvas).
310
+ if (session.host) {
311
+ postDragMessage(session.host, { dragSeq: session.seq, kind: "dragEnd" });
312
+ }
313
+ session.host = host;
314
+ session.seq = beginDragSession(host, session.src, session.srcData);
315
+ return;
316
+ }
317
+ // First canvas contact for this drag — the ghost is already up (set in onDragStart).
318
+ session = startSession(host, pending.src, pending.srcData);
319
+ });
320
+
321
+ return monitorForElements({
322
+ onDrag({ source, location }: MonitorDragArgs) {
323
+ const src = dragSrcOf(source.data);
324
+ if (!src) {
325
+ return;
326
+ }
327
+ const cursor = cursorOf(location);
328
+ if (session) {
329
+ sessionDrag(session, cursor);
330
+ return;
331
+ }
332
+ // The drag started off-canvas (no host bound yet); bind lazily when the pointer enters one.
333
+ const host = liveDragHostAt(cursor);
334
+ if (host) {
335
+ session = startSession(host, src, source.data);
336
+ setDragGhost(session.label, cursor.x, cursor.y);
337
+ sessionDrag(session, cursor);
338
+ }
339
+ },
340
+ onDragStart({ source, location }: MonitorDragArgs) {
341
+ const src = dragSrcOf(source.data);
342
+ if (!src) {
343
+ return;
344
+ }
345
+ // Retain the source for the drag's lifetime so a nativeDragEnter can bind late (see above).
346
+ pending = { src, srcData: source.data };
347
+ const cursor = cursorOf(location);
348
+ const host = liveDragHostAt(cursor);
349
+ // Bind the session to the host under the cursor (if any). The ghost shows immediately and
350
+ // Follows the pointer even before it enters a canvas (lazy bind happens in onDrag otherwise).
351
+ session = host ? startSession(host, src, source.data) : null;
352
+ setDragGhost(ghostLabel(src, source.data), cursor.x, cursor.y);
353
+ },
354
+ onDrop({ source, location }: MonitorDragArgs) {
355
+ const src = dragSrcOf(source.data);
356
+ if (!src) {
357
+ return;
358
+ }
359
+ pending = null;
360
+ const cursor = location.current.input ? cursorOf(location) : { x: 0, y: 0 };
361
+ if (!session) {
362
+ // Never bound a host (dropped off every canvas) — just hide the ghost; nothing to apply.
363
+ clearDragGhost();
364
+ return;
365
+ }
366
+ // While the cursor is over the canvas, the native over/drop stream goes to the IFRAME — this
367
+ // Monitor sees nothing, so pragmatic's location is stale: its cancel heuristic false-positives
368
+ // (current never advanced past initial) and its coords are frame-local. When the iframe
369
+ // Recently drove the session, its native-drop dropResult is the authoritative outcome and is
370
+ // In flight: keep the retained srcData for it and only schedule the no-reply fallback teardown
371
+ // (which also covers an Escape-cancel over the iframe — no dropResult ever arrives).
372
+ if (sawIframeDragOver(session.seq)) {
373
+ const { host, seq } = session;
374
+ session = null;
375
+ setTimeout(() => {
376
+ if (currentDragSession() !== seq) {
377
+ return;
378
+ }
379
+ if (host) {
380
+ postDragMessage(host, { dragSeq: seq, kind: "dragEnd" });
381
+ clearDropIndicator(host);
382
+ }
383
+ endDragSession(seq);
384
+ clearDragGhost();
385
+ }, DROP_RESULT_TIMEOUT);
386
+ return;
387
+ }
388
+ // A cancel (Escape/abort) snaps back to the initial cursor — tear down without applying.
389
+ if (isCancelDrop(location)) {
390
+ sessionCancel(session);
391
+ } else {
392
+ sessionDrop(session, cursor);
393
+ }
394
+ session = null;
395
+ },
396
+ });
397
+ }
@@ -0,0 +1,56 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Parent-realm live component preview — registers a project component's custom element and
4
+ * instantiates it in the editor document. Used by the browse grid and the components-palette DnD
5
+ * cards. (The stylebook renders components inside the canvas iframe instead, via `$elements` refs
6
+ * in its generated specimen doc.)
7
+ */
8
+
9
+ import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
10
+ import { projectState } from "../store";
11
+ import type { ComponentEntry } from "../files/components";
12
+
13
+ /**
14
+ * Render a live component preview by registering its custom element and instantiating it.
15
+ *
16
+ * @param {ComponentEntry} comp
17
+ * @returns {Promise<HTMLElement>}
18
+ */
19
+ export async function renderComponentPreview(comp: ComponentEntry) {
20
+ setSkipServerFunctions(true);
21
+ try {
22
+ if (comp.source === "npm") {
23
+ if (!customElements.get(comp.tagName)) {
24
+ return _componentFallback(comp.tagName);
25
+ }
26
+ } else {
27
+ if (comp.path && !comp.path.endsWith(".json")) {
28
+ // Format-class component sources (e.g. markdown) can't be imported as modules
29
+ return _componentFallback(comp.tagName);
30
+ }
31
+ const root = projectState?.projectRoot;
32
+ const url = `${location.origin}/${root ? `${root}/` : ""}${comp.path}`;
33
+ await defineElement(url);
34
+ }
35
+ const el = document.createElement(comp.tagName);
36
+ for (const p of comp.props || []) {
37
+ if (p.default !== undefined && p.default !== "false" && p.default !== "''") {
38
+ const val = String(p.default).replaceAll(/^'|'$/g, "");
39
+ el.setAttribute(p.name, val);
40
+ }
41
+ }
42
+ return el;
43
+ } catch (error) {
44
+ console.warn("Component preview failed:", comp.tagName, error);
45
+ return _componentFallback(comp.tagName);
46
+ }
47
+ }
48
+
49
+ /** @param {string} tagName */
50
+ function _componentFallback(tagName: string) {
51
+ const fallback = document.createElement("div");
52
+ fallback.style.cssText =
53
+ "padding:12px;border:1px dashed var(--border);border-radius:var(--radius);color:var(--fg-dim)";
54
+ fallback.textContent = `<${tagName}>`;
55
+ return fallback;
56
+ }
package/src/panels/dnd.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  monitorForElements,
11
11
  } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
12
12
  import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
13
+ import { disableNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview";
13
14
  import {
14
15
  attachInstruction,
15
16
  extractInstruction,
@@ -32,9 +33,11 @@ import {
32
33
  componentRegistry,
33
34
  computeRelativePath,
34
35
  } from "../files/components";
35
- import { renderComponentPreview } from "./stylebook-panel";
36
+ import { renderComponentPreview } from "./component-preview";
36
37
  import { defaultDef, unsafeTags } from "./shared";
38
+ import { elementAtPoint } from "../utils/geometry";
37
39
  import type { JxPath } from "../state";
40
+ import type { Tab } from "../tabs/tab";
38
41
  import type { JxMutableNode } from "@jxsuite/schema/types";
39
42
  import type { ComponentEntry } from "../files/components.js";
40
43
 
@@ -43,6 +46,11 @@ interface DragCanDragArgs {
43
46
  input: { clientX: number; clientY: number };
44
47
  }
45
48
 
49
+ /** The `onGenerateDragPreview` argument subset we forward to {@link disableNativeDragPreview}. */
50
+ interface DragPreviewArgs {
51
+ nativeSetDragImage: ((image: Element, x: number, y: number) => void) | null;
52
+ }
53
+
46
54
  interface DragDropSourceArgs {
47
55
  source: { data: Record<string, unknown>; element: HTMLElement };
48
56
  }
@@ -75,7 +83,7 @@ export function registerLayersDnD() {
75
83
  const cleanup = combine(
76
84
  draggable({
77
85
  canDrag({ element: _el, input }: DragCanDragArgs) {
78
- const target = document.elementFromPoint(input.clientX, input.clientY) as HTMLElement;
86
+ const target = elementAtPoint(input.clientX, input.clientY) as HTMLElement;
79
87
  if (target?.closest(".layer-actions")) {
80
88
  return false;
81
89
  }
@@ -85,6 +93,11 @@ export function registerLayersDnD() {
85
93
  getInitialData() {
86
94
  return { path: rowPath, type: "tree-node" };
87
95
  },
96
+ onGenerateDragPreview({ nativeSetDragImage }: DragPreviewArgs) {
97
+ // Suppress the browser's native drag image — the cross-frame ghost (Phase 4c) is the
98
+ // Only drag affordance, so a duplicate native preview would double up.
99
+ disableNativeDragPreview({ nativeSetDragImage });
100
+ },
88
101
  onDragStart() {
89
102
  row.classList.add("dragging");
90
103
  view.layerDragSourceHeight = row.offsetHeight;
@@ -162,7 +175,8 @@ export function registerLayersDnD() {
162
175
  const srcRow = srcData.type === "tree-node" && source.element;
163
176
  const wasExpanded = srcRow && Object.hasOwn(srcRow.dataset, "dndExpanded");
164
177
 
165
- applyDropInstruction(instruction, srcData, targetPath);
178
+ // Parent-originated layer drops legitimately target the active tab.
179
+ applyDropInstruction(activeTab.value, instruction, srcData, targetPath);
166
180
 
167
181
  if (wasExpanded) {
168
182
  const tab = activeTab.value;
@@ -221,6 +235,9 @@ export function registerComponentsDnD() {
221
235
  getInitialData() {
222
236
  return { fragment: structuredClone(instanceDef), type: "block" };
223
237
  },
238
+ onGenerateDragPreview({ nativeSetDragImage }: DragPreviewArgs) {
239
+ disableNativeDragPreview({ nativeSetDragImage });
240
+ },
224
241
  });
225
242
  view.dndCleanups.push(cleanup);
226
243
  }
@@ -248,6 +265,9 @@ export function registerElementsDnD() {
248
265
  getInitialData() {
249
266
  return { fragment: structuredClone(def), type: "block" };
250
267
  },
268
+ onGenerateDragPreview({ nativeSetDragImage }: DragPreviewArgs) {
269
+ disableNativeDragPreview({ nativeSetDragImage });
270
+ },
251
271
  });
252
272
  view.dndCleanups.push(cleanup);
253
273
  }
@@ -337,18 +357,25 @@ export function clearLayerDropGap(container: HTMLElement) {
337
357
  }
338
358
 
339
359
  /**
340
- * Apply a DnD instruction to the state
360
+ * Apply a DnD instruction to `tab`'s document. `tab` is the tab whose canvas the drop resolved in
361
+ * (host-routed for iframe drops — never the active tab at message time, which may have changed
362
+ * while the dropResult was in flight); a null tab is a no-op.
341
363
  *
364
+ * @param {Tab | null} tab
342
365
  * @param {{ type: string }} instruction
343
366
  * @param {Record<string, unknown>} srcData
344
367
  * @param {JxPath} targetPath
345
368
  */
346
369
  export function applyDropInstruction(
370
+ tab: Tab | null,
347
371
  instruction: { type: string },
348
372
  srcData: Record<string, unknown>,
349
373
  targetPath: JxPath,
350
374
  ) {
351
- const doc = activeTab.value?.doc.document as JxMutableNode;
375
+ if (!tab) {
376
+ return;
377
+ }
378
+ const doc = tab.doc.document as JxMutableNode;
352
379
  if (srcData.type === "tree-node") {
353
380
  const fromPath = srcData.path as JxPath;
354
381
  const targetParent = parentElementPath(targetPath) as JxPath;
@@ -361,19 +388,17 @@ export function applyDropInstruction(
361
388
 
362
389
  switch (instruction.type) {
363
390
  case "reorder-above": {
364
- transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetParent, targetIdx));
391
+ transactDoc(tab, (t) => mutateMoveNode(t, fromPath, targetParent, targetIdx));
365
392
  break;
366
393
  }
367
394
  case "reorder-below": {
368
- transactDoc(activeTab.value, (t) =>
369
- mutateMoveNode(t, fromPath, targetParent, targetIdx + 1),
370
- );
395
+ transactDoc(tab, (t) => mutateMoveNode(t, fromPath, targetParent, targetIdx + 1));
371
396
  break;
372
397
  }
373
398
  case "make-child": {
374
399
  const target = getNodeAtPath(doc, targetPath);
375
400
  const len = childList(target).length;
376
- transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetPath, len));
401
+ transactDoc(tab, (t) => mutateMoveNode(t, fromPath, targetPath, len));
377
402
  break;
378
403
  }
379
404
  default: {
@@ -389,7 +414,7 @@ export function applyDropInstruction(
389
414
 
390
415
  switch (instruction.type) {
391
416
  case "reorder-above": {
392
- transactDoc(activeTab.value, (t) =>
417
+ transactDoc(tab, (t) =>
393
418
  mutateInsertNode(
394
419
  t,
395
420
  targetParent,
@@ -400,7 +425,7 @@ export function applyDropInstruction(
400
425
  break;
401
426
  }
402
427
  case "reorder-below": {
403
- transactDoc(activeTab.value, (t) =>
428
+ transactDoc(tab, (t) =>
404
429
  mutateInsertNode(
405
430
  t,
406
431
  targetParent,
@@ -413,7 +438,7 @@ export function applyDropInstruction(
413
438
  case "make-child": {
414
439
  const target = getNodeAtPath(doc, targetPath);
415
440
  const len = childList(target).length;
416
- transactDoc(activeTab.value, (t) =>
441
+ transactDoc(tab, (t) =>
417
442
  mutateInsertNode(t, targetPath, len, structuredClone(srcData.fragment as JxMutableNode)),
418
443
  );
419
444
  break;
@@ -429,8 +454,7 @@ export function applyDropInstruction(
429
454
  if (tag && tag.includes("-")) {
430
455
  const comp = componentRegistry.find((c: ComponentEntry) => c.tagName === tag);
431
456
  if (comp) {
432
- const tab = activeTab.value;
433
- const elements = tab?.doc.document?.$elements || [];
457
+ const elements = tab.doc.document?.$elements || [];
434
458
  if (comp.source === "npm") {
435
459
  const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
436
460
  if (!specifier) {
@@ -440,7 +464,7 @@ export function applyDropInstruction(
440
464
  (e: JxMutableNode | string | { $ref: string }) => e === specifier || e === comp.package,
441
465
  );
442
466
  if (!alreadyImported) {
443
- transact(activeTab.value, (d: JxMutableNode) => {
467
+ transact(tab, (d: JxMutableNode) => {
444
468
  if (!d.$elements) {
445
469
  d.$elements = [];
446
470
  }
@@ -459,7 +483,7 @@ export function applyDropInstruction(
459
483
  });
460
484
  if (!alreadyImported && comp.path) {
461
485
  const relPath = computeRelativePath(tab?.documentPath ?? null, comp.path);
462
- transact(activeTab.value, (d: JxMutableNode) => {
486
+ transact(tab, (d: JxMutableNode) => {
463
487
  if (!d.$elements) {
464
488
  d.$elements = [];
465
489
  }