@archie/devtools 0.0.12 → 0.0.15

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.
@@ -37,7 +37,7 @@ __export(client_exports, {
37
37
  module.exports = __toCommonJS(client_exports);
38
38
 
39
39
  // src/client/dnd/DndProvider.tsx
40
- var import_react18 = require("react");
40
+ var import_react19 = require("react");
41
41
 
42
42
  // node_modules/@dnd-kit/core/dist/core.esm.js
43
43
  var import_react3 = __toESM(require("react"));
@@ -4104,18 +4104,498 @@ function mergeInspectorTheme(theme) {
4104
4104
  };
4105
4105
  }
4106
4106
 
4107
+ // src/client/dnd/design-mode/structural-policy.ts
4108
+ var STRUCTURAL_COMPONENT_RE = /(?:Page|Provider|RootLayout)$/;
4109
+ var STRUCTURAL_TAGS = /* @__PURE__ */ new Set(["html", "body"]);
4110
+ function isStructuralComponentName(componentName) {
4111
+ return !!componentName && STRUCTURAL_COMPONENT_RE.test(componentName);
4112
+ }
4113
+ function isHighLevelStructuralElement(el) {
4114
+ if (!el) return false;
4115
+ if (el.hasAttribute("data-dnd-structural")) return true;
4116
+ const tag = el.tagName.toLowerCase();
4117
+ if (STRUCTURAL_TAGS.has(tag)) return true;
4118
+ if (el.id === "root") return true;
4119
+ return tag === "main" && (el.parentElement === document.body || el.parentElement?.id === "root");
4120
+ }
4121
+ function isStructuralScannedElement(scanned) {
4122
+ if (!scanned) return false;
4123
+ if (isHighLevelStructuralElement(scanned.element)) return true;
4124
+ return isStructuralComponentName(scanned.editorMeta?.componentName);
4125
+ }
4126
+ function isStructuralContainer(containerId, nodeMap) {
4127
+ return isStructuralScannedElement(nodeMap.get(containerId));
4128
+ }
4129
+ function isValidLaneContainer(containerId, projection, nodeMap) {
4130
+ const container = projection.containerIndex.get(containerId);
4131
+ if (!container) return false;
4132
+ if (container.children.length === 0) return false;
4133
+ return !isStructuralContainer(containerId, nodeMap);
4134
+ }
4135
+ function buildBlockedStructuralContainerIds(projection, nodeMap, sourceContainerId) {
4136
+ const blocked = /* @__PURE__ */ new Set();
4137
+ for (const containerId of projection.containerIndex.keys()) {
4138
+ if (containerId === sourceContainerId) continue;
4139
+ if (isStructuralContainer(containerId, nodeMap)) blocked.add(containerId);
4140
+ }
4141
+ return blocked;
4142
+ }
4143
+
4144
+ // src/client/dnd/design-mode/fiber-bridge.ts
4145
+ var FIBER_KEY_PREFIX = "__reactFiber$";
4146
+ var PROPS_KEY_PREFIX = "__reactProps$";
4147
+ var metaCache = /* @__PURE__ */ new WeakMap();
4148
+ var directMetaCache = /* @__PURE__ */ new WeakMap();
4149
+ function hasEditorMetaInFiberChain(fiber) {
4150
+ let current = fiber;
4151
+ while (current) {
4152
+ if (current.memoizedProps?.__editorMeta) return true;
4153
+ current = current.return;
4154
+ }
4155
+ return false;
4156
+ }
4157
+ function compareFiberCandidates(candidate, currentBest) {
4158
+ if (!currentBest) return 1;
4159
+ const candidateScore = [
4160
+ candidate.hasMatchingProps ? 1 : 0,
4161
+ candidate.hasDirectEditorMeta ? 1 : 0,
4162
+ candidate.hasCompanionPropsKey ? 1 : 0,
4163
+ candidate.hasEditorMetaInReturnChain ? 1 : 0,
4164
+ candidate.order
4165
+ ];
4166
+ const currentScore = [
4167
+ currentBest.hasMatchingProps ? 1 : 0,
4168
+ currentBest.hasDirectEditorMeta ? 1 : 0,
4169
+ currentBest.hasCompanionPropsKey ? 1 : 0,
4170
+ currentBest.hasEditorMetaInReturnChain ? 1 : 0,
4171
+ currentBest.order
4172
+ ];
4173
+ for (let i = 0; i < candidateScore.length; i += 1) {
4174
+ if (candidateScore[i] === currentScore[i]) continue;
4175
+ return candidateScore[i] > currentScore[i] ? 1 : -1;
4176
+ }
4177
+ return 0;
4178
+ }
4179
+ function getFiberSelectionFromElement(el) {
4180
+ const record = el;
4181
+ const keys = Object.keys(record);
4182
+ let bestCandidate = null;
4183
+ for (let i = 0; i < keys.length; i += 1) {
4184
+ const fiberKey = keys[i];
4185
+ if (!fiberKey.startsWith(FIBER_KEY_PREFIX)) continue;
4186
+ const fiber = record[fiberKey];
4187
+ if (!fiber) continue;
4188
+ if (fiber.stateNode && fiber.stateNode !== el) continue;
4189
+ const suffix = fiberKey.slice(FIBER_KEY_PREFIX.length);
4190
+ const propsKey = `${PROPS_KEY_PREFIX}${suffix}`;
4191
+ const hasCompanionPropsKey = Object.prototype.hasOwnProperty.call(record, propsKey);
4192
+ const companionProps = hasCompanionPropsKey ? record[propsKey] : void 0;
4193
+ const candidate = {
4194
+ fiber,
4195
+ fiberKey,
4196
+ hasMatchingProps: hasCompanionPropsKey && fiber.memoizedProps === companionProps,
4197
+ hasDirectEditorMeta: !!fiber.memoizedProps?.__editorMeta,
4198
+ hasCompanionPropsKey,
4199
+ hasEditorMetaInReturnChain: hasEditorMetaInFiberChain(fiber),
4200
+ // Tiebreaker: higher index in Object.keys = later React fiber attachment.
4201
+ // Relies on spec-guaranteed insertion-order for string keys (ES2015+).
4202
+ order: i
4203
+ };
4204
+ if (compareFiberCandidates(candidate, bestCandidate) > 0) {
4205
+ bestCandidate = candidate;
4206
+ }
4207
+ }
4208
+ if (!bestCandidate) {
4209
+ return { fiber: null, fiberKey: null };
4210
+ }
4211
+ return { fiber: bestCandidate.fiber, fiberKey: bestCandidate.fiberKey };
4212
+ }
4213
+ function readFiberAwareCache(cache, el, selection) {
4214
+ const cached = cache.get(el);
4215
+ if (!cached) return void 0;
4216
+ if (cached.fiber !== selection.fiber) return void 0;
4217
+ if (cached.fiberKey !== selection.fiberKey) return void 0;
4218
+ return cached.value;
4219
+ }
4220
+ function writeFiberAwareCache(cache, el, selection, value) {
4221
+ cache.set(el, {
4222
+ fiber: selection.fiber,
4223
+ fiberKey: selection.fiberKey,
4224
+ value
4225
+ });
4226
+ return value;
4227
+ }
4228
+ function isRootSvg(el) {
4229
+ return el instanceof SVGElement && el.tagName.toLowerCase() === "svg";
4230
+ }
4231
+ function isPassthroughMeta(meta) {
4232
+ return !!meta?.staticProps && meta.staticProps.asChild === true;
4233
+ }
4234
+ function isWrapperMeta(meta) {
4235
+ if (!meta?.componentName) return false;
4236
+ return meta.componentName[0] !== meta.componentName[0].toLowerCase();
4237
+ }
4238
+ function hasOwnTextContent(el) {
4239
+ for (const node of Array.from(el.childNodes)) {
4240
+ if (node.nodeType !== Node.TEXT_NODE) continue;
4241
+ if (node.textContent?.trim()) return true;
4242
+ }
4243
+ return false;
4244
+ }
4245
+ function getCandidateChildren(el) {
4246
+ return Array.from(el.children).filter((child) => {
4247
+ if (!(child instanceof HTMLElement)) return false;
4248
+ if (child.hasAttribute("data-design-mode-ui")) return false;
4249
+ if (child.hasAttribute("data-design-mode-allow")) return false;
4250
+ return !shouldIgnoreAsVirtualRuntime(child);
4251
+ });
4252
+ }
4253
+ function shouldIgnoreAsVirtualRuntime(el) {
4254
+ const tag = el.tagName.toLowerCase();
4255
+ if (tag === "canvas") return true;
4256
+ if (tag === "script" || tag === "style" || tag === "noscript") return true;
4257
+ if (el instanceof SVGElement && !isRootSvg(el)) return true;
4258
+ return false;
4259
+ }
4260
+ function getDirectEditorMeta(el) {
4261
+ const selection = getFiberSelectionFromElement(el);
4262
+ const cached = readFiberAwareCache(directMetaCache, el, selection);
4263
+ if (cached !== void 0) return cached;
4264
+ const meta = selection.fiber?.memoizedProps?.__editorMeta;
4265
+ return writeFiberAwareCache(directMetaCache, el, selection, meta ?? null);
4266
+ }
4267
+ function getEditorMeta(el) {
4268
+ const selection = getFiberSelectionFromElement(el);
4269
+ const cached = readFiberAwareCache(metaCache, el, selection);
4270
+ if (cached !== void 0) return cached;
4271
+ let fiber = selection.fiber;
4272
+ while (fiber) {
4273
+ const meta = fiber.memoizedProps?.__editorMeta;
4274
+ if (meta) {
4275
+ return writeFiberAwareCache(metaCache, el, selection, meta);
4276
+ }
4277
+ fiber = fiber.return;
4278
+ }
4279
+ return writeFiberAwareCache(metaCache, el, selection, null);
4280
+ }
4281
+ function findMetaOwnerElement(el) {
4282
+ let current = el;
4283
+ while (current) {
4284
+ if (getEditorMeta(current)) return current;
4285
+ current = current.parentElement;
4286
+ }
4287
+ return null;
4288
+ }
4289
+ function resolveHostElementFromFiberChain(el) {
4290
+ if (shouldIgnoreAsVirtualRuntime(el)) return null;
4291
+ const directMeta = getDirectEditorMeta(el);
4292
+ const ownerMeta = directMeta ?? getEditorMeta(el);
4293
+ if (isPassthroughMeta(ownerMeta)) {
4294
+ const children = getCandidateChildren(el);
4295
+ if (children.length !== 1) return null;
4296
+ return children[0];
4297
+ }
4298
+ if (directMeta && !isWrapperMeta(directMeta)) return el;
4299
+ const MAX_STEPS = 10;
4300
+ const ownerNodeId = ownerMeta?.nodeId ?? null;
4301
+ let outermost = el;
4302
+ let parent = el.parentElement;
4303
+ for (let i = 0; i < MAX_STEPS; i += 1) {
4304
+ if (!parent || parent === document.body) break;
4305
+ if (shouldIgnoreAsVirtualRuntime(parent)) break;
4306
+ const parentMeta = getEditorMeta(parent);
4307
+ if (!parentMeta) break;
4308
+ if (ownerNodeId && parentMeta.nodeId !== ownerNodeId) break;
4309
+ if (isStructuralComponentName(parentMeta.componentName)) break;
4310
+ if (!isWrapperMeta(parentMeta)) break;
4311
+ if (isPassthroughMeta(parentMeta)) break;
4312
+ if (hasOwnTextContent(parent)) break;
4313
+ const children = getCandidateChildren(parent);
4314
+ if (children.length !== 1) break;
4315
+ outermost = parent;
4316
+ parent = parent.parentElement;
4317
+ }
4318
+ return outermost;
4319
+ }
4320
+ function resolveDragSurface(el) {
4321
+ const host = resolveHostElementFromFiberChain(el);
4322
+ if (!host) {
4323
+ return { host: null, metaOwner: null, kind: "direct-host" };
4324
+ }
4325
+ const metaOwner = findMetaOwnerElement(host) ?? findMetaOwnerElement(el);
4326
+ const ownerMeta = metaOwner ? getEditorMeta(metaOwner) : null;
4327
+ const kind = isPassthroughMeta(ownerMeta) ? "passthrough-slot" : metaOwner && metaOwner !== host && isWrapperMeta(ownerMeta) ? "wrapper-to-host" : "direct-host";
4328
+ return {
4329
+ host,
4330
+ metaOwner,
4331
+ kind
4332
+ };
4333
+ }
4334
+
4335
+ // src/client/dnd/design-mode/node-key.ts
4336
+ var _runtimeKeyMap = /* @__PURE__ */ new WeakMap();
4337
+ var _runtimeKeyCounter = 0;
4338
+ function getRuntimeKey(el) {
4339
+ const existing = _runtimeKeyMap.get(el);
4340
+ if (existing) return existing;
4341
+ const key2 = `runtime:${++_runtimeKeyCounter}`;
4342
+ _runtimeKeyMap.set(el, key2);
4343
+ return key2;
4344
+ }
4345
+ function getNodeKeyFromElement(el) {
4346
+ const resolved = resolveDragSurface(el);
4347
+ const host = resolved.host ?? el;
4348
+ const meta = getEditorMeta(host);
4349
+ if (meta) return meta.nodeId;
4350
+ const persisted = host.getAttribute("data-dnd-node-id");
4351
+ if (persisted) return persisted;
4352
+ return getRuntimeKey(host);
4353
+ }
4354
+ function getInspectorRefFromElement(el) {
4355
+ const resolved = resolveDragSurface(el);
4356
+ const host = resolved.host ?? el;
4357
+ const meta = getEditorMeta(host);
4358
+ if (!meta) return null;
4359
+ return { relativePath: meta.file, line: meta.line, column: meta.col };
4360
+ }
4361
+
4362
+ // src/client/dnd/design-mode/arrange-persistence-diagnostics.ts
4363
+ function isSourceBackedRuntimeNodeKey(nodeKey) {
4364
+ return typeof nodeKey === "string" && nodeKey.trim().length > 0 && !nodeKey.startsWith("runtime:");
4365
+ }
4366
+ function mergeRuntimeArrangeDiagnostics(diagnosticsEntries) {
4367
+ const blockedReasons = /* @__PURE__ */ new Set();
4368
+ const notes = /* @__PURE__ */ new Set();
4369
+ let structurallyUnsafe = false;
4370
+ for (const diagnostics of diagnosticsEntries) {
4371
+ if (!diagnostics) continue;
4372
+ if (diagnostics.structurallyUnsafe === true) {
4373
+ structurallyUnsafe = true;
4374
+ }
4375
+ for (const reason of diagnostics.blockedReasons ?? []) {
4376
+ if (typeof reason === "string" && reason.trim().length > 0) {
4377
+ blockedReasons.add(reason);
4378
+ }
4379
+ }
4380
+ for (const note of diagnostics.notes ?? []) {
4381
+ if (typeof note === "string" && note.trim().length > 0) {
4382
+ notes.add(note);
4383
+ }
4384
+ }
4385
+ }
4386
+ if (!structurallyUnsafe && blockedReasons.size === 0 && notes.size === 0) {
4387
+ return null;
4388
+ }
4389
+ return {
4390
+ ...structurallyUnsafe ? { structurallyUnsafe: true } : {},
4391
+ ...blockedReasons.size > 0 ? { blockedReasons: Array.from(blockedReasons) } : {},
4392
+ ...notes.size > 0 ? { notes: Array.from(notes) } : {}
4393
+ };
4394
+ }
4395
+ function detectDuplicateSourceBackedNodeKeys(nodeKeys) {
4396
+ const seenKeys = /* @__PURE__ */ new Set();
4397
+ const duplicateKeys = /* @__PURE__ */ new Set();
4398
+ for (const nodeKey of nodeKeys) {
4399
+ if (!isSourceBackedRuntimeNodeKey(nodeKey)) {
4400
+ continue;
4401
+ }
4402
+ if (seenKeys.has(nodeKey)) {
4403
+ duplicateKeys.add(nodeKey);
4404
+ continue;
4405
+ }
4406
+ seenKeys.add(nodeKey);
4407
+ }
4408
+ return Array.from(duplicateKeys);
4409
+ }
4410
+ function buildOccurrenceNodeId(nodeKey, occurrenceIndex) {
4411
+ return `${nodeKey}::${occurrenceIndex}`;
4412
+ }
4413
+ function buildEffectiveNodeIdsByCommitNodeId(commitNodeIds, nodeKeysByCommitNodeId) {
4414
+ const countsByNodeKey = /* @__PURE__ */ new Map();
4415
+ for (const commitNodeId of commitNodeIds) {
4416
+ const nodeKey = nodeKeysByCommitNodeId.get(commitNodeId);
4417
+ if (!isSourceBackedRuntimeNodeKey(nodeKey)) {
4418
+ continue;
4419
+ }
4420
+ countsByNodeKey.set(nodeKey, (countsByNodeKey.get(nodeKey) ?? 0) + 1);
4421
+ }
4422
+ const seenByNodeKey = /* @__PURE__ */ new Map();
4423
+ const effectiveNodeIdsByCommitNodeId = /* @__PURE__ */ new Map();
4424
+ for (const commitNodeId of commitNodeIds) {
4425
+ const nodeKey = nodeKeysByCommitNodeId.get(commitNodeId);
4426
+ if (!isSourceBackedRuntimeNodeKey(nodeKey) || (countsByNodeKey.get(nodeKey) ?? 0) < 2) {
4427
+ effectiveNodeIdsByCommitNodeId.set(commitNodeId, null);
4428
+ continue;
4429
+ }
4430
+ const occurrenceIndex = seenByNodeKey.get(nodeKey) ?? 0;
4431
+ seenByNodeKey.set(nodeKey, occurrenceIndex + 1);
4432
+ effectiveNodeIdsByCommitNodeId.set(
4433
+ commitNodeId,
4434
+ buildOccurrenceNodeId(nodeKey, occurrenceIndex)
4435
+ );
4436
+ }
4437
+ return effectiveNodeIdsByCommitNodeId;
4438
+ }
4439
+ function createCommitLaneIdentitySnapshot(params) {
4440
+ return {
4441
+ laneContainerId: params.laneContainerId,
4442
+ commitNodeIds: [...params.commitNodeIds],
4443
+ nodeKeysByCommitNodeId: new Map(params.nodeKeysByCommitNodeId),
4444
+ effectiveNodeIdsByCommitNodeId: buildEffectiveNodeIdsByCommitNodeId(
4445
+ params.commitNodeIds,
4446
+ params.nodeKeysByCommitNodeId
4447
+ ),
4448
+ committable: true
4449
+ };
4450
+ }
4451
+ function hasSafeOccurrenceIdentities(lane, duplicateNodeKey) {
4452
+ const effectiveNodeIds = lane.commitNodeIds.filter(
4453
+ (commitNodeId) => lane.nodeKeysByCommitNodeId.get(commitNodeId) === duplicateNodeKey
4454
+ ).map(
4455
+ (commitNodeId) => lane.effectiveNodeIdsByCommitNodeId.get(commitNodeId) ?? null
4456
+ );
4457
+ return effectiveNodeIds.length > 1 && effectiveNodeIds.every(
4458
+ (effectiveNodeId) => typeof effectiveNodeId === "string" && effectiveNodeId.length > 0
4459
+ ) && new Set(effectiveNodeIds).size === effectiveNodeIds.length;
4460
+ }
4461
+ function buildLanePersistenceDiagnostics(params) {
4462
+ const duplicateNodeKeys = detectDuplicateSourceBackedNodeKeys(
4463
+ buildNodeKeysFromCommitIds(
4464
+ params.lane.commitNodeIds,
4465
+ params.lane.nodeKeysByCommitNodeId
4466
+ )
4467
+ );
4468
+ if (duplicateNodeKeys.length === 0) {
4469
+ return null;
4470
+ }
4471
+ const safeDuplicateNodeKeys = duplicateNodeKeys.filter(
4472
+ (nodeKey) => hasSafeOccurrenceIdentities(params.lane, nodeKey)
4473
+ );
4474
+ if (safeDuplicateNodeKeys.length === duplicateNodeKeys.length) {
4475
+ return {
4476
+ notes: safeDuplicateNodeKeys.map(
4477
+ (nodeKey) => `Arrange lane ${params.lane.laneContainerId} repeats source-backed nodeKey ${nodeKey}, but runtime assigned explicit occurrence identities for each commit root.`
4478
+ )
4479
+ };
4480
+ }
4481
+ return {
4482
+ structurallyUnsafe: true,
4483
+ blockedReasons: duplicateNodeKeys.map(
4484
+ (nodeKey) => `Arrange lane ${params.lane.laneContainerId} contains multiple commit roots with the same source-backed nodeKey (${nodeKey}), so local save is unsafe.`
4485
+ ),
4486
+ notes: [
4487
+ `Lane ${params.lane.laneContainerId} duplicates source-backed nodeKeys: ${duplicateNodeKeys.join(", ")}`
4488
+ ]
4489
+ };
4490
+ }
4491
+ function removeCommitNodeId(commitNodeIds, commitNodeId) {
4492
+ return commitNodeIds.filter((candidate) => candidate !== commitNodeId);
4493
+ }
4494
+ function buildNodeKeysFromCommitIds(commitNodeIds, nodeKeysByCommitNodeId) {
4495
+ return commitNodeIds.map(
4496
+ (commitNodeId) => nodeKeysByCommitNodeId.get(commitNodeId) ?? null
4497
+ );
4498
+ }
4499
+ function captureCommitLaneIdentitySnapshot(lane, resolveNodeElement) {
4500
+ if (!lane?.committable) return null;
4501
+ const nodeKeysByCommitNodeId = /* @__PURE__ */ new Map();
4502
+ for (const commitNodeId of lane.orderedCommitNodeIds) {
4503
+ const element = resolveNodeElement(commitNodeId);
4504
+ if (!element) return null;
4505
+ nodeKeysByCommitNodeId.set(commitNodeId, getNodeKeyFromElement(element));
4506
+ }
4507
+ return createCommitLaneIdentitySnapshot({
4508
+ laneContainerId: lane.laneContainerId,
4509
+ nodeKeysByCommitNodeId,
4510
+ commitNodeIds: lane.orderedCommitNodeIds
4511
+ });
4512
+ }
4513
+ function projectCommitNodeIdsForMove(params) {
4514
+ const withoutMovedCommit = removeCommitNodeId(
4515
+ params.commitNodeIds,
4516
+ params.movedCommitNodeId
4517
+ );
4518
+ if (typeof params.targetIndex !== "number" || !Number.isFinite(params.targetIndex)) {
4519
+ return [...withoutMovedCommit, params.movedCommitNodeId];
4520
+ }
4521
+ const insertionIndex = Math.max(
4522
+ 0,
4523
+ Math.min(params.targetIndex, withoutMovedCommit.length)
4524
+ );
4525
+ return [
4526
+ ...withoutMovedCommit.slice(0, insertionIndex),
4527
+ params.movedCommitNodeId,
4528
+ ...withoutMovedCommit.slice(insertionIndex)
4529
+ ];
4530
+ }
4531
+ function buildMovePersistenceDiagnostics(params) {
4532
+ const {
4533
+ sourceLane,
4534
+ targetLane,
4535
+ movedCommitNodeId,
4536
+ sourceLaneContainerId,
4537
+ targetLaneContainerId,
4538
+ targetIndex
4539
+ } = params;
4540
+ const sameLane = !!sourceLaneContainerId && !!targetLaneContainerId && sourceLaneContainerId === targetLaneContainerId;
4541
+ const targetSnapshot = sameLane ? targetLane ?? sourceLane : targetLane;
4542
+ const sourceLaneAfter = !sameLane && sourceLane?.committable ? createCommitLaneIdentitySnapshot({
4543
+ laneContainerId: sourceLane.laneContainerId,
4544
+ commitNodeIds: removeCommitNodeId(
4545
+ sourceLane.commitNodeIds,
4546
+ movedCommitNodeId
4547
+ ),
4548
+ nodeKeysByCommitNodeId: sourceLane.nodeKeysByCommitNodeId
4549
+ }) : null;
4550
+ const targetLaneAfter = targetSnapshot?.committable ? createCommitLaneIdentitySnapshot({
4551
+ laneContainerId: targetSnapshot.laneContainerId,
4552
+ commitNodeIds: projectCommitNodeIdsForMove({
4553
+ commitNodeIds: targetSnapshot.commitNodeIds,
4554
+ movedCommitNodeId,
4555
+ targetIndex
4556
+ }),
4557
+ nodeKeysByCommitNodeId: new Map([
4558
+ ...targetSnapshot.nodeKeysByCommitNodeId,
4559
+ ...sourceLane?.nodeKeysByCommitNodeId.has(movedCommitNodeId) ? [
4560
+ [
4561
+ movedCommitNodeId,
4562
+ sourceLane.nodeKeysByCommitNodeId.get(movedCommitNodeId) ?? null
4563
+ ]
4564
+ ] : []
4565
+ ])
4566
+ }) : null;
4567
+ return mergeRuntimeArrangeDiagnostics([
4568
+ sourceLaneAfter ? buildLanePersistenceDiagnostics({ lane: sourceLaneAfter }) : null,
4569
+ targetLaneAfter ? buildLanePersistenceDiagnostics({ lane: targetLaneAfter }) : null
4570
+ ]);
4571
+ }
4572
+
4107
4573
  // src/client/dnd/design-mode/store.ts
4108
4574
  function summarizeEditorChangeEvent(event) {
4575
+ if (event.kind === "insert") {
4576
+ return `insert ${event.displayName} at ${event.containerId}[${event.index}]`;
4577
+ }
4109
4578
  if (event.kind === "move") {
4110
4579
  const selectedHint = event.selectedNodeKey && event.selectedNodeKey !== event.nodeKey ? ` (selected ${event.selectedNodeKey})` : "";
4111
4580
  const anchorHint = event.anchorNodeKey && event.anchorNodeKey !== event.nodeKey ? ` (anchor ${event.anchorNodeKey})` : "";
4112
- return `move ${event.nodeKey}: parent ${event.payload.before.parentNodeId}[${event.payload.before.index}] \u2192 ${event.payload.after.parentNodeId}[${event.payload.after.index}]${event.payload.after.commitNodeId ? ` (commit ${event.payload.after.commitNodeId}` : ""}${event.payload.after.beforeNodeId ? ` before ${event.payload.after.beforeNodeId}` : ""}${event.payload.after.afterNodeId ? ` after ${event.payload.after.afterNodeId}` : ""}${event.payload.after.commitNodeId ? ")" : ""}${selectedHint}${anchorHint}`;
4581
+ const beforeParentNodeId = event.payload.before.effectiveContainerNodeId ?? event.payload.before.parentNodeId;
4582
+ const afterParentNodeId = event.payload.after.effectiveContainerNodeId ?? event.payload.after.parentNodeId;
4583
+ const commitNodeId = event.payload.after.effectiveCommitNodeId ?? event.payload.after.commitNodeId;
4584
+ const beforeNodeId = event.payload.after.effectiveBeforeNodeId ?? event.payload.after.beforeNodeId;
4585
+ const afterNodeId = event.payload.after.effectiveAfterNodeId ?? event.payload.after.afterNodeId;
4586
+ return `move ${event.nodeKey}: parent ${beforeParentNodeId}[${event.payload.before.index}] \u2192 ${afterParentNodeId}[${event.payload.after.index}]${commitNodeId ? ` (commit ${commitNodeId}` : ""}${beforeNodeId ? ` before ${beforeNodeId}` : ""}${afterNodeId ? ` after ${afterNodeId}` : ""}${commitNodeId ? ")" : ""}${selectedHint}${anchorHint}`;
4113
4587
  }
4114
4588
  if (event.payload.after.mode === "grid-span") {
4115
4589
  return `resize(grid-span) ${event.nodeKey}: col ${event.payload.before.colSpan ?? 1}\u2192${event.payload.after.colSpan ?? 1}, row ${event.payload.before.rowSpan ?? 1}\u2192${event.payload.after.rowSpan ?? 1}`;
4116
4590
  }
4117
4591
  return `resize(px) ${event.nodeKey}: ${event.payload.before.width}\xD7${event.payload.before.height} \u2192 ${event.payload.after.width}\xD7${event.payload.after.height}`;
4118
4592
  }
4593
+ function buildFallbackOperationId(now, nodeId) {
4594
+ return `${now}:${nodeId}`;
4595
+ }
4596
+ function inferMoveCommandType(before, after) {
4597
+ return before.parentNodeId === after.parentNodeId ? "reorder" : "moveCrossContainer";
4598
+ }
4119
4599
  function rebuildNodeEdit(orderedChanges, nodeKey) {
4120
4600
  let latestMove;
4121
4601
  let latestResize;
@@ -4130,11 +4610,36 @@ function rebuildNodeEdit(orderedChanges, nodeKey) {
4130
4610
  nodeId = ev.nodeId;
4131
4611
  inspectorRef = ev.inspectorRef;
4132
4612
  if (ev.kind === "move") latestMove = ev.payload;
4133
- else latestResize = ev.payload;
4613
+ else if (ev.kind === "resize") latestResize = ev.payload;
4134
4614
  }
4135
4615
  if (count === 0) return null;
4136
4616
  return { nodeKey, nodeId, inspectorRef, latestMove, latestResize, changeCount: count, lastUpdatedAt: lastAt };
4137
4617
  }
4618
+ function coalesceOrderedChanges(changes) {
4619
+ if (changes.length <= 1) return changes;
4620
+ const result = [];
4621
+ for (const event of changes) {
4622
+ const prev = result[result.length - 1];
4623
+ if (prev && prev.kind === "resize" && event.kind === "resize" && prev.nodeKey === event.nodeKey) {
4624
+ result[result.length - 1] = {
4625
+ ...event,
4626
+ payload: { before: prev.payload.before, after: event.payload.after }
4627
+ };
4628
+ } else {
4629
+ result.push(event);
4630
+ }
4631
+ }
4632
+ return result;
4633
+ }
4634
+ function buildEmptyArrangeSessionState() {
4635
+ return {
4636
+ nodeEditsByKey: {},
4637
+ orderedChanges: [],
4638
+ undoneChanges: [],
4639
+ canUndo: false,
4640
+ canRedo: false
4641
+ };
4642
+ }
4138
4643
  var useDesignModeStore = create((set, get) => ({
4139
4644
  // UI state
4140
4645
  enabled: false,
@@ -4150,8 +4655,10 @@ var useDesignModeStore = create((set, get) => ({
4150
4655
  nodeEditsByKey: {},
4151
4656
  orderedChanges: [],
4152
4657
  undoneChanges: [],
4658
+ // External drag
4659
+ externalDrag: null,
4153
4660
  // UI actions
4154
- toggle: () => set((s) => ({ enabled: !s.enabled })),
4661
+ toggle: () => set((s) => ({ enabled: !s.enabled, ...buildEmptyArrangeSessionState() })),
4155
4662
  setEnabled: (enabled) => set({ enabled }),
4156
4663
  setHovered: (selector) => set({ hoveredSelector: selector }),
4157
4664
  setSelected: (selector) => set({ selectedSelector: selector }),
@@ -4166,8 +4673,11 @@ var useDesignModeStore = create((set, get) => ({
4166
4673
  canUndo: false,
4167
4674
  canRedo: false
4168
4675
  }),
4676
+ resetArrangeSession: () => set(buildEmptyArrangeSessionState()),
4169
4677
  // Change tracking actions
4170
4678
  recordMoveChange: ({
4679
+ operationId,
4680
+ commandType,
4171
4681
  nodeKey,
4172
4682
  nodeId,
4173
4683
  inspectorRef,
@@ -4176,13 +4686,24 @@ var useDesignModeStore = create((set, get) => ({
4176
4686
  selectedInspectorRef,
4177
4687
  anchorNodeKey,
4178
4688
  anchorNodeId,
4689
+ sourceContainer,
4690
+ sourceBeforeSibling,
4691
+ sourceAfterSibling,
4692
+ container,
4693
+ beforeSibling,
4694
+ afterSibling,
4695
+ diagnostics,
4179
4696
  componentName,
4180
4697
  before,
4181
4698
  after
4182
4699
  }) => {
4183
4700
  const now = Date.now();
4701
+ const normalizedOperationId = operationId ?? buildFallbackOperationId(now, nodeId);
4702
+ const normalizedCommandType = commandType ?? inferMoveCommandType(before, after);
4184
4703
  const event = {
4185
4704
  kind: "move",
4705
+ operationId: normalizedOperationId,
4706
+ commandType: normalizedCommandType,
4186
4707
  nodeKey,
4187
4708
  nodeId,
4188
4709
  inspectorRef,
@@ -4191,6 +4712,13 @@ var useDesignModeStore = create((set, get) => ({
4191
4712
  selectedInspectorRef,
4192
4713
  anchorNodeKey,
4193
4714
  anchorNodeId,
4715
+ sourceContainer: sourceContainer ?? null,
4716
+ sourceBeforeSibling: sourceBeforeSibling ?? null,
4717
+ sourceAfterSibling: sourceAfterSibling ?? null,
4718
+ container: container ?? null,
4719
+ beforeSibling: beforeSibling ?? null,
4720
+ afterSibling: afterSibling ?? null,
4721
+ diagnostics: diagnostics ?? null,
4194
4722
  payload: { before, after },
4195
4723
  at: now
4196
4724
  };
@@ -4213,10 +4741,20 @@ var useDesignModeStore = create((set, get) => ({
4213
4741
  };
4214
4742
  });
4215
4743
  },
4216
- recordResizeChange: ({ nodeKey, nodeId, inspectorRef, componentName, before, after }) => {
4744
+ recordResizeChange: ({
4745
+ operationId,
4746
+ nodeKey,
4747
+ nodeId,
4748
+ inspectorRef,
4749
+ componentName,
4750
+ before,
4751
+ after
4752
+ }) => {
4217
4753
  const now = Date.now();
4754
+ const normalizedOperationId = operationId ?? buildFallbackOperationId(now, nodeId);
4218
4755
  const event = {
4219
4756
  kind: "resize",
4757
+ operationId: normalizedOperationId,
4220
4758
  nodeKey,
4221
4759
  nodeId,
4222
4760
  inspectorRef,
@@ -4242,6 +4780,52 @@ var useDesignModeStore = create((set, get) => ({
4242
4780
  };
4243
4781
  });
4244
4782
  },
4783
+ recordInsertChange: ({
4784
+ operationId,
4785
+ nodeKey,
4786
+ nodeId,
4787
+ inspectorRef,
4788
+ displayName,
4789
+ html,
4790
+ containerId,
4791
+ containerNodeKey,
4792
+ index
4793
+ }) => {
4794
+ const now = Date.now();
4795
+ const normalizedOperationId = operationId ?? buildFallbackOperationId(now, nodeId);
4796
+ const event = {
4797
+ kind: "insert",
4798
+ operationId: normalizedOperationId,
4799
+ nodeKey,
4800
+ nodeId,
4801
+ inspectorRef,
4802
+ displayName,
4803
+ html,
4804
+ containerId,
4805
+ containerNodeKey,
4806
+ index,
4807
+ at: now
4808
+ };
4809
+ set((s) => {
4810
+ const prev = s.nodeEditsByKey[nodeKey];
4811
+ const meta = {
4812
+ nodeKey,
4813
+ nodeId,
4814
+ inspectorRef,
4815
+ latestMove: prev?.latestMove,
4816
+ latestResize: prev?.latestResize,
4817
+ changeCount: (prev?.changeCount ?? 0) + 1,
4818
+ lastUpdatedAt: now
4819
+ };
4820
+ return {
4821
+ orderedChanges: [...s.orderedChanges, event],
4822
+ nodeEditsByKey: { ...s.nodeEditsByKey, [nodeKey]: meta },
4823
+ undoneChanges: []
4824
+ };
4825
+ });
4826
+ },
4827
+ startExternalDrag: (component) => set({ externalDrag: component }),
4828
+ cancelExternalDrag: () => set({ externalDrag: null }),
4245
4829
  undoLastChange: () => {
4246
4830
  const { orderedChanges, undoneChanges } = get();
4247
4831
  if (orderedChanges.length === 0) return null;
@@ -4267,7 +4851,8 @@ var useDesignModeStore = create((set, get) => ({
4267
4851
  },
4268
4852
  clearChanges: () => set({ orderedChanges: [], undoneChanges: [], nodeEditsByKey: {} }),
4269
4853
  exportChangesForAI: () => {
4270
- const { orderedChanges, nodeEditsByKey } = get();
4854
+ const { orderedChanges: rawChanges, nodeEditsByKey } = get();
4855
+ const orderedChanges = coalesceOrderedChanges(rawChanges);
4271
4856
  const changesByFile = {};
4272
4857
  for (const event of orderedChanges) {
4273
4858
  const fileKey = event.inspectorRef?.relativePath ?? "__runtime__";
@@ -4281,180 +4866,127 @@ var useDesignModeStore = create((set, get) => ({
4281
4866
  lines.push(` ${summarizeEditorChangeEvent(ev)}`);
4282
4867
  }
4283
4868
  }
4869
+ const diagnostics = mergeRuntimeArrangeDiagnostics(
4870
+ orderedChanges.map(
4871
+ (event) => event.kind === "move" ? event.diagnostics : null
4872
+ )
4873
+ );
4284
4874
  return {
4285
4875
  changesByFile,
4286
4876
  finalSnapshot: { ...nodeEditsByKey },
4287
- aiPromptContext: lines.join("\n")
4877
+ aiPromptContext: lines.join("\n"),
4878
+ ...diagnostics ? { diagnostics } : {}
4288
4879
  };
4289
4880
  }
4290
4881
  }));
4291
4882
 
4292
4883
  // src/client/dnd/design-mode/DesignModeOverlay.tsx
4293
- var import_react16 = require("react");
4884
+ var import_react17 = require("react");
4294
4885
 
4295
4886
  // src/client/dnd/design-mode/useElementScanner.ts
4296
4887
  var import_react6 = require("react");
4297
4888
 
4298
- // src/client/dnd/design-mode/structural-policy.ts
4299
- var STRUCTURAL_COMPONENT_RE = /(?:Layout|Page|Provider|App)$/;
4300
- var STRUCTURAL_TAGS = /* @__PURE__ */ new Set(["html", "body"]);
4301
- function isStructuralComponentName(componentName) {
4302
- return !!componentName && STRUCTURAL_COMPONENT_RE.test(componentName);
4889
+ // src/client/dnd/design-mode/helpers.ts
4890
+ var DND_NODE_ID_ATTR = "data-dnd-node-id";
4891
+ var PAGE_EDGE_GUARD = 10;
4892
+ var HARD_PAGE_EDGE_GUARD = 4;
4893
+ var SAFE_ZONE_INSET = 8;
4894
+ function clamp(value, min, max) {
4895
+ return Math.max(min, Math.min(max, value));
4303
4896
  }
4304
- function isHighLevelStructuralElement(el) {
4305
- if (!el) return false;
4306
- const tag = el.tagName.toLowerCase();
4307
- if (STRUCTURAL_TAGS.has(tag)) return true;
4308
- if (el.id === "root") return true;
4309
- return tag === "main" && (el.parentElement === document.body || el.parentElement?.id === "root");
4897
+ function buildNodeSelector(nodeId) {
4898
+ return `[${DND_NODE_ID_ATTR}="${escapeCssAttrValue(nodeId)}"]`;
4310
4899
  }
4311
- function isStructuralScannedElement(scanned) {
4312
- if (!scanned) return false;
4313
- if (isHighLevelStructuralElement(scanned.element)) return true;
4314
- return isStructuralComponentName(scanned.editorMeta?.componentName);
4900
+ var LARGE_NODE_NEST_BLOCK_MIN_WIDTH = 520;
4901
+ var LARGE_NODE_NEST_BLOCK_MIN_HEIGHT = 260;
4902
+ var LARGE_NODE_NEST_BLOCK_MIN_AREA = 18e4;
4903
+ function isEligibleForDrag(sc) {
4904
+ const source = sc.eligibilitySource ?? "editor-meta";
4905
+ return source === "editor-meta" || source === "force-allow" || source === "inferred-sortable-descendant";
4315
4906
  }
4316
- function isStructuralContainer(containerId, nodeMap) {
4317
- return isStructuralScannedElement(nodeMap.get(containerId));
4907
+ function isPointerInEdgeZone(px, py, guard) {
4908
+ return px <= guard || py <= guard || px >= window.innerWidth - guard || py >= window.innerHeight - guard;
4318
4909
  }
4319
- function isValidLaneContainer(containerId, projection, nodeMap) {
4320
- const container = projection.containerIndex.get(containerId);
4321
- if (!container) return false;
4322
- if (container.children.length < 2) return false;
4323
- return !isStructuralContainer(containerId, nodeMap);
4910
+ function isPointerInPageEdgeZone(px, py) {
4911
+ return isPointerInEdgeZone(px, py, PAGE_EDGE_GUARD);
4324
4912
  }
4325
- function buildBlockedStructuralContainerIds(projection, nodeMap, sourceContainerId) {
4326
- const blocked = /* @__PURE__ */ new Set();
4327
- for (const containerId of projection.containerIndex.keys()) {
4328
- if (containerId === sourceContainerId) continue;
4329
- if (isStructuralContainer(containerId, nodeMap)) blocked.add(containerId);
4330
- }
4331
- return blocked;
4913
+ function isPointerInHardPageEdgeZone(px, py) {
4914
+ return isPointerInEdgeZone(px, py, HARD_PAGE_EDGE_GUARD);
4332
4915
  }
4333
-
4334
- // src/client/dnd/design-mode/fiber-bridge.ts
4335
- var metaCache = /* @__PURE__ */ new WeakMap();
4336
- var directMetaCache = /* @__PURE__ */ new WeakMap();
4337
- function getFiberFromElement(el) {
4338
- const keys = Object.keys(el);
4339
- for (let i = 0; i < keys.length; i++) {
4340
- if (keys[i].startsWith("__reactFiber$")) {
4341
- return el[keys[i]];
4342
- }
4343
- }
4344
- return null;
4916
+ function resolveSafeZoneRect() {
4917
+ const root = document.getElementById("root");
4918
+ if (!root) return null;
4919
+ const rect = root.getBoundingClientRect();
4920
+ const left = rect.left + SAFE_ZONE_INSET;
4921
+ const top = rect.top + SAFE_ZONE_INSET;
4922
+ const right = rect.right - SAFE_ZONE_INSET;
4923
+ const bottom = rect.bottom - SAFE_ZONE_INSET;
4924
+ if (right <= left || bottom <= top) return null;
4925
+ return { left, top, right, bottom };
4345
4926
  }
4346
- function isRootSvg(el) {
4347
- return el instanceof SVGElement && el.tagName.toLowerCase() === "svg";
4927
+ function isPointerInSafeZone(px, py, safeZone) {
4928
+ if (!safeZone) return true;
4929
+ return px >= safeZone.left && px <= safeZone.right && py >= safeZone.top && py <= safeZone.bottom;
4348
4930
  }
4349
- function isPassthroughMeta(meta) {
4350
- return !!meta?.staticProps && meta.staticProps.asChild === true;
4931
+ function isPointerOutsideSafeZone(px, py, safeZone) {
4932
+ return !isPointerInSafeZone(px, py, safeZone);
4351
4933
  }
4352
- function isWrapperMeta(meta) {
4353
- if (!meta?.componentName) return false;
4354
- return meta.componentName[0] !== meta.componentName[0].toLowerCase();
4934
+ function isPointerBlocked(px, py, safeZone) {
4935
+ return isPointerInHardPageEdgeZone(px, py) || isPointerOutsideSafeZone(px, py, safeZone);
4355
4936
  }
4356
- function hasOwnTextContent(el) {
4357
- for (const node of Array.from(el.childNodes)) {
4358
- if (node.nodeType !== Node.TEXT_NODE) continue;
4359
- if (node.textContent?.trim()) return true;
4937
+ function shouldBlockNestForLargeNode(w, h) {
4938
+ return w >= LARGE_NODE_NEST_BLOCK_MIN_WIDTH || h >= LARGE_NODE_NEST_BLOCK_MIN_HEIGHT || w * h >= LARGE_NODE_NEST_BLOCK_MIN_AREA;
4939
+ }
4940
+ function findScrollableAncestor(start, dx, dy) {
4941
+ let el = start;
4942
+ while (el && el !== document.body) {
4943
+ const cs = getComputedStyle(el);
4944
+ if (dy !== 0 && (cs.overflowY === "auto" || cs.overflowY === "scroll") && el.scrollHeight > el.clientHeight + 1) return el;
4945
+ if (dx !== 0 && (cs.overflowX === "auto" || cs.overflowX === "scroll") && el.scrollWidth > el.clientWidth + 1) return el;
4946
+ el = el.parentElement;
4360
4947
  }
4361
- return false;
4948
+ return null;
4362
4949
  }
4363
- function getCandidateChildren(el) {
4364
- return Array.from(el.children).filter((child) => {
4365
- if (!(child instanceof HTMLElement)) return false;
4366
- if (child.hasAttribute("data-design-mode-ui")) return false;
4367
- if (child.hasAttribute("data-design-mode-allow")) return false;
4368
- return !shouldIgnoreAsVirtualRuntime(child);
4369
- });
4950
+ function escapeCssAttrValue(value) {
4951
+ const esc = globalThis.CSS?.escape;
4952
+ return esc ? esc(value) : value.replace(/["\\]/g, "\\$&");
4370
4953
  }
4371
- function shouldIgnoreAsVirtualRuntime(el) {
4372
- const tag = el.tagName.toLowerCase();
4373
- if (tag === "canvas") return true;
4374
- if (tag === "script" || tag === "style" || tag === "noscript") return true;
4375
- if (el instanceof SVGElement && !isRootSvg(el)) return true;
4376
- return false;
4954
+ function isDesignModeUiElement(el) {
4955
+ if (!(el instanceof HTMLElement)) return false;
4956
+ return el.hasAttribute("data-design-mode-ui") || !!el.closest("[data-design-mode-ui]");
4377
4957
  }
4378
- function getDirectEditorMeta(el) {
4379
- const cached = directMetaCache.get(el);
4380
- if (cached !== void 0) return cached;
4381
- const fiber = getFiberFromElement(el);
4382
- const meta = fiber?.memoizedProps?.__editorMeta;
4383
- const resolved = meta ?? null;
4384
- directMetaCache.set(el, resolved);
4385
- return resolved;
4958
+ function isElementScrollable(el) {
4959
+ if (!(el instanceof HTMLElement)) return false;
4960
+ const cs = getComputedStyle(el);
4961
+ const overY = cs.overflowY;
4962
+ const overX = cs.overflowX;
4963
+ return (overY === "auto" || overY === "scroll") && el.scrollHeight > el.clientHeight || (overX === "auto" || overX === "scroll") && el.scrollWidth > el.clientWidth;
4386
4964
  }
4387
- function getEditorMeta(el) {
4388
- const cached = metaCache.get(el);
4389
- if (cached !== void 0) return cached;
4390
- let fiber = getFiberFromElement(el);
4391
- while (fiber) {
4392
- const meta = fiber.memoizedProps?.__editorMeta;
4393
- if (meta) {
4394
- metaCache.set(el, meta);
4395
- return meta;
4965
+ function resolveScrollableFromHitStack(stack) {
4966
+ for (const hit of stack) {
4967
+ if (isDesignModeUiElement(hit)) continue;
4968
+ let el = hit;
4969
+ while (el) {
4970
+ if (isElementScrollable(el)) return el;
4971
+ el = el.parentElement;
4396
4972
  }
4397
- fiber = fiber.return;
4398
- }
4399
- metaCache.set(el, null);
4400
- return null;
4401
- }
4402
- function findMetaOwnerElement(el) {
4403
- let current = el;
4404
- while (current) {
4405
- if (getEditorMeta(current)) return current;
4406
- current = current.parentElement;
4407
4973
  }
4408
4974
  return null;
4409
4975
  }
4410
- function resolveHostElementFromFiberChain(el) {
4411
- if (shouldIgnoreAsVirtualRuntime(el)) return null;
4412
- const directMeta = getDirectEditorMeta(el);
4413
- const ownerMeta = directMeta ?? getEditorMeta(el);
4414
- if (isPassthroughMeta(ownerMeta)) {
4415
- const children = getCandidateChildren(el);
4416
- if (children.length !== 1) return null;
4417
- return children[0];
4418
- }
4419
- if (directMeta && !isWrapperMeta(directMeta)) return el;
4420
- const MAX_STEPS = 10;
4421
- const ownerNodeId = ownerMeta?.nodeId ?? null;
4422
- let outermost = el;
4423
- let parent = el.parentElement;
4424
- for (let i = 0; i < MAX_STEPS; i += 1) {
4425
- if (!parent || parent === document.body) break;
4426
- if (shouldIgnoreAsVirtualRuntime(parent)) break;
4427
- const parentMeta = getEditorMeta(parent);
4428
- if (!parentMeta) break;
4429
- if (ownerNodeId && parentMeta.nodeId !== ownerNodeId) break;
4430
- if (isStructuralComponentName(parentMeta.componentName)) break;
4431
- if (!isWrapperMeta(parentMeta)) break;
4432
- if (isPassthroughMeta(parentMeta)) break;
4433
- if (hasOwnTextContent(parent)) break;
4434
- const children = getCandidateChildren(parent);
4435
- if (children.length !== 1) break;
4436
- outermost = parent;
4437
- parent = parent.parentElement;
4438
- }
4439
- return outermost;
4440
- }
4441
- function resolveDragSurface(el) {
4442
- const host = resolveHostElementFromFiberChain(el);
4443
- if (!host) {
4444
- return { host: null, metaOwner: null, kind: "direct-host" };
4976
+ var _undoParentNodeIdMap = /* @__PURE__ */ new WeakMap();
4977
+ var _undoParentNodeIdCounter = 0;
4978
+ function ensureParentNodeId(el) {
4979
+ const existing = el.getAttribute(DND_NODE_ID_ATTR);
4980
+ if (existing) return existing;
4981
+ if (!_undoParentNodeIdMap.has(el)) {
4982
+ _undoParentNodeIdMap.set(el, `dnd-parent:${++_undoParentNodeIdCounter}`);
4445
4983
  }
4446
- const metaOwner = findMetaOwnerElement(host) ?? findMetaOwnerElement(el);
4447
- const ownerMeta = metaOwner ? getEditorMeta(metaOwner) : null;
4448
- const kind = isPassthroughMeta(ownerMeta) ? "passthrough-slot" : metaOwner && metaOwner !== host && isWrapperMeta(ownerMeta) ? "wrapper-to-host" : "direct-host";
4449
- return {
4450
- host,
4451
- metaOwner,
4452
- kind
4453
- };
4984
+ const id = _undoParentNodeIdMap.get(el);
4985
+ el.setAttribute(DND_NODE_ID_ATTR, id);
4986
+ return id;
4454
4987
  }
4455
4988
 
4456
4989
  // src/client/dnd/design-mode/useElementScanner.ts
4457
- var DND_NODE_ID_ATTR = "data-dnd-node-id";
4458
4990
  function resolveContainerStrategy(el) {
4459
4991
  const cs = getComputedStyle(el);
4460
4992
  const d = cs.display;
@@ -4485,7 +5017,7 @@ function getRuntimeNodeId(el) {
4485
5017
  }
4486
5018
  function getEditorMetaNodeId(el) {
4487
5019
  if (!(el instanceof HTMLElement)) return null;
4488
- return getEditorMeta(el)?.nodeId ?? null;
5020
+ return getDirectEditorMeta(el)?.nodeId ?? null;
4489
5021
  }
4490
5022
  function isInspectorEligible(el) {
4491
5023
  if (getEditorMetaNodeId(el) !== null) return "editor-meta";
@@ -4509,7 +5041,7 @@ function inferEligibilityFromSortableParent(el) {
4509
5041
  if (child.hasAttribute("data-design-mode-ui")) return false;
4510
5042
  return !isRuntimeVisualInternal(child);
4511
5043
  });
4512
- if (siblingElements.length < 2) return null;
5044
+ if (siblingElements.length < 1) return null;
4513
5045
  return "inferred-sortable-descendant";
4514
5046
  }
4515
5047
  function ensureUniqueNodeId(base, used) {
@@ -5063,7 +5595,15 @@ function buildProjection(elements, rectOverrides) {
5063
5595
  }
5064
5596
  const containerIndex = /* @__PURE__ */ new Map();
5065
5597
  for (const node of nodeMap.values()) {
5066
- if (node.children.length === 0) continue;
5598
+ if (node.children.length === 0) {
5599
+ const scanInfo = elementIndex.get(node.id);
5600
+ const eligible = scanInfo?.editorMeta || scanInfo?.eligibilitySource === "force-allow";
5601
+ if (!eligible) continue;
5602
+ node.sortable = true;
5603
+ node.strategy = "vertical";
5604
+ containerIndex.set(node.id, node);
5605
+ continue;
5606
+ }
5067
5607
  node.sortable = true;
5068
5608
  const firstChild = elementIndex.get(node.children[0]);
5069
5609
  node.strategy = firstChild?.containerStrategy ?? "vertical";
@@ -5134,31 +5674,25 @@ function buildVerticalSlotRect(children, nodeMap, cRect, i, n) {
5134
5674
  const top = i === 0 ? cRect.top : nodeMap.get(children[i - 1])?.rect.bottom ?? cRect.top;
5135
5675
  const bottom = i === n ? cRect.bottom : nodeMap.get(children[i])?.rect.top ?? cRect.bottom;
5136
5676
  const height = Math.max(0, bottom - top);
5137
- return {
5138
- x: cRect.left,
5139
- y: top,
5140
- left: cRect.left,
5141
- top,
5142
- right: cRect.right,
5143
- bottom: top + height,
5144
- width: cRect.width,
5145
- height
5146
- };
5677
+ const prevRect = i > 0 ? nodeMap.get(children[i - 1])?.rect : null;
5678
+ const nextRect = i < n ? nodeMap.get(children[i])?.rect : null;
5679
+ const refRect = prevRect ?? nextRect;
5680
+ const left = refRect ? refRect.left : cRect.left;
5681
+ const right = refRect ? refRect.right : cRect.right;
5682
+ const width = Math.max(0, right - left);
5683
+ return { x: left, y: top, left, top, right, bottom: top + height, width, height };
5147
5684
  }
5148
5685
  function buildHorizontalSlotRect(children, nodeMap, cRect, i, n) {
5149
5686
  const left = i === 0 ? cRect.left : nodeMap.get(children[i - 1])?.rect.right ?? cRect.left;
5150
5687
  const right = i === n ? cRect.right : nodeMap.get(children[i])?.rect.left ?? cRect.right;
5151
5688
  const width = Math.max(0, right - left);
5152
- return {
5153
- x: left,
5154
- y: cRect.top,
5155
- left,
5156
- top: cRect.top,
5157
- right: left + width,
5158
- bottom: cRect.bottom,
5159
- width,
5160
- height: cRect.height
5161
- };
5689
+ const prevRect = i > 0 ? nodeMap.get(children[i - 1])?.rect : null;
5690
+ const nextRect = i < n ? nodeMap.get(children[i])?.rect : null;
5691
+ const refRect = prevRect ?? nextRect;
5692
+ const top = refRect ? refRect.top : cRect.top;
5693
+ const bottom = refRect ? refRect.bottom : cRect.bottom;
5694
+ const height = Math.max(0, bottom - top);
5695
+ return { x: left, y: top, left, top, right: left + width, bottom, width, height };
5162
5696
  }
5163
5697
  function buildRectSlotRect(children, nodeMap, cRect, i, n, rows) {
5164
5698
  const findRow = (childIdx) => rows.find((r) => childIdx >= r.startIdx && childIdx <= r.endIdx) ?? null;
@@ -5222,6 +5756,79 @@ function buildRectSlotRect(children, nodeMap, cRect, i, n, rows) {
5222
5756
  };
5223
5757
  }
5224
5758
 
5759
+ // src/client/dnd/design-mode/history.ts
5760
+ function applyInsertCommand(command, direction, resolveNodeElement) {
5761
+ if (direction === "undo") {
5762
+ const el = resolveNodeElement(command.nodeId);
5763
+ if (el?.isConnected) el.remove();
5764
+ return true;
5765
+ }
5766
+ const container = resolveNodeElement(command.containerId);
5767
+ if (!container) return false;
5768
+ const existing = resolveNodeElement(command.nodeId);
5769
+ if (existing?.isConnected) return true;
5770
+ const template = document.createElement("template");
5771
+ template.innerHTML = command.html.trim();
5772
+ const newEl = template.content.firstElementChild;
5773
+ if (!newEl) return false;
5774
+ newEl.setAttribute("data-dnd-node-id", command.nodeId);
5775
+ newEl.setAttribute("data-dnd-force-allow", "");
5776
+ newEl.setAttribute("data-dnd-moved", "");
5777
+ const kids = Array.from(container.children).filter(
5778
+ (c) => c instanceof HTMLElement
5779
+ );
5780
+ container.insertBefore(newEl, kids[command.index] ?? null);
5781
+ return true;
5782
+ }
5783
+ var DEFAULT_LIMIT = 300;
5784
+ var DndCommandHistory = class {
5785
+ constructor(limit = DEFAULT_LIMIT) {
5786
+ __publicField(this, "limit", limit);
5787
+ __publicField(this, "undoStack", []);
5788
+ __publicField(this, "redoStack", []);
5789
+ }
5790
+ execute(command, apply) {
5791
+ const applied = apply(command, "redo");
5792
+ if (!applied) return false;
5793
+ this.undoStack.push(command);
5794
+ if (this.undoStack.length > this.limit) this.undoStack.shift();
5795
+ this.redoStack = [];
5796
+ return true;
5797
+ }
5798
+ undo(apply) {
5799
+ const command = this.undoStack.pop();
5800
+ if (!command) return false;
5801
+ const applied = apply(command, "undo");
5802
+ if (!applied) {
5803
+ this.undoStack.push(command);
5804
+ return false;
5805
+ }
5806
+ this.redoStack.push(command);
5807
+ return true;
5808
+ }
5809
+ redo(apply) {
5810
+ const command = this.redoStack.pop();
5811
+ if (!command) return false;
5812
+ const applied = apply(command, "redo");
5813
+ if (!applied) {
5814
+ this.redoStack.push(command);
5815
+ return false;
5816
+ }
5817
+ this.undoStack.push(command);
5818
+ return true;
5819
+ }
5820
+ clear() {
5821
+ this.undoStack = [];
5822
+ this.redoStack = [];
5823
+ }
5824
+ canUndo() {
5825
+ return this.undoStack.length > 0;
5826
+ }
5827
+ canRedo() {
5828
+ return this.redoStack.length > 0;
5829
+ }
5830
+ };
5831
+
5225
5832
  // src/client/dnd/design-mode/resize.ts
5226
5833
  function splitTopLevelSpaceSeparated(value) {
5227
5834
  const tokens = [];
@@ -5253,9 +5860,6 @@ function parseSpanValue(value) {
5253
5860
  const parsed = Number.parseInt(match[1], 10);
5254
5861
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
5255
5862
  }
5256
- function clamp(value, min, max) {
5257
- return Math.max(min, Math.min(max, value));
5258
- }
5259
5863
  function resolveResizeTargetElement(selectedEl) {
5260
5864
  const elementChildren = Array.from(selectedEl.children).filter(
5261
5865
  (child2) => child2 instanceof HTMLElement
@@ -5336,100 +5940,6 @@ function applyStyleSnapshot(el, snapshot) {
5336
5940
  }
5337
5941
  }
5338
5942
 
5339
- // src/client/dnd/design-mode/helpers.ts
5340
- var DND_NODE_ID_ATTR2 = "data-dnd-node-id";
5341
- var PAGE_EDGE_GUARD = 10;
5342
- var HARD_PAGE_EDGE_GUARD = 4;
5343
- var SAFE_ZONE_INSET = 8;
5344
- var LARGE_NODE_NEST_BLOCK_MIN_WIDTH = 520;
5345
- var LARGE_NODE_NEST_BLOCK_MIN_HEIGHT = 260;
5346
- var LARGE_NODE_NEST_BLOCK_MIN_AREA = 18e4;
5347
- function isEligibleForDrag(sc) {
5348
- const source = sc.eligibilitySource ?? "editor-meta";
5349
- return source === "editor-meta" || source === "force-allow" || source === "inferred-sortable-descendant";
5350
- }
5351
- function isPointerInPageEdgeZone(px, py) {
5352
- return px <= PAGE_EDGE_GUARD || py <= PAGE_EDGE_GUARD || px >= window.innerWidth - PAGE_EDGE_GUARD || py >= window.innerHeight - PAGE_EDGE_GUARD;
5353
- }
5354
- function isPointerInHardPageEdgeZone(px, py) {
5355
- return px <= HARD_PAGE_EDGE_GUARD || py <= HARD_PAGE_EDGE_GUARD || px >= window.innerWidth - HARD_PAGE_EDGE_GUARD || py >= window.innerHeight - HARD_PAGE_EDGE_GUARD;
5356
- }
5357
- function resolveSafeZoneRect() {
5358
- const root = document.getElementById("root");
5359
- if (!root) return null;
5360
- const rect = root.getBoundingClientRect();
5361
- const left = rect.left + SAFE_ZONE_INSET;
5362
- const top = rect.top + SAFE_ZONE_INSET;
5363
- const right = rect.right - SAFE_ZONE_INSET;
5364
- const bottom = rect.bottom - SAFE_ZONE_INSET;
5365
- if (right <= left || bottom <= top) return null;
5366
- return { left, top, right, bottom };
5367
- }
5368
- function isPointerInSafeZone(px, py, safeZone) {
5369
- if (!safeZone) return true;
5370
- return px >= safeZone.left && px <= safeZone.right && py >= safeZone.top && py <= safeZone.bottom;
5371
- }
5372
- function isPointerOutsideSafeZone(px, py, safeZone) {
5373
- return !isPointerInSafeZone(px, py, safeZone);
5374
- }
5375
- function isPointerBlocked(px, py, safeZone) {
5376
- return isPointerInHardPageEdgeZone(px, py) || isPointerOutsideSafeZone(px, py, safeZone);
5377
- }
5378
- function shouldBlockNestForLargeNode(w, h) {
5379
- return w >= LARGE_NODE_NEST_BLOCK_MIN_WIDTH || h >= LARGE_NODE_NEST_BLOCK_MIN_HEIGHT || w * h >= LARGE_NODE_NEST_BLOCK_MIN_AREA;
5380
- }
5381
- function findScrollableAncestor(start, dx, dy) {
5382
- let el = start;
5383
- while (el && el !== document.body) {
5384
- const cs = getComputedStyle(el);
5385
- if (dy !== 0 && (cs.overflowY === "auto" || cs.overflowY === "scroll") && el.scrollHeight > el.clientHeight + 1) return el;
5386
- if (dx !== 0 && (cs.overflowX === "auto" || cs.overflowX === "scroll") && el.scrollWidth > el.clientWidth + 1) return el;
5387
- el = el.parentElement;
5388
- }
5389
- return null;
5390
- }
5391
- function escapeCssAttrValue(value) {
5392
- const esc = globalThis.CSS?.escape;
5393
- return esc ? esc(value) : value.replace(/["\\]/g, "\\$&");
5394
- }
5395
- function isDesignModeUiElement(el) {
5396
- if (!(el instanceof HTMLElement)) return false;
5397
- return el.hasAttribute("data-design-mode-ui") || !!el.closest("[data-design-mode-ui]");
5398
- }
5399
- function isElementScrollable(el) {
5400
- if (!(el instanceof HTMLElement)) return false;
5401
- const cs = getComputedStyle(el);
5402
- const overY = cs.overflowY;
5403
- const overX = cs.overflowX;
5404
- return (overY === "auto" || overY === "scroll") && el.scrollHeight > el.clientHeight || (overX === "auto" || overX === "scroll") && el.scrollWidth > el.clientWidth;
5405
- }
5406
- function resolveScrollableFromHitStack(stack) {
5407
- for (const hit of stack) {
5408
- if (isDesignModeUiElement(hit)) continue;
5409
- let el = hit;
5410
- while (el) {
5411
- if (isElementScrollable(el)) return el;
5412
- el = el.parentElement;
5413
- }
5414
- }
5415
- return null;
5416
- }
5417
- var _undoParentNodeIdMap = /* @__PURE__ */ new WeakMap();
5418
- var _undoParentNodeIdCounter = 0;
5419
- function ensureParentNodeId(el) {
5420
- const existing = el.getAttribute(DND_NODE_ID_ATTR2);
5421
- if (existing) return existing;
5422
- if (!_undoParentNodeIdMap.has(el)) {
5423
- _undoParentNodeIdMap.set(el, `dnd-parent:${++_undoParentNodeIdCounter}`);
5424
- }
5425
- const id = _undoParentNodeIdMap.get(el);
5426
- el.setAttribute(DND_NODE_ID_ATTR2, id);
5427
- return id;
5428
- }
5429
- function ensureElementNodeId(el) {
5430
- return ensureParentNodeId(el);
5431
- }
5432
-
5433
5943
  // src/client/dnd/design-mode/drag-preview.tsx
5434
5944
  var import_react9 = require("react");
5435
5945
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -5530,7 +6040,7 @@ function findNearestContainerId(nodeId, projection, nodeMap, allowStructuralFall
5530
6040
  const parentId = node?.parentId ?? null;
5531
6041
  if (!parentId) break;
5532
6042
  const parentContainer = projection.containerIndex.get(parentId);
5533
- if (parentContainer && parentContainer.children.length >= 2) {
6043
+ if (parentContainer && parentContainer.children.length >= 1) {
5534
6044
  if (isValidLaneContainer(parentId, projection, nodeMap)) return parentId;
5535
6045
  if (allowStructuralFallback && !structuralFallback) structuralFallback = parentId;
5536
6046
  }
@@ -5615,12 +6125,6 @@ function findDirectChildUnderParent(child, parent) {
5615
6125
  }
5616
6126
  return null;
5617
6127
  }
5618
- function isContiguous(indices) {
5619
- if (indices.length === 0) return false;
5620
- const min = Math.min(...indices);
5621
- const max = Math.max(...indices);
5622
- return max - min + 1 === indices.length;
5623
- }
5624
6128
  function createPlacement(parentNodeId, laneContainerId, commitNodeId, beforeNodeId, afterNodeId, index) {
5625
6129
  return {
5626
6130
  parentNodeId,
@@ -5634,7 +6138,16 @@ function createPlacement(parentNodeId, laneContainerId, commitNodeId, beforeNode
5634
6138
  function buildCommitLaneSnapshot(laneContainerId, projection, nodeMap) {
5635
6139
  const lane = projection.containerIndex.get(laneContainerId);
5636
6140
  const laneElement = nodeMap.get(laneContainerId)?.element ?? null;
5637
- if (!lane || lane.children.length === 0 || !laneElement) return null;
6141
+ if (!lane || !laneElement) return null;
6142
+ if (lane.children.length === 0) {
6143
+ return {
6144
+ laneContainerId,
6145
+ commitParentNodeId: ensureParentNodeId(laneElement),
6146
+ orderedCommitNodeIds: [],
6147
+ commitNodeByAnchorId: /* @__PURE__ */ new Map(),
6148
+ committable: true
6149
+ };
6150
+ }
5638
6151
  const childElements = lane.children.map((childId) => nodeMap.get(childId)?.element ?? null).filter((el) => !!el);
5639
6152
  if (childElements.length !== lane.children.length) return null;
5640
6153
  const commitParent = findLowestCommonAncestor(childElements, laneElement);
@@ -5651,19 +6164,18 @@ function buildCommitLaneSnapshot(laneContainerId, projection, nodeMap) {
5651
6164
  if (seenRoots.has(commitRoot)) return null;
5652
6165
  seenRoots.add(commitRoot);
5653
6166
  commitRoots.push(commitRoot);
5654
- commitNodeByAnchorId.set(anchorNodeId, ensureElementNodeId(commitRoot));
6167
+ commitNodeByAnchorId.set(anchorNodeId, ensureParentNodeId(commitRoot));
5655
6168
  }
5656
6169
  const childList = Array.from(commitParent.children).filter(
5657
6170
  (child) => child instanceof HTMLElement
5658
6171
  );
5659
6172
  const commitRootIndices = commitRoots.map((root) => childList.indexOf(root));
5660
6173
  if (commitRootIndices.some((index) => index < 0)) return null;
5661
- if (!isContiguous(commitRootIndices)) return null;
5662
6174
  const domOrderedRoots = [...commitRoots].sort(
5663
6175
  (a, b) => childList.indexOf(a) - childList.indexOf(b)
5664
6176
  );
5665
- const projectionOrderedIds = commitRoots.map((root) => ensureElementNodeId(root));
5666
- const domOrderedIds = domOrderedRoots.map((root) => ensureElementNodeId(root));
6177
+ const projectionOrderedIds = commitRoots.map((root) => ensureParentNodeId(root));
6178
+ const domOrderedIds = domOrderedRoots.map((root) => ensureParentNodeId(root));
5667
6179
  if (projectionOrderedIds.length !== domOrderedIds.length) return null;
5668
6180
  if (projectionOrderedIds.some((id, index) => id !== domOrderedIds[index])) return null;
5669
6181
  return {
@@ -5708,7 +6220,7 @@ function resolveAppendCommitPlacement(parentEl, commitNodeId, laneContainerId) {
5708
6220
  const children = Array.from(parentEl.children).filter(
5709
6221
  (child) => child instanceof HTMLElement
5710
6222
  );
5711
- const lastChild = children.length > 0 ? ensureElementNodeId(children[children.length - 1]) : null;
6223
+ const lastChild = children.length > 0 ? ensureParentNodeId(children[children.length - 1]) : null;
5712
6224
  return createPlacement(
5713
6225
  ensureParentNodeId(parentEl),
5714
6226
  laneContainerId,
@@ -5763,6 +6275,58 @@ function createDesignModeCollisionDetection(blockedContainerIds) {
5763
6275
  };
5764
6276
  }
5765
6277
 
6278
+ // src/client/dnd/design-mode/arrange-session.ts
6279
+ function serializeArrangeChange(event, index) {
6280
+ return {
6281
+ id: `${event.kind}:${event.operationId}:${index}`,
6282
+ kind: event.kind,
6283
+ operationId: event.operationId,
6284
+ commandType: event.kind === "move" ? event.commandType : void 0,
6285
+ nodeKey: event.nodeKey,
6286
+ nodeId: event.nodeId,
6287
+ inspectorRef: event.inspectorRef,
6288
+ selectedNodeKey: event.kind === "move" ? event.selectedNodeKey : void 0,
6289
+ selectedNodeId: event.kind === "move" ? event.selectedNodeId : void 0,
6290
+ selectedInspectorRef: event.kind === "move" ? event.selectedInspectorRef : void 0,
6291
+ anchorNodeKey: event.kind === "move" ? event.anchorNodeKey : void 0,
6292
+ anchorNodeId: event.kind === "move" ? event.anchorNodeId : void 0,
6293
+ container: event.kind === "move" ? event.container : void 0,
6294
+ beforeSibling: event.kind === "move" ? event.beforeSibling : void 0,
6295
+ afterSibling: event.kind === "move" ? event.afterSibling : void 0,
6296
+ sourceContainer: event.kind === "move" ? event.sourceContainer : void 0,
6297
+ sourceBeforeSibling: event.kind === "move" ? event.sourceBeforeSibling : void 0,
6298
+ sourceAfterSibling: event.kind === "move" ? event.sourceAfterSibling : void 0,
6299
+ diagnostics: event.kind === "move" ? event.diagnostics : void 0,
6300
+ // Insert-specific fields
6301
+ displayName: event.kind === "insert" ? event.displayName : void 0,
6302
+ html: event.kind === "insert" ? event.html : void 0,
6303
+ containerId: event.kind === "insert" ? event.containerId : void 0,
6304
+ containerNodeKey: event.kind === "insert" ? event.containerNodeKey : void 0,
6305
+ insertIndex: event.kind === "insert" ? event.index : void 0,
6306
+ summary: summarizeEditorChangeEvent(event),
6307
+ at: event.at
6308
+ };
6309
+ }
6310
+ function buildArrangeSessionSnapshot(params) {
6311
+ const { enabled, aiOutput } = params;
6312
+ const coalescedChanges = coalesceOrderedChanges(params.orderedChanges);
6313
+ const updatedAt = typeof params.updatedAt === "number" ? params.updatedAt : Date.now();
6314
+ return {
6315
+ mode: enabled ? "arrange" : "off",
6316
+ canUndo: params.orderedChanges.length > 0,
6317
+ changes: coalescedChanges.map(
6318
+ (event, index) => serializeArrangeChange(event, index)
6319
+ ),
6320
+ diagnostics: aiOutput.diagnostics ?? null,
6321
+ output: {
6322
+ json: aiOutput,
6323
+ prompt: aiOutput.aiPromptContext,
6324
+ summary: coalescedChanges.length === 0 ? "No arrange changes recorded yet." : `${coalescedChanges.length} arrange change${coalescedChanges.length === 1 ? "" : "s"} recorded.`
6325
+ },
6326
+ updatedAt
6327
+ };
6328
+ }
6329
+
5766
6330
  // src/client/dnd/design-mode/useDesignModeAutoScroll.ts
5767
6331
  var import_react10 = require("react");
5768
6332
  function edgeVelocity(distanceToEdge, zone, maxSpeed) {
@@ -5778,11 +6342,16 @@ function findScrollableAtPoint(x, y) {
5778
6342
  return resolveScrollableFromHitStack(stack) ?? (document.scrollingElement ?? document.documentElement);
5779
6343
  }
5780
6344
  function getScrollerKey(el) {
5781
- return el.getAttribute?.(DND_NODE_ID_ATTR2) ?? el.id ?? el.tagName.toLowerCase();
6345
+ return el.getAttribute?.(DND_NODE_ID_ATTR) ?? el.id ?? el.tagName.toLowerCase();
5782
6346
  }
5783
6347
  function pointerInsideRect(x, y, rect, inset = 0) {
5784
6348
  return x >= rect.left - inset && x <= rect.right + inset && y >= rect.top - inset && y <= rect.bottom + inset;
5785
6349
  }
6350
+ var AUTO_SCROLL_ZONE_PX = 120;
6351
+ var AUTO_SCROLL_MAX_SPEED = 24;
6352
+ var SCROLLER_RECT_RECALC_INTERVAL = 8;
6353
+ var SCROLLER_SWITCH_FRAMES = 3;
6354
+ var SCROLLER_EDGE_HYSTERESIS_PX = 18;
5786
6355
  function useDesignModeAutoScroll({
5787
6356
  active,
5788
6357
  mouseRef,
@@ -5791,11 +6360,6 @@ function useDesignModeAutoScroll({
5791
6360
  }) {
5792
6361
  (0, import_react10.useEffect)(() => {
5793
6362
  if (!active) return;
5794
- const ZONE = 120;
5795
- const MAX = 24;
5796
- const RECALC_INTERVAL = 8;
5797
- const SWITCH_FRAMES = 3;
5798
- const SCROLLER_EDGE_HYSTERESIS = 18;
5799
6363
  let scrollEl = findScrollableAtPoint(mouseRef.current.x, mouseRef.current.y);
5800
6364
  let cachedRect = scrollEl.getBoundingClientRect();
5801
6365
  let rectAge = 0;
@@ -5803,117 +6367,71 @@ function useDesignModeAutoScroll({
5803
6367
  const tick = () => {
5804
6368
  const { x, y } = mouseRef.current;
5805
6369
  const safeZone = safeZoneRef.current;
5806
- if (++rectAge > RECALC_INTERVAL) {
6370
+ if (++rectAge > SCROLLER_RECT_RECALC_INTERVAL) {
5807
6371
  const candidate = findScrollableAtPoint(x, y);
5808
6372
  const candidateKey = getScrollerKey(candidate);
5809
6373
  const currentKey = getScrollerKey(scrollEl);
5810
6374
  const currentRect = scrollEl.getBoundingClientRect();
5811
- if (candidateKey !== currentKey && !pointerInsideRect(x, y, currentRect, SCROLLER_EDGE_HYSTERESIS)) {
6375
+ if (candidateKey !== currentKey && !pointerInsideRect(x, y, currentRect, SCROLLER_EDGE_HYSTERESIS_PX)) {
5812
6376
  const pendingKey = autoScrollStateRef.current.pendingScrollerKey;
5813
6377
  const pendingFrames = pendingKey === candidateKey ? (autoScrollStateRef.current.pendingFrames ?? 0) + 1 : 1;
5814
6378
  autoScrollStateRef.current.pendingScrollerKey = candidateKey;
5815
6379
  autoScrollStateRef.current.pendingFrames = pendingFrames;
5816
- if (pendingFrames >= SWITCH_FRAMES) {
6380
+ if (pendingFrames >= SCROLLER_SWITCH_FRAMES) {
5817
6381
  scrollEl = candidate;
5818
6382
  cachedRect = candidate.getBoundingClientRect();
5819
6383
  autoScrollStateRef.current.pendingScrollerKey = null;
5820
6384
  autoScrollStateRef.current.pendingFrames = 0;
5821
- }
5822
- } else {
5823
- scrollEl = candidateKey === currentKey ? scrollEl : candidate;
5824
- cachedRect = scrollEl.getBoundingClientRect();
5825
- autoScrollStateRef.current.pendingScrollerKey = null;
5826
- autoScrollStateRef.current.pendingFrames = 0;
5827
- }
5828
- rectAge = 0;
5829
- }
5830
- if (isPointerInPageEdgeZone(x, y) || isPointerOutsideSafeZone(x, y, safeZone)) {
5831
- autoScrollStateRef.current = {
5832
- ...autoScrollStateRef.current,
5833
- scrollerKey: null,
5834
- vx: 0,
5835
- vy: 0
5836
- };
5837
- raf = requestAnimationFrame(tick);
5838
- return;
5839
- }
5840
- const dTop = y - cachedRect.top;
5841
- const dBot = cachedRect.bottom - y;
5842
- let dy = 0;
5843
- if (dTop > 0 && dTop < ZONE) dy = -edgeVelocity(dTop, ZONE, MAX);
5844
- else if (dBot > 0 && dBot < ZONE) dy = edgeVelocity(dBot, ZONE, MAX);
5845
- const dLeft = x - cachedRect.left;
5846
- const dRight = cachedRect.right - x;
5847
- let dx = 0;
5848
- if (dLeft > 0 && dLeft < ZONE) dx = -edgeVelocity(dLeft, ZONE, MAX);
5849
- else if (dRight > 0 && dRight < ZONE) dx = edgeVelocity(dRight, ZONE, MAX);
5850
- if (dy !== 0) scrollEl.scrollTop += dy;
5851
- if (dx !== 0) scrollEl.scrollLeft += dx;
5852
- autoScrollStateRef.current = {
5853
- ...autoScrollStateRef.current,
5854
- scrollerKey: getScrollerKey(scrollEl),
5855
- vx: dx,
5856
- vy: dy
5857
- };
5858
- raf = requestAnimationFrame(tick);
5859
- };
5860
- raf = requestAnimationFrame(tick);
5861
- return () => cancelAnimationFrame(raf);
5862
- }, [active, autoScrollStateRef, mouseRef, safeZoneRef]);
5863
- }
5864
-
5865
- // src/client/dnd/design-mode/useOverlayRefs.ts
5866
- var import_react11 = require("react");
5867
-
5868
- // src/client/dnd/design-mode/history.ts
5869
- var DEFAULT_LIMIT = 300;
5870
- var DndCommandHistory = class {
5871
- constructor(limit = DEFAULT_LIMIT) {
5872
- this.limit = limit;
5873
- __publicField(this, "undoStack", []);
5874
- __publicField(this, "redoStack", []);
5875
- }
5876
- execute(command, apply) {
5877
- const applied = apply(command, "redo");
5878
- if (!applied) return false;
5879
- this.undoStack.push(command);
5880
- if (this.undoStack.length > this.limit) this.undoStack.shift();
5881
- this.redoStack = [];
5882
- return true;
5883
- }
5884
- undo(apply) {
5885
- const command = this.undoStack.pop();
5886
- if (!command) return false;
5887
- const applied = apply(command, "undo");
5888
- if (!applied) {
5889
- this.undoStack.push(command);
5890
- return false;
5891
- }
5892
- this.redoStack.push(command);
5893
- return true;
5894
- }
5895
- redo(apply) {
5896
- const command = this.redoStack.pop();
5897
- if (!command) return false;
5898
- const applied = apply(command, "redo");
5899
- if (!applied) {
5900
- this.redoStack.push(command);
5901
- return false;
5902
- }
5903
- this.undoStack.push(command);
5904
- return true;
5905
- }
5906
- clear() {
5907
- this.undoStack = [];
5908
- this.redoStack = [];
5909
- }
5910
- canUndo() {
5911
- return this.undoStack.length > 0;
5912
- }
5913
- canRedo() {
5914
- return this.redoStack.length > 0;
5915
- }
5916
- };
6385
+ }
6386
+ } else {
6387
+ scrollEl = candidateKey === currentKey ? scrollEl : candidate;
6388
+ cachedRect = scrollEl.getBoundingClientRect();
6389
+ autoScrollStateRef.current.pendingScrollerKey = null;
6390
+ autoScrollStateRef.current.pendingFrames = 0;
6391
+ }
6392
+ rectAge = 0;
6393
+ }
6394
+ if (isPointerInPageEdgeZone(x, y) || isPointerOutsideSafeZone(x, y, safeZone)) {
6395
+ autoScrollStateRef.current = {
6396
+ ...autoScrollStateRef.current,
6397
+ scrollerKey: null,
6398
+ vx: 0,
6399
+ vy: 0
6400
+ };
6401
+ raf = requestAnimationFrame(tick);
6402
+ return;
6403
+ }
6404
+ const dTop = y - cachedRect.top;
6405
+ const dBot = cachedRect.bottom - y;
6406
+ let dy = 0;
6407
+ if (dTop > 0 && dTop < AUTO_SCROLL_ZONE_PX)
6408
+ dy = -edgeVelocity(dTop, AUTO_SCROLL_ZONE_PX, AUTO_SCROLL_MAX_SPEED);
6409
+ else if (dBot > 0 && dBot < AUTO_SCROLL_ZONE_PX)
6410
+ dy = edgeVelocity(dBot, AUTO_SCROLL_ZONE_PX, AUTO_SCROLL_MAX_SPEED);
6411
+ const dLeft = x - cachedRect.left;
6412
+ const dRight = cachedRect.right - x;
6413
+ let dx = 0;
6414
+ if (dLeft > 0 && dLeft < AUTO_SCROLL_ZONE_PX)
6415
+ dx = -edgeVelocity(dLeft, AUTO_SCROLL_ZONE_PX, AUTO_SCROLL_MAX_SPEED);
6416
+ else if (dRight > 0 && dRight < AUTO_SCROLL_ZONE_PX)
6417
+ dx = edgeVelocity(dRight, AUTO_SCROLL_ZONE_PX, AUTO_SCROLL_MAX_SPEED);
6418
+ if (dy !== 0) scrollEl.scrollTop += dy;
6419
+ if (dx !== 0) scrollEl.scrollLeft += dx;
6420
+ autoScrollStateRef.current = {
6421
+ ...autoScrollStateRef.current,
6422
+ scrollerKey: getScrollerKey(scrollEl),
6423
+ vx: dx,
6424
+ vy: dy
6425
+ };
6426
+ raf = requestAnimationFrame(tick);
6427
+ };
6428
+ raf = requestAnimationFrame(tick);
6429
+ return () => cancelAnimationFrame(raf);
6430
+ }, [active, autoScrollStateRef, mouseRef, safeZoneRef]);
6431
+ }
6432
+
6433
+ // src/client/dnd/design-mode/useOverlayRefs.ts
6434
+ var import_react11 = require("react");
5917
6435
 
5918
6436
  // src/client/dnd/design-mode/sortable-preview-animator.ts
5919
6437
  function isIdentityTransform(transform) {
@@ -6127,43 +6645,6 @@ function useHoverDetection({
6127
6645
  // src/client/dnd/design-mode/useDragVisualLoop.ts
6128
6646
  var import_react13 = require("react");
6129
6647
 
6130
- // src/client/dnd/design-mode/drag-lock.ts
6131
- var MIN_UNLOCK_MARGIN = 32;
6132
- var MAX_UNLOCK_MARGIN = 72;
6133
- var MIN_UNLOCK_DWELL_MS = 170;
6134
- var MAX_UNLOCK_DWELL_MS = 300;
6135
- function clamp2(value, min, max) {
6136
- return Math.min(max, Math.max(min, value));
6137
- }
6138
- function resolveCrossUnlockParams(rect) {
6139
- const width = Math.max(0, rect.right - rect.left);
6140
- const height = Math.max(0, rect.bottom - rect.top);
6141
- const shortest = Math.max(1, Math.min(width, height));
6142
- const margin = Math.round(clamp2(shortest * 0.14, MIN_UNLOCK_MARGIN, MAX_UNLOCK_MARGIN));
6143
- const dwellMs = Math.round(clamp2(shortest * 0.7, MIN_UNLOCK_DWELL_MS, MAX_UNLOCK_DWELL_MS));
6144
- return { margin, dwellMs };
6145
- }
6146
- function checkCrossUnlock(lock, adjustedPx, adjustedPy, lockRect, now, margin, dwellMs, graceMs) {
6147
- if (lock.isCrossUnlocked) {
6148
- if (lock.unlockUntilTs !== null && now < lock.unlockUntilTs) return lock;
6149
- return lock;
6150
- }
6151
- const outsideBand = adjustedPx < lockRect.left - margin || adjustedPx > lockRect.right + margin || adjustedPy < lockRect.top - margin || adjustedPy > lockRect.bottom + margin;
6152
- if (!outsideBand) {
6153
- return lock.exitSince !== null ? { ...lock, exitSince: null } : lock;
6154
- }
6155
- const exitSince = lock.exitSince ?? now;
6156
- if (now - exitSince >= dwellMs) {
6157
- return {
6158
- containerId: lock.containerId,
6159
- isCrossUnlocked: true,
6160
- exitSince,
6161
- unlockUntilTs: now + graceMs
6162
- };
6163
- }
6164
- return lock.exitSince !== null ? lock : { ...lock, exitSince: now };
6165
- }
6166
-
6167
6648
  // src/client/dnd/design-mode/quadtree.ts
6168
6649
  function overlaps(a, b) {
6169
6650
  return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
@@ -6173,6 +6654,7 @@ function fitsInside(outer, inner) {
6173
6654
  }
6174
6655
  var CAPACITY = 8;
6175
6656
  var MAX_DEPTH2 = 8;
6657
+ var ROOT_BOUNDS_PAD = 100;
6176
6658
  var QTNode = class _QTNode {
6177
6659
  constructor(bounds, depth = 0) {
6178
6660
  __publicField(this, "bounds");
@@ -6270,12 +6752,11 @@ var Quadtree = class _Quadtree {
6270
6752
  if (r.right > maxX) maxX = r.right;
6271
6753
  if (r.bottom > maxY) maxY = r.bottom;
6272
6754
  }
6273
- const PAD = 100;
6274
6755
  const root = new QTNode({
6275
- x: minX - PAD,
6276
- y: minY - PAD,
6277
- w: maxX - minX + 2 * PAD,
6278
- h: maxY - minY + 2 * PAD
6756
+ x: minX - ROOT_BOUNDS_PAD,
6757
+ y: minY - ROOT_BOUNDS_PAD,
6758
+ w: maxX - minX + 2 * ROOT_BOUNDS_PAD,
6759
+ h: maxY - minY + 2 * ROOT_BOUNDS_PAD
6279
6760
  });
6280
6761
  for (const [id, r] of rects) {
6281
6762
  if (r.width > 0 && r.height > 0) {
@@ -6377,9 +6858,9 @@ function getSlotQueryRadius() {
6377
6858
  const vh = window.innerHeight;
6378
6859
  return Math.min(500, Math.max(100, Math.max(vw, vh) * 0.2));
6379
6860
  }
6380
- var NEST_EDGE_FRACTION = 0.28;
6381
- var ORIGIN_RETURN_BAND_PX = 56;
6382
- var LAST_CONTAINER_REENTRY_SLOP_PX = 40;
6861
+ var NEST_EDGE_FRACTION = 0.2;
6862
+ var ORIGIN_RETURN_BAND_PX = 24;
6863
+ var LAST_CONTAINER_REENTRY_SLOP_PX = 16;
6383
6864
  var ROW_OVERLAP_TOLERANCE_PX = 1;
6384
6865
  function normalizeContainerSet(containers) {
6385
6866
  if (!containers) return null;
@@ -6457,8 +6938,6 @@ var DragIntentEngine = class {
6457
6938
  // Pre-computed set of slot IDs that would produce no movement for the active item.
6458
6939
  __publicField(this, "noOpSlotIds");
6459
6940
  __publicField(this, "originContainerId");
6460
- __publicField(this, "laneRootContainerId");
6461
- __publicField(this, "laneParentId");
6462
6941
  __publicField(this, "laneAllowedContainers");
6463
6942
  __publicField(this, "blockedContainerIds");
6464
6943
  __publicField(this, "targetPlane", "lane");
@@ -6470,8 +6949,6 @@ var DragIntentEngine = class {
6470
6949
  this.queryRadius = getSlotQueryRadius();
6471
6950
  this.slotsByContainer = buildSlotsByContainer(projection.slotIndex);
6472
6951
  this.originContainerId = this.getNodeParentContainerId(activeId);
6473
- this.laneRootContainerId = options?.laneRootContainerId ?? this.originContainerId;
6474
- this.laneParentId = this.laneRootContainerId ? this.containerIndex.get(this.laneRootContainerId)?.parentId ?? null : null;
6475
6952
  this.laneAllowedContainers = normalizeContainerSet(options?.laneAllowedContainers);
6476
6953
  this.blockedContainerIds = normalizeContainerSet(options?.blockedContainerIds);
6477
6954
  const noOpSlotIds = /* @__PURE__ */ new Set();
@@ -6503,10 +6980,6 @@ var DragIntentEngine = class {
6503
6980
  setTargetPlane(plane) {
6504
6981
  this.targetPlane = plane;
6505
6982
  }
6506
- setLaneAllowedContainers(containers) {
6507
- this.laneAllowedContainers = normalizeContainerSet(containers);
6508
- this.reset();
6509
- }
6510
6983
  getTargetPlane() {
6511
6984
  return this.targetPlane;
6512
6985
  }
@@ -6605,7 +7078,7 @@ var DragIntentEngine = class {
6605
7078
  if (this.isInvalidDropContainer(pointerContainerId)) return null;
6606
7079
  for (const childId of container.children) {
6607
7080
  if (childId === this.activeId) continue;
6608
- if (this.containerIndex.has(childId)) continue;
7081
+ if (this.isInvalidDropContainer(childId)) continue;
6609
7082
  const childNode = this.getNodeRect(childId);
6610
7083
  if (!childNode) continue;
6611
7084
  if (!pointInRect(px, py, childNode)) continue;
@@ -6630,9 +7103,16 @@ var DragIntentEngine = class {
6630
7103
  if (laneLock && !this.isLaneContainer(id)) continue;
6631
7104
  if (!pointInRect(px, py, node.rect)) continue;
6632
7105
  if (this.hasUsableSlots(id) === false) continue;
6633
- pointerContainers.push({ id, depth: node.depth, area: node.rect.width * node.rect.height });
7106
+ const r = node.rect;
7107
+ const edgeDistX = Math.min(px - r.left, r.right - px);
7108
+ const edgeDistY = Math.min(py - r.top, r.bottom - py);
7109
+ const minDim = Math.min(r.width, r.height);
7110
+ const edgeFraction = Math.min(edgeDistX, edgeDistY) / Math.max(1, minDim);
7111
+ const edgePenalty = edgeFraction < 0.1 ? 1 : 0;
7112
+ pointerContainers.push({ id, depth: node.depth, area: node.rect.width * node.rect.height, edgePenalty });
6634
7113
  }
6635
7114
  pointerContainers.sort((a, b) => {
7115
+ if (a.edgePenalty !== b.edgePenalty) return a.edgePenalty - b.edgePenalty;
6636
7116
  if (b.depth !== a.depth) return b.depth - a.depth;
6637
7117
  return a.area - b.area;
6638
7118
  });
@@ -6645,7 +7125,9 @@ var DragIntentEngine = class {
6645
7125
  }
6646
7126
  if (this.originContainerId && this.originContainerId !== pointerContainerId && !this.isInvalidDropContainer(this.originContainerId) && !this.isBlockedContainer(this.originContainerId) && (!laneLock || this.isLaneContainer(this.originContainerId)) && this.hasUsableSlots(this.originContainerId)) {
6647
7127
  const originNode = this.containerIndex.get(this.originContainerId);
6648
- if (originNode && pointInExpandedRect(px, py, originNode.rect, ORIGIN_RETURN_BAND_PX)) {
7128
+ const currentNode = pointerContainerId ? this.containerIndex.get(pointerContainerId) : null;
7129
+ const originIsDeeper = originNode && (!currentNode || originNode.depth >= currentNode.depth);
7130
+ if (originIsDeeper && originNode && pointInExpandedRect(px, py, originNode.rect, ORIGIN_RETURN_BAND_PX)) {
6649
7131
  pointerContainerId = this.originContainerId;
6650
7132
  }
6651
7133
  }
@@ -6712,8 +7194,13 @@ var DragIntentEngine = class {
6712
7194
  }
6713
7195
  return n;
6714
7196
  }
7197
+ const SLOT_ZONE_PX = 30;
6715
7198
  for (const child of childRects) {
6716
7199
  const midY = child.rect.top + child.rect.height / 2;
7200
+ const clampedTop = Math.max(child.rect.top, midY - SLOT_ZONE_PX);
7201
+ if (py < clampedTop) return child.index;
7202
+ const clampedBottom = Math.min(child.rect.bottom, midY + SLOT_ZONE_PX);
7203
+ if (py > clampedBottom) continue;
6717
7204
  if (py < midY) return child.index;
6718
7205
  }
6719
7206
  return n;
@@ -6782,15 +7269,14 @@ var DragIntentEngine = class {
6782
7269
  hasUsableSlots(containerId) {
6783
7270
  const filtered = this.getContainerSlots(containerId);
6784
7271
  if (filtered.length > 0) return true;
6785
- return (this.slotsByContainer.get(containerId)?.length ?? 0) > 0;
7272
+ const raw = this.slotsByContainer.get(containerId) ?? [];
7273
+ if (raw.length === 0) return false;
7274
+ const container = this.containerIndex.get(containerId);
7275
+ return !container || container.children.length <= 1;
6786
7276
  }
6787
7277
  isLaneContainer(containerId) {
6788
7278
  if (this.laneAllowedContainers) return this.laneAllowedContainers.has(containerId);
6789
- if (!this.laneRootContainerId) return true;
6790
- if (containerId === this.laneRootContainerId) return true;
6791
- const node = this.containerIndex.get(containerId);
6792
- if (!node) return false;
6793
- return node.parentId === this.laneParentId;
7279
+ return true;
6794
7280
  }
6795
7281
  isBlockedContainer(containerId) {
6796
7282
  return this.blockedContainerIds?.has(containerId) ?? false;
@@ -6801,9 +7287,11 @@ function createDragIntentEngine(projection, activeId, options) {
6801
7287
  }
6802
7288
 
6803
7289
  // src/client/dnd/design-mode/drop-line.ts
7290
+ var DROP_LINE_THICKNESS_PX = 2;
7291
+ var DROP_LINE_MIN_LENGTH_PX = 40;
6804
7292
  function slotDropLine(slot, container, projectionTree, sdx, sdy) {
6805
- const T = 2;
6806
- const MIN = 40;
7293
+ const T = DROP_LINE_THICKNESS_PX;
7294
+ const MIN = DROP_LINE_MIN_LENGTH_PX;
6807
7295
  const bp = computeSlotBoundaryPoint(slot, container, projectionTree);
6808
7296
  if (container.strategy === "horizontal") {
6809
7297
  return {
@@ -6843,9 +7331,6 @@ function slotDropLine(slot, container, projectionTree, sdx, sdy) {
6843
7331
  }
6844
7332
 
6845
7333
  // src/client/dnd/design-mode/preview-layout.ts
6846
- function clampIndex(index, min, max) {
6847
- return Math.max(min, Math.min(index, max));
6848
- }
6849
7334
  function getContainerChildren(projection, containerId) {
6850
7335
  return [...projection.containerIndex.get(containerId)?.children ?? []];
6851
7336
  }
@@ -6965,7 +7450,7 @@ function buildPreviewLayout({
6965
7450
  const originalItemIds = getContainerChildren(projection, containerId);
6966
7451
  const activeIndex = originalItemIds.indexOf(anchorNodeId);
6967
7452
  if (activeIndex < 0) return null;
6968
- const overIndex = clampIndex(targetIndex, 0, Math.max(0, originalItemIds.length - 1));
7453
+ const overIndex = clamp(targetIndex, 0, Math.max(0, originalItemIds.length - 1));
6969
7454
  const previewItemIds = arrayMove(originalItemIds, activeIndex, overIndex);
6970
7455
  const strategy = getContainerStrategy(projection, containerId);
6971
7456
  containers.set(containerId, {
@@ -7005,7 +7490,7 @@ function buildPreviewLayout({
7005
7490
  activePreviewContainers.add(sourceContainerId);
7006
7491
  const targetOriginalItemIds = targetChildren.filter((id) => id !== anchorNodeId);
7007
7492
  const targetPreviewItemIds = [...targetOriginalItemIds];
7008
- const targetOverIndex = clampIndex(targetIndex, 0, targetOriginalItemIds.length);
7493
+ const targetOverIndex = clamp(targetIndex, 0, targetOriginalItemIds.length);
7009
7494
  targetPreviewItemIds.splice(targetOverIndex, 0, anchorNodeId);
7010
7495
  const targetStrategy = getContainerStrategy(projection, targetContainerId);
7011
7496
  const targetStrategyItemIds = [...targetOriginalItemIds, anchorNodeId];
@@ -7037,9 +7522,8 @@ function buildPreviewLayout({
7037
7522
  }
7038
7523
 
7039
7524
  // src/client/dnd/design-mode/useDragVisualLoop.ts
7040
- var CONTAINER_SWITCH_HYSTERESIS = 32;
7041
- var CROSS_UNLOCK_GRACE_MS = 320;
7042
- var SMALL_NODE_NEST_DWELL_MS = 180;
7525
+ var CONTAINER_SWITCH_HYSTERESIS = 12;
7526
+ var SMALL_NODE_NEST_DWELL_MS = 100;
7043
7527
  var LARGE_NODE_NEST_DWELL_MS = 320;
7044
7528
  function useDragVisualLoop({
7045
7529
  activeDrag,
@@ -7088,33 +7572,12 @@ function useDragVisualLoop({
7088
7572
  const scrollDy = window.scrollY - snapshot.scroll.y;
7089
7573
  const adjustedPx = px + scrollDx;
7090
7574
  const adjustedPy = py + scrollDy;
7091
- const lockContainer = refs.lockRef.current.containerId ? refs.projectionRef.current.containerIndex.get(refs.lockRef.current.containerId) ?? null : null;
7092
- if (!refs.lockRef.current.isCrossUnlocked && lockContainer) {
7093
- const { margin, dwellMs } = resolveCrossUnlockParams(lockContainer.rect);
7094
- const nextLock = checkCrossUnlock(
7095
- refs.lockRef.current,
7096
- adjustedPx,
7097
- adjustedPy,
7098
- lockContainer.rect,
7099
- performance.now(),
7100
- margin,
7101
- dwellMs,
7102
- CROSS_UNLOCK_GRACE_MS
7103
- );
7104
- if (nextLock !== refs.lockRef.current) {
7105
- refs.lockRef.current = nextLock;
7106
- if (nextLock.isCrossUnlocked) engine.setLaneAllowedContainers(null);
7107
- }
7108
- }
7109
7575
  const nestPreview = engine.peekNestIntent(px, py, window.scrollX, window.scrollY, snapshot.scroll.x, snapshot.scroll.y);
7110
7576
  const isLargeDraggedNode = shouldBlockNestForLargeNode(activeDrag.w, activeDrag.h);
7111
7577
  let allowNest = false;
7112
7578
  if (nestPreview) {
7113
7579
  const childId = nestPreview.slotId.split("::nest::")[1] ?? "";
7114
- const siblings = engine.getContainerChildren(nestPreview.containerId);
7115
- const isSiblingList = siblings.length >= 2;
7116
- const isSourceContainerNest = nestPreview.containerId === refs.sourceContainerIdRef.current;
7117
- allowNest = !!childId && !isSiblingList && !isLargeDraggedNode && !isSourceContainerNest;
7580
+ allowNest = !!childId && !isLargeDraggedNode;
7118
7581
  }
7119
7582
  if (!allowNest) {
7120
7583
  refs.nestCandidateRef.current = null;
@@ -7124,7 +7587,12 @@ function useDragVisualLoop({
7124
7587
  const now = performance.now();
7125
7588
  if (!refs.nestCandidateRef.current || refs.nestCandidateRef.current.id !== nestPreview.slotId) {
7126
7589
  refs.nestCandidateRef.current = { id: nestPreview.slotId, since: now };
7127
- refs.nestTargetRef.current = null;
7590
+ const hintTargetId = nestPreview.slotId.split("::nest::")[1] ?? "";
7591
+ refs.nestTargetRef.current = refs.nodeMapRef.current.get(hintTargetId) ?? null;
7592
+ const hintRect = snapshot.rects.get(hintTargetId);
7593
+ if (hintRect) {
7594
+ setInsideRect(new DOMRect(hintRect.left - scrollDx, hintRect.top - scrollDy, hintRect.width, hintRect.height));
7595
+ }
7128
7596
  setTargetPlane("lane");
7129
7597
  } else if (now - refs.nestCandidateRef.current.since >= (isLargeDraggedNode ? LARGE_NODE_NEST_DWELL_MS : SMALL_NODE_NEST_DWELL_MS)) {
7130
7598
  setTargetPlane("nest");
@@ -7183,18 +7651,6 @@ function useDragVisualLoop({
7183
7651
  setTargetPlane("lane");
7184
7652
  refs.nestCandidateRef.current = null;
7185
7653
  refs.nestTargetRef.current = null;
7186
- const lockedParentId = refs.lockRef.current.containerId;
7187
- if (!refs.lockRef.current.isCrossUnlocked && (!lockedParentId || intent.containerId !== lockedParentId)) {
7188
- refs.lastIntentRef.current = null;
7189
- refs.visualIntentRef.current = null;
7190
- refs.resolvedReorderRef.current = null;
7191
- refs.finalVisualKeyRef.current = null;
7192
- setInsideRect(null);
7193
- setDropLine(null);
7194
- animator.clear();
7195
- rafId = requestAnimationFrame(tick);
7196
- return;
7197
- }
7198
7654
  refs.lastIntentRef.current = intent;
7199
7655
  refs.visualIntentRef.current = intent;
7200
7656
  setInsideRect(null);
@@ -7226,19 +7682,43 @@ function useDragVisualLoop({
7226
7682
  }
7227
7683
  const sourceCommitLane = refs.sourceCommitLaneRef.current ?? getCommitLaneSnapshot(sourceContainerId);
7228
7684
  const targetCommitLane = getCommitLaneSnapshot(intent.containerId);
7229
- const commitPlacement = resolveCommitPlacementFromVisualTarget(
7685
+ let commitPlacement = resolveCommitPlacementFromVisualTarget(
7230
7686
  targetCommitLane,
7231
7687
  sourceCommitLane,
7232
7688
  activeDrag.anchorNodeId,
7233
7689
  targetInsertIndex
7234
7690
  );
7235
7691
  if (!commitPlacement) {
7236
- refs.resolvedReorderRef.current = null;
7237
- refs.finalVisualKeyRef.current = null;
7238
- setDropLine(null);
7239
- animator.clear();
7240
- rafId = requestAnimationFrame(tick);
7241
- return;
7692
+ const containerEl = refs.nodeMapRef.current.get(intent.containerId)?.element;
7693
+ const sourceCommitNodeId = refs.sourceCommitLaneRef.current?.commitNodeByAnchorId.get(activeDrag.anchorNodeId) ?? activeDrag.anchorNodeId;
7694
+ if (!containerEl) {
7695
+ refs.resolvedReorderRef.current = null;
7696
+ refs.finalVisualKeyRef.current = null;
7697
+ setDropLine(null);
7698
+ animator.clear();
7699
+ rafId = requestAnimationFrame(tick);
7700
+ return;
7701
+ }
7702
+ const targetChildren = Array.from(containerEl.children).filter(
7703
+ (child) => child instanceof HTMLElement
7704
+ );
7705
+ const filteredChildren = targetChildren.filter(
7706
+ (child) => child.getAttribute(DND_NODE_ID_ATTR) !== sourceCommitNodeId
7707
+ );
7708
+ const idx = Math.max(0, Math.min(targetInsertIndex, filteredChildren.length));
7709
+ const beforeChild = filteredChildren[idx] ?? null;
7710
+ const afterChild = idx > 0 ? filteredChildren[idx - 1] ?? null : null;
7711
+ const parentNodeId = ensureParentNodeId(containerEl);
7712
+ const beforeNodeId = beforeChild?.getAttribute(DND_NODE_ID_ATTR) ? ensureParentNodeId(beforeChild) : null;
7713
+ const afterNodeId = afterChild?.getAttribute(DND_NODE_ID_ATTR) ? ensureParentNodeId(afterChild) : null;
7714
+ commitPlacement = {
7715
+ parentNodeId,
7716
+ laneContainerId: intent.containerId,
7717
+ commitNodeId: sourceCommitNodeId,
7718
+ beforeNodeId,
7719
+ afterNodeId,
7720
+ index: idx
7721
+ };
7242
7722
  }
7243
7723
  setDropLine(slotDropLine(slot, container, proj.projectionTree, scrollDx, scrollDy));
7244
7724
  refs.resolvedReorderRef.current = {
@@ -7283,56 +7763,175 @@ function useDragVisualLoop({
7283
7763
 
7284
7764
  // src/client/dnd/design-mode/useDragLifecycle.ts
7285
7765
  var import_react14 = require("react");
7286
-
7287
- // src/client/dnd/design-mode/node-key.ts
7288
- var _runtimeKeyMap = /* @__PURE__ */ new WeakMap();
7289
- var _runtimeKeyCounter = 0;
7290
- function getRuntimeKey(el) {
7291
- const existing = _runtimeKeyMap.get(el);
7292
- if (existing) return existing;
7293
- const key2 = `runtime:${++_runtimeKeyCounter}`;
7294
- _runtimeKeyMap.set(el, key2);
7295
- return key2;
7296
- }
7297
- function getNodeKeyFromElement(el) {
7298
- const resolved = resolveDragSurface(el);
7299
- const host = resolved.host ?? el;
7300
- const meta = getEditorMeta(host);
7301
- if (meta) return meta.nodeId;
7302
- const persisted = host.getAttribute("data-dnd-node-id");
7303
- if (persisted) return persisted;
7304
- return getRuntimeKey(host);
7305
- }
7306
- function getInspectorRefFromElement(el) {
7307
- const resolved = resolveDragSurface(el);
7308
- const host = resolved.host ?? el;
7309
- const meta = getEditorMeta(host);
7310
- if (!meta) return null;
7311
- return { relativePath: meta.file, line: meta.line, column: meta.col };
7312
- }
7313
-
7314
- // src/client/dnd/design-mode/useDragLifecycle.ts
7315
- var SOFT_COMMIT_DISTANCE_PX = 14;
7316
- function isLaneCommitValid(intent, resolvedReorder, lockedParentId) {
7317
- if (!intent || intent.mode !== "reorder") return false;
7318
- if (!resolvedReorder) return false;
7319
- if (!lockedParentId) return false;
7320
- if (intent.containerId !== lockedParentId) return false;
7321
- if (resolvedReorder.visual.slotId !== intent.slotId) return false;
7322
- if (resolvedReorder.visual.parentNodeId !== intent.containerId) return false;
7323
- return true;
7324
- }
7325
- function buildRuntimeIdentityLocal(nodeId, resolveNodeElement) {
7766
+ function buildRuntimeIdentityLocal(nodeId, resolveNodeElement, effectiveLaneSnapshot) {
7326
7767
  if (!nodeId) return null;
7327
7768
  const element = resolveNodeElement(nodeId);
7328
7769
  if (!element) return null;
7770
+ const nodeKey = getNodeKeyFromElement(element);
7771
+ const inspectorRef = getInspectorRefFromElement(element);
7772
+ const laneEffectiveNodeId = effectiveLaneSnapshot?.effectiveNodeIdsByCommitNodeId.get(nodeId) ?? null;
7773
+ const effectiveNodeId = typeof laneEffectiveNodeId === "string" && laneEffectiveNodeId.length > 0 ? laneEffectiveNodeId : isSourceBackedRuntimeNodeKey(nodeKey) ? nodeKey : null;
7774
+ const shouldExposeEffectiveIdentity = typeof effectiveNodeId === "string" && effectiveNodeId.length > 0;
7329
7775
  return {
7330
7776
  nodeId,
7331
- nodeKey: getNodeKeyFromElement(element),
7332
- selector: element.getAttribute(DND_NODE_ID_ATTR2) ? `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(nodeId)}"]` : null,
7333
- inspectorRef: getInspectorRefFromElement(element)
7777
+ nodeKey,
7778
+ selector: element.getAttribute(DND_NODE_ID_ATTR) ? buildNodeSelector(nodeId) : null,
7779
+ inspectorRef,
7780
+ ...shouldExposeEffectiveIdentity ? {
7781
+ effectiveNodeId,
7782
+ effectiveNodeKey: effectiveNodeId,
7783
+ effectiveInspectorRef: inspectorRef
7784
+ } : {}
7785
+ };
7786
+ }
7787
+ function buildMovePayloadWithEffectiveIdentities(params) {
7788
+ const {
7789
+ placement,
7790
+ containerIdentity,
7791
+ beforeSiblingIdentity,
7792
+ afterSiblingIdentity,
7793
+ commitIdentity
7794
+ } = params;
7795
+ return {
7796
+ ...placement,
7797
+ effectiveContainerNodeId: containerIdentity?.effectiveNodeId ?? null,
7798
+ effectiveContainerNodeKey: containerIdentity?.effectiveNodeKey ?? null,
7799
+ effectiveContainerInspectorRef: containerIdentity?.effectiveInspectorRef ?? null,
7800
+ effectiveBeforeNodeId: beforeSiblingIdentity?.effectiveNodeId ?? null,
7801
+ effectiveBeforeNodeKey: beforeSiblingIdentity?.effectiveNodeKey ?? null,
7802
+ effectiveBeforeInspectorRef: beforeSiblingIdentity?.effectiveInspectorRef ?? null,
7803
+ effectiveAfterNodeId: afterSiblingIdentity?.effectiveNodeId ?? null,
7804
+ effectiveAfterNodeKey: afterSiblingIdentity?.effectiveNodeKey ?? null,
7805
+ effectiveAfterInspectorRef: afterSiblingIdentity?.effectiveInspectorRef ?? null,
7806
+ effectiveCommitNodeId: commitIdentity?.effectiveNodeId ?? null,
7807
+ effectiveCommitNodeKey: commitIdentity?.effectiveNodeKey ?? null,
7808
+ effectiveCommitInspectorRef: commitIdentity?.effectiveInspectorRef ?? null
7334
7809
  };
7335
7810
  }
7811
+ function recordCommittedMove(args) {
7812
+ const {
7813
+ command,
7814
+ anchorNodeId,
7815
+ selectedNodeId,
7816
+ anchorEl,
7817
+ selectedEl,
7818
+ nodeKey,
7819
+ inspRef,
7820
+ selectedNodeKey,
7821
+ selectedInspectorRef,
7822
+ sourceLaneSnapshot,
7823
+ targetLaneSnapshot,
7824
+ resolveNodeElement,
7825
+ recordMoveChange
7826
+ } = args;
7827
+ const sourceLaneIdentitySnapshot = captureCommitLaneIdentitySnapshot(
7828
+ sourceLaneSnapshot,
7829
+ resolveNodeElement
7830
+ );
7831
+ const targetLaneIdentitySnapshot = command.to.laneContainerId === command.from.laneContainerId ? sourceLaneIdentitySnapshot : captureCommitLaneIdentitySnapshot(targetLaneSnapshot, resolveNodeElement);
7832
+ const targetLaneAfterIdentitySnapshot = targetLaneIdentitySnapshot ? createCommitLaneIdentitySnapshot({
7833
+ laneContainerId: targetLaneIdentitySnapshot.laneContainerId,
7834
+ commitNodeIds: projectCommitNodeIdsForMove({
7835
+ commitNodeIds: targetLaneIdentitySnapshot.commitNodeIds,
7836
+ movedCommitNodeId: command.commitNodeId,
7837
+ targetIndex: command.to.index
7838
+ }),
7839
+ nodeKeysByCommitNodeId: new Map([
7840
+ ...targetLaneIdentitySnapshot.nodeKeysByCommitNodeId,
7841
+ ...sourceLaneIdentitySnapshot?.nodeKeysByCommitNodeId.has(command.commitNodeId) ? [[command.commitNodeId, sourceLaneIdentitySnapshot.nodeKeysByCommitNodeId.get(command.commitNodeId) ?? null]] : []
7842
+ ])
7843
+ }) : null;
7844
+ const sourceContainerIdentity = buildRuntimeIdentityLocal(
7845
+ command.from.parentNodeId ?? command.from.laneContainerId,
7846
+ resolveNodeElement,
7847
+ sourceLaneIdentitySnapshot
7848
+ );
7849
+ const sourceBeforeSiblingIdentity = buildRuntimeIdentityLocal(command.from.beforeNodeId, resolveNodeElement, sourceLaneIdentitySnapshot);
7850
+ const sourceAfterSiblingIdentity = buildRuntimeIdentityLocal(command.from.afterNodeId, resolveNodeElement, sourceLaneIdentitySnapshot);
7851
+ const sourceCommitIdentity = buildRuntimeIdentityLocal(command.commitNodeId, resolveNodeElement, sourceLaneIdentitySnapshot);
7852
+ const containerIdentity = buildRuntimeIdentityLocal(
7853
+ command.to.parentNodeId ?? command.to.laneContainerId,
7854
+ resolveNodeElement,
7855
+ targetLaneAfterIdentitySnapshot
7856
+ );
7857
+ const beforeSiblingIdentity = buildRuntimeIdentityLocal(command.to.beforeNodeId, resolveNodeElement, targetLaneAfterIdentitySnapshot);
7858
+ const afterSiblingIdentity = buildRuntimeIdentityLocal(command.to.afterNodeId, resolveNodeElement, targetLaneAfterIdentitySnapshot);
7859
+ const targetCommitIdentity = buildRuntimeIdentityLocal(command.commitNodeId, resolveNodeElement, targetLaneAfterIdentitySnapshot);
7860
+ const diagnostics = buildMovePersistenceDiagnostics({
7861
+ sourceLane: sourceLaneIdentitySnapshot,
7862
+ targetLane: targetLaneIdentitySnapshot,
7863
+ movedCommitNodeId: command.commitNodeId,
7864
+ sourceLaneContainerId: command.from.laneContainerId,
7865
+ targetLaneContainerId: command.to.laneContainerId,
7866
+ targetIndex: command.to.index
7867
+ });
7868
+ const componentName = getEditorMeta(anchorEl)?.componentName;
7869
+ const operationId = `${command.at}:${anchorNodeId}`;
7870
+ const beforePayload = buildMovePayloadWithEffectiveIdentities({
7871
+ placement: command.from,
7872
+ containerIdentity: sourceContainerIdentity,
7873
+ beforeSiblingIdentity: sourceBeforeSiblingIdentity,
7874
+ afterSiblingIdentity: sourceAfterSiblingIdentity,
7875
+ commitIdentity: sourceCommitIdentity
7876
+ });
7877
+ const afterPayload = buildMovePayloadWithEffectiveIdentities({
7878
+ placement: command.to,
7879
+ containerIdentity,
7880
+ beforeSiblingIdentity,
7881
+ afterSiblingIdentity,
7882
+ commitIdentity: targetCommitIdentity
7883
+ });
7884
+ recordMoveChange({
7885
+ operationId,
7886
+ commandType: command.type,
7887
+ nodeKey,
7888
+ nodeId: anchorNodeId,
7889
+ inspectorRef: inspRef,
7890
+ selectedNodeKey,
7891
+ selectedNodeId,
7892
+ selectedInspectorRef,
7893
+ anchorNodeKey: nodeKey,
7894
+ anchorNodeId,
7895
+ sourceContainer: sourceContainerIdentity,
7896
+ sourceBeforeSibling: sourceBeforeSiblingIdentity,
7897
+ sourceAfterSibling: sourceAfterSiblingIdentity,
7898
+ diagnostics,
7899
+ container: containerIdentity,
7900
+ beforeSibling: beforeSiblingIdentity,
7901
+ afterSibling: afterSiblingIdentity,
7902
+ componentName,
7903
+ before: beforePayload,
7904
+ after: afterPayload
7905
+ });
7906
+ dispatchRuntimeEvent({
7907
+ type: "element-moved",
7908
+ operationId,
7909
+ commandType: command.type,
7910
+ selected: {
7911
+ nodeId: selectedNodeId,
7912
+ nodeKey: selectedNodeKey,
7913
+ selector: selectedEl.getAttribute(DND_NODE_ID_ATTR) ? buildNodeSelector(selectedNodeId) : null,
7914
+ inspectorRef: selectedInspectorRef,
7915
+ ...targetCommitIdentity?.effectiveNodeId ? { effectiveNodeId: targetCommitIdentity.effectiveNodeId, effectiveNodeKey: targetCommitIdentity.effectiveNodeKey } : {}
7916
+ },
7917
+ anchor: {
7918
+ nodeId: anchorNodeId,
7919
+ nodeKey,
7920
+ selector: anchorEl.getAttribute(DND_NODE_ID_ATTR) ? buildNodeSelector(anchorNodeId) : null,
7921
+ inspectorRef: inspRef,
7922
+ ...targetCommitIdentity?.effectiveNodeId ? { effectiveNodeId: targetCommitIdentity.effectiveNodeId, effectiveNodeKey: targetCommitIdentity.effectiveNodeKey } : {}
7923
+ },
7924
+ sourceContainer: sourceContainerIdentity,
7925
+ sourceBeforeSibling: sourceBeforeSiblingIdentity,
7926
+ sourceAfterSibling: sourceAfterSiblingIdentity,
7927
+ container: containerIdentity,
7928
+ beforeSibling: beforeSiblingIdentity,
7929
+ afterSibling: afterSiblingIdentity,
7930
+ diagnostics,
7931
+ before: beforePayload,
7932
+ after: afterPayload
7933
+ });
7934
+ }
7336
7935
  function useDragLifecycle({
7337
7936
  refs,
7338
7937
  pause,
@@ -7414,12 +8013,10 @@ function useDragLifecycle({
7414
8013
  const srcContainerId = anchor.sourceContainerId;
7415
8014
  const sourceCommitLane = getCommitLaneSnapshot(srcContainerId);
7416
8015
  const startPosition = getCurrentCommitPlacement(anchorNodeId, srcContainerId);
7417
- const laneAllowed = srcContainerId ? /* @__PURE__ */ new Set([srcContainerId]) : null;
7418
8016
  const blockedContainerIds = buildBlockedStructuralContainerIds(proj, refs.nodeMapRef.current, srcContainerId);
7419
8017
  refs.blockedContainerIdsRef.current = blockedContainerIds;
7420
8018
  refs.engineRef.current = createDragIntentEngine(proj, anchorNodeId, {
7421
8019
  laneRootContainerId: srcContainerId,
7422
- laneAllowedContainers: laneAllowed,
7423
8020
  blockedContainerIds
7424
8021
  });
7425
8022
  refs.engineRef.current.setTargetPlane("lane");
@@ -7427,7 +8024,7 @@ function useDragLifecycle({
7427
8024
  refs.sourceContainerIdRef.current = srcContainerId;
7428
8025
  refs.sourceCommitLaneRef.current = sourceCommitLane;
7429
8026
  refs.dragSessionAnchorRef.current = anchor;
7430
- refs.lockRef.current = { containerId: srcContainerId, isCrossUnlocked: false, exitSince: null, unlockUntilTs: null };
8027
+ refs.lockRef.current = { containerId: srcContainerId, isCrossUnlocked: true, exitSince: null, unlockUntilTs: null };
7431
8028
  refs.dragStartPositionRef.current = startPosition;
7432
8029
  refs.safeZoneRef.current = resolveSafeZoneRect();
7433
8030
  pause();
@@ -7453,22 +8050,34 @@ function useDragLifecycle({
7453
8050
  const pointer = refs.mouseRef.current;
7454
8051
  const safeZone = refs.safeZoneRef.current;
7455
8052
  const blockedByGuard = isPointerOutsideSafeZone(pointer.x, pointer.y, safeZone) || isPointerInHardPageEdgeZone(pointer.x, pointer.y);
7456
- const sourcePos = refs.dragStartPositionRef.current ?? getCurrentCommitPlacement(anchorNodeId, refs.sourceContainerIdRef.current);
8053
+ let sourcePos = refs.dragStartPositionRef.current ?? getCurrentCommitPlacement(anchorNodeId, refs.sourceContainerIdRef.current);
8054
+ if (!sourcePos && activeEl && activeEl.parentElement) {
8055
+ const parentEl = activeEl.parentElement;
8056
+ const commitNodeId = activeEl.getAttribute(DND_NODE_ID_ATTR) ?? anchorNodeId;
8057
+ const siblings = Array.from(parentEl.children).filter((c) => c instanceof HTMLElement);
8058
+ const idx = siblings.indexOf(activeEl);
8059
+ if (idx >= 0) {
8060
+ sourcePos = {
8061
+ parentNodeId: ensureParentNodeId(parentEl),
8062
+ laneContainerId: refs.sourceContainerIdRef.current,
8063
+ commitNodeId,
8064
+ beforeNodeId: idx < siblings.length - 1 ? siblings[idx + 1].getAttribute(DND_NODE_ID_ATTR) ?? ensureParentNodeId(siblings[idx + 1]) : null,
8065
+ afterNodeId: idx > 0 ? siblings[idx - 1].getAttribute(DND_NODE_ID_ATTR) ?? ensureParentNodeId(siblings[idx - 1]) : null,
8066
+ index: idx
8067
+ };
8068
+ }
8069
+ }
7457
8070
  const intent = refs.visualIntentRef.current;
7458
- const finalVisual = refs.finalVisualKeyRef.current;
7459
8071
  const nestTarget = refs.nestTargetRef.current;
7460
8072
  const intentHasContainer = intent ? refs.projectionRef.current.containerIndex.has(intent.containerId) : false;
7461
8073
  const intentHasSlot = intent ? intent.mode === "nest" || refs.projectionRef.current.slotIndex.has(intent.slotId) : false;
7462
8074
  const intentValid = !!intent && intentHasContainer && intentHasSlot;
7463
8075
  if (!blockedByGuard && activeEl && sourcePos && (nestTarget || intentValid)) {
7464
8076
  const dropTargetId = nestTarget?.nodeId ?? intent?.containerId;
7465
- if (dropTargetId) {
7466
- const targetSc = refs.nodeMapRef.current.get(dropTargetId);
7467
- if (!targetSc || !isEligibleForDrag(targetSc)) {
7468
- clearDragState();
7469
- syncHistoryAvailability();
7470
- return;
7471
- }
8077
+ if (dropTargetId && !refs.nodeMapRef.current.has(dropTargetId)) {
8078
+ clearDragState();
8079
+ syncHistoryAvailability();
8080
+ return;
7472
8081
  }
7473
8082
  let command = null;
7474
8083
  if (nestTarget) {
@@ -7488,46 +8097,8 @@ function useDragLifecycle({
7488
8097
  };
7489
8098
  }
7490
8099
  } else if (intentValid && intent.mode === "reorder") {
7491
- const lockedParentId = refs.lockRef.current.containerId;
7492
- if (!refs.lockRef.current.isCrossUnlocked && (!lockedParentId || intent.containerId !== lockedParentId)) {
7493
- clearDragState();
7494
- syncHistoryAvailability();
7495
- return;
7496
- }
7497
- const snapshot = refs.snapshotRef.current;
7498
- const targetContainer = refs.projectionRef.current.containerIndex.get(intent.containerId);
7499
- if (targetContainer) {
7500
- const scrollDx = window.scrollX - (snapshot?.scroll.x ?? 0);
7501
- const scrollDy = window.scrollY - (snapshot?.scroll.y ?? 0);
7502
- const adjPx = pointer.x + scrollDx;
7503
- const adjPy = pointer.y + scrollDy;
7504
- const r = targetContainer.rect;
7505
- const inContainer = adjPx >= r.left - 40 && adjPx <= r.right + 40 && adjPy >= r.top - 40 && adjPy <= r.bottom + 40;
7506
- if (!inContainer) {
7507
- const softFallbackAllowed = finalVisual?.mode === "reorder" && finalVisual.containerId === intent.containerId && (() => {
7508
- const slot = refs.projectionRef.current.slotIndex.get(finalVisual.slotId);
7509
- const container = refs.projectionRef.current.containerIndex.get(finalVisual.containerId);
7510
- if (!slot || !container) return false;
7511
- const bp = computeSlotBoundaryPoint(slot, container, refs.projectionRef.current.projectionTree);
7512
- const dist = Math.hypot(adjPx - bp.x, adjPy - bp.y);
7513
- return dist <= SOFT_COMMIT_DISTANCE_PX;
7514
- })();
7515
- if (!softFallbackAllowed) {
7516
- clearDragState();
7517
- syncHistoryAvailability();
7518
- return;
7519
- }
7520
- }
7521
- }
7522
8100
  const resolvedReorder = refs.resolvedReorderRef.current;
7523
- const laneValid = refs.lockRef.current.isCrossUnlocked ? !!resolvedReorder && resolvedReorder.visual.slotId === intent.slotId && resolvedReorder.visual.parentNodeId === intent.containerId : isLaneCommitValid(intent, resolvedReorder, lockedParentId);
7524
- if (!laneValid) {
7525
- clearDragState();
7526
- syncHistoryAvailability();
7527
- return;
7528
- }
7529
- const visualParity = !!finalVisual && finalVisual.mode === "reorder" && finalVisual.anchorNodeId === anchorNodeId && finalVisual.slotId === resolvedReorder.visual.slotId && finalVisual.parentNodeId === resolvedReorder.visual.parentNodeId && finalVisual.index === resolvedReorder.visual.index && finalVisual.containerId === intent.containerId;
7530
- if (!visualParity) {
8101
+ if (!resolvedReorder || resolvedReorder.visual.containerId !== intent.containerId) {
7531
8102
  clearDragState();
7532
8103
  syncHistoryAvailability();
7533
8104
  return;
@@ -7543,56 +8114,32 @@ function useDragLifecycle({
7543
8114
  };
7544
8115
  }
7545
8116
  if (command) {
8117
+ const anchorEl = resolveNodeElement(anchorNodeId) ?? activeEl;
8118
+ const selectedEl = resolveNodeElement(selectedNodeId) ?? anchorEl;
8119
+ const nodeKey = getNodeKeyFromElement(anchorEl);
8120
+ const inspRef = getInspectorRefFromElement(anchorEl);
8121
+ const selectedNodeKey = getNodeKeyFromElement(selectedEl);
8122
+ const selectedInspectorRef = getInspectorRefFromElement(selectedEl);
8123
+ const sourceLaneSnapshot = refs.sourceCommitLaneRef.current ?? getCommitLaneSnapshot(command.from.laneContainerId);
8124
+ const targetLaneSnapshot = command.to.laneContainerId === command.from.laneContainerId ? sourceLaneSnapshot : getCommitLaneSnapshot(command.to.laneContainerId);
7546
8125
  const committed = refs.historyRef.current.execute(command, applyCommand);
7547
8126
  if (committed) {
7548
8127
  activeEl.setAttribute("data-dnd-moved", "");
7549
8128
  selectElement(null);
7550
- const anchorEl = resolveNodeElement(anchorNodeId) ?? activeEl;
7551
- const selectedEl = resolveNodeElement(selectedNodeId) ?? anchorEl;
7552
- const nodeKey = getNodeKeyFromElement(anchorEl);
7553
- const inspRef = getInspectorRefFromElement(anchorEl);
7554
- const selectedNodeKey = getNodeKeyFromElement(selectedEl);
7555
- const selectedInspectorRef = getInspectorRefFromElement(selectedEl);
7556
- recordMoveChange({
8129
+ recordCommittedMove({
8130
+ command,
8131
+ anchorNodeId,
8132
+ selectedNodeId,
8133
+ anchorEl,
8134
+ selectedEl,
7557
8135
  nodeKey,
7558
- nodeId: anchorNodeId,
7559
- inspectorRef: inspRef,
8136
+ inspRef,
7560
8137
  selectedNodeKey,
7561
- selectedNodeId,
7562
8138
  selectedInspectorRef,
7563
- anchorNodeKey: nodeKey,
7564
- anchorNodeId,
7565
- componentName: getEditorMeta(anchorEl)?.componentName,
7566
- before: command.from,
7567
- after: command.to
7568
- });
7569
- const containerIdentity = buildRuntimeIdentityLocal(
7570
- command.to.laneContainerId ?? command.to.parentNodeId,
7571
- resolveNodeElement
7572
- );
7573
- const beforeSiblingIdentity = buildRuntimeIdentityLocal(command.to.beforeNodeId, resolveNodeElement);
7574
- const afterSiblingIdentity = buildRuntimeIdentityLocal(command.to.afterNodeId, resolveNodeElement);
7575
- dispatchRuntimeEvent({
7576
- type: "element-moved",
7577
- operationId: `${command.at}:${anchorNodeId}`,
7578
- commandType: command.type,
7579
- selected: {
7580
- nodeId: selectedNodeId,
7581
- nodeKey: selectedNodeKey,
7582
- selector: selectedEl.getAttribute(DND_NODE_ID_ATTR2) ? `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(selectedNodeId)}"]` : null,
7583
- inspectorRef: selectedInspectorRef
7584
- },
7585
- anchor: {
7586
- nodeId: anchorNodeId,
7587
- nodeKey,
7588
- selector: anchorEl.getAttribute(DND_NODE_ID_ATTR2) ? `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(anchorNodeId)}"]` : null,
7589
- inspectorRef: inspRef
7590
- },
7591
- container: containerIdentity,
7592
- beforeSibling: beforeSiblingIdentity,
7593
- afterSibling: afterSiblingIdentity,
7594
- before: command.from,
7595
- after: command.to
8139
+ sourceLaneSnapshot,
8140
+ targetLaneSnapshot,
8141
+ resolveNodeElement,
8142
+ recordMoveChange
7596
8143
  });
7597
8144
  }
7598
8145
  }
@@ -7631,7 +8178,7 @@ function useResizeSession({
7631
8178
  const rect = resizeTargetEl.getBoundingClientRect();
7632
8179
  const selectedNodeKey = getNodeKeyFromElement(selectedEl);
7633
8180
  const targetNodeKey = getNodeKeyFromElement(resizeTargetEl);
7634
- const targetNodeId = ensureElementNodeId(resizeTargetEl);
8181
+ const targetNodeId = ensureParentNodeId(resizeTargetEl);
7635
8182
  const startColSpan = parseSpanValue(getComputedStyle(resizeTargetEl).gridColumnEnd || resizeTargetEl.style.gridColumnEnd);
7636
8183
  const startRowSpan = parseSpanValue(getComputedStyle(resizeTargetEl).gridRowEnd || resizeTargetEl.style.gridRowEnd);
7637
8184
  const columnTrackCount = getGridTrackCount(resizeTargetParent ? getComputedStyle(resizeTargetParent).gridTemplateColumns : null);
@@ -7739,8 +8286,10 @@ function useResizeSession({
7739
8286
  after: { width: afterW, height: afterH, colSpan: afterColSpan, rowSpan: afterRowSpan, styles: afterStyles },
7740
8287
  at: Date.now()
7741
8288
  };
8289
+ const operationId = `${resizeCmd.at}:${selectedNodeId}`;
7742
8290
  refs.historyRef.current.execute(resizeCmd, applyCommand);
7743
8291
  recordResizeChange({
8292
+ operationId,
7744
8293
  nodeKey: selectedNodeKey,
7745
8294
  nodeId: selectedNodeId,
7746
8295
  inspectorRef,
@@ -7750,17 +8299,17 @@ function useResizeSession({
7750
8299
  });
7751
8300
  dispatchRuntimeEvent({
7752
8301
  type: "element-resized",
7753
- operationId: `${resizeCmd.at}:${selectedNodeId}`,
8302
+ operationId,
7754
8303
  selected: {
7755
8304
  nodeId: selectedNodeId,
7756
8305
  nodeKey: selectedNodeKey,
7757
- selector: `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(selectedNodeId)}"]`,
8306
+ selector: buildNodeSelector(selectedNodeId),
7758
8307
  inspectorRef
7759
8308
  },
7760
8309
  anchor: {
7761
8310
  nodeId: targetNodeId,
7762
8311
  nodeKey: targetNodeKey,
7763
- selector: `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(targetNodeId)}"]`,
8312
+ selector: buildNodeSelector(targetNodeId),
7764
8313
  inspectorRef: getInspectorRefFromElement(element)
7765
8314
  },
7766
8315
  resize: {
@@ -7801,6 +8350,169 @@ function useResizeSession({
7801
8350
  return { handleResizeHandlePointerDown };
7802
8351
  }
7803
8352
 
8353
+ // src/client/dnd/design-mode/useExternalDragLoop.ts
8354
+ var import_react16 = require("react");
8355
+ var EXTERNAL_DRAG_ACTIVE_ID = "__external-drag-placeholder__";
8356
+ function useExternalDragLoop({
8357
+ externalDrag,
8358
+ refs,
8359
+ elements,
8360
+ resolveNodeElement,
8361
+ setDropLine
8362
+ }) {
8363
+ const engineRef = (0, import_react16.useRef)(null);
8364
+ const lastIntentRef = (0, import_react16.useRef)(null);
8365
+ (0, import_react16.useEffect)(() => {
8366
+ if (!externalDrag) {
8367
+ engineRef.current = null;
8368
+ lastIntentRef.current = null;
8369
+ setDropLine(null);
8370
+ externalDragMouse.commitRequested = false;
8371
+ return;
8372
+ }
8373
+ const projection = buildProjection(elements);
8374
+ if (projection.containerIndex.size === 0) return;
8375
+ const rects = /* @__PURE__ */ new Map();
8376
+ for (const el of elements) {
8377
+ rects.set(el.nodeId, el.rect);
8378
+ }
8379
+ const nodeMap = /* @__PURE__ */ new Map();
8380
+ for (const el of elements) nodeMap.set(el.nodeId, el);
8381
+ const blockedContainerIds = buildBlockedStructuralContainerIds(projection, nodeMap, null);
8382
+ const engine = new DragIntentEngine(projection, EXTERNAL_DRAG_ACTIVE_ID, {
8383
+ blockedContainerIds
8384
+ });
8385
+ engineRef.current = engine;
8386
+ const scrollX = window.scrollX;
8387
+ const scrollY = window.scrollY;
8388
+ let rafId;
8389
+ const tick = () => {
8390
+ if (externalDragMouse.commitRequested) {
8391
+ externalDragMouse.commitRequested = false;
8392
+ commitExternalDrop(
8393
+ externalDragMouse.x,
8394
+ externalDragMouse.y,
8395
+ engine,
8396
+ externalDrag,
8397
+ refs,
8398
+ resolveNodeElement,
8399
+ setDropLine
8400
+ );
8401
+ return;
8402
+ }
8403
+ const intent = engine.resolve(
8404
+ externalDragMouse.x,
8405
+ externalDragMouse.y,
8406
+ window.scrollX,
8407
+ window.scrollY,
8408
+ scrollX,
8409
+ scrollY
8410
+ );
8411
+ if (intent && intent.mode === "reorder") {
8412
+ lastIntentRef.current = intent;
8413
+ const slot = projection.slotIndex.get(intent.slotId);
8414
+ const container = projection.containerIndex.get(intent.containerId);
8415
+ if (slot && container) {
8416
+ const scrollDx = window.scrollX - scrollX;
8417
+ const scrollDy = window.scrollY - scrollY;
8418
+ setDropLine(slotDropLine(slot, container, projection.projectionTree, scrollDx, scrollDy));
8419
+ } else {
8420
+ setDropLine(null);
8421
+ }
8422
+ } else {
8423
+ lastIntentRef.current = null;
8424
+ setDropLine(null);
8425
+ }
8426
+ rafId = requestAnimationFrame(tick);
8427
+ };
8428
+ rafId = requestAnimationFrame(tick);
8429
+ return () => cancelAnimationFrame(rafId);
8430
+ }, [externalDrag, elements, refs, resolveNodeElement, setDropLine]);
8431
+ }
8432
+ function commitExternalDrop(x, y, engine, component, refs, resolveNodeElement, setDropLine) {
8433
+ const scrollX = window.scrollX;
8434
+ const scrollY = window.scrollY;
8435
+ const intent = engine.resolve(x, y, scrollX, scrollY, scrollX, scrollY);
8436
+ if (!intent || intent.mode !== "reorder") {
8437
+ useDesignModeStore.getState().cancelExternalDrag();
8438
+ setDropLine(null);
8439
+ return;
8440
+ }
8441
+ const containerEl = resolveNodeElement(intent.containerId);
8442
+ if (!containerEl) {
8443
+ useDesignModeStore.getState().cancelExternalDrag();
8444
+ setDropLine(null);
8445
+ return;
8446
+ }
8447
+ const template = document.createElement("template");
8448
+ template.innerHTML = component.html.trim();
8449
+ const newElement = template.content.firstElementChild;
8450
+ if (!newElement) {
8451
+ useDesignModeStore.getState().cancelExternalDrag();
8452
+ setDropLine(null);
8453
+ return;
8454
+ }
8455
+ const nodeId = `insert:${Date.now()}`;
8456
+ newElement.setAttribute(DND_NODE_ID_ATTR, nodeId);
8457
+ newElement.setAttribute("data-dnd-force-allow", "");
8458
+ newElement.setAttribute("data-dnd-moved", "");
8459
+ const children = Array.from(containerEl.children).filter(
8460
+ (child) => child instanceof HTMLElement
8461
+ );
8462
+ const beforeChild = children[intent.index] ?? null;
8463
+ containerEl.insertBefore(newElement, beforeChild);
8464
+ const insertCommand = {
8465
+ type: "insert",
8466
+ nodeId,
8467
+ html: component.html,
8468
+ containerId: intent.containerId,
8469
+ index: intent.index,
8470
+ at: Date.now()
8471
+ };
8472
+ const applied = refs.historyRef.current.execute(insertCommand, (command, direction) => {
8473
+ if (command.type !== "insert") return false;
8474
+ return applyInsertCommand(command, direction, resolveNodeElement);
8475
+ });
8476
+ if (applied) {
8477
+ const nodeKey = getNodeKeyFromElement(newElement);
8478
+ const inspectorRef = getInspectorRefFromElement(newElement);
8479
+ const containerNodeKey = getNodeKeyFromElement(containerEl);
8480
+ let containerIdentity = null;
8481
+ if (containerEl) {
8482
+ containerIdentity = {
8483
+ nodeId: intent.containerId,
8484
+ nodeKey: getNodeKeyFromElement(containerEl),
8485
+ selector: containerEl.getAttribute(DND_NODE_ID_ATTR) ? buildNodeSelector(intent.containerId) : null,
8486
+ inspectorRef: getInspectorRefFromElement(containerEl)
8487
+ };
8488
+ }
8489
+ const operationId = `${insertCommand.at}:${nodeId}`;
8490
+ useDesignModeStore.getState().recordInsertChange({
8491
+ operationId,
8492
+ nodeKey,
8493
+ nodeId,
8494
+ inspectorRef,
8495
+ displayName: component.displayName,
8496
+ html: component.html,
8497
+ containerId: intent.containerId,
8498
+ containerNodeKey,
8499
+ index: intent.index
8500
+ });
8501
+ dispatchRuntimeEvent({
8502
+ type: "element-inserted",
8503
+ operationId,
8504
+ nodeId,
8505
+ nodeKey,
8506
+ displayName: component.displayName,
8507
+ html: component.html,
8508
+ container: containerIdentity,
8509
+ index: intent.index
8510
+ });
8511
+ }
8512
+ setDropLine(null);
8513
+ useDesignModeStore.getState().cancelExternalDrag();
8514
+ }
8515
+
7804
8516
  // src/client/dnd/design-mode/DesignModeOverlay.tsx
7805
8517
  var import_jsx_runtime4 = require("react/jsx-runtime");
7806
8518
  var DROP_ANIMATION = { duration: DURATION_MS, easing: EASING };
@@ -7846,11 +8558,11 @@ function HitArea({
7846
8558
  onResizeHandlePointerDown
7847
8559
  }) {
7848
8560
  const { setNodeRef, listeners, attributes, isDragging } = useDraggable({ id: scanned.nodeId });
7849
- (0, import_react16.useEffect)(() => {
8561
+ (0, import_react17.useEffect)(() => {
7850
8562
  setNodeRef(scanned.element);
7851
8563
  return () => setNodeRef(null);
7852
8564
  }, [setNodeRef, scanned.element]);
7853
- (0, import_react16.useLayoutEffect)(() => {
8565
+ (0, import_react17.useLayoutEffect)(() => {
7854
8566
  const el = scanned.element;
7855
8567
  if (isDragging) {
7856
8568
  el.style.opacity = "0";
@@ -7858,16 +8570,16 @@ function HitArea({
7858
8570
  el.style.removeProperty("opacity");
7859
8571
  }
7860
8572
  }, [isDragging, scanned.element]);
7861
- (0, import_react16.useEffect)(() => () => {
8573
+ (0, import_react17.useEffect)(() => () => {
7862
8574
  scanned.element.style.removeProperty("opacity");
7863
8575
  scanned.element.style.removeProperty("transform");
7864
8576
  scanned.element.style.removeProperty("transition");
7865
8577
  scanned.element.style.removeProperty("will-change");
7866
8578
  }, [scanned.element]);
7867
- const divRef = (0, import_react16.useRef)(null);
7868
- const [hoverResizeHandle, setHoverResizeHandle] = (0, import_react16.useState)(null);
8579
+ const divRef = (0, import_react17.useRef)(null);
8580
+ const [hoverResizeHandle, setHoverResizeHandle] = (0, import_react17.useState)(null);
7869
8581
  const isSelected = scanned.nodeId === selectedNodeId;
7870
- (0, import_react16.useEffect)(() => {
8582
+ (0, import_react17.useEffect)(() => {
7871
8583
  const div = divRef.current;
7872
8584
  if (!div) return;
7873
8585
  const onWheel = (e) => {
@@ -7938,13 +8650,14 @@ function DesignModeOverlay() {
7938
8650
  const redoLastChange = useDesignModeStore((s) => s.redoLastChange);
7939
8651
  const orderedChanges = useDesignModeStore((s) => s.orderedChanges);
7940
8652
  const exportChangesForAI = useDesignModeStore((s) => s.exportChangesForAI);
8653
+ const externalDrag = useDesignModeStore((s) => s.externalDrag);
7941
8654
  const { elements, pause, resume } = useElementScanner(enabled);
7942
- const nodeMap = (0, import_react16.useMemo)(() => {
8655
+ const nodeMap = (0, import_react17.useMemo)(() => {
7943
8656
  const m = /* @__PURE__ */ new Map();
7944
8657
  for (const el of elements) m.set(el.nodeId, el);
7945
8658
  return m;
7946
8659
  }, [elements]);
7947
- const draggableElements = (0, import_react16.useMemo)(() => {
8660
+ const draggableElements = (0, import_react17.useMemo)(() => {
7948
8661
  if (elements.length === 0) return [];
7949
8662
  const projection = buildProjection(elements);
7950
8663
  return collectDraggableAnchors(elements, projection, nodeMap);
@@ -7952,21 +8665,21 @@ function DesignModeOverlay() {
7952
8665
  const refs = useOverlayRefs(undoRequestId, redoRequestId);
7953
8666
  refs.elementsRef.current = elements;
7954
8667
  refs.nodeMapRef.current = nodeMap;
7955
- (0, import_react16.useEffect)(() => {
8668
+ (0, import_react17.useEffect)(() => {
7956
8669
  const m = /* @__PURE__ */ new Map();
7957
8670
  for (const el of draggableElements) m.set(el.nodeId, el);
7958
8671
  refs.draggableNodeMapRef.current = m;
7959
8672
  }, [draggableElements]);
7960
- const resolveNodeElement = (0, import_react16.useCallback)((nodeId) => {
8673
+ const resolveNodeElement = (0, import_react17.useCallback)((nodeId) => {
7961
8674
  const known = refs.nodeMapRef.current.get(nodeId)?.element;
7962
8675
  if (known) return known;
7963
- return document.querySelector(`[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(nodeId)}"]`);
8676
+ return document.querySelector(buildNodeSelector(nodeId));
7964
8677
  }, []);
7965
- const clearCommitLaneCache = (0, import_react16.useCallback)(() => {
8678
+ const clearCommitLaneCache = (0, import_react17.useCallback)(() => {
7966
8679
  refs.commitLaneCacheRef.current.clear();
7967
8680
  refs.sourceCommitLaneRef.current = null;
7968
8681
  }, []);
7969
- const getCommitLaneSnapshot = (0, import_react16.useCallback)((laneContainerId) => {
8682
+ const getCommitLaneSnapshot = (0, import_react17.useCallback)((laneContainerId) => {
7970
8683
  if (!laneContainerId) return null;
7971
8684
  if (refs.commitLaneCacheRef.current.has(laneContainerId)) {
7972
8685
  return refs.commitLaneCacheRef.current.get(laneContainerId) ?? null;
@@ -7975,15 +8688,15 @@ function DesignModeOverlay() {
7975
8688
  refs.commitLaneCacheRef.current.set(laneContainerId, snapshot);
7976
8689
  return snapshot;
7977
8690
  }, []);
7978
- const getCurrentCommitPlacement = (0, import_react16.useCallback)((anchorNodeId, laneContainerId) => {
8691
+ const getCurrentCommitPlacement = (0, import_react17.useCallback)((anchorNodeId, laneContainerId) => {
7979
8692
  const lane = getCommitLaneSnapshot(laneContainerId);
7980
8693
  return resolveCurrentCommitPlacement(lane, anchorNodeId);
7981
8694
  }, [getCommitLaneSnapshot]);
7982
- const applyCommand = (0, import_react16.useCallback)((command, direction) => {
8695
+ const applyCommand = (0, import_react17.useCallback)((command, direction) => {
7983
8696
  if (command.type === "resize") {
7984
8697
  const resCmd = command;
7985
8698
  const targetId = resCmd.targetNodeId ?? resCmd.nodeId;
7986
- const targetEl = document.querySelector(`[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(targetId)}"]`);
8699
+ const targetEl = document.querySelector(buildNodeSelector(targetId));
7987
8700
  if (!targetEl) return false;
7988
8701
  const snapshot = direction === "redo" ? resCmd.after : resCmd.before;
7989
8702
  if (snapshot.styles) {
@@ -7994,29 +8707,36 @@ function DesignModeOverlay() {
7994
8707
  }
7995
8708
  return true;
7996
8709
  }
8710
+ if (command.type === "insert") {
8711
+ return applyInsertCommand(command, direction, resolveNodeElement);
8712
+ }
7997
8713
  const applied = applyDndCommandPlacement(command, direction, resolveNodeElement);
7998
8714
  if (!applied) return false;
7999
8715
  clearCommitLaneCache();
8000
8716
  return true;
8001
8717
  }, [clearCommitLaneCache, resolveNodeElement]);
8002
- const syncHistoryAvailability = (0, import_react16.useCallback)(() => {
8718
+ const syncHistoryAvailability = (0, import_react17.useCallback)(() => {
8003
8719
  if (historyMode === "host") {
8004
8720
  setHistoryAvailability(false, false);
8005
8721
  return;
8006
8722
  }
8007
8723
  setHistoryAvailability(refs.historyRef.current.canUndo(), refs.historyRef.current.canRedo());
8008
8724
  }, [historyMode, setHistoryAvailability]);
8725
+ (0, import_react17.useEffect)(() => {
8726
+ refs.historyRef.current.clear();
8727
+ syncHistoryAvailability();
8728
+ }, [enabled, syncHistoryAvailability]);
8009
8729
  const sensors = useSensors(
8010
8730
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
8011
8731
  );
8012
- const collisionDetection = (0, import_react16.useCallback)(
8732
+ const collisionDetection = (0, import_react17.useCallback)(
8013
8733
  (args) => createDesignModeCollisionDetection(refs.blockedContainerIdsRef.current)(args),
8014
8734
  []
8015
8735
  // eslint-disable-line react-hooks/exhaustive-deps -- refs are stable
8016
8736
  );
8017
- const [insideRect, setInsideRect] = (0, import_react16.useState)(null);
8018
- const [dropLine, setDropLine] = (0, import_react16.useState)(null);
8019
- (0, import_react16.useEffect)(() => {
8737
+ const [insideRect, setInsideRect] = (0, import_react17.useState)(null);
8738
+ const [dropLine, setDropLine] = (0, import_react17.useState)(null);
8739
+ (0, import_react17.useEffect)(() => {
8020
8740
  if (!enabled) return;
8021
8741
  const fn = (e) => {
8022
8742
  refs.mouseRef.current = { x: e.clientX, y: e.clientY };
@@ -8024,7 +8744,7 @@ function DesignModeOverlay() {
8024
8744
  window.addEventListener("pointermove", fn, { capture: true, passive: true });
8025
8745
  return () => window.removeEventListener("pointermove", fn, { capture: true });
8026
8746
  }, [enabled]);
8027
- (0, import_react16.useEffect)(() => {
8747
+ (0, import_react17.useEffect)(() => {
8028
8748
  if (!enabled) return;
8029
8749
  if (historyMode !== "local") return;
8030
8750
  const onKeyDown = (e) => {
@@ -8053,7 +8773,7 @@ function DesignModeOverlay() {
8053
8773
  setInsideRect,
8054
8774
  setDropLine
8055
8775
  });
8056
- (0, import_react16.useEffect)(() => {
8776
+ (0, import_react17.useEffect)(() => {
8057
8777
  if (!enabled || activeDrag) return;
8058
8778
  if (undoRequestId === refs.processedUndoIdRef.current) return;
8059
8779
  refs.processedUndoIdRef.current = undoRequestId;
@@ -8070,7 +8790,7 @@ function DesignModeOverlay() {
8070
8790
  });
8071
8791
  syncHistoryAvailability();
8072
8792
  }, [undoRequestId, enabled, activeDrag, applyCommand, resume, selectElement, syncHistoryAvailability, undoLastChange]);
8073
- (0, import_react16.useEffect)(() => {
8793
+ (0, import_react17.useEffect)(() => {
8074
8794
  if (!enabled || activeDrag || historyMode !== "local") return;
8075
8795
  if (redoRequestId === refs.processedRedoIdRef.current) return;
8076
8796
  refs.processedRedoIdRef.current = redoRequestId;
@@ -8082,42 +8802,29 @@ function DesignModeOverlay() {
8082
8802
  }
8083
8803
  syncHistoryAvailability();
8084
8804
  }, [redoRequestId, enabled, activeDrag, applyCommand, historyMode, resume, selectElement, syncHistoryAvailability, redoLastChange]);
8085
- (0, import_react16.useEffect)(() => {
8805
+ (0, import_react17.useEffect)(() => {
8086
8806
  syncHistoryAvailability();
8087
8807
  }, [syncHistoryAvailability]);
8088
- (0, import_react16.useEffect)(() => {
8808
+ (0, import_react17.useEffect)(() => {
8089
8809
  dispatchRuntimeEvent({
8090
8810
  type: "design-mode-changed",
8091
8811
  mode: enabled ? "design" : "off"
8092
8812
  });
8093
8813
  }, [enabled]);
8094
- (0, import_react16.useEffect)(() => {
8814
+ (0, import_react17.useEffect)(() => {
8095
8815
  const aiOutput = exportChangesForAI();
8096
8816
  const updatedAt = orderedChanges.length > 0 ? orderedChanges[orderedChanges.length - 1]?.at ?? Date.now() : Date.now();
8097
8817
  dispatchRuntimeEvent({
8098
8818
  type: "arrange-session-changed",
8099
- session: {
8100
- mode: enabled ? "arrange" : "off",
8101
- canUndo: orderedChanges.length > 0,
8102
- changes: orderedChanges.map((event, index) => ({
8103
- id: `${event.kind}:${event.nodeKey}:${event.at}:${index}`,
8104
- kind: event.kind,
8105
- nodeKey: event.nodeKey,
8106
- nodeId: event.nodeId,
8107
- inspectorRef: event.inspectorRef,
8108
- summary: summarizeEditorChangeEvent(event),
8109
- at: event.at
8110
- })),
8111
- output: {
8112
- json: aiOutput,
8113
- prompt: aiOutput.aiPromptContext,
8114
- summary: orderedChanges.length === 0 ? "No arrange changes recorded yet." : `${orderedChanges.length} arrange change${orderedChanges.length === 1 ? "" : "s"} recorded.`
8115
- },
8819
+ session: buildArrangeSessionSnapshot({
8820
+ enabled,
8821
+ orderedChanges,
8822
+ aiOutput,
8116
8823
  updatedAt
8117
- }
8824
+ })
8118
8825
  });
8119
8826
  }, [enabled, exportChangesForAI, orderedChanges]);
8120
- (0, import_react16.useEffect)(() => {
8827
+ (0, import_react17.useEffect)(() => {
8121
8828
  if (!enabled) return;
8122
8829
  document.body.classList.add("design-mode-active");
8123
8830
  return () => document.body.classList.remove("design-mode-active");
@@ -8130,6 +8837,13 @@ function DesignModeOverlay() {
8130
8837
  autoScrollStateRef: refs.autoScrollStateRef
8131
8838
  });
8132
8839
  useDragVisualLoop({ activeDrag, refs, getCommitLaneSnapshot, setInsideRect, setDropLine });
8840
+ useExternalDragLoop({
8841
+ externalDrag,
8842
+ refs,
8843
+ elements,
8844
+ resolveNodeElement,
8845
+ setDropLine
8846
+ });
8133
8847
  const { handleResizeHandlePointerDown } = useResizeSession({
8134
8848
  enabled,
8135
8849
  refs,
@@ -8185,7 +8899,7 @@ function DesignModeOverlay() {
8185
8899
  }
8186
8900
  }
8187
8901
  ),
8188
- dropLine && activeDrag && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
8902
+ dropLine && (activeDrag ?? externalDrag) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
8189
8903
  "div",
8190
8904
  {
8191
8905
  "data-design-mode-ui": true,
@@ -8210,8 +8924,8 @@ function DesignModeOverlay() {
8210
8924
  }
8211
8925
 
8212
8926
  // src/client/dnd/design-mode/DesignModeErrorBoundary.tsx
8213
- var import_react17 = __toESM(require("react"));
8214
- var DesignModeErrorBoundary = class extends import_react17.default.Component {
8927
+ var import_react18 = __toESM(require("react"));
8928
+ var DesignModeErrorBoundary = class extends import_react18.default.Component {
8215
8929
  constructor(props) {
8216
8930
  super(props);
8217
8931
  this.state = { hasError: false };
@@ -8244,35 +8958,40 @@ var combinedCollisionDetection = (args) => {
8244
8958
 
8245
8959
  // src/client/dnd/DndProvider.tsx
8246
8960
  var import_jsx_runtime5 = require("react/jsx-runtime");
8961
+ var externalDragMouse = { x: 0, y: 0, commitRequested: false };
8247
8962
  function DndProvider({ children }) {
8248
8963
  const setActive = useDndStore((s) => s.setActive);
8249
8964
  const dragEndHandlers = useDndStore((s) => s.dragEndHandlers);
8250
- const toggle = useDesignModeStore((s) => s.toggle);
8251
8965
  const designModeEnabled = useDesignModeStore((s) => s.enabled);
8252
- const setDesignModeEnabled = useDesignModeStore((s) => s.setEnabled);
8253
8966
  const setHistoryMode = useDesignModeStore((s) => s.setHistoryMode);
8254
8967
  const setInspectorTheme = useDesignModeStore((s) => s.setInspectorTheme);
8255
8968
  const requestUndo = useDesignModeStore((s) => s.requestUndo);
8256
- (0, import_react18.useEffect)(() => {
8969
+ const transitionDesignMode = (0, import_react19.useCallback)((nextEnabled) => {
8970
+ const designModeState = useDesignModeStore.getState();
8971
+ if (designModeState.enabled === nextEnabled) return;
8972
+ designModeState.resetArrangeSession();
8973
+ designModeState.setEnabled(nextEnabled);
8974
+ }, []);
8975
+ (0, import_react19.useEffect)(() => {
8257
8976
  const handleKeyDown = (e) => {
8258
8977
  if (e.ctrlKey && e.shiftKey && e.key === "D") {
8259
8978
  e.preventDefault();
8260
- toggle();
8979
+ transitionDesignMode(!useDesignModeStore.getState().enabled);
8261
8980
  }
8262
8981
  };
8263
8982
  window.addEventListener("keydown", handleKeyDown);
8264
8983
  return () => window.removeEventListener("keydown", handleKeyDown);
8265
- }, [toggle]);
8266
- (0, import_react18.useEffect)(() => {
8984
+ }, [transitionDesignMode]);
8985
+ (0, import_react19.useEffect)(() => {
8267
8986
  const handleHostCommand = (event) => {
8268
8987
  const detail = event.detail;
8269
8988
  if (!detail || typeof detail !== "object") return;
8270
8989
  switch (detail.type) {
8271
8990
  case "sync-editor-state":
8272
8991
  if (detail.mode === "design") {
8273
- setDesignModeEnabled(true);
8992
+ transitionDesignMode(true);
8274
8993
  } else if (detail.mode === "inspect" || detail.mode === "off") {
8275
- setDesignModeEnabled(false);
8994
+ transitionDesignMode(false);
8276
8995
  }
8277
8996
  if (detail.historyMode) {
8278
8997
  setHistoryMode(detail.historyMode);
@@ -8282,7 +9001,7 @@ function DndProvider({ children }) {
8282
9001
  }
8283
9002
  break;
8284
9003
  case "set-design-mode":
8285
- setDesignModeEnabled(detail.enabled);
9004
+ transitionDesignMode(detail.enabled);
8286
9005
  break;
8287
9006
  case "set-history-mode":
8288
9007
  setHistoryMode(detail.historyMode);
@@ -8293,6 +9012,21 @@ function DndProvider({ children }) {
8293
9012
  case "request-undo":
8294
9013
  requestUndo();
8295
9014
  break;
9015
+ case "external-drag-start":
9016
+ useDesignModeStore.getState().startExternalDrag(detail.component);
9017
+ break;
9018
+ case "external-drag-move":
9019
+ externalDragMouse.x = detail.clientX;
9020
+ externalDragMouse.y = detail.clientY;
9021
+ break;
9022
+ case "external-drag-end":
9023
+ externalDragMouse.x = detail.clientX;
9024
+ externalDragMouse.y = detail.clientY;
9025
+ externalDragMouse.commitRequested = true;
9026
+ break;
9027
+ case "external-drag-cancel":
9028
+ useDesignModeStore.getState().cancelExternalDrag();
9029
+ break;
8296
9030
  default:
8297
9031
  break;
8298
9032
  }
@@ -8302,7 +9036,7 @@ function DndProvider({ children }) {
8302
9036
  window.removeEventListener(HOST_COMMAND_EVENT, handleHostCommand);
8303
9037
  };
8304
9038
  }, [
8305
- setDesignModeEnabled,
9039
+ transitionDesignMode,
8306
9040
  setHistoryMode,
8307
9041
  setInspectorTheme,
8308
9042
  requestUndo
@@ -8310,7 +9044,7 @@ function DndProvider({ children }) {
8310
9044
  const pointerSensor = useSensor(PointerSensor, { activationConstraint: { distance: 8 } });
8311
9045
  const keyboardSensor = useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates });
8312
9046
  const sensors = useSensors(pointerSensor, keyboardSensor);
8313
- const handleDragStart = (0, import_react18.useCallback)(
9047
+ const handleDragStart = (0, import_react19.useCallback)(
8314
9048
  (event) => {
8315
9049
  const { active } = event;
8316
9050
  const element = document.querySelector(`[data-canvas-node="${active.id}"]`);
@@ -8318,7 +9052,7 @@ function DndProvider({ children }) {
8318
9052
  },
8319
9053
  [setActive]
8320
9054
  );
8321
- const handleDragEnd = (0, import_react18.useCallback)(
9055
+ const handleDragEnd = (0, import_react19.useCallback)(
8322
9056
  (event) => {
8323
9057
  const { active, over } = event;
8324
9058
  const dragEndEvent = {
@@ -8334,9 +9068,10 @@ function DndProvider({ children }) {
8334
9068
  },
8335
9069
  [dragEndHandlers, setActive]
8336
9070
  );
8337
- const handleDesignModeCrash = (0, import_react18.useCallback)(() => {
9071
+ const handleDesignModeCrash = (0, import_react19.useCallback)(() => {
8338
9072
  const designModeState = useDesignModeStore.getState();
8339
9073
  designModeState.resetAll();
9074
+ designModeState.resetArrangeSession();
8340
9075
  designModeState.setEnabled(false);
8341
9076
  }, []);
8342
9077
  if (designModeEnabled) {
@@ -8377,7 +9112,7 @@ function DndProvider({ children }) {
8377
9112
  }
8378
9113
 
8379
9114
  // src/client/useArchieDevToolsRuntime.ts
8380
- var import_react20 = require("react");
9115
+ var import_react21 = require("react");
8381
9116
 
8382
9117
  // src/client/inject-inspector/archieOrigins.ts
8383
9118
  var ARCHIE_HOST_ORIGINS = [
@@ -8483,10 +9218,9 @@ function injectInspector(opts = {}) {
8483
9218
  document.head.appendChild(script);
8484
9219
  return null;
8485
9220
  }
8486
- const localInspectorModulePath = "./inspector.js";
8487
9221
  return import(
8488
9222
  /* @vite-ignore */
8489
- localInspectorModulePath
9223
+ "./inspector.js"
8490
9224
  ).then((m) => {
8491
9225
  try {
8492
9226
  m.default();
@@ -8572,13 +9306,13 @@ function setupArchieRouteListener(router) {
8572
9306
  }
8573
9307
 
8574
9308
  // src/client/runtime/domMetaSanitizer.ts
8575
- var import_react19 = __toESM(require("react"));
9309
+ var import_react20 = __toESM(require("react"));
8576
9310
  var _installed = false;
8577
9311
  function installDomMetaSanitizer() {
8578
9312
  if (_installed) return;
8579
9313
  _installed = true;
8580
- const _origCE = import_react19.default.createElement.bind(import_react19.default);
8581
- import_react19.default.createElement = function patchedCreateElement(type, props, ...children) {
9314
+ const _origCE = import_react20.default.createElement.bind(import_react20.default);
9315
+ import_react20.default.createElement = function patchedCreateElement(type, props, ...children) {
8582
9316
  if (typeof type === "string" && props != null && "__editorMeta" in props) {
8583
9317
  const { __editorMeta: _stripped, ...rest } = props;
8584
9318
  return _origCE(type, rest, ...children);
@@ -8602,18 +9336,26 @@ installDomMetaSanitizer();
8602
9336
  installDomMetaSanitizer();
8603
9337
  function useArchieDevToolsRuntime({
8604
9338
  router,
8605
- inspector = true
9339
+ inspector,
9340
+ inspectorOptions
8606
9341
  }) {
8607
- (0, import_react20.useEffect)(() => {
9342
+ const stableOptionsKey = JSON.stringify(inspectorOptions ?? null);
9343
+ const inspectorOptionsRef = (0, import_react21.useRef)(inspectorOptions);
9344
+ inspectorOptionsRef.current = inspectorOptions;
9345
+ (0, import_react21.useEffect)(() => {
8608
9346
  if (process.env.NODE_ENV !== "development") {
8609
9347
  return;
8610
9348
  }
8611
9349
  const unsubscribe = setupArchieRouteListener(router);
8612
- if (inspector) {
8613
- injectInspector({ scriptSource: "remote" });
9350
+ if (inspector !== false) {
9351
+ const isLocalHost = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
9352
+ injectInspector({
9353
+ scriptSource: isLocalHost ? "local" : "remote",
9354
+ ...inspectorOptionsRef.current
9355
+ });
8614
9356
  }
8615
9357
  return unsubscribe;
8616
- }, [router, inspector]);
9358
+ }, [router, inspector, stableOptionsKey]);
8617
9359
  }
8618
9360
 
8619
9361
  // src/client/ArchieDevToolProvider.tsx
@@ -8621,9 +9363,10 @@ var import_jsx_runtime6 = require("react/jsx-runtime");
8621
9363
  function ArchieDevToolProvider({
8622
9364
  children,
8623
9365
  router,
8624
- inspector = true
9366
+ inspector,
9367
+ inspectorOptions
8625
9368
  }) {
8626
- useArchieDevToolsRuntime({ router, inspector });
9369
+ useArchieDevToolsRuntime({ router, inspector, inspectorOptions });
8627
9370
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DndProvider, { children });
8628
9371
  }
8629
9372
  // Annotate the CommonJS export names for ESM import in node: