@archie/devtools 0.0.12-beta → 0.0.13

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.
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunk-QZ7TP4HQ.mjs";
4
4
 
5
5
  // src/client/dnd/DndProvider.tsx
6
- import { useCallback as useCallback9, useEffect as useEffect12 } from "react";
6
+ import { useCallback as useCallback9, useEffect as useEffect13 } from "react";
7
7
 
8
8
  // node_modules/@dnd-kit/core/dist/core.esm.js
9
9
  import React2, { createContext, useContext, useEffect as useEffect2, useState as useState2, useCallback as useCallback3, useMemo as useMemo2, useRef as useRef2, memo, useReducer, cloneElement, forwardRef } from "react";
@@ -4070,19 +4070,498 @@ function mergeInspectorTheme(theme) {
4070
4070
  };
4071
4071
  }
4072
4072
 
4073
+ // src/client/dnd/design-mode/structural-policy.ts
4074
+ var STRUCTURAL_COMPONENT_RE = /(?:Page|Provider|RootLayout)$/;
4075
+ var STRUCTURAL_TAGS = /* @__PURE__ */ new Set(["html", "body"]);
4076
+ function isStructuralComponentName(componentName) {
4077
+ return !!componentName && STRUCTURAL_COMPONENT_RE.test(componentName);
4078
+ }
4079
+ function isHighLevelStructuralElement(el) {
4080
+ if (!el) return false;
4081
+ if (el.hasAttribute("data-dnd-structural")) return true;
4082
+ const tag = el.tagName.toLowerCase();
4083
+ if (STRUCTURAL_TAGS.has(tag)) return true;
4084
+ if (el.id === "root") return true;
4085
+ return tag === "main" && (el.parentElement === document.body || el.parentElement?.id === "root");
4086
+ }
4087
+ function isStructuralScannedElement(scanned) {
4088
+ if (!scanned) return false;
4089
+ if (isHighLevelStructuralElement(scanned.element)) return true;
4090
+ return isStructuralComponentName(scanned.editorMeta?.componentName);
4091
+ }
4092
+ function isStructuralContainer(containerId, nodeMap) {
4093
+ return isStructuralScannedElement(nodeMap.get(containerId));
4094
+ }
4095
+ function isValidLaneContainer(containerId, projection, nodeMap) {
4096
+ const container = projection.containerIndex.get(containerId);
4097
+ if (!container) return false;
4098
+ if (container.children.length === 0) return false;
4099
+ return !isStructuralContainer(containerId, nodeMap);
4100
+ }
4101
+ function buildBlockedStructuralContainerIds(projection, nodeMap, sourceContainerId) {
4102
+ const blocked = /* @__PURE__ */ new Set();
4103
+ for (const containerId of projection.containerIndex.keys()) {
4104
+ if (containerId === sourceContainerId) continue;
4105
+ if (isStructuralContainer(containerId, nodeMap)) blocked.add(containerId);
4106
+ }
4107
+ return blocked;
4108
+ }
4109
+
4110
+ // src/client/dnd/design-mode/fiber-bridge.ts
4111
+ var FIBER_KEY_PREFIX = "__reactFiber$";
4112
+ var PROPS_KEY_PREFIX = "__reactProps$";
4113
+ var metaCache = /* @__PURE__ */ new WeakMap();
4114
+ var directMetaCache = /* @__PURE__ */ new WeakMap();
4115
+ function hasEditorMetaInFiberChain(fiber) {
4116
+ let current = fiber;
4117
+ while (current) {
4118
+ if (current.memoizedProps?.__editorMeta) return true;
4119
+ current = current.return;
4120
+ }
4121
+ return false;
4122
+ }
4123
+ function compareFiberCandidates(candidate, currentBest) {
4124
+ if (!currentBest) return 1;
4125
+ const candidateScore = [
4126
+ candidate.hasMatchingProps ? 1 : 0,
4127
+ candidate.hasDirectEditorMeta ? 1 : 0,
4128
+ candidate.hasCompanionPropsKey ? 1 : 0,
4129
+ candidate.hasEditorMetaInReturnChain ? 1 : 0,
4130
+ candidate.order
4131
+ ];
4132
+ const currentScore = [
4133
+ currentBest.hasMatchingProps ? 1 : 0,
4134
+ currentBest.hasDirectEditorMeta ? 1 : 0,
4135
+ currentBest.hasCompanionPropsKey ? 1 : 0,
4136
+ currentBest.hasEditorMetaInReturnChain ? 1 : 0,
4137
+ currentBest.order
4138
+ ];
4139
+ for (let i = 0; i < candidateScore.length; i += 1) {
4140
+ if (candidateScore[i] === currentScore[i]) continue;
4141
+ return candidateScore[i] > currentScore[i] ? 1 : -1;
4142
+ }
4143
+ return 0;
4144
+ }
4145
+ function getFiberSelectionFromElement(el) {
4146
+ const record = el;
4147
+ const keys = Object.keys(record);
4148
+ let bestCandidate = null;
4149
+ for (let i = 0; i < keys.length; i += 1) {
4150
+ const fiberKey = keys[i];
4151
+ if (!fiberKey.startsWith(FIBER_KEY_PREFIX)) continue;
4152
+ const fiber = record[fiberKey];
4153
+ if (!fiber) continue;
4154
+ if (fiber.stateNode && fiber.stateNode !== el) continue;
4155
+ const suffix = fiberKey.slice(FIBER_KEY_PREFIX.length);
4156
+ const propsKey = `${PROPS_KEY_PREFIX}${suffix}`;
4157
+ const hasCompanionPropsKey = Object.prototype.hasOwnProperty.call(record, propsKey);
4158
+ const companionProps = hasCompanionPropsKey ? record[propsKey] : void 0;
4159
+ const candidate = {
4160
+ fiber,
4161
+ fiberKey,
4162
+ hasMatchingProps: hasCompanionPropsKey && fiber.memoizedProps === companionProps,
4163
+ hasDirectEditorMeta: !!fiber.memoizedProps?.__editorMeta,
4164
+ hasCompanionPropsKey,
4165
+ hasEditorMetaInReturnChain: hasEditorMetaInFiberChain(fiber),
4166
+ // Tiebreaker: higher index in Object.keys = later React fiber attachment.
4167
+ // Relies on spec-guaranteed insertion-order for string keys (ES2015+).
4168
+ order: i
4169
+ };
4170
+ if (compareFiberCandidates(candidate, bestCandidate) > 0) {
4171
+ bestCandidate = candidate;
4172
+ }
4173
+ }
4174
+ if (!bestCandidate) {
4175
+ return { fiber: null, fiberKey: null };
4176
+ }
4177
+ return { fiber: bestCandidate.fiber, fiberKey: bestCandidate.fiberKey };
4178
+ }
4179
+ function readFiberAwareCache(cache, el, selection) {
4180
+ const cached = cache.get(el);
4181
+ if (!cached) return void 0;
4182
+ if (cached.fiber !== selection.fiber) return void 0;
4183
+ if (cached.fiberKey !== selection.fiberKey) return void 0;
4184
+ return cached.value;
4185
+ }
4186
+ function writeFiberAwareCache(cache, el, selection, value) {
4187
+ cache.set(el, {
4188
+ fiber: selection.fiber,
4189
+ fiberKey: selection.fiberKey,
4190
+ value
4191
+ });
4192
+ return value;
4193
+ }
4194
+ function isRootSvg(el) {
4195
+ return el instanceof SVGElement && el.tagName.toLowerCase() === "svg";
4196
+ }
4197
+ function isPassthroughMeta(meta) {
4198
+ return !!meta?.staticProps && meta.staticProps.asChild === true;
4199
+ }
4200
+ function isWrapperMeta(meta) {
4201
+ if (!meta?.componentName) return false;
4202
+ return meta.componentName[0] !== meta.componentName[0].toLowerCase();
4203
+ }
4204
+ function hasOwnTextContent(el) {
4205
+ for (const node of Array.from(el.childNodes)) {
4206
+ if (node.nodeType !== Node.TEXT_NODE) continue;
4207
+ if (node.textContent?.trim()) return true;
4208
+ }
4209
+ return false;
4210
+ }
4211
+ function getCandidateChildren(el) {
4212
+ return Array.from(el.children).filter((child) => {
4213
+ if (!(child instanceof HTMLElement)) return false;
4214
+ if (child.hasAttribute("data-design-mode-ui")) return false;
4215
+ if (child.hasAttribute("data-design-mode-allow")) return false;
4216
+ return !shouldIgnoreAsVirtualRuntime(child);
4217
+ });
4218
+ }
4219
+ function shouldIgnoreAsVirtualRuntime(el) {
4220
+ const tag = el.tagName.toLowerCase();
4221
+ if (tag === "canvas") return true;
4222
+ if (tag === "script" || tag === "style" || tag === "noscript") return true;
4223
+ if (el instanceof SVGElement && !isRootSvg(el)) return true;
4224
+ return false;
4225
+ }
4226
+ function getDirectEditorMeta(el) {
4227
+ const selection = getFiberSelectionFromElement(el);
4228
+ const cached = readFiberAwareCache(directMetaCache, el, selection);
4229
+ if (cached !== void 0) return cached;
4230
+ const meta = selection.fiber?.memoizedProps?.__editorMeta;
4231
+ return writeFiberAwareCache(directMetaCache, el, selection, meta ?? null);
4232
+ }
4233
+ function getEditorMeta(el) {
4234
+ const selection = getFiberSelectionFromElement(el);
4235
+ const cached = readFiberAwareCache(metaCache, el, selection);
4236
+ if (cached !== void 0) return cached;
4237
+ let fiber = selection.fiber;
4238
+ while (fiber) {
4239
+ const meta = fiber.memoizedProps?.__editorMeta;
4240
+ if (meta) {
4241
+ return writeFiberAwareCache(metaCache, el, selection, meta);
4242
+ }
4243
+ fiber = fiber.return;
4244
+ }
4245
+ return writeFiberAwareCache(metaCache, el, selection, null);
4246
+ }
4247
+ function findMetaOwnerElement(el) {
4248
+ let current = el;
4249
+ while (current) {
4250
+ if (getEditorMeta(current)) return current;
4251
+ current = current.parentElement;
4252
+ }
4253
+ return null;
4254
+ }
4255
+ function resolveHostElementFromFiberChain(el) {
4256
+ if (shouldIgnoreAsVirtualRuntime(el)) return null;
4257
+ const directMeta = getDirectEditorMeta(el);
4258
+ const ownerMeta = directMeta ?? getEditorMeta(el);
4259
+ if (isPassthroughMeta(ownerMeta)) {
4260
+ const children = getCandidateChildren(el);
4261
+ if (children.length !== 1) return null;
4262
+ return children[0];
4263
+ }
4264
+ if (directMeta && !isWrapperMeta(directMeta)) return el;
4265
+ const MAX_STEPS = 10;
4266
+ const ownerNodeId = ownerMeta?.nodeId ?? null;
4267
+ let outermost = el;
4268
+ let parent = el.parentElement;
4269
+ for (let i = 0; i < MAX_STEPS; i += 1) {
4270
+ if (!parent || parent === document.body) break;
4271
+ if (shouldIgnoreAsVirtualRuntime(parent)) break;
4272
+ const parentMeta = getEditorMeta(parent);
4273
+ if (!parentMeta) break;
4274
+ if (ownerNodeId && parentMeta.nodeId !== ownerNodeId) break;
4275
+ if (isStructuralComponentName(parentMeta.componentName)) break;
4276
+ if (!isWrapperMeta(parentMeta)) break;
4277
+ if (isPassthroughMeta(parentMeta)) break;
4278
+ if (hasOwnTextContent(parent)) break;
4279
+ const children = getCandidateChildren(parent);
4280
+ if (children.length !== 1) break;
4281
+ outermost = parent;
4282
+ parent = parent.parentElement;
4283
+ }
4284
+ return outermost;
4285
+ }
4286
+ function resolveDragSurface(el) {
4287
+ const host = resolveHostElementFromFiberChain(el);
4288
+ if (!host) {
4289
+ return { host: null, metaOwner: null, kind: "direct-host" };
4290
+ }
4291
+ const metaOwner = findMetaOwnerElement(host) ?? findMetaOwnerElement(el);
4292
+ const ownerMeta = metaOwner ? getEditorMeta(metaOwner) : null;
4293
+ const kind = isPassthroughMeta(ownerMeta) ? "passthrough-slot" : metaOwner && metaOwner !== host && isWrapperMeta(ownerMeta) ? "wrapper-to-host" : "direct-host";
4294
+ return {
4295
+ host,
4296
+ metaOwner,
4297
+ kind
4298
+ };
4299
+ }
4300
+
4301
+ // src/client/dnd/design-mode/node-key.ts
4302
+ var _runtimeKeyMap = /* @__PURE__ */ new WeakMap();
4303
+ var _runtimeKeyCounter = 0;
4304
+ function getRuntimeKey(el) {
4305
+ const existing = _runtimeKeyMap.get(el);
4306
+ if (existing) return existing;
4307
+ const key2 = `runtime:${++_runtimeKeyCounter}`;
4308
+ _runtimeKeyMap.set(el, key2);
4309
+ return key2;
4310
+ }
4311
+ function getNodeKeyFromElement(el) {
4312
+ const resolved = resolveDragSurface(el);
4313
+ const host = resolved.host ?? el;
4314
+ const meta = getEditorMeta(host);
4315
+ if (meta) return meta.nodeId;
4316
+ const persisted = host.getAttribute("data-dnd-node-id");
4317
+ if (persisted) return persisted;
4318
+ return getRuntimeKey(host);
4319
+ }
4320
+ function getInspectorRefFromElement(el) {
4321
+ const resolved = resolveDragSurface(el);
4322
+ const host = resolved.host ?? el;
4323
+ const meta = getEditorMeta(host);
4324
+ if (!meta) return null;
4325
+ return { relativePath: meta.file, line: meta.line, column: meta.col };
4326
+ }
4327
+
4328
+ // src/client/dnd/design-mode/arrange-persistence-diagnostics.ts
4329
+ function isSourceBackedRuntimeNodeKey(nodeKey) {
4330
+ return typeof nodeKey === "string" && nodeKey.trim().length > 0 && !nodeKey.startsWith("runtime:");
4331
+ }
4332
+ function mergeRuntimeArrangeDiagnostics(diagnosticsEntries) {
4333
+ const blockedReasons = /* @__PURE__ */ new Set();
4334
+ const notes = /* @__PURE__ */ new Set();
4335
+ let structurallyUnsafe = false;
4336
+ for (const diagnostics of diagnosticsEntries) {
4337
+ if (!diagnostics) continue;
4338
+ if (diagnostics.structurallyUnsafe === true) {
4339
+ structurallyUnsafe = true;
4340
+ }
4341
+ for (const reason of diagnostics.blockedReasons ?? []) {
4342
+ if (typeof reason === "string" && reason.trim().length > 0) {
4343
+ blockedReasons.add(reason);
4344
+ }
4345
+ }
4346
+ for (const note of diagnostics.notes ?? []) {
4347
+ if (typeof note === "string" && note.trim().length > 0) {
4348
+ notes.add(note);
4349
+ }
4350
+ }
4351
+ }
4352
+ if (!structurallyUnsafe && blockedReasons.size === 0 && notes.size === 0) {
4353
+ return null;
4354
+ }
4355
+ return {
4356
+ ...structurallyUnsafe ? { structurallyUnsafe: true } : {},
4357
+ ...blockedReasons.size > 0 ? { blockedReasons: Array.from(blockedReasons) } : {},
4358
+ ...notes.size > 0 ? { notes: Array.from(notes) } : {}
4359
+ };
4360
+ }
4361
+ function detectDuplicateSourceBackedNodeKeys(nodeKeys) {
4362
+ const seenKeys = /* @__PURE__ */ new Set();
4363
+ const duplicateKeys = /* @__PURE__ */ new Set();
4364
+ for (const nodeKey of nodeKeys) {
4365
+ if (!isSourceBackedRuntimeNodeKey(nodeKey)) {
4366
+ continue;
4367
+ }
4368
+ if (seenKeys.has(nodeKey)) {
4369
+ duplicateKeys.add(nodeKey);
4370
+ continue;
4371
+ }
4372
+ seenKeys.add(nodeKey);
4373
+ }
4374
+ return Array.from(duplicateKeys);
4375
+ }
4376
+ function buildOccurrenceNodeId(nodeKey, occurrenceIndex) {
4377
+ return `${nodeKey}::${occurrenceIndex}`;
4378
+ }
4379
+ function buildEffectiveNodeIdsByCommitNodeId(commitNodeIds, nodeKeysByCommitNodeId) {
4380
+ const countsByNodeKey = /* @__PURE__ */ new Map();
4381
+ for (const commitNodeId of commitNodeIds) {
4382
+ const nodeKey = nodeKeysByCommitNodeId.get(commitNodeId);
4383
+ if (!isSourceBackedRuntimeNodeKey(nodeKey)) {
4384
+ continue;
4385
+ }
4386
+ countsByNodeKey.set(nodeKey, (countsByNodeKey.get(nodeKey) ?? 0) + 1);
4387
+ }
4388
+ const seenByNodeKey = /* @__PURE__ */ new Map();
4389
+ const effectiveNodeIdsByCommitNodeId = /* @__PURE__ */ new Map();
4390
+ for (const commitNodeId of commitNodeIds) {
4391
+ const nodeKey = nodeKeysByCommitNodeId.get(commitNodeId);
4392
+ if (!isSourceBackedRuntimeNodeKey(nodeKey) || (countsByNodeKey.get(nodeKey) ?? 0) < 2) {
4393
+ effectiveNodeIdsByCommitNodeId.set(commitNodeId, null);
4394
+ continue;
4395
+ }
4396
+ const occurrenceIndex = seenByNodeKey.get(nodeKey) ?? 0;
4397
+ seenByNodeKey.set(nodeKey, occurrenceIndex + 1);
4398
+ effectiveNodeIdsByCommitNodeId.set(
4399
+ commitNodeId,
4400
+ buildOccurrenceNodeId(nodeKey, occurrenceIndex)
4401
+ );
4402
+ }
4403
+ return effectiveNodeIdsByCommitNodeId;
4404
+ }
4405
+ function createCommitLaneIdentitySnapshot(params) {
4406
+ return {
4407
+ laneContainerId: params.laneContainerId,
4408
+ commitNodeIds: [...params.commitNodeIds],
4409
+ nodeKeysByCommitNodeId: new Map(params.nodeKeysByCommitNodeId),
4410
+ effectiveNodeIdsByCommitNodeId: buildEffectiveNodeIdsByCommitNodeId(
4411
+ params.commitNodeIds,
4412
+ params.nodeKeysByCommitNodeId
4413
+ ),
4414
+ committable: true
4415
+ };
4416
+ }
4417
+ function hasSafeOccurrenceIdentities(lane, duplicateNodeKey) {
4418
+ const effectiveNodeIds = lane.commitNodeIds.filter(
4419
+ (commitNodeId) => lane.nodeKeysByCommitNodeId.get(commitNodeId) === duplicateNodeKey
4420
+ ).map(
4421
+ (commitNodeId) => lane.effectiveNodeIdsByCommitNodeId.get(commitNodeId) ?? null
4422
+ );
4423
+ return effectiveNodeIds.length > 1 && effectiveNodeIds.every(
4424
+ (effectiveNodeId) => typeof effectiveNodeId === "string" && effectiveNodeId.length > 0
4425
+ ) && new Set(effectiveNodeIds).size === effectiveNodeIds.length;
4426
+ }
4427
+ function buildLanePersistenceDiagnostics(params) {
4428
+ const duplicateNodeKeys = detectDuplicateSourceBackedNodeKeys(
4429
+ buildNodeKeysFromCommitIds(
4430
+ params.lane.commitNodeIds,
4431
+ params.lane.nodeKeysByCommitNodeId
4432
+ )
4433
+ );
4434
+ if (duplicateNodeKeys.length === 0) {
4435
+ return null;
4436
+ }
4437
+ const safeDuplicateNodeKeys = duplicateNodeKeys.filter(
4438
+ (nodeKey) => hasSafeOccurrenceIdentities(params.lane, nodeKey)
4439
+ );
4440
+ if (safeDuplicateNodeKeys.length === duplicateNodeKeys.length) {
4441
+ return {
4442
+ notes: safeDuplicateNodeKeys.map(
4443
+ (nodeKey) => `Arrange lane ${params.lane.laneContainerId} repeats source-backed nodeKey ${nodeKey}, but runtime assigned explicit occurrence identities for each commit root.`
4444
+ )
4445
+ };
4446
+ }
4447
+ return {
4448
+ structurallyUnsafe: true,
4449
+ blockedReasons: duplicateNodeKeys.map(
4450
+ (nodeKey) => `Arrange lane ${params.lane.laneContainerId} contains multiple commit roots with the same source-backed nodeKey (${nodeKey}), so local save is unsafe.`
4451
+ ),
4452
+ notes: [
4453
+ `Lane ${params.lane.laneContainerId} duplicates source-backed nodeKeys: ${duplicateNodeKeys.join(", ")}`
4454
+ ]
4455
+ };
4456
+ }
4457
+ function removeCommitNodeId(commitNodeIds, commitNodeId) {
4458
+ return commitNodeIds.filter((candidate) => candidate !== commitNodeId);
4459
+ }
4460
+ function buildNodeKeysFromCommitIds(commitNodeIds, nodeKeysByCommitNodeId) {
4461
+ return commitNodeIds.map(
4462
+ (commitNodeId) => nodeKeysByCommitNodeId.get(commitNodeId) ?? null
4463
+ );
4464
+ }
4465
+ function captureCommitLaneIdentitySnapshot(lane, resolveNodeElement) {
4466
+ if (!lane?.committable) return null;
4467
+ const nodeKeysByCommitNodeId = /* @__PURE__ */ new Map();
4468
+ for (const commitNodeId of lane.orderedCommitNodeIds) {
4469
+ const element = resolveNodeElement(commitNodeId);
4470
+ if (!element) return null;
4471
+ nodeKeysByCommitNodeId.set(commitNodeId, getNodeKeyFromElement(element));
4472
+ }
4473
+ return createCommitLaneIdentitySnapshot({
4474
+ laneContainerId: lane.laneContainerId,
4475
+ nodeKeysByCommitNodeId,
4476
+ commitNodeIds: lane.orderedCommitNodeIds
4477
+ });
4478
+ }
4479
+ function projectCommitNodeIdsForMove(params) {
4480
+ const withoutMovedCommit = removeCommitNodeId(
4481
+ params.commitNodeIds,
4482
+ params.movedCommitNodeId
4483
+ );
4484
+ if (typeof params.targetIndex !== "number" || !Number.isFinite(params.targetIndex)) {
4485
+ return [...withoutMovedCommit, params.movedCommitNodeId];
4486
+ }
4487
+ const insertionIndex = Math.max(
4488
+ 0,
4489
+ Math.min(params.targetIndex, withoutMovedCommit.length)
4490
+ );
4491
+ return [
4492
+ ...withoutMovedCommit.slice(0, insertionIndex),
4493
+ params.movedCommitNodeId,
4494
+ ...withoutMovedCommit.slice(insertionIndex)
4495
+ ];
4496
+ }
4497
+ function buildMovePersistenceDiagnostics(params) {
4498
+ const {
4499
+ sourceLane,
4500
+ targetLane,
4501
+ movedCommitNodeId,
4502
+ sourceLaneContainerId,
4503
+ targetLaneContainerId,
4504
+ targetIndex
4505
+ } = params;
4506
+ const sameLane = !!sourceLaneContainerId && !!targetLaneContainerId && sourceLaneContainerId === targetLaneContainerId;
4507
+ const targetSnapshot = sameLane ? targetLane ?? sourceLane : targetLane;
4508
+ const sourceLaneAfter = !sameLane && sourceLane?.committable ? createCommitLaneIdentitySnapshot({
4509
+ laneContainerId: sourceLane.laneContainerId,
4510
+ commitNodeIds: removeCommitNodeId(
4511
+ sourceLane.commitNodeIds,
4512
+ movedCommitNodeId
4513
+ ),
4514
+ nodeKeysByCommitNodeId: sourceLane.nodeKeysByCommitNodeId
4515
+ }) : null;
4516
+ const targetLaneAfter = targetSnapshot?.committable ? createCommitLaneIdentitySnapshot({
4517
+ laneContainerId: targetSnapshot.laneContainerId,
4518
+ commitNodeIds: projectCommitNodeIdsForMove({
4519
+ commitNodeIds: targetSnapshot.commitNodeIds,
4520
+ movedCommitNodeId,
4521
+ targetIndex
4522
+ }),
4523
+ nodeKeysByCommitNodeId: new Map([
4524
+ ...targetSnapshot.nodeKeysByCommitNodeId,
4525
+ ...sourceLane?.nodeKeysByCommitNodeId.has(movedCommitNodeId) ? [
4526
+ [
4527
+ movedCommitNodeId,
4528
+ sourceLane.nodeKeysByCommitNodeId.get(movedCommitNodeId) ?? null
4529
+ ]
4530
+ ] : []
4531
+ ])
4532
+ }) : null;
4533
+ return mergeRuntimeArrangeDiagnostics([
4534
+ sourceLaneAfter ? buildLanePersistenceDiagnostics({ lane: sourceLaneAfter }) : null,
4535
+ targetLaneAfter ? buildLanePersistenceDiagnostics({ lane: targetLaneAfter }) : null
4536
+ ]);
4537
+ }
4538
+
4073
4539
  // src/client/dnd/design-mode/store.ts
4074
4540
  function summarizeEditorChangeEvent(event) {
4541
+ if (event.kind === "insert") {
4542
+ return `insert ${event.displayName} at ${event.containerId}[${event.index}]`;
4543
+ }
4075
4544
  if (event.kind === "move") {
4076
4545
  const selectedHint = event.selectedNodeKey && event.selectedNodeKey !== event.nodeKey ? ` (selected ${event.selectedNodeKey})` : "";
4077
4546
  const anchorHint = event.anchorNodeKey && event.anchorNodeKey !== event.nodeKey ? ` (anchor ${event.anchorNodeKey})` : "";
4078
- const commandHint = event.commandType ? ` ${event.commandType}` : "";
4079
- return `move${commandHint} ${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}`;
4547
+ const beforeParentNodeId = event.payload.before.effectiveContainerNodeId ?? event.payload.before.parentNodeId;
4548
+ const afterParentNodeId = event.payload.after.effectiveContainerNodeId ?? event.payload.after.parentNodeId;
4549
+ const commitNodeId = event.payload.after.effectiveCommitNodeId ?? event.payload.after.commitNodeId;
4550
+ const beforeNodeId = event.payload.after.effectiveBeforeNodeId ?? event.payload.after.beforeNodeId;
4551
+ const afterNodeId = event.payload.after.effectiveAfterNodeId ?? event.payload.after.afterNodeId;
4552
+ 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}`;
4080
4553
  }
4081
4554
  if (event.payload.after.mode === "grid-span") {
4082
4555
  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}`;
4083
4556
  }
4084
4557
  return `resize(px) ${event.nodeKey}: ${event.payload.before.width}\xD7${event.payload.before.height} \u2192 ${event.payload.after.width}\xD7${event.payload.after.height}`;
4085
4558
  }
4559
+ function buildFallbackOperationId(now, nodeId) {
4560
+ return `${now}:${nodeId}`;
4561
+ }
4562
+ function inferMoveCommandType(before, after) {
4563
+ return before.parentNodeId === after.parentNodeId ? "reorder" : "moveCrossContainer";
4564
+ }
4086
4565
  function rebuildNodeEdit(orderedChanges, nodeKey) {
4087
4566
  let latestMove;
4088
4567
  let latestResize;
@@ -4097,11 +4576,36 @@ function rebuildNodeEdit(orderedChanges, nodeKey) {
4097
4576
  nodeId = ev.nodeId;
4098
4577
  inspectorRef = ev.inspectorRef;
4099
4578
  if (ev.kind === "move") latestMove = ev.payload;
4100
- else latestResize = ev.payload;
4579
+ else if (ev.kind === "resize") latestResize = ev.payload;
4101
4580
  }
4102
4581
  if (count === 0) return null;
4103
4582
  return { nodeKey, nodeId, inspectorRef, latestMove, latestResize, changeCount: count, lastUpdatedAt: lastAt };
4104
4583
  }
4584
+ function coalesceOrderedChanges(changes) {
4585
+ if (changes.length <= 1) return changes;
4586
+ const result = [];
4587
+ for (const event of changes) {
4588
+ const prev = result[result.length - 1];
4589
+ if (prev && prev.kind === "resize" && event.kind === "resize" && prev.nodeKey === event.nodeKey) {
4590
+ result[result.length - 1] = {
4591
+ ...event,
4592
+ payload: { before: prev.payload.before, after: event.payload.after }
4593
+ };
4594
+ } else {
4595
+ result.push(event);
4596
+ }
4597
+ }
4598
+ return result;
4599
+ }
4600
+ function buildEmptyArrangeSessionState() {
4601
+ return {
4602
+ nodeEditsByKey: {},
4603
+ orderedChanges: [],
4604
+ undoneChanges: [],
4605
+ canUndo: false,
4606
+ canRedo: false
4607
+ };
4608
+ }
4105
4609
  var useDesignModeStore = create((set, get) => ({
4106
4610
  // UI state
4107
4611
  enabled: false,
@@ -4117,8 +4621,10 @@ var useDesignModeStore = create((set, get) => ({
4117
4621
  nodeEditsByKey: {},
4118
4622
  orderedChanges: [],
4119
4623
  undoneChanges: [],
4624
+ // External drag
4625
+ externalDrag: null,
4120
4626
  // UI actions
4121
- toggle: () => set((s) => ({ enabled: !s.enabled })),
4627
+ toggle: () => set((s) => ({ enabled: !s.enabled, ...buildEmptyArrangeSessionState() })),
4122
4628
  setEnabled: (enabled) => set({ enabled }),
4123
4629
  setHovered: (selector) => set({ hoveredSelector: selector }),
4124
4630
  setSelected: (selector) => set({ selectedSelector: selector }),
@@ -4133,6 +4639,7 @@ var useDesignModeStore = create((set, get) => ({
4133
4639
  canUndo: false,
4134
4640
  canRedo: false
4135
4641
  }),
4642
+ resetArrangeSession: () => set(buildEmptyArrangeSessionState()),
4136
4643
  // Change tracking actions
4137
4644
  recordMoveChange: ({
4138
4645
  operationId,
@@ -4145,18 +4652,24 @@ var useDesignModeStore = create((set, get) => ({
4145
4652
  selectedInspectorRef,
4146
4653
  anchorNodeKey,
4147
4654
  anchorNodeId,
4655
+ sourceContainer,
4656
+ sourceBeforeSibling,
4657
+ sourceAfterSibling,
4148
4658
  container,
4149
4659
  beforeSibling,
4150
4660
  afterSibling,
4661
+ diagnostics,
4151
4662
  componentName,
4152
4663
  before,
4153
4664
  after
4154
4665
  }) => {
4155
4666
  const now = Date.now();
4667
+ const normalizedOperationId = operationId ?? buildFallbackOperationId(now, nodeId);
4668
+ const normalizedCommandType = commandType ?? inferMoveCommandType(before, after);
4156
4669
  const event = {
4157
4670
  kind: "move",
4158
- operationId: operationId ?? `${now}:${nodeId}`,
4159
- commandType: commandType ?? "reorder",
4671
+ operationId: normalizedOperationId,
4672
+ commandType: normalizedCommandType,
4160
4673
  nodeKey,
4161
4674
  nodeId,
4162
4675
  inspectorRef,
@@ -4165,9 +4678,13 @@ var useDesignModeStore = create((set, get) => ({
4165
4678
  selectedInspectorRef,
4166
4679
  anchorNodeKey,
4167
4680
  anchorNodeId,
4681
+ sourceContainer: sourceContainer ?? null,
4682
+ sourceBeforeSibling: sourceBeforeSibling ?? null,
4683
+ sourceAfterSibling: sourceAfterSibling ?? null,
4168
4684
  container: container ?? null,
4169
4685
  beforeSibling: beforeSibling ?? null,
4170
4686
  afterSibling: afterSibling ?? null,
4687
+ diagnostics: diagnostics ?? null,
4171
4688
  payload: { before, after },
4172
4689
  at: now
4173
4690
  };
@@ -4190,11 +4707,20 @@ var useDesignModeStore = create((set, get) => ({
4190
4707
  };
4191
4708
  });
4192
4709
  },
4193
- recordResizeChange: ({ operationId, nodeKey, nodeId, inspectorRef, componentName, before, after }) => {
4710
+ recordResizeChange: ({
4711
+ operationId,
4712
+ nodeKey,
4713
+ nodeId,
4714
+ inspectorRef,
4715
+ componentName,
4716
+ before,
4717
+ after
4718
+ }) => {
4194
4719
  const now = Date.now();
4720
+ const normalizedOperationId = operationId ?? buildFallbackOperationId(now, nodeId);
4195
4721
  const event = {
4196
4722
  kind: "resize",
4197
- operationId: operationId ?? `${now}:${nodeId}`,
4723
+ operationId: normalizedOperationId,
4198
4724
  nodeKey,
4199
4725
  nodeId,
4200
4726
  inspectorRef,
@@ -4220,6 +4746,52 @@ var useDesignModeStore = create((set, get) => ({
4220
4746
  };
4221
4747
  });
4222
4748
  },
4749
+ recordInsertChange: ({
4750
+ operationId,
4751
+ nodeKey,
4752
+ nodeId,
4753
+ inspectorRef,
4754
+ displayName,
4755
+ html,
4756
+ containerId,
4757
+ containerNodeKey,
4758
+ index
4759
+ }) => {
4760
+ const now = Date.now();
4761
+ const normalizedOperationId = operationId ?? buildFallbackOperationId(now, nodeId);
4762
+ const event = {
4763
+ kind: "insert",
4764
+ operationId: normalizedOperationId,
4765
+ nodeKey,
4766
+ nodeId,
4767
+ inspectorRef,
4768
+ displayName,
4769
+ html,
4770
+ containerId,
4771
+ containerNodeKey,
4772
+ index,
4773
+ at: now
4774
+ };
4775
+ set((s) => {
4776
+ const prev = s.nodeEditsByKey[nodeKey];
4777
+ const meta = {
4778
+ nodeKey,
4779
+ nodeId,
4780
+ inspectorRef,
4781
+ latestMove: prev?.latestMove,
4782
+ latestResize: prev?.latestResize,
4783
+ changeCount: (prev?.changeCount ?? 0) + 1,
4784
+ lastUpdatedAt: now
4785
+ };
4786
+ return {
4787
+ orderedChanges: [...s.orderedChanges, event],
4788
+ nodeEditsByKey: { ...s.nodeEditsByKey, [nodeKey]: meta },
4789
+ undoneChanges: []
4790
+ };
4791
+ });
4792
+ },
4793
+ startExternalDrag: (component) => set({ externalDrag: component }),
4794
+ cancelExternalDrag: () => set({ externalDrag: null }),
4223
4795
  undoLastChange: () => {
4224
4796
  const { orderedChanges, undoneChanges } = get();
4225
4797
  if (orderedChanges.length === 0) return null;
@@ -4245,7 +4817,8 @@ var useDesignModeStore = create((set, get) => ({
4245
4817
  },
4246
4818
  clearChanges: () => set({ orderedChanges: [], undoneChanges: [], nodeEditsByKey: {} }),
4247
4819
  exportChangesForAI: () => {
4248
- const { orderedChanges, nodeEditsByKey } = get();
4820
+ const { orderedChanges: rawChanges, nodeEditsByKey } = get();
4821
+ const orderedChanges = coalesceOrderedChanges(rawChanges);
4249
4822
  const changesByFile = {};
4250
4823
  for (const event of orderedChanges) {
4251
4824
  const fileKey = event.inspectorRef?.relativePath ?? "__runtime__";
@@ -4259,180 +4832,127 @@ var useDesignModeStore = create((set, get) => ({
4259
4832
  lines.push(` ${summarizeEditorChangeEvent(ev)}`);
4260
4833
  }
4261
4834
  }
4835
+ const diagnostics = mergeRuntimeArrangeDiagnostics(
4836
+ orderedChanges.map(
4837
+ (event) => event.kind === "move" ? event.diagnostics : null
4838
+ )
4839
+ );
4262
4840
  return {
4263
4841
  changesByFile,
4264
4842
  finalSnapshot: { ...nodeEditsByKey },
4265
- aiPromptContext: lines.join("\n")
4843
+ aiPromptContext: lines.join("\n"),
4844
+ ...diagnostics ? { diagnostics } : {}
4266
4845
  };
4267
4846
  }
4268
4847
  }));
4269
4848
 
4270
4849
  // src/client/dnd/design-mode/DesignModeOverlay.tsx
4271
- import { useRef as useRef9, useCallback as useCallback8, useEffect as useEffect11, useLayoutEffect as useLayoutEffect2, useState as useState6, useMemo as useMemo6 } from "react";
4850
+ import { useRef as useRef10, useCallback as useCallback8, useEffect as useEffect12, useLayoutEffect as useLayoutEffect2, useState as useState6, useMemo as useMemo6 } from "react";
4272
4851
 
4273
4852
  // src/client/dnd/design-mode/useElementScanner.ts
4274
4853
  import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef4 } from "react";
4275
4854
 
4276
- // src/client/dnd/design-mode/structural-policy.ts
4277
- var STRUCTURAL_COMPONENT_RE = /(?:Layout|Page|Provider|App)$/;
4278
- var STRUCTURAL_TAGS = /* @__PURE__ */ new Set(["html", "body"]);
4279
- function isStructuralComponentName(componentName) {
4280
- return !!componentName && STRUCTURAL_COMPONENT_RE.test(componentName);
4855
+ // src/client/dnd/design-mode/helpers.ts
4856
+ var DND_NODE_ID_ATTR = "data-dnd-node-id";
4857
+ var PAGE_EDGE_GUARD = 10;
4858
+ var HARD_PAGE_EDGE_GUARD = 4;
4859
+ var SAFE_ZONE_INSET = 8;
4860
+ function clamp(value, min, max) {
4861
+ return Math.max(min, Math.min(max, value));
4281
4862
  }
4282
- function isHighLevelStructuralElement(el) {
4283
- if (!el) return false;
4284
- const tag = el.tagName.toLowerCase();
4285
- if (STRUCTURAL_TAGS.has(tag)) return true;
4286
- if (el.id === "root") return true;
4287
- return tag === "main" && (el.parentElement === document.body || el.parentElement?.id === "root");
4863
+ function buildNodeSelector(nodeId) {
4864
+ return `[${DND_NODE_ID_ATTR}="${escapeCssAttrValue(nodeId)}"]`;
4288
4865
  }
4289
- function isStructuralScannedElement(scanned) {
4290
- if (!scanned) return false;
4291
- if (isHighLevelStructuralElement(scanned.element)) return true;
4292
- return isStructuralComponentName(scanned.editorMeta?.componentName);
4866
+ var LARGE_NODE_NEST_BLOCK_MIN_WIDTH = 520;
4867
+ var LARGE_NODE_NEST_BLOCK_MIN_HEIGHT = 260;
4868
+ var LARGE_NODE_NEST_BLOCK_MIN_AREA = 18e4;
4869
+ function isEligibleForDrag(sc) {
4870
+ const source = sc.eligibilitySource ?? "editor-meta";
4871
+ return source === "editor-meta" || source === "force-allow" || source === "inferred-sortable-descendant";
4293
4872
  }
4294
- function isStructuralContainer(containerId, nodeMap) {
4295
- return isStructuralScannedElement(nodeMap.get(containerId));
4873
+ function isPointerInEdgeZone(px, py, guard) {
4874
+ return px <= guard || py <= guard || px >= window.innerWidth - guard || py >= window.innerHeight - guard;
4296
4875
  }
4297
- function isValidLaneContainer(containerId, projection, nodeMap) {
4298
- const container = projection.containerIndex.get(containerId);
4299
- if (!container) return false;
4300
- if (container.children.length < 2) return false;
4301
- return !isStructuralContainer(containerId, nodeMap);
4876
+ function isPointerInPageEdgeZone(px, py) {
4877
+ return isPointerInEdgeZone(px, py, PAGE_EDGE_GUARD);
4302
4878
  }
4303
- function buildBlockedStructuralContainerIds(projection, nodeMap, sourceContainerId) {
4304
- const blocked = /* @__PURE__ */ new Set();
4305
- for (const containerId of projection.containerIndex.keys()) {
4306
- if (containerId === sourceContainerId) continue;
4307
- if (isStructuralContainer(containerId, nodeMap)) blocked.add(containerId);
4308
- }
4309
- return blocked;
4879
+ function isPointerInHardPageEdgeZone(px, py) {
4880
+ return isPointerInEdgeZone(px, py, HARD_PAGE_EDGE_GUARD);
4310
4881
  }
4311
-
4312
- // src/client/dnd/design-mode/fiber-bridge.ts
4313
- var metaCache = /* @__PURE__ */ new WeakMap();
4314
- var directMetaCache = /* @__PURE__ */ new WeakMap();
4315
- function getFiberFromElement(el) {
4316
- const keys = Object.keys(el);
4317
- for (let i = 0; i < keys.length; i++) {
4318
- if (keys[i].startsWith("__reactFiber$")) {
4319
- return el[keys[i]];
4320
- }
4321
- }
4322
- return null;
4882
+ function resolveSafeZoneRect() {
4883
+ const root = document.getElementById("root");
4884
+ if (!root) return null;
4885
+ const rect = root.getBoundingClientRect();
4886
+ const left = rect.left + SAFE_ZONE_INSET;
4887
+ const top = rect.top + SAFE_ZONE_INSET;
4888
+ const right = rect.right - SAFE_ZONE_INSET;
4889
+ const bottom = rect.bottom - SAFE_ZONE_INSET;
4890
+ if (right <= left || bottom <= top) return null;
4891
+ return { left, top, right, bottom };
4323
4892
  }
4324
- function isRootSvg(el) {
4325
- return el instanceof SVGElement && el.tagName.toLowerCase() === "svg";
4893
+ function isPointerInSafeZone(px, py, safeZone) {
4894
+ if (!safeZone) return true;
4895
+ return px >= safeZone.left && px <= safeZone.right && py >= safeZone.top && py <= safeZone.bottom;
4326
4896
  }
4327
- function isPassthroughMeta(meta) {
4328
- return !!meta?.staticProps && meta.staticProps.asChild === true;
4897
+ function isPointerOutsideSafeZone(px, py, safeZone) {
4898
+ return !isPointerInSafeZone(px, py, safeZone);
4329
4899
  }
4330
- function isWrapperMeta(meta) {
4331
- if (!meta?.componentName) return false;
4332
- return meta.componentName[0] !== meta.componentName[0].toLowerCase();
4900
+ function isPointerBlocked(px, py, safeZone) {
4901
+ return isPointerInHardPageEdgeZone(px, py) || isPointerOutsideSafeZone(px, py, safeZone);
4333
4902
  }
4334
- function hasOwnTextContent(el) {
4335
- for (const node of Array.from(el.childNodes)) {
4336
- if (node.nodeType !== Node.TEXT_NODE) continue;
4337
- if (node.textContent?.trim()) return true;
4903
+ function shouldBlockNestForLargeNode(w, h) {
4904
+ return w >= LARGE_NODE_NEST_BLOCK_MIN_WIDTH || h >= LARGE_NODE_NEST_BLOCK_MIN_HEIGHT || w * h >= LARGE_NODE_NEST_BLOCK_MIN_AREA;
4905
+ }
4906
+ function findScrollableAncestor(start, dx, dy) {
4907
+ let el = start;
4908
+ while (el && el !== document.body) {
4909
+ const cs = getComputedStyle(el);
4910
+ if (dy !== 0 && (cs.overflowY === "auto" || cs.overflowY === "scroll") && el.scrollHeight > el.clientHeight + 1) return el;
4911
+ if (dx !== 0 && (cs.overflowX === "auto" || cs.overflowX === "scroll") && el.scrollWidth > el.clientWidth + 1) return el;
4912
+ el = el.parentElement;
4338
4913
  }
4339
- return false;
4914
+ return null;
4340
4915
  }
4341
- function getCandidateChildren(el) {
4342
- return Array.from(el.children).filter((child) => {
4343
- if (!(child instanceof HTMLElement)) return false;
4344
- if (child.hasAttribute("data-design-mode-ui")) return false;
4345
- if (child.hasAttribute("data-design-mode-allow")) return false;
4346
- return !shouldIgnoreAsVirtualRuntime(child);
4347
- });
4916
+ function escapeCssAttrValue(value) {
4917
+ const esc = globalThis.CSS?.escape;
4918
+ return esc ? esc(value) : value.replace(/["\\]/g, "\\$&");
4348
4919
  }
4349
- function shouldIgnoreAsVirtualRuntime(el) {
4350
- const tag = el.tagName.toLowerCase();
4351
- if (tag === "canvas") return true;
4352
- if (tag === "script" || tag === "style" || tag === "noscript") return true;
4353
- if (el instanceof SVGElement && !isRootSvg(el)) return true;
4354
- return false;
4920
+ function isDesignModeUiElement(el) {
4921
+ if (!(el instanceof HTMLElement)) return false;
4922
+ return el.hasAttribute("data-design-mode-ui") || !!el.closest("[data-design-mode-ui]");
4355
4923
  }
4356
- function getDirectEditorMeta(el) {
4357
- const cached = directMetaCache.get(el);
4358
- if (cached !== void 0) return cached;
4359
- const fiber = getFiberFromElement(el);
4360
- const meta = fiber?.memoizedProps?.__editorMeta;
4361
- const resolved = meta ?? null;
4362
- directMetaCache.set(el, resolved);
4363
- return resolved;
4924
+ function isElementScrollable(el) {
4925
+ if (!(el instanceof HTMLElement)) return false;
4926
+ const cs = getComputedStyle(el);
4927
+ const overY = cs.overflowY;
4928
+ const overX = cs.overflowX;
4929
+ return (overY === "auto" || overY === "scroll") && el.scrollHeight > el.clientHeight || (overX === "auto" || overX === "scroll") && el.scrollWidth > el.clientWidth;
4364
4930
  }
4365
- function getEditorMeta(el) {
4366
- const cached = metaCache.get(el);
4367
- if (cached !== void 0) return cached;
4368
- let fiber = getFiberFromElement(el);
4369
- while (fiber) {
4370
- const meta = fiber.memoizedProps?.__editorMeta;
4371
- if (meta) {
4372
- metaCache.set(el, meta);
4373
- return meta;
4931
+ function resolveScrollableFromHitStack(stack) {
4932
+ for (const hit of stack) {
4933
+ if (isDesignModeUiElement(hit)) continue;
4934
+ let el = hit;
4935
+ while (el) {
4936
+ if (isElementScrollable(el)) return el;
4937
+ el = el.parentElement;
4374
4938
  }
4375
- fiber = fiber.return;
4376
4939
  }
4377
- metaCache.set(el, null);
4378
4940
  return null;
4379
4941
  }
4380
- function findMetaOwnerElement(el) {
4381
- let current = el;
4382
- while (current) {
4383
- if (getEditorMeta(current)) return current;
4384
- current = current.parentElement;
4385
- }
4386
- return null;
4387
- }
4388
- function resolveHostElementFromFiberChain(el) {
4389
- if (shouldIgnoreAsVirtualRuntime(el)) return null;
4390
- const directMeta = getDirectEditorMeta(el);
4391
- const ownerMeta = directMeta ?? getEditorMeta(el);
4392
- if (isPassthroughMeta(ownerMeta)) {
4393
- const children = getCandidateChildren(el);
4394
- if (children.length !== 1) return null;
4395
- return children[0];
4396
- }
4397
- if (directMeta && !isWrapperMeta(directMeta)) return el;
4398
- const MAX_STEPS = 10;
4399
- const ownerNodeId = ownerMeta?.nodeId ?? null;
4400
- let outermost = el;
4401
- let parent = el.parentElement;
4402
- for (let i = 0; i < MAX_STEPS; i += 1) {
4403
- if (!parent || parent === document.body) break;
4404
- if (shouldIgnoreAsVirtualRuntime(parent)) break;
4405
- const parentMeta = getEditorMeta(parent);
4406
- if (!parentMeta) break;
4407
- if (ownerNodeId && parentMeta.nodeId !== ownerNodeId) break;
4408
- if (isStructuralComponentName(parentMeta.componentName)) break;
4409
- if (!isWrapperMeta(parentMeta)) break;
4410
- if (isPassthroughMeta(parentMeta)) break;
4411
- if (hasOwnTextContent(parent)) break;
4412
- const children = getCandidateChildren(parent);
4413
- if (children.length !== 1) break;
4414
- outermost = parent;
4415
- parent = parent.parentElement;
4416
- }
4417
- return outermost;
4418
- }
4419
- function resolveDragSurface(el) {
4420
- const host = resolveHostElementFromFiberChain(el);
4421
- if (!host) {
4422
- return { host: null, metaOwner: null, kind: "direct-host" };
4423
- }
4424
- const metaOwner = findMetaOwnerElement(host) ?? findMetaOwnerElement(el);
4425
- const ownerMeta = metaOwner ? getEditorMeta(metaOwner) : null;
4426
- const kind = isPassthroughMeta(ownerMeta) ? "passthrough-slot" : metaOwner && metaOwner !== host && isWrapperMeta(ownerMeta) ? "wrapper-to-host" : "direct-host";
4427
- return {
4428
- host,
4429
- metaOwner,
4430
- kind
4431
- };
4942
+ var _undoParentNodeIdMap = /* @__PURE__ */ new WeakMap();
4943
+ var _undoParentNodeIdCounter = 0;
4944
+ function ensureParentNodeId(el) {
4945
+ const existing = el.getAttribute(DND_NODE_ID_ATTR);
4946
+ if (existing) return existing;
4947
+ if (!_undoParentNodeIdMap.has(el)) {
4948
+ _undoParentNodeIdMap.set(el, `dnd-parent:${++_undoParentNodeIdCounter}`);
4949
+ }
4950
+ const id = _undoParentNodeIdMap.get(el);
4951
+ el.setAttribute(DND_NODE_ID_ATTR, id);
4952
+ return id;
4432
4953
  }
4433
4954
 
4434
4955
  // src/client/dnd/design-mode/useElementScanner.ts
4435
- var DND_NODE_ID_ATTR = "data-dnd-node-id";
4436
4956
  function resolveContainerStrategy(el) {
4437
4957
  const cs = getComputedStyle(el);
4438
4958
  const d = cs.display;
@@ -4463,7 +4983,7 @@ function getRuntimeNodeId(el) {
4463
4983
  }
4464
4984
  function getEditorMetaNodeId(el) {
4465
4985
  if (!(el instanceof HTMLElement)) return null;
4466
- return getEditorMeta(el)?.nodeId ?? null;
4986
+ return getDirectEditorMeta(el)?.nodeId ?? null;
4467
4987
  }
4468
4988
  function isInspectorEligible(el) {
4469
4989
  if (getEditorMetaNodeId(el) !== null) return "editor-meta";
@@ -4487,7 +5007,7 @@ function inferEligibilityFromSortableParent(el) {
4487
5007
  if (child.hasAttribute("data-design-mode-ui")) return false;
4488
5008
  return !isRuntimeVisualInternal(child);
4489
5009
  });
4490
- if (siblingElements.length < 2) return null;
5010
+ if (siblingElements.length < 1) return null;
4491
5011
  return "inferred-sortable-descendant";
4492
5012
  }
4493
5013
  function ensureUniqueNodeId(base, used) {
@@ -5041,7 +5561,15 @@ function buildProjection(elements, rectOverrides) {
5041
5561
  }
5042
5562
  const containerIndex = /* @__PURE__ */ new Map();
5043
5563
  for (const node of nodeMap.values()) {
5044
- if (node.children.length === 0) continue;
5564
+ if (node.children.length === 0) {
5565
+ const scanInfo = elementIndex.get(node.id);
5566
+ const eligible = scanInfo?.editorMeta || scanInfo?.eligibilitySource === "force-allow";
5567
+ if (!eligible) continue;
5568
+ node.sortable = true;
5569
+ node.strategy = "vertical";
5570
+ containerIndex.set(node.id, node);
5571
+ continue;
5572
+ }
5045
5573
  node.sortable = true;
5046
5574
  const firstChild = elementIndex.get(node.children[0]);
5047
5575
  node.strategy = firstChild?.containerStrategy ?? "vertical";
@@ -5112,31 +5640,25 @@ function buildVerticalSlotRect(children, nodeMap, cRect, i, n) {
5112
5640
  const top = i === 0 ? cRect.top : nodeMap.get(children[i - 1])?.rect.bottom ?? cRect.top;
5113
5641
  const bottom = i === n ? cRect.bottom : nodeMap.get(children[i])?.rect.top ?? cRect.bottom;
5114
5642
  const height = Math.max(0, bottom - top);
5115
- return {
5116
- x: cRect.left,
5117
- y: top,
5118
- left: cRect.left,
5119
- top,
5120
- right: cRect.right,
5121
- bottom: top + height,
5122
- width: cRect.width,
5123
- height
5124
- };
5643
+ const prevRect = i > 0 ? nodeMap.get(children[i - 1])?.rect : null;
5644
+ const nextRect = i < n ? nodeMap.get(children[i])?.rect : null;
5645
+ const refRect = prevRect ?? nextRect;
5646
+ const left = refRect ? refRect.left : cRect.left;
5647
+ const right = refRect ? refRect.right : cRect.right;
5648
+ const width = Math.max(0, right - left);
5649
+ return { x: left, y: top, left, top, right, bottom: top + height, width, height };
5125
5650
  }
5126
5651
  function buildHorizontalSlotRect(children, nodeMap, cRect, i, n) {
5127
5652
  const left = i === 0 ? cRect.left : nodeMap.get(children[i - 1])?.rect.right ?? cRect.left;
5128
5653
  const right = i === n ? cRect.right : nodeMap.get(children[i])?.rect.left ?? cRect.right;
5129
5654
  const width = Math.max(0, right - left);
5130
- return {
5131
- x: left,
5132
- y: cRect.top,
5133
- left,
5134
- top: cRect.top,
5135
- right: left + width,
5136
- bottom: cRect.bottom,
5137
- width,
5138
- height: cRect.height
5139
- };
5655
+ const prevRect = i > 0 ? nodeMap.get(children[i - 1])?.rect : null;
5656
+ const nextRect = i < n ? nodeMap.get(children[i])?.rect : null;
5657
+ const refRect = prevRect ?? nextRect;
5658
+ const top = refRect ? refRect.top : cRect.top;
5659
+ const bottom = refRect ? refRect.bottom : cRect.bottom;
5660
+ const height = Math.max(0, bottom - top);
5661
+ return { x: left, y: top, left, top, right: left + width, bottom, width, height };
5140
5662
  }
5141
5663
  function buildRectSlotRect(children, nodeMap, cRect, i, n, rows) {
5142
5664
  const findRow = (childIdx) => rows.find((r) => childIdx >= r.startIdx && childIdx <= r.endIdx) ?? null;
@@ -5200,6 +5722,79 @@ function buildRectSlotRect(children, nodeMap, cRect, i, n, rows) {
5200
5722
  };
5201
5723
  }
5202
5724
 
5725
+ // src/client/dnd/design-mode/history.ts
5726
+ function applyInsertCommand(command, direction, resolveNodeElement) {
5727
+ if (direction === "undo") {
5728
+ const el = resolveNodeElement(command.nodeId);
5729
+ if (el?.isConnected) el.remove();
5730
+ return true;
5731
+ }
5732
+ const container = resolveNodeElement(command.containerId);
5733
+ if (!container) return false;
5734
+ const existing = resolveNodeElement(command.nodeId);
5735
+ if (existing?.isConnected) return true;
5736
+ const template = document.createElement("template");
5737
+ template.innerHTML = command.html.trim();
5738
+ const newEl = template.content.firstElementChild;
5739
+ if (!newEl) return false;
5740
+ newEl.setAttribute("data-dnd-node-id", command.nodeId);
5741
+ newEl.setAttribute("data-dnd-force-allow", "");
5742
+ newEl.setAttribute("data-dnd-moved", "");
5743
+ const kids = Array.from(container.children).filter(
5744
+ (c) => c instanceof HTMLElement
5745
+ );
5746
+ container.insertBefore(newEl, kids[command.index] ?? null);
5747
+ return true;
5748
+ }
5749
+ var DEFAULT_LIMIT = 300;
5750
+ var DndCommandHistory = class {
5751
+ constructor(limit = DEFAULT_LIMIT) {
5752
+ this.limit = limit;
5753
+ __publicField(this, "undoStack", []);
5754
+ __publicField(this, "redoStack", []);
5755
+ }
5756
+ execute(command, apply) {
5757
+ const applied = apply(command, "redo");
5758
+ if (!applied) return false;
5759
+ this.undoStack.push(command);
5760
+ if (this.undoStack.length > this.limit) this.undoStack.shift();
5761
+ this.redoStack = [];
5762
+ return true;
5763
+ }
5764
+ undo(apply) {
5765
+ const command = this.undoStack.pop();
5766
+ if (!command) return false;
5767
+ const applied = apply(command, "undo");
5768
+ if (!applied) {
5769
+ this.undoStack.push(command);
5770
+ return false;
5771
+ }
5772
+ this.redoStack.push(command);
5773
+ return true;
5774
+ }
5775
+ redo(apply) {
5776
+ const command = this.redoStack.pop();
5777
+ if (!command) return false;
5778
+ const applied = apply(command, "redo");
5779
+ if (!applied) {
5780
+ this.redoStack.push(command);
5781
+ return false;
5782
+ }
5783
+ this.undoStack.push(command);
5784
+ return true;
5785
+ }
5786
+ clear() {
5787
+ this.undoStack = [];
5788
+ this.redoStack = [];
5789
+ }
5790
+ canUndo() {
5791
+ return this.undoStack.length > 0;
5792
+ }
5793
+ canRedo() {
5794
+ return this.redoStack.length > 0;
5795
+ }
5796
+ };
5797
+
5203
5798
  // src/client/dnd/design-mode/resize.ts
5204
5799
  function splitTopLevelSpaceSeparated(value) {
5205
5800
  const tokens = [];
@@ -5231,9 +5826,6 @@ function parseSpanValue(value) {
5231
5826
  const parsed = Number.parseInt(match[1], 10);
5232
5827
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
5233
5828
  }
5234
- function clamp(value, min, max) {
5235
- return Math.max(min, Math.min(max, value));
5236
- }
5237
5829
  function resolveResizeTargetElement(selectedEl) {
5238
5830
  const elementChildren = Array.from(selectedEl.children).filter(
5239
5831
  (child2) => child2 instanceof HTMLElement
@@ -5314,100 +5906,6 @@ function applyStyleSnapshot(el, snapshot) {
5314
5906
  }
5315
5907
  }
5316
5908
 
5317
- // src/client/dnd/design-mode/helpers.ts
5318
- var DND_NODE_ID_ATTR2 = "data-dnd-node-id";
5319
- var PAGE_EDGE_GUARD = 10;
5320
- var HARD_PAGE_EDGE_GUARD = 4;
5321
- var SAFE_ZONE_INSET = 8;
5322
- var LARGE_NODE_NEST_BLOCK_MIN_WIDTH = 520;
5323
- var LARGE_NODE_NEST_BLOCK_MIN_HEIGHT = 260;
5324
- var LARGE_NODE_NEST_BLOCK_MIN_AREA = 18e4;
5325
- function isEligibleForDrag(sc) {
5326
- const source = sc.eligibilitySource ?? "editor-meta";
5327
- return source === "editor-meta" || source === "force-allow" || source === "inferred-sortable-descendant";
5328
- }
5329
- function isPointerInPageEdgeZone(px, py) {
5330
- return px <= PAGE_EDGE_GUARD || py <= PAGE_EDGE_GUARD || px >= window.innerWidth - PAGE_EDGE_GUARD || py >= window.innerHeight - PAGE_EDGE_GUARD;
5331
- }
5332
- function isPointerInHardPageEdgeZone(px, py) {
5333
- 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;
5334
- }
5335
- function resolveSafeZoneRect() {
5336
- const root = document.getElementById("root");
5337
- if (!root) return null;
5338
- const rect = root.getBoundingClientRect();
5339
- const left = rect.left + SAFE_ZONE_INSET;
5340
- const top = rect.top + SAFE_ZONE_INSET;
5341
- const right = rect.right - SAFE_ZONE_INSET;
5342
- const bottom = rect.bottom - SAFE_ZONE_INSET;
5343
- if (right <= left || bottom <= top) return null;
5344
- return { left, top, right, bottom };
5345
- }
5346
- function isPointerInSafeZone(px, py, safeZone) {
5347
- if (!safeZone) return true;
5348
- return px >= safeZone.left && px <= safeZone.right && py >= safeZone.top && py <= safeZone.bottom;
5349
- }
5350
- function isPointerOutsideSafeZone(px, py, safeZone) {
5351
- return !isPointerInSafeZone(px, py, safeZone);
5352
- }
5353
- function isPointerBlocked(px, py, safeZone) {
5354
- return isPointerInHardPageEdgeZone(px, py) || isPointerOutsideSafeZone(px, py, safeZone);
5355
- }
5356
- function shouldBlockNestForLargeNode(w, h) {
5357
- return w >= LARGE_NODE_NEST_BLOCK_MIN_WIDTH || h >= LARGE_NODE_NEST_BLOCK_MIN_HEIGHT || w * h >= LARGE_NODE_NEST_BLOCK_MIN_AREA;
5358
- }
5359
- function findScrollableAncestor(start, dx, dy) {
5360
- let el = start;
5361
- while (el && el !== document.body) {
5362
- const cs = getComputedStyle(el);
5363
- if (dy !== 0 && (cs.overflowY === "auto" || cs.overflowY === "scroll") && el.scrollHeight > el.clientHeight + 1) return el;
5364
- if (dx !== 0 && (cs.overflowX === "auto" || cs.overflowX === "scroll") && el.scrollWidth > el.clientWidth + 1) return el;
5365
- el = el.parentElement;
5366
- }
5367
- return null;
5368
- }
5369
- function escapeCssAttrValue(value) {
5370
- const esc = globalThis.CSS?.escape;
5371
- return esc ? esc(value) : value.replace(/["\\]/g, "\\$&");
5372
- }
5373
- function isDesignModeUiElement(el) {
5374
- if (!(el instanceof HTMLElement)) return false;
5375
- return el.hasAttribute("data-design-mode-ui") || !!el.closest("[data-design-mode-ui]");
5376
- }
5377
- function isElementScrollable(el) {
5378
- if (!(el instanceof HTMLElement)) return false;
5379
- const cs = getComputedStyle(el);
5380
- const overY = cs.overflowY;
5381
- const overX = cs.overflowX;
5382
- return (overY === "auto" || overY === "scroll") && el.scrollHeight > el.clientHeight || (overX === "auto" || overX === "scroll") && el.scrollWidth > el.clientWidth;
5383
- }
5384
- function resolveScrollableFromHitStack(stack) {
5385
- for (const hit of stack) {
5386
- if (isDesignModeUiElement(hit)) continue;
5387
- let el = hit;
5388
- while (el) {
5389
- if (isElementScrollable(el)) return el;
5390
- el = el.parentElement;
5391
- }
5392
- }
5393
- return null;
5394
- }
5395
- var _undoParentNodeIdMap = /* @__PURE__ */ new WeakMap();
5396
- var _undoParentNodeIdCounter = 0;
5397
- function ensureParentNodeId(el) {
5398
- const existing = el.getAttribute(DND_NODE_ID_ATTR2);
5399
- if (existing) return existing;
5400
- if (!_undoParentNodeIdMap.has(el)) {
5401
- _undoParentNodeIdMap.set(el, `dnd-parent:${++_undoParentNodeIdCounter}`);
5402
- }
5403
- const id = _undoParentNodeIdMap.get(el);
5404
- el.setAttribute(DND_NODE_ID_ATTR2, id);
5405
- return id;
5406
- }
5407
- function ensureElementNodeId(el) {
5408
- return ensureParentNodeId(el);
5409
- }
5410
-
5411
5909
  // src/client/dnd/design-mode/drag-preview.tsx
5412
5910
  import { useRef as useRef7, useEffect as useEffect6 } from "react";
5413
5911
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -5508,7 +6006,7 @@ function findNearestContainerId(nodeId, projection, nodeMap, allowStructuralFall
5508
6006
  const parentId = node?.parentId ?? null;
5509
6007
  if (!parentId) break;
5510
6008
  const parentContainer = projection.containerIndex.get(parentId);
5511
- if (parentContainer && parentContainer.children.length >= 2) {
6009
+ if (parentContainer && parentContainer.children.length >= 1) {
5512
6010
  if (isValidLaneContainer(parentId, projection, nodeMap)) return parentId;
5513
6011
  if (allowStructuralFallback && !structuralFallback) structuralFallback = parentId;
5514
6012
  }
@@ -5593,12 +6091,6 @@ function findDirectChildUnderParent(child, parent) {
5593
6091
  }
5594
6092
  return null;
5595
6093
  }
5596
- function isContiguous(indices) {
5597
- if (indices.length === 0) return false;
5598
- const min = Math.min(...indices);
5599
- const max = Math.max(...indices);
5600
- return max - min + 1 === indices.length;
5601
- }
5602
6094
  function createPlacement(parentNodeId, laneContainerId, commitNodeId, beforeNodeId, afterNodeId, index) {
5603
6095
  return {
5604
6096
  parentNodeId,
@@ -5612,7 +6104,16 @@ function createPlacement(parentNodeId, laneContainerId, commitNodeId, beforeNode
5612
6104
  function buildCommitLaneSnapshot(laneContainerId, projection, nodeMap) {
5613
6105
  const lane = projection.containerIndex.get(laneContainerId);
5614
6106
  const laneElement = nodeMap.get(laneContainerId)?.element ?? null;
5615
- if (!lane || lane.children.length === 0 || !laneElement) return null;
6107
+ if (!lane || !laneElement) return null;
6108
+ if (lane.children.length === 0) {
6109
+ return {
6110
+ laneContainerId,
6111
+ commitParentNodeId: ensureParentNodeId(laneElement),
6112
+ orderedCommitNodeIds: [],
6113
+ commitNodeByAnchorId: /* @__PURE__ */ new Map(),
6114
+ committable: true
6115
+ };
6116
+ }
5616
6117
  const childElements = lane.children.map((childId) => nodeMap.get(childId)?.element ?? null).filter((el) => !!el);
5617
6118
  if (childElements.length !== lane.children.length) return null;
5618
6119
  const commitParent = findLowestCommonAncestor(childElements, laneElement);
@@ -5629,19 +6130,18 @@ function buildCommitLaneSnapshot(laneContainerId, projection, nodeMap) {
5629
6130
  if (seenRoots.has(commitRoot)) return null;
5630
6131
  seenRoots.add(commitRoot);
5631
6132
  commitRoots.push(commitRoot);
5632
- commitNodeByAnchorId.set(anchorNodeId, ensureElementNodeId(commitRoot));
6133
+ commitNodeByAnchorId.set(anchorNodeId, ensureParentNodeId(commitRoot));
5633
6134
  }
5634
6135
  const childList = Array.from(commitParent.children).filter(
5635
6136
  (child) => child instanceof HTMLElement
5636
6137
  );
5637
6138
  const commitRootIndices = commitRoots.map((root) => childList.indexOf(root));
5638
6139
  if (commitRootIndices.some((index) => index < 0)) return null;
5639
- if (!isContiguous(commitRootIndices)) return null;
5640
6140
  const domOrderedRoots = [...commitRoots].sort(
5641
6141
  (a, b) => childList.indexOf(a) - childList.indexOf(b)
5642
6142
  );
5643
- const projectionOrderedIds = commitRoots.map((root) => ensureElementNodeId(root));
5644
- const domOrderedIds = domOrderedRoots.map((root) => ensureElementNodeId(root));
6143
+ const projectionOrderedIds = commitRoots.map((root) => ensureParentNodeId(root));
6144
+ const domOrderedIds = domOrderedRoots.map((root) => ensureParentNodeId(root));
5645
6145
  if (projectionOrderedIds.length !== domOrderedIds.length) return null;
5646
6146
  if (projectionOrderedIds.some((id, index) => id !== domOrderedIds[index])) return null;
5647
6147
  return {
@@ -5686,7 +6186,7 @@ function resolveAppendCommitPlacement(parentEl, commitNodeId, laneContainerId) {
5686
6186
  const children = Array.from(parentEl.children).filter(
5687
6187
  (child) => child instanceof HTMLElement
5688
6188
  );
5689
- const lastChild = children.length > 0 ? ensureElementNodeId(children[children.length - 1]) : null;
6189
+ const lastChild = children.length > 0 ? ensureParentNodeId(children[children.length - 1]) : null;
5690
6190
  return createPlacement(
5691
6191
  ensureParentNodeId(parentEl),
5692
6192
  laneContainerId,
@@ -5741,6 +6241,58 @@ function createDesignModeCollisionDetection(blockedContainerIds) {
5741
6241
  };
5742
6242
  }
5743
6243
 
6244
+ // src/client/dnd/design-mode/arrange-session.ts
6245
+ function serializeArrangeChange(event, index) {
6246
+ return {
6247
+ id: `${event.kind}:${event.operationId}:${index}`,
6248
+ kind: event.kind,
6249
+ operationId: event.operationId,
6250
+ commandType: event.kind === "move" ? event.commandType : void 0,
6251
+ nodeKey: event.nodeKey,
6252
+ nodeId: event.nodeId,
6253
+ inspectorRef: event.inspectorRef,
6254
+ selectedNodeKey: event.kind === "move" ? event.selectedNodeKey : void 0,
6255
+ selectedNodeId: event.kind === "move" ? event.selectedNodeId : void 0,
6256
+ selectedInspectorRef: event.kind === "move" ? event.selectedInspectorRef : void 0,
6257
+ anchorNodeKey: event.kind === "move" ? event.anchorNodeKey : void 0,
6258
+ anchorNodeId: event.kind === "move" ? event.anchorNodeId : void 0,
6259
+ container: event.kind === "move" ? event.container : void 0,
6260
+ beforeSibling: event.kind === "move" ? event.beforeSibling : void 0,
6261
+ afterSibling: event.kind === "move" ? event.afterSibling : void 0,
6262
+ sourceContainer: event.kind === "move" ? event.sourceContainer : void 0,
6263
+ sourceBeforeSibling: event.kind === "move" ? event.sourceBeforeSibling : void 0,
6264
+ sourceAfterSibling: event.kind === "move" ? event.sourceAfterSibling : void 0,
6265
+ diagnostics: event.kind === "move" ? event.diagnostics : void 0,
6266
+ // Insert-specific fields
6267
+ displayName: event.kind === "insert" ? event.displayName : void 0,
6268
+ html: event.kind === "insert" ? event.html : void 0,
6269
+ containerId: event.kind === "insert" ? event.containerId : void 0,
6270
+ containerNodeKey: event.kind === "insert" ? event.containerNodeKey : void 0,
6271
+ insertIndex: event.kind === "insert" ? event.index : void 0,
6272
+ summary: summarizeEditorChangeEvent(event),
6273
+ at: event.at
6274
+ };
6275
+ }
6276
+ function buildArrangeSessionSnapshot(params) {
6277
+ const { enabled, aiOutput } = params;
6278
+ const coalescedChanges = coalesceOrderedChanges(params.orderedChanges);
6279
+ const updatedAt = typeof params.updatedAt === "number" ? params.updatedAt : Date.now();
6280
+ return {
6281
+ mode: enabled ? "arrange" : "off",
6282
+ canUndo: params.orderedChanges.length > 0,
6283
+ changes: coalescedChanges.map(
6284
+ (event, index) => serializeArrangeChange(event, index)
6285
+ ),
6286
+ diagnostics: aiOutput.diagnostics ?? null,
6287
+ output: {
6288
+ json: aiOutput,
6289
+ prompt: aiOutput.aiPromptContext,
6290
+ summary: coalescedChanges.length === 0 ? "No arrange changes recorded yet." : `${coalescedChanges.length} arrange change${coalescedChanges.length === 1 ? "" : "s"} recorded.`
6291
+ },
6292
+ updatedAt
6293
+ };
6294
+ }
6295
+
5744
6296
  // src/client/dnd/design-mode/useDesignModeAutoScroll.ts
5745
6297
  import { useEffect as useEffect7 } from "react";
5746
6298
  function edgeVelocity(distanceToEdge, zone, maxSpeed) {
@@ -5756,11 +6308,16 @@ function findScrollableAtPoint(x, y) {
5756
6308
  return resolveScrollableFromHitStack(stack) ?? (document.scrollingElement ?? document.documentElement);
5757
6309
  }
5758
6310
  function getScrollerKey(el) {
5759
- return el.getAttribute?.(DND_NODE_ID_ATTR2) ?? el.id ?? el.tagName.toLowerCase();
6311
+ return el.getAttribute?.(DND_NODE_ID_ATTR) ?? el.id ?? el.tagName.toLowerCase();
5760
6312
  }
5761
6313
  function pointerInsideRect(x, y, rect, inset = 0) {
5762
6314
  return x >= rect.left - inset && x <= rect.right + inset && y >= rect.top - inset && y <= rect.bottom + inset;
5763
6315
  }
6316
+ var AUTO_SCROLL_ZONE_PX = 120;
6317
+ var AUTO_SCROLL_MAX_SPEED = 24;
6318
+ var SCROLLER_RECT_RECALC_INTERVAL = 8;
6319
+ var SCROLLER_SWITCH_FRAMES = 3;
6320
+ var SCROLLER_EDGE_HYSTERESIS_PX = 18;
5764
6321
  function useDesignModeAutoScroll({
5765
6322
  active,
5766
6323
  mouseRef,
@@ -5769,11 +6326,6 @@ function useDesignModeAutoScroll({
5769
6326
  }) {
5770
6327
  useEffect7(() => {
5771
6328
  if (!active) return;
5772
- const ZONE = 120;
5773
- const MAX = 24;
5774
- const RECALC_INTERVAL = 8;
5775
- const SWITCH_FRAMES = 3;
5776
- const SCROLLER_EDGE_HYSTERESIS = 18;
5777
6329
  let scrollEl = findScrollableAtPoint(mouseRef.current.x, mouseRef.current.y);
5778
6330
  let cachedRect = scrollEl.getBoundingClientRect();
5779
6331
  let rectAge = 0;
@@ -5781,17 +6333,17 @@ function useDesignModeAutoScroll({
5781
6333
  const tick = () => {
5782
6334
  const { x, y } = mouseRef.current;
5783
6335
  const safeZone = safeZoneRef.current;
5784
- if (++rectAge > RECALC_INTERVAL) {
6336
+ if (++rectAge > SCROLLER_RECT_RECALC_INTERVAL) {
5785
6337
  const candidate = findScrollableAtPoint(x, y);
5786
6338
  const candidateKey = getScrollerKey(candidate);
5787
6339
  const currentKey = getScrollerKey(scrollEl);
5788
6340
  const currentRect = scrollEl.getBoundingClientRect();
5789
- if (candidateKey !== currentKey && !pointerInsideRect(x, y, currentRect, SCROLLER_EDGE_HYSTERESIS)) {
6341
+ if (candidateKey !== currentKey && !pointerInsideRect(x, y, currentRect, SCROLLER_EDGE_HYSTERESIS_PX)) {
5790
6342
  const pendingKey = autoScrollStateRef.current.pendingScrollerKey;
5791
6343
  const pendingFrames = pendingKey === candidateKey ? (autoScrollStateRef.current.pendingFrames ?? 0) + 1 : 1;
5792
6344
  autoScrollStateRef.current.pendingScrollerKey = candidateKey;
5793
6345
  autoScrollStateRef.current.pendingFrames = pendingFrames;
5794
- if (pendingFrames >= SWITCH_FRAMES) {
6346
+ if (pendingFrames >= SCROLLER_SWITCH_FRAMES) {
5795
6347
  scrollEl = candidate;
5796
6348
  cachedRect = candidate.getBoundingClientRect();
5797
6349
  autoScrollStateRef.current.pendingScrollerKey = null;
@@ -5816,82 +6368,36 @@ function useDesignModeAutoScroll({
5816
6368
  return;
5817
6369
  }
5818
6370
  const dTop = y - cachedRect.top;
5819
- const dBot = cachedRect.bottom - y;
5820
- let dy = 0;
5821
- if (dTop > 0 && dTop < ZONE) dy = -edgeVelocity(dTop, ZONE, MAX);
5822
- else if (dBot > 0 && dBot < ZONE) dy = edgeVelocity(dBot, ZONE, MAX);
5823
- const dLeft = x - cachedRect.left;
5824
- const dRight = cachedRect.right - x;
5825
- let dx = 0;
5826
- if (dLeft > 0 && dLeft < ZONE) dx = -edgeVelocity(dLeft, ZONE, MAX);
5827
- else if (dRight > 0 && dRight < ZONE) dx = edgeVelocity(dRight, ZONE, MAX);
5828
- if (dy !== 0) scrollEl.scrollTop += dy;
5829
- if (dx !== 0) scrollEl.scrollLeft += dx;
5830
- autoScrollStateRef.current = {
5831
- ...autoScrollStateRef.current,
5832
- scrollerKey: getScrollerKey(scrollEl),
5833
- vx: dx,
5834
- vy: dy
5835
- };
5836
- raf = requestAnimationFrame(tick);
5837
- };
5838
- raf = requestAnimationFrame(tick);
5839
- return () => cancelAnimationFrame(raf);
5840
- }, [active, autoScrollStateRef, mouseRef, safeZoneRef]);
5841
- }
5842
-
5843
- // src/client/dnd/design-mode/useOverlayRefs.ts
5844
- import { useRef as useRef8, useMemo as useMemo5 } from "react";
5845
-
5846
- // src/client/dnd/design-mode/history.ts
5847
- var DEFAULT_LIMIT = 300;
5848
- var DndCommandHistory = class {
5849
- constructor(limit = DEFAULT_LIMIT) {
5850
- this.limit = limit;
5851
- __publicField(this, "undoStack", []);
5852
- __publicField(this, "redoStack", []);
5853
- }
5854
- execute(command, apply) {
5855
- const applied = apply(command, "redo");
5856
- if (!applied) return false;
5857
- this.undoStack.push(command);
5858
- if (this.undoStack.length > this.limit) this.undoStack.shift();
5859
- this.redoStack = [];
5860
- return true;
5861
- }
5862
- undo(apply) {
5863
- const command = this.undoStack.pop();
5864
- if (!command) return false;
5865
- const applied = apply(command, "undo");
5866
- if (!applied) {
5867
- this.undoStack.push(command);
5868
- return false;
5869
- }
5870
- this.redoStack.push(command);
5871
- return true;
5872
- }
5873
- redo(apply) {
5874
- const command = this.redoStack.pop();
5875
- if (!command) return false;
5876
- const applied = apply(command, "redo");
5877
- if (!applied) {
5878
- this.redoStack.push(command);
5879
- return false;
5880
- }
5881
- this.undoStack.push(command);
5882
- return true;
5883
- }
5884
- clear() {
5885
- this.undoStack = [];
5886
- this.redoStack = [];
5887
- }
5888
- canUndo() {
5889
- return this.undoStack.length > 0;
5890
- }
5891
- canRedo() {
5892
- return this.redoStack.length > 0;
5893
- }
5894
- };
6371
+ const dBot = cachedRect.bottom - y;
6372
+ let dy = 0;
6373
+ if (dTop > 0 && dTop < AUTO_SCROLL_ZONE_PX)
6374
+ dy = -edgeVelocity(dTop, AUTO_SCROLL_ZONE_PX, AUTO_SCROLL_MAX_SPEED);
6375
+ else if (dBot > 0 && dBot < AUTO_SCROLL_ZONE_PX)
6376
+ dy = edgeVelocity(dBot, AUTO_SCROLL_ZONE_PX, AUTO_SCROLL_MAX_SPEED);
6377
+ const dLeft = x - cachedRect.left;
6378
+ const dRight = cachedRect.right - x;
6379
+ let dx = 0;
6380
+ if (dLeft > 0 && dLeft < AUTO_SCROLL_ZONE_PX)
6381
+ dx = -edgeVelocity(dLeft, AUTO_SCROLL_ZONE_PX, AUTO_SCROLL_MAX_SPEED);
6382
+ else if (dRight > 0 && dRight < AUTO_SCROLL_ZONE_PX)
6383
+ dx = edgeVelocity(dRight, AUTO_SCROLL_ZONE_PX, AUTO_SCROLL_MAX_SPEED);
6384
+ if (dy !== 0) scrollEl.scrollTop += dy;
6385
+ if (dx !== 0) scrollEl.scrollLeft += dx;
6386
+ autoScrollStateRef.current = {
6387
+ ...autoScrollStateRef.current,
6388
+ scrollerKey: getScrollerKey(scrollEl),
6389
+ vx: dx,
6390
+ vy: dy
6391
+ };
6392
+ raf = requestAnimationFrame(tick);
6393
+ };
6394
+ raf = requestAnimationFrame(tick);
6395
+ return () => cancelAnimationFrame(raf);
6396
+ }, [active, autoScrollStateRef, mouseRef, safeZoneRef]);
6397
+ }
6398
+
6399
+ // src/client/dnd/design-mode/useOverlayRefs.ts
6400
+ import { useRef as useRef8, useMemo as useMemo5 } from "react";
5895
6401
 
5896
6402
  // src/client/dnd/design-mode/sortable-preview-animator.ts
5897
6403
  function isIdentityTransform(transform) {
@@ -6105,43 +6611,6 @@ function useHoverDetection({
6105
6611
  // src/client/dnd/design-mode/useDragVisualLoop.ts
6106
6612
  import { useEffect as useEffect9 } from "react";
6107
6613
 
6108
- // src/client/dnd/design-mode/drag-lock.ts
6109
- var MIN_UNLOCK_MARGIN = 32;
6110
- var MAX_UNLOCK_MARGIN = 72;
6111
- var MIN_UNLOCK_DWELL_MS = 170;
6112
- var MAX_UNLOCK_DWELL_MS = 300;
6113
- function clamp2(value, min, max) {
6114
- return Math.min(max, Math.max(min, value));
6115
- }
6116
- function resolveCrossUnlockParams(rect) {
6117
- const width = Math.max(0, rect.right - rect.left);
6118
- const height = Math.max(0, rect.bottom - rect.top);
6119
- const shortest = Math.max(1, Math.min(width, height));
6120
- const margin = Math.round(clamp2(shortest * 0.14, MIN_UNLOCK_MARGIN, MAX_UNLOCK_MARGIN));
6121
- const dwellMs = Math.round(clamp2(shortest * 0.7, MIN_UNLOCK_DWELL_MS, MAX_UNLOCK_DWELL_MS));
6122
- return { margin, dwellMs };
6123
- }
6124
- function checkCrossUnlock(lock, adjustedPx, adjustedPy, lockRect, now, margin, dwellMs, graceMs) {
6125
- if (lock.isCrossUnlocked) {
6126
- if (lock.unlockUntilTs !== null && now < lock.unlockUntilTs) return lock;
6127
- return lock;
6128
- }
6129
- const outsideBand = adjustedPx < lockRect.left - margin || adjustedPx > lockRect.right + margin || adjustedPy < lockRect.top - margin || adjustedPy > lockRect.bottom + margin;
6130
- if (!outsideBand) {
6131
- return lock.exitSince !== null ? { ...lock, exitSince: null } : lock;
6132
- }
6133
- const exitSince = lock.exitSince ?? now;
6134
- if (now - exitSince >= dwellMs) {
6135
- return {
6136
- containerId: lock.containerId,
6137
- isCrossUnlocked: true,
6138
- exitSince,
6139
- unlockUntilTs: now + graceMs
6140
- };
6141
- }
6142
- return lock.exitSince !== null ? lock : { ...lock, exitSince: now };
6143
- }
6144
-
6145
6614
  // src/client/dnd/design-mode/quadtree.ts
6146
6615
  function overlaps(a, b) {
6147
6616
  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;
@@ -6151,6 +6620,7 @@ function fitsInside(outer, inner) {
6151
6620
  }
6152
6621
  var CAPACITY = 8;
6153
6622
  var MAX_DEPTH2 = 8;
6623
+ var ROOT_BOUNDS_PAD = 100;
6154
6624
  var QTNode = class _QTNode {
6155
6625
  constructor(bounds, depth = 0) {
6156
6626
  __publicField(this, "bounds");
@@ -6248,12 +6718,11 @@ var Quadtree = class _Quadtree {
6248
6718
  if (r.right > maxX) maxX = r.right;
6249
6719
  if (r.bottom > maxY) maxY = r.bottom;
6250
6720
  }
6251
- const PAD = 100;
6252
6721
  const root = new QTNode({
6253
- x: minX - PAD,
6254
- y: minY - PAD,
6255
- w: maxX - minX + 2 * PAD,
6256
- h: maxY - minY + 2 * PAD
6722
+ x: minX - ROOT_BOUNDS_PAD,
6723
+ y: minY - ROOT_BOUNDS_PAD,
6724
+ w: maxX - minX + 2 * ROOT_BOUNDS_PAD,
6725
+ h: maxY - minY + 2 * ROOT_BOUNDS_PAD
6257
6726
  });
6258
6727
  for (const [id, r] of rects) {
6259
6728
  if (r.width > 0 && r.height > 0) {
@@ -6355,9 +6824,9 @@ function getSlotQueryRadius() {
6355
6824
  const vh = window.innerHeight;
6356
6825
  return Math.min(500, Math.max(100, Math.max(vw, vh) * 0.2));
6357
6826
  }
6358
- var NEST_EDGE_FRACTION = 0.28;
6359
- var ORIGIN_RETURN_BAND_PX = 56;
6360
- var LAST_CONTAINER_REENTRY_SLOP_PX = 40;
6827
+ var NEST_EDGE_FRACTION = 0.2;
6828
+ var ORIGIN_RETURN_BAND_PX = 24;
6829
+ var LAST_CONTAINER_REENTRY_SLOP_PX = 16;
6361
6830
  var ROW_OVERLAP_TOLERANCE_PX = 1;
6362
6831
  function normalizeContainerSet(containers) {
6363
6832
  if (!containers) return null;
@@ -6435,8 +6904,6 @@ var DragIntentEngine = class {
6435
6904
  // Pre-computed set of slot IDs that would produce no movement for the active item.
6436
6905
  __publicField(this, "noOpSlotIds");
6437
6906
  __publicField(this, "originContainerId");
6438
- __publicField(this, "laneRootContainerId");
6439
- __publicField(this, "laneParentId");
6440
6907
  __publicField(this, "laneAllowedContainers");
6441
6908
  __publicField(this, "blockedContainerIds");
6442
6909
  __publicField(this, "targetPlane", "lane");
@@ -6448,8 +6915,6 @@ var DragIntentEngine = class {
6448
6915
  this.queryRadius = getSlotQueryRadius();
6449
6916
  this.slotsByContainer = buildSlotsByContainer(projection.slotIndex);
6450
6917
  this.originContainerId = this.getNodeParentContainerId(activeId);
6451
- this.laneRootContainerId = options?.laneRootContainerId ?? this.originContainerId;
6452
- this.laneParentId = this.laneRootContainerId ? this.containerIndex.get(this.laneRootContainerId)?.parentId ?? null : null;
6453
6918
  this.laneAllowedContainers = normalizeContainerSet(options?.laneAllowedContainers);
6454
6919
  this.blockedContainerIds = normalizeContainerSet(options?.blockedContainerIds);
6455
6920
  const noOpSlotIds = /* @__PURE__ */ new Set();
@@ -6481,10 +6946,6 @@ var DragIntentEngine = class {
6481
6946
  setTargetPlane(plane) {
6482
6947
  this.targetPlane = plane;
6483
6948
  }
6484
- setLaneAllowedContainers(containers) {
6485
- this.laneAllowedContainers = normalizeContainerSet(containers);
6486
- this.reset();
6487
- }
6488
6949
  getTargetPlane() {
6489
6950
  return this.targetPlane;
6490
6951
  }
@@ -6583,7 +7044,7 @@ var DragIntentEngine = class {
6583
7044
  if (this.isInvalidDropContainer(pointerContainerId)) return null;
6584
7045
  for (const childId of container.children) {
6585
7046
  if (childId === this.activeId) continue;
6586
- if (this.containerIndex.has(childId)) continue;
7047
+ if (this.isInvalidDropContainer(childId)) continue;
6587
7048
  const childNode = this.getNodeRect(childId);
6588
7049
  if (!childNode) continue;
6589
7050
  if (!pointInRect(px, py, childNode)) continue;
@@ -6608,9 +7069,16 @@ var DragIntentEngine = class {
6608
7069
  if (laneLock && !this.isLaneContainer(id)) continue;
6609
7070
  if (!pointInRect(px, py, node.rect)) continue;
6610
7071
  if (this.hasUsableSlots(id) === false) continue;
6611
- pointerContainers.push({ id, depth: node.depth, area: node.rect.width * node.rect.height });
7072
+ const r = node.rect;
7073
+ const edgeDistX = Math.min(px - r.left, r.right - px);
7074
+ const edgeDistY = Math.min(py - r.top, r.bottom - py);
7075
+ const minDim = Math.min(r.width, r.height);
7076
+ const edgeFraction = Math.min(edgeDistX, edgeDistY) / Math.max(1, minDim);
7077
+ const edgePenalty = edgeFraction < 0.1 ? 1 : 0;
7078
+ pointerContainers.push({ id, depth: node.depth, area: node.rect.width * node.rect.height, edgePenalty });
6612
7079
  }
6613
7080
  pointerContainers.sort((a, b) => {
7081
+ if (a.edgePenalty !== b.edgePenalty) return a.edgePenalty - b.edgePenalty;
6614
7082
  if (b.depth !== a.depth) return b.depth - a.depth;
6615
7083
  return a.area - b.area;
6616
7084
  });
@@ -6623,7 +7091,9 @@ var DragIntentEngine = class {
6623
7091
  }
6624
7092
  if (this.originContainerId && this.originContainerId !== pointerContainerId && !this.isInvalidDropContainer(this.originContainerId) && !this.isBlockedContainer(this.originContainerId) && (!laneLock || this.isLaneContainer(this.originContainerId)) && this.hasUsableSlots(this.originContainerId)) {
6625
7093
  const originNode = this.containerIndex.get(this.originContainerId);
6626
- if (originNode && pointInExpandedRect(px, py, originNode.rect, ORIGIN_RETURN_BAND_PX)) {
7094
+ const currentNode = pointerContainerId ? this.containerIndex.get(pointerContainerId) : null;
7095
+ const originIsDeeper = originNode && (!currentNode || originNode.depth >= currentNode.depth);
7096
+ if (originIsDeeper && originNode && pointInExpandedRect(px, py, originNode.rect, ORIGIN_RETURN_BAND_PX)) {
6627
7097
  pointerContainerId = this.originContainerId;
6628
7098
  }
6629
7099
  }
@@ -6690,8 +7160,13 @@ var DragIntentEngine = class {
6690
7160
  }
6691
7161
  return n;
6692
7162
  }
7163
+ const SLOT_ZONE_PX = 30;
6693
7164
  for (const child of childRects) {
6694
7165
  const midY = child.rect.top + child.rect.height / 2;
7166
+ const clampedTop = Math.max(child.rect.top, midY - SLOT_ZONE_PX);
7167
+ if (py < clampedTop) return child.index;
7168
+ const clampedBottom = Math.min(child.rect.bottom, midY + SLOT_ZONE_PX);
7169
+ if (py > clampedBottom) continue;
6695
7170
  if (py < midY) return child.index;
6696
7171
  }
6697
7172
  return n;
@@ -6760,15 +7235,14 @@ var DragIntentEngine = class {
6760
7235
  hasUsableSlots(containerId) {
6761
7236
  const filtered = this.getContainerSlots(containerId);
6762
7237
  if (filtered.length > 0) return true;
6763
- return (this.slotsByContainer.get(containerId)?.length ?? 0) > 0;
7238
+ const raw = this.slotsByContainer.get(containerId) ?? [];
7239
+ if (raw.length === 0) return false;
7240
+ const container = this.containerIndex.get(containerId);
7241
+ return !container || container.children.length <= 1;
6764
7242
  }
6765
7243
  isLaneContainer(containerId) {
6766
7244
  if (this.laneAllowedContainers) return this.laneAllowedContainers.has(containerId);
6767
- if (!this.laneRootContainerId) return true;
6768
- if (containerId === this.laneRootContainerId) return true;
6769
- const node = this.containerIndex.get(containerId);
6770
- if (!node) return false;
6771
- return node.parentId === this.laneParentId;
7245
+ return true;
6772
7246
  }
6773
7247
  isBlockedContainer(containerId) {
6774
7248
  return this.blockedContainerIds?.has(containerId) ?? false;
@@ -6779,9 +7253,11 @@ function createDragIntentEngine(projection, activeId, options) {
6779
7253
  }
6780
7254
 
6781
7255
  // src/client/dnd/design-mode/drop-line.ts
7256
+ var DROP_LINE_THICKNESS_PX = 2;
7257
+ var DROP_LINE_MIN_LENGTH_PX = 40;
6782
7258
  function slotDropLine(slot, container, projectionTree, sdx, sdy) {
6783
- const T = 2;
6784
- const MIN = 40;
7259
+ const T = DROP_LINE_THICKNESS_PX;
7260
+ const MIN = DROP_LINE_MIN_LENGTH_PX;
6785
7261
  const bp = computeSlotBoundaryPoint(slot, container, projectionTree);
6786
7262
  if (container.strategy === "horizontal") {
6787
7263
  return {
@@ -6821,9 +7297,6 @@ function slotDropLine(slot, container, projectionTree, sdx, sdy) {
6821
7297
  }
6822
7298
 
6823
7299
  // src/client/dnd/design-mode/preview-layout.ts
6824
- function clampIndex(index, min, max) {
6825
- return Math.max(min, Math.min(index, max));
6826
- }
6827
7300
  function getContainerChildren(projection, containerId) {
6828
7301
  return [...projection.containerIndex.get(containerId)?.children ?? []];
6829
7302
  }
@@ -6943,7 +7416,7 @@ function buildPreviewLayout({
6943
7416
  const originalItemIds = getContainerChildren(projection, containerId);
6944
7417
  const activeIndex = originalItemIds.indexOf(anchorNodeId);
6945
7418
  if (activeIndex < 0) return null;
6946
- const overIndex = clampIndex(targetIndex, 0, Math.max(0, originalItemIds.length - 1));
7419
+ const overIndex = clamp(targetIndex, 0, Math.max(0, originalItemIds.length - 1));
6947
7420
  const previewItemIds = arrayMove(originalItemIds, activeIndex, overIndex);
6948
7421
  const strategy = getContainerStrategy(projection, containerId);
6949
7422
  containers.set(containerId, {
@@ -6983,7 +7456,7 @@ function buildPreviewLayout({
6983
7456
  activePreviewContainers.add(sourceContainerId);
6984
7457
  const targetOriginalItemIds = targetChildren.filter((id) => id !== anchorNodeId);
6985
7458
  const targetPreviewItemIds = [...targetOriginalItemIds];
6986
- const targetOverIndex = clampIndex(targetIndex, 0, targetOriginalItemIds.length);
7459
+ const targetOverIndex = clamp(targetIndex, 0, targetOriginalItemIds.length);
6987
7460
  targetPreviewItemIds.splice(targetOverIndex, 0, anchorNodeId);
6988
7461
  const targetStrategy = getContainerStrategy(projection, targetContainerId);
6989
7462
  const targetStrategyItemIds = [...targetOriginalItemIds, anchorNodeId];
@@ -7015,9 +7488,8 @@ function buildPreviewLayout({
7015
7488
  }
7016
7489
 
7017
7490
  // src/client/dnd/design-mode/useDragVisualLoop.ts
7018
- var CONTAINER_SWITCH_HYSTERESIS = 32;
7019
- var CROSS_UNLOCK_GRACE_MS = 320;
7020
- var SMALL_NODE_NEST_DWELL_MS = 180;
7491
+ var CONTAINER_SWITCH_HYSTERESIS = 12;
7492
+ var SMALL_NODE_NEST_DWELL_MS = 100;
7021
7493
  var LARGE_NODE_NEST_DWELL_MS = 320;
7022
7494
  function useDragVisualLoop({
7023
7495
  activeDrag,
@@ -7066,33 +7538,12 @@ function useDragVisualLoop({
7066
7538
  const scrollDy = window.scrollY - snapshot.scroll.y;
7067
7539
  const adjustedPx = px + scrollDx;
7068
7540
  const adjustedPy = py + scrollDy;
7069
- const lockContainer = refs.lockRef.current.containerId ? refs.projectionRef.current.containerIndex.get(refs.lockRef.current.containerId) ?? null : null;
7070
- if (!refs.lockRef.current.isCrossUnlocked && lockContainer) {
7071
- const { margin, dwellMs } = resolveCrossUnlockParams(lockContainer.rect);
7072
- const nextLock = checkCrossUnlock(
7073
- refs.lockRef.current,
7074
- adjustedPx,
7075
- adjustedPy,
7076
- lockContainer.rect,
7077
- performance.now(),
7078
- margin,
7079
- dwellMs,
7080
- CROSS_UNLOCK_GRACE_MS
7081
- );
7082
- if (nextLock !== refs.lockRef.current) {
7083
- refs.lockRef.current = nextLock;
7084
- if (nextLock.isCrossUnlocked) engine.setLaneAllowedContainers(null);
7085
- }
7086
- }
7087
7541
  const nestPreview = engine.peekNestIntent(px, py, window.scrollX, window.scrollY, snapshot.scroll.x, snapshot.scroll.y);
7088
7542
  const isLargeDraggedNode = shouldBlockNestForLargeNode(activeDrag.w, activeDrag.h);
7089
7543
  let allowNest = false;
7090
7544
  if (nestPreview) {
7091
7545
  const childId = nestPreview.slotId.split("::nest::")[1] ?? "";
7092
- const siblings = engine.getContainerChildren(nestPreview.containerId);
7093
- const isSiblingList = siblings.length >= 2;
7094
- const isSourceContainerNest = nestPreview.containerId === refs.sourceContainerIdRef.current;
7095
- allowNest = !!childId && !isSiblingList && !isLargeDraggedNode && !isSourceContainerNest;
7546
+ allowNest = !!childId && !isLargeDraggedNode;
7096
7547
  }
7097
7548
  if (!allowNest) {
7098
7549
  refs.nestCandidateRef.current = null;
@@ -7102,7 +7553,12 @@ function useDragVisualLoop({
7102
7553
  const now = performance.now();
7103
7554
  if (!refs.nestCandidateRef.current || refs.nestCandidateRef.current.id !== nestPreview.slotId) {
7104
7555
  refs.nestCandidateRef.current = { id: nestPreview.slotId, since: now };
7105
- refs.nestTargetRef.current = null;
7556
+ const hintTargetId = nestPreview.slotId.split("::nest::")[1] ?? "";
7557
+ refs.nestTargetRef.current = refs.nodeMapRef.current.get(hintTargetId) ?? null;
7558
+ const hintRect = snapshot.rects.get(hintTargetId);
7559
+ if (hintRect) {
7560
+ setInsideRect(new DOMRect(hintRect.left - scrollDx, hintRect.top - scrollDy, hintRect.width, hintRect.height));
7561
+ }
7106
7562
  setTargetPlane("lane");
7107
7563
  } else if (now - refs.nestCandidateRef.current.since >= (isLargeDraggedNode ? LARGE_NODE_NEST_DWELL_MS : SMALL_NODE_NEST_DWELL_MS)) {
7108
7564
  setTargetPlane("nest");
@@ -7161,18 +7617,6 @@ function useDragVisualLoop({
7161
7617
  setTargetPlane("lane");
7162
7618
  refs.nestCandidateRef.current = null;
7163
7619
  refs.nestTargetRef.current = null;
7164
- const lockedParentId = refs.lockRef.current.containerId;
7165
- if (!refs.lockRef.current.isCrossUnlocked && (!lockedParentId || intent.containerId !== lockedParentId)) {
7166
- refs.lastIntentRef.current = null;
7167
- refs.visualIntentRef.current = null;
7168
- refs.resolvedReorderRef.current = null;
7169
- refs.finalVisualKeyRef.current = null;
7170
- setInsideRect(null);
7171
- setDropLine(null);
7172
- animator.clear();
7173
- rafId = requestAnimationFrame(tick);
7174
- return;
7175
- }
7176
7620
  refs.lastIntentRef.current = intent;
7177
7621
  refs.visualIntentRef.current = intent;
7178
7622
  setInsideRect(null);
@@ -7204,19 +7648,43 @@ function useDragVisualLoop({
7204
7648
  }
7205
7649
  const sourceCommitLane = refs.sourceCommitLaneRef.current ?? getCommitLaneSnapshot(sourceContainerId);
7206
7650
  const targetCommitLane = getCommitLaneSnapshot(intent.containerId);
7207
- const commitPlacement = resolveCommitPlacementFromVisualTarget(
7651
+ let commitPlacement = resolveCommitPlacementFromVisualTarget(
7208
7652
  targetCommitLane,
7209
7653
  sourceCommitLane,
7210
7654
  activeDrag.anchorNodeId,
7211
7655
  targetInsertIndex
7212
7656
  );
7213
7657
  if (!commitPlacement) {
7214
- refs.resolvedReorderRef.current = null;
7215
- refs.finalVisualKeyRef.current = null;
7216
- setDropLine(null);
7217
- animator.clear();
7218
- rafId = requestAnimationFrame(tick);
7219
- return;
7658
+ const containerEl = refs.nodeMapRef.current.get(intent.containerId)?.element;
7659
+ const sourceCommitNodeId = refs.sourceCommitLaneRef.current?.commitNodeByAnchorId.get(activeDrag.anchorNodeId) ?? activeDrag.anchorNodeId;
7660
+ if (!containerEl) {
7661
+ refs.resolvedReorderRef.current = null;
7662
+ refs.finalVisualKeyRef.current = null;
7663
+ setDropLine(null);
7664
+ animator.clear();
7665
+ rafId = requestAnimationFrame(tick);
7666
+ return;
7667
+ }
7668
+ const targetChildren = Array.from(containerEl.children).filter(
7669
+ (child) => child instanceof HTMLElement
7670
+ );
7671
+ const filteredChildren = targetChildren.filter(
7672
+ (child) => child.getAttribute(DND_NODE_ID_ATTR) !== sourceCommitNodeId
7673
+ );
7674
+ const idx = Math.max(0, Math.min(targetInsertIndex, filteredChildren.length));
7675
+ const beforeChild = filteredChildren[idx] ?? null;
7676
+ const afterChild = idx > 0 ? filteredChildren[idx - 1] ?? null : null;
7677
+ const parentNodeId = ensureParentNodeId(containerEl);
7678
+ const beforeNodeId = beforeChild?.getAttribute(DND_NODE_ID_ATTR) ? ensureParentNodeId(beforeChild) : null;
7679
+ const afterNodeId = afterChild?.getAttribute(DND_NODE_ID_ATTR) ? ensureParentNodeId(afterChild) : null;
7680
+ commitPlacement = {
7681
+ parentNodeId,
7682
+ laneContainerId: intent.containerId,
7683
+ commitNodeId: sourceCommitNodeId,
7684
+ beforeNodeId,
7685
+ afterNodeId,
7686
+ index: idx
7687
+ };
7220
7688
  }
7221
7689
  setDropLine(slotDropLine(slot, container, proj.projectionTree, scrollDx, scrollDy));
7222
7690
  refs.resolvedReorderRef.current = {
@@ -7261,56 +7729,175 @@ function useDragVisualLoop({
7261
7729
 
7262
7730
  // src/client/dnd/design-mode/useDragLifecycle.ts
7263
7731
  import { useState as useState5, useCallback as useCallback6 } from "react";
7264
-
7265
- // src/client/dnd/design-mode/node-key.ts
7266
- var _runtimeKeyMap = /* @__PURE__ */ new WeakMap();
7267
- var _runtimeKeyCounter = 0;
7268
- function getRuntimeKey(el) {
7269
- const existing = _runtimeKeyMap.get(el);
7270
- if (existing) return existing;
7271
- const key2 = `runtime:${++_runtimeKeyCounter}`;
7272
- _runtimeKeyMap.set(el, key2);
7273
- return key2;
7274
- }
7275
- function getNodeKeyFromElement(el) {
7276
- const resolved = resolveDragSurface(el);
7277
- const host = resolved.host ?? el;
7278
- const meta = getEditorMeta(host);
7279
- if (meta) return meta.nodeId;
7280
- const persisted = host.getAttribute("data-dnd-node-id");
7281
- if (persisted) return persisted;
7282
- return getRuntimeKey(host);
7283
- }
7284
- function getInspectorRefFromElement(el) {
7285
- const resolved = resolveDragSurface(el);
7286
- const host = resolved.host ?? el;
7287
- const meta = getEditorMeta(host);
7288
- if (!meta) return null;
7289
- return { relativePath: meta.file, line: meta.line, column: meta.col };
7290
- }
7291
-
7292
- // src/client/dnd/design-mode/useDragLifecycle.ts
7293
- var SOFT_COMMIT_DISTANCE_PX = 14;
7294
- function isLaneCommitValid(intent, resolvedReorder, lockedParentId) {
7295
- if (!intent || intent.mode !== "reorder") return false;
7296
- if (!resolvedReorder) return false;
7297
- if (!lockedParentId) return false;
7298
- if (intent.containerId !== lockedParentId) return false;
7299
- if (resolvedReorder.visual.slotId !== intent.slotId) return false;
7300
- if (resolvedReorder.visual.parentNodeId !== intent.containerId) return false;
7301
- return true;
7302
- }
7303
- function buildRuntimeIdentityLocal(nodeId, resolveNodeElement) {
7732
+ function buildRuntimeIdentityLocal(nodeId, resolveNodeElement, effectiveLaneSnapshot) {
7304
7733
  if (!nodeId) return null;
7305
7734
  const element = resolveNodeElement(nodeId);
7306
7735
  if (!element) return null;
7736
+ const nodeKey = getNodeKeyFromElement(element);
7737
+ const inspectorRef = getInspectorRefFromElement(element);
7738
+ const laneEffectiveNodeId = effectiveLaneSnapshot?.effectiveNodeIdsByCommitNodeId.get(nodeId) ?? null;
7739
+ const effectiveNodeId = typeof laneEffectiveNodeId === "string" && laneEffectiveNodeId.length > 0 ? laneEffectiveNodeId : isSourceBackedRuntimeNodeKey(nodeKey) ? nodeKey : null;
7740
+ const shouldExposeEffectiveIdentity = typeof effectiveNodeId === "string" && effectiveNodeId.length > 0;
7307
7741
  return {
7308
7742
  nodeId,
7309
- nodeKey: getNodeKeyFromElement(element),
7310
- selector: element.getAttribute(DND_NODE_ID_ATTR2) ? `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(nodeId)}"]` : null,
7311
- inspectorRef: getInspectorRefFromElement(element)
7743
+ nodeKey,
7744
+ selector: element.getAttribute(DND_NODE_ID_ATTR) ? buildNodeSelector(nodeId) : null,
7745
+ inspectorRef,
7746
+ ...shouldExposeEffectiveIdentity ? {
7747
+ effectiveNodeId,
7748
+ effectiveNodeKey: effectiveNodeId,
7749
+ effectiveInspectorRef: inspectorRef
7750
+ } : {}
7751
+ };
7752
+ }
7753
+ function buildMovePayloadWithEffectiveIdentities(params) {
7754
+ const {
7755
+ placement,
7756
+ containerIdentity,
7757
+ beforeSiblingIdentity,
7758
+ afterSiblingIdentity,
7759
+ commitIdentity
7760
+ } = params;
7761
+ return {
7762
+ ...placement,
7763
+ effectiveContainerNodeId: containerIdentity?.effectiveNodeId ?? null,
7764
+ effectiveContainerNodeKey: containerIdentity?.effectiveNodeKey ?? null,
7765
+ effectiveContainerInspectorRef: containerIdentity?.effectiveInspectorRef ?? null,
7766
+ effectiveBeforeNodeId: beforeSiblingIdentity?.effectiveNodeId ?? null,
7767
+ effectiveBeforeNodeKey: beforeSiblingIdentity?.effectiveNodeKey ?? null,
7768
+ effectiveBeforeInspectorRef: beforeSiblingIdentity?.effectiveInspectorRef ?? null,
7769
+ effectiveAfterNodeId: afterSiblingIdentity?.effectiveNodeId ?? null,
7770
+ effectiveAfterNodeKey: afterSiblingIdentity?.effectiveNodeKey ?? null,
7771
+ effectiveAfterInspectorRef: afterSiblingIdentity?.effectiveInspectorRef ?? null,
7772
+ effectiveCommitNodeId: commitIdentity?.effectiveNodeId ?? null,
7773
+ effectiveCommitNodeKey: commitIdentity?.effectiveNodeKey ?? null,
7774
+ effectiveCommitInspectorRef: commitIdentity?.effectiveInspectorRef ?? null
7312
7775
  };
7313
7776
  }
7777
+ function recordCommittedMove(args) {
7778
+ const {
7779
+ command,
7780
+ anchorNodeId,
7781
+ selectedNodeId,
7782
+ anchorEl,
7783
+ selectedEl,
7784
+ nodeKey,
7785
+ inspRef,
7786
+ selectedNodeKey,
7787
+ selectedInspectorRef,
7788
+ sourceLaneSnapshot,
7789
+ targetLaneSnapshot,
7790
+ resolveNodeElement,
7791
+ recordMoveChange
7792
+ } = args;
7793
+ const sourceLaneIdentitySnapshot = captureCommitLaneIdentitySnapshot(
7794
+ sourceLaneSnapshot,
7795
+ resolveNodeElement
7796
+ );
7797
+ const targetLaneIdentitySnapshot = command.to.laneContainerId === command.from.laneContainerId ? sourceLaneIdentitySnapshot : captureCommitLaneIdentitySnapshot(targetLaneSnapshot, resolveNodeElement);
7798
+ const targetLaneAfterIdentitySnapshot = targetLaneIdentitySnapshot ? createCommitLaneIdentitySnapshot({
7799
+ laneContainerId: targetLaneIdentitySnapshot.laneContainerId,
7800
+ commitNodeIds: projectCommitNodeIdsForMove({
7801
+ commitNodeIds: targetLaneIdentitySnapshot.commitNodeIds,
7802
+ movedCommitNodeId: command.commitNodeId,
7803
+ targetIndex: command.to.index
7804
+ }),
7805
+ nodeKeysByCommitNodeId: new Map([
7806
+ ...targetLaneIdentitySnapshot.nodeKeysByCommitNodeId,
7807
+ ...sourceLaneIdentitySnapshot?.nodeKeysByCommitNodeId.has(command.commitNodeId) ? [[command.commitNodeId, sourceLaneIdentitySnapshot.nodeKeysByCommitNodeId.get(command.commitNodeId) ?? null]] : []
7808
+ ])
7809
+ }) : null;
7810
+ const sourceContainerIdentity = buildRuntimeIdentityLocal(
7811
+ command.from.parentNodeId ?? command.from.laneContainerId,
7812
+ resolveNodeElement,
7813
+ sourceLaneIdentitySnapshot
7814
+ );
7815
+ const sourceBeforeSiblingIdentity = buildRuntimeIdentityLocal(command.from.beforeNodeId, resolveNodeElement, sourceLaneIdentitySnapshot);
7816
+ const sourceAfterSiblingIdentity = buildRuntimeIdentityLocal(command.from.afterNodeId, resolveNodeElement, sourceLaneIdentitySnapshot);
7817
+ const sourceCommitIdentity = buildRuntimeIdentityLocal(command.commitNodeId, resolveNodeElement, sourceLaneIdentitySnapshot);
7818
+ const containerIdentity = buildRuntimeIdentityLocal(
7819
+ command.to.parentNodeId ?? command.to.laneContainerId,
7820
+ resolveNodeElement,
7821
+ targetLaneAfterIdentitySnapshot
7822
+ );
7823
+ const beforeSiblingIdentity = buildRuntimeIdentityLocal(command.to.beforeNodeId, resolveNodeElement, targetLaneAfterIdentitySnapshot);
7824
+ const afterSiblingIdentity = buildRuntimeIdentityLocal(command.to.afterNodeId, resolveNodeElement, targetLaneAfterIdentitySnapshot);
7825
+ const targetCommitIdentity = buildRuntimeIdentityLocal(command.commitNodeId, resolveNodeElement, targetLaneAfterIdentitySnapshot);
7826
+ const diagnostics = buildMovePersistenceDiagnostics({
7827
+ sourceLane: sourceLaneIdentitySnapshot,
7828
+ targetLane: targetLaneIdentitySnapshot,
7829
+ movedCommitNodeId: command.commitNodeId,
7830
+ sourceLaneContainerId: command.from.laneContainerId,
7831
+ targetLaneContainerId: command.to.laneContainerId,
7832
+ targetIndex: command.to.index
7833
+ });
7834
+ const componentName = getEditorMeta(anchorEl)?.componentName;
7835
+ const operationId = `${command.at}:${anchorNodeId}`;
7836
+ const beforePayload = buildMovePayloadWithEffectiveIdentities({
7837
+ placement: command.from,
7838
+ containerIdentity: sourceContainerIdentity,
7839
+ beforeSiblingIdentity: sourceBeforeSiblingIdentity,
7840
+ afterSiblingIdentity: sourceAfterSiblingIdentity,
7841
+ commitIdentity: sourceCommitIdentity
7842
+ });
7843
+ const afterPayload = buildMovePayloadWithEffectiveIdentities({
7844
+ placement: command.to,
7845
+ containerIdentity,
7846
+ beforeSiblingIdentity,
7847
+ afterSiblingIdentity,
7848
+ commitIdentity: targetCommitIdentity
7849
+ });
7850
+ recordMoveChange({
7851
+ operationId,
7852
+ commandType: command.type,
7853
+ nodeKey,
7854
+ nodeId: anchorNodeId,
7855
+ inspectorRef: inspRef,
7856
+ selectedNodeKey,
7857
+ selectedNodeId,
7858
+ selectedInspectorRef,
7859
+ anchorNodeKey: nodeKey,
7860
+ anchorNodeId,
7861
+ sourceContainer: sourceContainerIdentity,
7862
+ sourceBeforeSibling: sourceBeforeSiblingIdentity,
7863
+ sourceAfterSibling: sourceAfterSiblingIdentity,
7864
+ diagnostics,
7865
+ container: containerIdentity,
7866
+ beforeSibling: beforeSiblingIdentity,
7867
+ afterSibling: afterSiblingIdentity,
7868
+ componentName,
7869
+ before: beforePayload,
7870
+ after: afterPayload
7871
+ });
7872
+ dispatchRuntimeEvent({
7873
+ type: "element-moved",
7874
+ operationId,
7875
+ commandType: command.type,
7876
+ selected: {
7877
+ nodeId: selectedNodeId,
7878
+ nodeKey: selectedNodeKey,
7879
+ selector: selectedEl.getAttribute(DND_NODE_ID_ATTR) ? buildNodeSelector(selectedNodeId) : null,
7880
+ inspectorRef: selectedInspectorRef,
7881
+ ...targetCommitIdentity?.effectiveNodeId ? { effectiveNodeId: targetCommitIdentity.effectiveNodeId, effectiveNodeKey: targetCommitIdentity.effectiveNodeKey } : {}
7882
+ },
7883
+ anchor: {
7884
+ nodeId: anchorNodeId,
7885
+ nodeKey,
7886
+ selector: anchorEl.getAttribute(DND_NODE_ID_ATTR) ? buildNodeSelector(anchorNodeId) : null,
7887
+ inspectorRef: inspRef,
7888
+ ...targetCommitIdentity?.effectiveNodeId ? { effectiveNodeId: targetCommitIdentity.effectiveNodeId, effectiveNodeKey: targetCommitIdentity.effectiveNodeKey } : {}
7889
+ },
7890
+ sourceContainer: sourceContainerIdentity,
7891
+ sourceBeforeSibling: sourceBeforeSiblingIdentity,
7892
+ sourceAfterSibling: sourceAfterSiblingIdentity,
7893
+ container: containerIdentity,
7894
+ beforeSibling: beforeSiblingIdentity,
7895
+ afterSibling: afterSiblingIdentity,
7896
+ diagnostics,
7897
+ before: beforePayload,
7898
+ after: afterPayload
7899
+ });
7900
+ }
7314
7901
  function useDragLifecycle({
7315
7902
  refs,
7316
7903
  pause,
@@ -7392,12 +7979,10 @@ function useDragLifecycle({
7392
7979
  const srcContainerId = anchor.sourceContainerId;
7393
7980
  const sourceCommitLane = getCommitLaneSnapshot(srcContainerId);
7394
7981
  const startPosition = getCurrentCommitPlacement(anchorNodeId, srcContainerId);
7395
- const laneAllowed = srcContainerId ? /* @__PURE__ */ new Set([srcContainerId]) : null;
7396
7982
  const blockedContainerIds = buildBlockedStructuralContainerIds(proj, refs.nodeMapRef.current, srcContainerId);
7397
7983
  refs.blockedContainerIdsRef.current = blockedContainerIds;
7398
7984
  refs.engineRef.current = createDragIntentEngine(proj, anchorNodeId, {
7399
7985
  laneRootContainerId: srcContainerId,
7400
- laneAllowedContainers: laneAllowed,
7401
7986
  blockedContainerIds
7402
7987
  });
7403
7988
  refs.engineRef.current.setTargetPlane("lane");
@@ -7405,7 +7990,7 @@ function useDragLifecycle({
7405
7990
  refs.sourceContainerIdRef.current = srcContainerId;
7406
7991
  refs.sourceCommitLaneRef.current = sourceCommitLane;
7407
7992
  refs.dragSessionAnchorRef.current = anchor;
7408
- refs.lockRef.current = { containerId: srcContainerId, isCrossUnlocked: false, exitSince: null, unlockUntilTs: null };
7993
+ refs.lockRef.current = { containerId: srcContainerId, isCrossUnlocked: true, exitSince: null, unlockUntilTs: null };
7409
7994
  refs.dragStartPositionRef.current = startPosition;
7410
7995
  refs.safeZoneRef.current = resolveSafeZoneRect();
7411
7996
  pause();
@@ -7431,22 +8016,34 @@ function useDragLifecycle({
7431
8016
  const pointer = refs.mouseRef.current;
7432
8017
  const safeZone = refs.safeZoneRef.current;
7433
8018
  const blockedByGuard = isPointerOutsideSafeZone(pointer.x, pointer.y, safeZone) || isPointerInHardPageEdgeZone(pointer.x, pointer.y);
7434
- const sourcePos = refs.dragStartPositionRef.current ?? getCurrentCommitPlacement(anchorNodeId, refs.sourceContainerIdRef.current);
8019
+ let sourcePos = refs.dragStartPositionRef.current ?? getCurrentCommitPlacement(anchorNodeId, refs.sourceContainerIdRef.current);
8020
+ if (!sourcePos && activeEl && activeEl.parentElement) {
8021
+ const parentEl = activeEl.parentElement;
8022
+ const commitNodeId = activeEl.getAttribute(DND_NODE_ID_ATTR) ?? anchorNodeId;
8023
+ const siblings = Array.from(parentEl.children).filter((c) => c instanceof HTMLElement);
8024
+ const idx = siblings.indexOf(activeEl);
8025
+ if (idx >= 0) {
8026
+ sourcePos = {
8027
+ parentNodeId: ensureParentNodeId(parentEl),
8028
+ laneContainerId: refs.sourceContainerIdRef.current,
8029
+ commitNodeId,
8030
+ beforeNodeId: idx < siblings.length - 1 ? siblings[idx + 1].getAttribute(DND_NODE_ID_ATTR) ?? ensureParentNodeId(siblings[idx + 1]) : null,
8031
+ afterNodeId: idx > 0 ? siblings[idx - 1].getAttribute(DND_NODE_ID_ATTR) ?? ensureParentNodeId(siblings[idx - 1]) : null,
8032
+ index: idx
8033
+ };
8034
+ }
8035
+ }
7435
8036
  const intent = refs.visualIntentRef.current;
7436
- const finalVisual = refs.finalVisualKeyRef.current;
7437
8037
  const nestTarget = refs.nestTargetRef.current;
7438
8038
  const intentHasContainer = intent ? refs.projectionRef.current.containerIndex.has(intent.containerId) : false;
7439
8039
  const intentHasSlot = intent ? intent.mode === "nest" || refs.projectionRef.current.slotIndex.has(intent.slotId) : false;
7440
8040
  const intentValid = !!intent && intentHasContainer && intentHasSlot;
7441
8041
  if (!blockedByGuard && activeEl && sourcePos && (nestTarget || intentValid)) {
7442
8042
  const dropTargetId = nestTarget?.nodeId ?? intent?.containerId;
7443
- if (dropTargetId) {
7444
- const targetSc = refs.nodeMapRef.current.get(dropTargetId);
7445
- if (!targetSc || !isEligibleForDrag(targetSc)) {
7446
- clearDragState();
7447
- syncHistoryAvailability();
7448
- return;
7449
- }
8043
+ if (dropTargetId && !refs.nodeMapRef.current.has(dropTargetId)) {
8044
+ clearDragState();
8045
+ syncHistoryAvailability();
8046
+ return;
7450
8047
  }
7451
8048
  let command = null;
7452
8049
  if (nestTarget) {
@@ -7466,46 +8063,8 @@ function useDragLifecycle({
7466
8063
  };
7467
8064
  }
7468
8065
  } else if (intentValid && intent.mode === "reorder") {
7469
- const lockedParentId = refs.lockRef.current.containerId;
7470
- if (!refs.lockRef.current.isCrossUnlocked && (!lockedParentId || intent.containerId !== lockedParentId)) {
7471
- clearDragState();
7472
- syncHistoryAvailability();
7473
- return;
7474
- }
7475
- const snapshot = refs.snapshotRef.current;
7476
- const targetContainer = refs.projectionRef.current.containerIndex.get(intent.containerId);
7477
- if (targetContainer) {
7478
- const scrollDx = window.scrollX - (snapshot?.scroll.x ?? 0);
7479
- const scrollDy = window.scrollY - (snapshot?.scroll.y ?? 0);
7480
- const adjPx = pointer.x + scrollDx;
7481
- const adjPy = pointer.y + scrollDy;
7482
- const r = targetContainer.rect;
7483
- const inContainer = adjPx >= r.left - 40 && adjPx <= r.right + 40 && adjPy >= r.top - 40 && adjPy <= r.bottom + 40;
7484
- if (!inContainer) {
7485
- const softFallbackAllowed = finalVisual?.mode === "reorder" && finalVisual.containerId === intent.containerId && (() => {
7486
- const slot = refs.projectionRef.current.slotIndex.get(finalVisual.slotId);
7487
- const container = refs.projectionRef.current.containerIndex.get(finalVisual.containerId);
7488
- if (!slot || !container) return false;
7489
- const bp = computeSlotBoundaryPoint(slot, container, refs.projectionRef.current.projectionTree);
7490
- const dist = Math.hypot(adjPx - bp.x, adjPy - bp.y);
7491
- return dist <= SOFT_COMMIT_DISTANCE_PX;
7492
- })();
7493
- if (!softFallbackAllowed) {
7494
- clearDragState();
7495
- syncHistoryAvailability();
7496
- return;
7497
- }
7498
- }
7499
- }
7500
8066
  const resolvedReorder = refs.resolvedReorderRef.current;
7501
- const laneValid = refs.lockRef.current.isCrossUnlocked ? !!resolvedReorder && resolvedReorder.visual.slotId === intent.slotId && resolvedReorder.visual.parentNodeId === intent.containerId : isLaneCommitValid(intent, resolvedReorder, lockedParentId);
7502
- if (!laneValid) {
7503
- clearDragState();
7504
- syncHistoryAvailability();
7505
- return;
7506
- }
7507
- 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;
7508
- if (!visualParity) {
8067
+ if (!resolvedReorder || resolvedReorder.visual.containerId !== intent.containerId) {
7509
8068
  clearDragState();
7510
8069
  syncHistoryAvailability();
7511
8070
  return;
@@ -7521,62 +8080,32 @@ function useDragLifecycle({
7521
8080
  };
7522
8081
  }
7523
8082
  if (command) {
8083
+ const anchorEl = resolveNodeElement(anchorNodeId) ?? activeEl;
8084
+ const selectedEl = resolveNodeElement(selectedNodeId) ?? anchorEl;
8085
+ const nodeKey = getNodeKeyFromElement(anchorEl);
8086
+ const inspRef = getInspectorRefFromElement(anchorEl);
8087
+ const selectedNodeKey = getNodeKeyFromElement(selectedEl);
8088
+ const selectedInspectorRef = getInspectorRefFromElement(selectedEl);
8089
+ const sourceLaneSnapshot = refs.sourceCommitLaneRef.current ?? getCommitLaneSnapshot(command.from.laneContainerId);
8090
+ const targetLaneSnapshot = command.to.laneContainerId === command.from.laneContainerId ? sourceLaneSnapshot : getCommitLaneSnapshot(command.to.laneContainerId);
7524
8091
  const committed = refs.historyRef.current.execute(command, applyCommand);
7525
8092
  if (committed) {
7526
8093
  activeEl.setAttribute("data-dnd-moved", "");
7527
8094
  selectElement(null);
7528
- const anchorEl = resolveNodeElement(anchorNodeId) ?? activeEl;
7529
- const selectedEl = resolveNodeElement(selectedNodeId) ?? anchorEl;
7530
- const nodeKey = getNodeKeyFromElement(anchorEl);
7531
- const inspRef = getInspectorRefFromElement(anchorEl);
7532
- const selectedNodeKey = getNodeKeyFromElement(selectedEl);
7533
- const selectedInspectorRef = getInspectorRefFromElement(selectedEl);
7534
- const containerIdentity = buildRuntimeIdentityLocal(
7535
- command.to.parentNodeId ?? command.to.laneContainerId,
7536
- resolveNodeElement
7537
- );
7538
- const beforeSiblingIdentity = buildRuntimeIdentityLocal(command.to.beforeNodeId, resolveNodeElement);
7539
- const afterSiblingIdentity = buildRuntimeIdentityLocal(command.to.afterNodeId, resolveNodeElement);
7540
- const operationId = `${command.at}:${anchorNodeId}`;
7541
- recordMoveChange({
7542
- operationId,
7543
- commandType: command.type,
8095
+ recordCommittedMove({
8096
+ command,
8097
+ anchorNodeId,
8098
+ selectedNodeId,
8099
+ anchorEl,
8100
+ selectedEl,
7544
8101
  nodeKey,
7545
- nodeId: anchorNodeId,
7546
- inspectorRef: inspRef,
8102
+ inspRef,
7547
8103
  selectedNodeKey,
7548
- selectedNodeId,
7549
8104
  selectedInspectorRef,
7550
- anchorNodeKey: nodeKey,
7551
- anchorNodeId,
7552
- container: containerIdentity,
7553
- beforeSibling: beforeSiblingIdentity,
7554
- afterSibling: afterSiblingIdentity,
7555
- componentName: getEditorMeta(anchorEl)?.componentName,
7556
- before: command.from,
7557
- after: command.to
7558
- });
7559
- dispatchRuntimeEvent({
7560
- type: "element-moved",
7561
- operationId,
7562
- commandType: command.type,
7563
- selected: {
7564
- nodeId: selectedNodeId,
7565
- nodeKey: selectedNodeKey,
7566
- selector: selectedEl.getAttribute(DND_NODE_ID_ATTR2) ? `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(selectedNodeId)}"]` : null,
7567
- inspectorRef: selectedInspectorRef
7568
- },
7569
- anchor: {
7570
- nodeId: anchorNodeId,
7571
- nodeKey,
7572
- selector: anchorEl.getAttribute(DND_NODE_ID_ATTR2) ? `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(anchorNodeId)}"]` : null,
7573
- inspectorRef: inspRef
7574
- },
7575
- container: containerIdentity,
7576
- beforeSibling: beforeSiblingIdentity,
7577
- afterSibling: afterSiblingIdentity,
7578
- before: command.from,
7579
- after: command.to
8105
+ sourceLaneSnapshot,
8106
+ targetLaneSnapshot,
8107
+ resolveNodeElement,
8108
+ recordMoveChange
7580
8109
  });
7581
8110
  }
7582
8111
  }
@@ -7615,7 +8144,7 @@ function useResizeSession({
7615
8144
  const rect = resizeTargetEl.getBoundingClientRect();
7616
8145
  const selectedNodeKey = getNodeKeyFromElement(selectedEl);
7617
8146
  const targetNodeKey = getNodeKeyFromElement(resizeTargetEl);
7618
- const targetNodeId = ensureElementNodeId(resizeTargetEl);
8147
+ const targetNodeId = ensureParentNodeId(resizeTargetEl);
7619
8148
  const startColSpan = parseSpanValue(getComputedStyle(resizeTargetEl).gridColumnEnd || resizeTargetEl.style.gridColumnEnd);
7620
8149
  const startRowSpan = parseSpanValue(getComputedStyle(resizeTargetEl).gridRowEnd || resizeTargetEl.style.gridRowEnd);
7621
8150
  const columnTrackCount = getGridTrackCount(resizeTargetParent ? getComputedStyle(resizeTargetParent).gridTemplateColumns : null);
@@ -7740,13 +8269,13 @@ function useResizeSession({
7740
8269
  selected: {
7741
8270
  nodeId: selectedNodeId,
7742
8271
  nodeKey: selectedNodeKey,
7743
- selector: `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(selectedNodeId)}"]`,
8272
+ selector: buildNodeSelector(selectedNodeId),
7744
8273
  inspectorRef
7745
8274
  },
7746
8275
  anchor: {
7747
8276
  nodeId: targetNodeId,
7748
8277
  nodeKey: targetNodeKey,
7749
- selector: `[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(targetNodeId)}"]`,
8278
+ selector: buildNodeSelector(targetNodeId),
7750
8279
  inspectorRef: getInspectorRefFromElement(element)
7751
8280
  },
7752
8281
  resize: {
@@ -7787,6 +8316,169 @@ function useResizeSession({
7787
8316
  return { handleResizeHandlePointerDown };
7788
8317
  }
7789
8318
 
8319
+ // src/client/dnd/design-mode/useExternalDragLoop.ts
8320
+ import { useEffect as useEffect11, useRef as useRef9 } from "react";
8321
+ var EXTERNAL_DRAG_ACTIVE_ID = "__external-drag-placeholder__";
8322
+ function useExternalDragLoop({
8323
+ externalDrag,
8324
+ refs,
8325
+ elements,
8326
+ resolveNodeElement,
8327
+ setDropLine
8328
+ }) {
8329
+ const engineRef = useRef9(null);
8330
+ const lastIntentRef = useRef9(null);
8331
+ useEffect11(() => {
8332
+ if (!externalDrag) {
8333
+ engineRef.current = null;
8334
+ lastIntentRef.current = null;
8335
+ setDropLine(null);
8336
+ externalDragMouse.commitRequested = false;
8337
+ return;
8338
+ }
8339
+ const projection = buildProjection(elements);
8340
+ if (projection.containerIndex.size === 0) return;
8341
+ const rects = /* @__PURE__ */ new Map();
8342
+ for (const el of elements) {
8343
+ rects.set(el.nodeId, el.rect);
8344
+ }
8345
+ const nodeMap = /* @__PURE__ */ new Map();
8346
+ for (const el of elements) nodeMap.set(el.nodeId, el);
8347
+ const blockedContainerIds = buildBlockedStructuralContainerIds(projection, nodeMap, null);
8348
+ const engine = new DragIntentEngine(projection, EXTERNAL_DRAG_ACTIVE_ID, {
8349
+ blockedContainerIds
8350
+ });
8351
+ engineRef.current = engine;
8352
+ const scrollX = window.scrollX;
8353
+ const scrollY = window.scrollY;
8354
+ let rafId;
8355
+ const tick = () => {
8356
+ if (externalDragMouse.commitRequested) {
8357
+ externalDragMouse.commitRequested = false;
8358
+ commitExternalDrop(
8359
+ externalDragMouse.x,
8360
+ externalDragMouse.y,
8361
+ engine,
8362
+ externalDrag,
8363
+ refs,
8364
+ resolveNodeElement,
8365
+ setDropLine
8366
+ );
8367
+ return;
8368
+ }
8369
+ const intent = engine.resolve(
8370
+ externalDragMouse.x,
8371
+ externalDragMouse.y,
8372
+ window.scrollX,
8373
+ window.scrollY,
8374
+ scrollX,
8375
+ scrollY
8376
+ );
8377
+ if (intent && intent.mode === "reorder") {
8378
+ lastIntentRef.current = intent;
8379
+ const slot = projection.slotIndex.get(intent.slotId);
8380
+ const container = projection.containerIndex.get(intent.containerId);
8381
+ if (slot && container) {
8382
+ const scrollDx = window.scrollX - scrollX;
8383
+ const scrollDy = window.scrollY - scrollY;
8384
+ setDropLine(slotDropLine(slot, container, projection.projectionTree, scrollDx, scrollDy));
8385
+ } else {
8386
+ setDropLine(null);
8387
+ }
8388
+ } else {
8389
+ lastIntentRef.current = null;
8390
+ setDropLine(null);
8391
+ }
8392
+ rafId = requestAnimationFrame(tick);
8393
+ };
8394
+ rafId = requestAnimationFrame(tick);
8395
+ return () => cancelAnimationFrame(rafId);
8396
+ }, [externalDrag, elements, refs, resolveNodeElement, setDropLine]);
8397
+ }
8398
+ function commitExternalDrop(x, y, engine, component, refs, resolveNodeElement, setDropLine) {
8399
+ const scrollX = window.scrollX;
8400
+ const scrollY = window.scrollY;
8401
+ const intent = engine.resolve(x, y, scrollX, scrollY, scrollX, scrollY);
8402
+ if (!intent || intent.mode !== "reorder") {
8403
+ useDesignModeStore.getState().cancelExternalDrag();
8404
+ setDropLine(null);
8405
+ return;
8406
+ }
8407
+ const containerEl = resolveNodeElement(intent.containerId);
8408
+ if (!containerEl) {
8409
+ useDesignModeStore.getState().cancelExternalDrag();
8410
+ setDropLine(null);
8411
+ return;
8412
+ }
8413
+ const template = document.createElement("template");
8414
+ template.innerHTML = component.html.trim();
8415
+ const newElement = template.content.firstElementChild;
8416
+ if (!newElement) {
8417
+ useDesignModeStore.getState().cancelExternalDrag();
8418
+ setDropLine(null);
8419
+ return;
8420
+ }
8421
+ const nodeId = `insert:${Date.now()}`;
8422
+ newElement.setAttribute(DND_NODE_ID_ATTR, nodeId);
8423
+ newElement.setAttribute("data-dnd-force-allow", "");
8424
+ newElement.setAttribute("data-dnd-moved", "");
8425
+ const children = Array.from(containerEl.children).filter(
8426
+ (child) => child instanceof HTMLElement
8427
+ );
8428
+ const beforeChild = children[intent.index] ?? null;
8429
+ containerEl.insertBefore(newElement, beforeChild);
8430
+ const insertCommand = {
8431
+ type: "insert",
8432
+ nodeId,
8433
+ html: component.html,
8434
+ containerId: intent.containerId,
8435
+ index: intent.index,
8436
+ at: Date.now()
8437
+ };
8438
+ const applied = refs.historyRef.current.execute(insertCommand, (command, direction) => {
8439
+ if (command.type !== "insert") return false;
8440
+ return applyInsertCommand(command, direction, resolveNodeElement);
8441
+ });
8442
+ if (applied) {
8443
+ const nodeKey = getNodeKeyFromElement(newElement);
8444
+ const inspectorRef = getInspectorRefFromElement(newElement);
8445
+ const containerNodeKey = getNodeKeyFromElement(containerEl);
8446
+ let containerIdentity = null;
8447
+ if (containerEl) {
8448
+ containerIdentity = {
8449
+ nodeId: intent.containerId,
8450
+ nodeKey: getNodeKeyFromElement(containerEl),
8451
+ selector: containerEl.getAttribute(DND_NODE_ID_ATTR) ? buildNodeSelector(intent.containerId) : null,
8452
+ inspectorRef: getInspectorRefFromElement(containerEl)
8453
+ };
8454
+ }
8455
+ const operationId = `${insertCommand.at}:${nodeId}`;
8456
+ useDesignModeStore.getState().recordInsertChange({
8457
+ operationId,
8458
+ nodeKey,
8459
+ nodeId,
8460
+ inspectorRef,
8461
+ displayName: component.displayName,
8462
+ html: component.html,
8463
+ containerId: intent.containerId,
8464
+ containerNodeKey,
8465
+ index: intent.index
8466
+ });
8467
+ dispatchRuntimeEvent({
8468
+ type: "element-inserted",
8469
+ operationId,
8470
+ nodeId,
8471
+ nodeKey,
8472
+ displayName: component.displayName,
8473
+ html: component.html,
8474
+ container: containerIdentity,
8475
+ index: intent.index
8476
+ });
8477
+ }
8478
+ setDropLine(null);
8479
+ useDesignModeStore.getState().cancelExternalDrag();
8480
+ }
8481
+
7790
8482
  // src/client/dnd/design-mode/DesignModeOverlay.tsx
7791
8483
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
7792
8484
  var DROP_ANIMATION = { duration: DURATION_MS, easing: EASING };
@@ -7832,7 +8524,7 @@ function HitArea({
7832
8524
  onResizeHandlePointerDown
7833
8525
  }) {
7834
8526
  const { setNodeRef, listeners, attributes, isDragging } = useDraggable({ id: scanned.nodeId });
7835
- useEffect11(() => {
8527
+ useEffect12(() => {
7836
8528
  setNodeRef(scanned.element);
7837
8529
  return () => setNodeRef(null);
7838
8530
  }, [setNodeRef, scanned.element]);
@@ -7844,16 +8536,16 @@ function HitArea({
7844
8536
  el.style.removeProperty("opacity");
7845
8537
  }
7846
8538
  }, [isDragging, scanned.element]);
7847
- useEffect11(() => () => {
8539
+ useEffect12(() => () => {
7848
8540
  scanned.element.style.removeProperty("opacity");
7849
8541
  scanned.element.style.removeProperty("transform");
7850
8542
  scanned.element.style.removeProperty("transition");
7851
8543
  scanned.element.style.removeProperty("will-change");
7852
8544
  }, [scanned.element]);
7853
- const divRef = useRef9(null);
8545
+ const divRef = useRef10(null);
7854
8546
  const [hoverResizeHandle, setHoverResizeHandle] = useState6(null);
7855
8547
  const isSelected = scanned.nodeId === selectedNodeId;
7856
- useEffect11(() => {
8548
+ useEffect12(() => {
7857
8549
  const div = divRef.current;
7858
8550
  if (!div) return;
7859
8551
  const onWheel = (e) => {
@@ -7924,6 +8616,7 @@ function DesignModeOverlay() {
7924
8616
  const redoLastChange = useDesignModeStore((s) => s.redoLastChange);
7925
8617
  const orderedChanges = useDesignModeStore((s) => s.orderedChanges);
7926
8618
  const exportChangesForAI = useDesignModeStore((s) => s.exportChangesForAI);
8619
+ const externalDrag = useDesignModeStore((s) => s.externalDrag);
7927
8620
  const { elements, pause, resume } = useElementScanner(enabled);
7928
8621
  const nodeMap = useMemo6(() => {
7929
8622
  const m = /* @__PURE__ */ new Map();
@@ -7938,7 +8631,7 @@ function DesignModeOverlay() {
7938
8631
  const refs = useOverlayRefs(undoRequestId, redoRequestId);
7939
8632
  refs.elementsRef.current = elements;
7940
8633
  refs.nodeMapRef.current = nodeMap;
7941
- useEffect11(() => {
8634
+ useEffect12(() => {
7942
8635
  const m = /* @__PURE__ */ new Map();
7943
8636
  for (const el of draggableElements) m.set(el.nodeId, el);
7944
8637
  refs.draggableNodeMapRef.current = m;
@@ -7946,7 +8639,7 @@ function DesignModeOverlay() {
7946
8639
  const resolveNodeElement = useCallback8((nodeId) => {
7947
8640
  const known = refs.nodeMapRef.current.get(nodeId)?.element;
7948
8641
  if (known) return known;
7949
- return document.querySelector(`[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(nodeId)}"]`);
8642
+ return document.querySelector(buildNodeSelector(nodeId));
7950
8643
  }, []);
7951
8644
  const clearCommitLaneCache = useCallback8(() => {
7952
8645
  refs.commitLaneCacheRef.current.clear();
@@ -7969,7 +8662,7 @@ function DesignModeOverlay() {
7969
8662
  if (command.type === "resize") {
7970
8663
  const resCmd = command;
7971
8664
  const targetId = resCmd.targetNodeId ?? resCmd.nodeId;
7972
- const targetEl = document.querySelector(`[${DND_NODE_ID_ATTR2}="${escapeCssAttrValue(targetId)}"]`);
8665
+ const targetEl = document.querySelector(buildNodeSelector(targetId));
7973
8666
  if (!targetEl) return false;
7974
8667
  const snapshot = direction === "redo" ? resCmd.after : resCmd.before;
7975
8668
  if (snapshot.styles) {
@@ -7980,6 +8673,9 @@ function DesignModeOverlay() {
7980
8673
  }
7981
8674
  return true;
7982
8675
  }
8676
+ if (command.type === "insert") {
8677
+ return applyInsertCommand(command, direction, resolveNodeElement);
8678
+ }
7983
8679
  const applied = applyDndCommandPlacement(command, direction, resolveNodeElement);
7984
8680
  if (!applied) return false;
7985
8681
  clearCommitLaneCache();
@@ -7992,6 +8688,10 @@ function DesignModeOverlay() {
7992
8688
  }
7993
8689
  setHistoryAvailability(refs.historyRef.current.canUndo(), refs.historyRef.current.canRedo());
7994
8690
  }, [historyMode, setHistoryAvailability]);
8691
+ useEffect12(() => {
8692
+ refs.historyRef.current.clear();
8693
+ syncHistoryAvailability();
8694
+ }, [enabled, syncHistoryAvailability]);
7995
8695
  const sensors = useSensors(
7996
8696
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
7997
8697
  );
@@ -8002,7 +8702,7 @@ function DesignModeOverlay() {
8002
8702
  );
8003
8703
  const [insideRect, setInsideRect] = useState6(null);
8004
8704
  const [dropLine, setDropLine] = useState6(null);
8005
- useEffect11(() => {
8705
+ useEffect12(() => {
8006
8706
  if (!enabled) return;
8007
8707
  const fn = (e) => {
8008
8708
  refs.mouseRef.current = { x: e.clientX, y: e.clientY };
@@ -8010,7 +8710,7 @@ function DesignModeOverlay() {
8010
8710
  window.addEventListener("pointermove", fn, { capture: true, passive: true });
8011
8711
  return () => window.removeEventListener("pointermove", fn, { capture: true });
8012
8712
  }, [enabled]);
8013
- useEffect11(() => {
8713
+ useEffect12(() => {
8014
8714
  if (!enabled) return;
8015
8715
  if (historyMode !== "local") return;
8016
8716
  const onKeyDown = (e) => {
@@ -8039,7 +8739,7 @@ function DesignModeOverlay() {
8039
8739
  setInsideRect,
8040
8740
  setDropLine
8041
8741
  });
8042
- useEffect11(() => {
8742
+ useEffect12(() => {
8043
8743
  if (!enabled || activeDrag) return;
8044
8744
  if (undoRequestId === refs.processedUndoIdRef.current) return;
8045
8745
  refs.processedUndoIdRef.current = undoRequestId;
@@ -8056,7 +8756,7 @@ function DesignModeOverlay() {
8056
8756
  });
8057
8757
  syncHistoryAvailability();
8058
8758
  }, [undoRequestId, enabled, activeDrag, applyCommand, resume, selectElement, syncHistoryAvailability, undoLastChange]);
8059
- useEffect11(() => {
8759
+ useEffect12(() => {
8060
8760
  if (!enabled || activeDrag || historyMode !== "local") return;
8061
8761
  if (redoRequestId === refs.processedRedoIdRef.current) return;
8062
8762
  refs.processedRedoIdRef.current = redoRequestId;
@@ -8068,49 +8768,29 @@ function DesignModeOverlay() {
8068
8768
  }
8069
8769
  syncHistoryAvailability();
8070
8770
  }, [redoRequestId, enabled, activeDrag, applyCommand, historyMode, resume, selectElement, syncHistoryAvailability, redoLastChange]);
8071
- useEffect11(() => {
8771
+ useEffect12(() => {
8072
8772
  syncHistoryAvailability();
8073
8773
  }, [syncHistoryAvailability]);
8074
- useEffect11(() => {
8774
+ useEffect12(() => {
8075
8775
  dispatchRuntimeEvent({
8076
8776
  type: "design-mode-changed",
8077
8777
  mode: enabled ? "design" : "off"
8078
8778
  });
8079
8779
  }, [enabled]);
8080
- useEffect11(() => {
8780
+ useEffect12(() => {
8081
8781
  const aiOutput = exportChangesForAI();
8082
8782
  const updatedAt = orderedChanges.length > 0 ? orderedChanges[orderedChanges.length - 1]?.at ?? Date.now() : Date.now();
8083
8783
  dispatchRuntimeEvent({
8084
8784
  type: "arrange-session-changed",
8085
- session: {
8086
- mode: enabled ? "arrange" : "off",
8087
- canUndo: orderedChanges.length > 0,
8088
- changes: orderedChanges.map((event, index) => ({
8089
- id: `${event.kind}:${event.operationId}:${index}`,
8090
- kind: event.kind,
8091
- operationId: event.operationId,
8092
- commandType: event.kind === "move" ? event.commandType : void 0,
8093
- nodeKey: event.nodeKey,
8094
- nodeId: event.nodeId,
8095
- inspectorRef: event.inspectorRef,
8096
- selectedNodeKey: event.kind === "move" ? event.selectedNodeKey : void 0,
8097
- selectedNodeId: event.kind === "move" ? event.selectedNodeId : void 0,
8098
- selectedInspectorRef: event.kind === "move" ? event.selectedInspectorRef : void 0,
8099
- anchorNodeKey: event.kind === "move" ? event.anchorNodeKey : void 0,
8100
- anchorNodeId: event.kind === "move" ? event.anchorNodeId : void 0,
8101
- summary: summarizeEditorChangeEvent(event),
8102
- at: event.at
8103
- })),
8104
- output: {
8105
- json: aiOutput,
8106
- prompt: aiOutput.aiPromptContext,
8107
- summary: orderedChanges.length === 0 ? "No arrange changes recorded yet." : `${orderedChanges.length} arrange change${orderedChanges.length === 1 ? "" : "s"} recorded.`
8108
- },
8785
+ session: buildArrangeSessionSnapshot({
8786
+ enabled,
8787
+ orderedChanges,
8788
+ aiOutput,
8109
8789
  updatedAt
8110
- }
8790
+ })
8111
8791
  });
8112
8792
  }, [enabled, exportChangesForAI, orderedChanges]);
8113
- useEffect11(() => {
8793
+ useEffect12(() => {
8114
8794
  if (!enabled) return;
8115
8795
  document.body.classList.add("design-mode-active");
8116
8796
  return () => document.body.classList.remove("design-mode-active");
@@ -8123,6 +8803,13 @@ function DesignModeOverlay() {
8123
8803
  autoScrollStateRef: refs.autoScrollStateRef
8124
8804
  });
8125
8805
  useDragVisualLoop({ activeDrag, refs, getCommitLaneSnapshot, setInsideRect, setDropLine });
8806
+ useExternalDragLoop({
8807
+ externalDrag,
8808
+ refs,
8809
+ elements,
8810
+ resolveNodeElement,
8811
+ setDropLine
8812
+ });
8126
8813
  const { handleResizeHandlePointerDown } = useResizeSession({
8127
8814
  enabled,
8128
8815
  refs,
@@ -8178,7 +8865,7 @@ function DesignModeOverlay() {
8178
8865
  }
8179
8866
  }
8180
8867
  ),
8181
- dropLine && activeDrag && /* @__PURE__ */ jsx4(
8868
+ dropLine && (activeDrag ?? externalDrag) && /* @__PURE__ */ jsx4(
8182
8869
  "div",
8183
8870
  {
8184
8871
  "data-design-mode-ui": true,
@@ -8237,35 +8924,40 @@ var combinedCollisionDetection = (args) => {
8237
8924
 
8238
8925
  // src/client/dnd/DndProvider.tsx
8239
8926
  import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
8927
+ var externalDragMouse = { x: 0, y: 0, commitRequested: false };
8240
8928
  function DndProvider({ children }) {
8241
8929
  const setActive = useDndStore((s) => s.setActive);
8242
8930
  const dragEndHandlers = useDndStore((s) => s.dragEndHandlers);
8243
- const toggle = useDesignModeStore((s) => s.toggle);
8244
8931
  const designModeEnabled = useDesignModeStore((s) => s.enabled);
8245
- const setDesignModeEnabled = useDesignModeStore((s) => s.setEnabled);
8246
8932
  const setHistoryMode = useDesignModeStore((s) => s.setHistoryMode);
8247
8933
  const setInspectorTheme = useDesignModeStore((s) => s.setInspectorTheme);
8248
8934
  const requestUndo = useDesignModeStore((s) => s.requestUndo);
8249
- useEffect12(() => {
8935
+ const transitionDesignMode = useCallback9((nextEnabled) => {
8936
+ const designModeState = useDesignModeStore.getState();
8937
+ if (designModeState.enabled === nextEnabled) return;
8938
+ designModeState.resetArrangeSession();
8939
+ designModeState.setEnabled(nextEnabled);
8940
+ }, []);
8941
+ useEffect13(() => {
8250
8942
  const handleKeyDown = (e) => {
8251
8943
  if (e.ctrlKey && e.shiftKey && e.key === "D") {
8252
8944
  e.preventDefault();
8253
- toggle();
8945
+ transitionDesignMode(!useDesignModeStore.getState().enabled);
8254
8946
  }
8255
8947
  };
8256
8948
  window.addEventListener("keydown", handleKeyDown);
8257
8949
  return () => window.removeEventListener("keydown", handleKeyDown);
8258
- }, [toggle]);
8259
- useEffect12(() => {
8950
+ }, [transitionDesignMode]);
8951
+ useEffect13(() => {
8260
8952
  const handleHostCommand = (event) => {
8261
8953
  const detail = event.detail;
8262
8954
  if (!detail || typeof detail !== "object") return;
8263
8955
  switch (detail.type) {
8264
8956
  case "sync-editor-state":
8265
8957
  if (detail.mode === "design") {
8266
- setDesignModeEnabled(true);
8958
+ transitionDesignMode(true);
8267
8959
  } else if (detail.mode === "inspect" || detail.mode === "off") {
8268
- setDesignModeEnabled(false);
8960
+ transitionDesignMode(false);
8269
8961
  }
8270
8962
  if (detail.historyMode) {
8271
8963
  setHistoryMode(detail.historyMode);
@@ -8275,7 +8967,7 @@ function DndProvider({ children }) {
8275
8967
  }
8276
8968
  break;
8277
8969
  case "set-design-mode":
8278
- setDesignModeEnabled(detail.enabled);
8970
+ transitionDesignMode(detail.enabled);
8279
8971
  break;
8280
8972
  case "set-history-mode":
8281
8973
  setHistoryMode(detail.historyMode);
@@ -8286,6 +8978,21 @@ function DndProvider({ children }) {
8286
8978
  case "request-undo":
8287
8979
  requestUndo();
8288
8980
  break;
8981
+ case "external-drag-start":
8982
+ useDesignModeStore.getState().startExternalDrag(detail.component);
8983
+ break;
8984
+ case "external-drag-move":
8985
+ externalDragMouse.x = detail.clientX;
8986
+ externalDragMouse.y = detail.clientY;
8987
+ break;
8988
+ case "external-drag-end":
8989
+ externalDragMouse.x = detail.clientX;
8990
+ externalDragMouse.y = detail.clientY;
8991
+ externalDragMouse.commitRequested = true;
8992
+ break;
8993
+ case "external-drag-cancel":
8994
+ useDesignModeStore.getState().cancelExternalDrag();
8995
+ break;
8289
8996
  default:
8290
8997
  break;
8291
8998
  }
@@ -8295,7 +9002,7 @@ function DndProvider({ children }) {
8295
9002
  window.removeEventListener(HOST_COMMAND_EVENT, handleHostCommand);
8296
9003
  };
8297
9004
  }, [
8298
- setDesignModeEnabled,
9005
+ transitionDesignMode,
8299
9006
  setHistoryMode,
8300
9007
  setInspectorTheme,
8301
9008
  requestUndo
@@ -8330,6 +9037,7 @@ function DndProvider({ children }) {
8330
9037
  const handleDesignModeCrash = useCallback9(() => {
8331
9038
  const designModeState = useDesignModeStore.getState();
8332
9039
  designModeState.resetAll();
9040
+ designModeState.resetArrangeSession();
8333
9041
  designModeState.setEnabled(false);
8334
9042
  }, []);
8335
9043
  if (designModeEnabled) {
@@ -8370,7 +9078,7 @@ function DndProvider({ children }) {
8370
9078
  }
8371
9079
 
8372
9080
  // src/client/useArchieDevToolsRuntime.ts
8373
- import { useEffect as useEffect13 } from "react";
9081
+ import { useEffect as useEffect14, useRef as useRef11 } from "react";
8374
9082
 
8375
9083
  // src/client/inject-inspector/archieOrigins.ts
8376
9084
  var ARCHIE_HOST_ORIGINS = [
@@ -8476,10 +9184,9 @@ function injectInspector(opts = {}) {
8476
9184
  document.head.appendChild(script);
8477
9185
  return null;
8478
9186
  }
8479
- const localInspectorModulePath = "./inspector.js";
8480
9187
  return import(
8481
9188
  /* @vite-ignore */
8482
- localInspectorModulePath
9189
+ "./inspector.js"
8483
9190
  ).then((m) => {
8484
9191
  try {
8485
9192
  m.default();
@@ -8595,18 +9302,26 @@ installDomMetaSanitizer();
8595
9302
  installDomMetaSanitizer();
8596
9303
  function useArchieDevToolsRuntime({
8597
9304
  router,
8598
- inspector = true
9305
+ inspector,
9306
+ inspectorOptions
8599
9307
  }) {
8600
- useEffect13(() => {
9308
+ const stableOptionsKey = JSON.stringify(inspectorOptions ?? null);
9309
+ const inspectorOptionsRef = useRef11(inspectorOptions);
9310
+ inspectorOptionsRef.current = inspectorOptions;
9311
+ useEffect14(() => {
8601
9312
  if (process.env.NODE_ENV !== "development") {
8602
9313
  return;
8603
9314
  }
8604
9315
  const unsubscribe = setupArchieRouteListener(router);
8605
- if (inspector) {
8606
- injectInspector({ scriptSource: "remote" });
9316
+ if (inspector !== false) {
9317
+ const isLocalHost = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
9318
+ injectInspector({
9319
+ scriptSource: isLocalHost ? "local" : "remote",
9320
+ ...inspectorOptionsRef.current
9321
+ });
8607
9322
  }
8608
9323
  return unsubscribe;
8609
- }, [router, inspector]);
9324
+ }, [router, inspector, stableOptionsKey]);
8610
9325
  }
8611
9326
 
8612
9327
  // src/client/ArchieDevToolProvider.tsx
@@ -8614,9 +9329,10 @@ import { jsx as jsx6 } from "react/jsx-runtime";
8614
9329
  function ArchieDevToolProvider({
8615
9330
  children,
8616
9331
  router,
8617
- inspector = true
9332
+ inspector,
9333
+ inspectorOptions
8618
9334
  }) {
8619
- useArchieDevToolsRuntime({ router, inspector });
9335
+ useArchieDevToolsRuntime({ router, inspector, inspectorOptions });
8620
9336
  return /* @__PURE__ */ jsx6(DndProvider, { children });
8621
9337
  }
8622
9338
  export {