@parhelia/core 0.1.12927 → 0.1.12931

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/dist/agents-view/AgentCard.js +4 -1
  2. package/dist/agents-view/AgentCard.js.map +1 -1
  3. package/dist/agents-view/AgentProfileIcon.js +8 -8
  4. package/dist/agents-view/AgentProfileIcon.js.map +1 -1
  5. package/dist/agents-view/AgentsView.js +1 -1
  6. package/dist/client-components/index.d.ts +1 -1
  7. package/dist/client-components/index.js +1 -1
  8. package/dist/client-components/index.js.map +1 -1
  9. package/dist/components/WorkspaceHomeView.d.ts +13 -0
  10. package/dist/components/WorkspaceHomeView.js +13 -0
  11. package/dist/components/WorkspaceHomeView.js.map +1 -0
  12. package/dist/components/ui/styled-dialog-title.d.ts +3 -1
  13. package/dist/components/ui/styled-dialog-title.js +2 -2
  14. package/dist/components/ui/styled-dialog-title.js.map +1 -1
  15. package/dist/config/config.js +7 -19
  16. package/dist/config/config.js.map +1 -1
  17. package/dist/config/notificationRoutes.js +1 -1
  18. package/dist/config/notificationRoutes.js.map +1 -1
  19. package/dist/editor/ConfirmationDialog.js +1 -1
  20. package/dist/editor/ConfirmationDialog.js.map +1 -1
  21. package/dist/editor/FieldListField.js +8 -0
  22. package/dist/editor/FieldListField.js.map +1 -1
  23. package/dist/editor/ImageEditButton.js +14 -2
  24. package/dist/editor/ImageEditButton.js.map +1 -1
  25. package/dist/editor/ImageEditor.js +1 -1
  26. package/dist/editor/ImageEditor.js.map +1 -1
  27. package/dist/editor/MainLayout.js +2 -1
  28. package/dist/editor/MainLayout.js.map +1 -1
  29. package/dist/editor/PerfProfiler.d.ts +11 -0
  30. package/dist/editor/PerfProfiler.js +29 -0
  31. package/dist/editor/PerfProfiler.js.map +1 -0
  32. package/dist/editor/PictureEditor.js +1 -1
  33. package/dist/editor/PictureEditor.js.map +1 -1
  34. package/dist/editor/ai/Agents.js +15 -79
  35. package/dist/editor/ai/Agents.js.map +1 -1
  36. package/dist/editor/ai/GuidanceOverlay.js +4 -9
  37. package/dist/editor/ai/GuidanceOverlay.js.map +1 -1
  38. package/dist/editor/ai/InlineAiDialog.js +2 -2
  39. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  40. package/dist/editor/ai/dialogs/capturePageScreenshot.js +1 -1
  41. package/dist/editor/ai/dialogs/capturePageScreenshot.js.map +1 -1
  42. package/dist/editor/ai/terminal/components/AgentMemoryPanel.d.ts +12 -0
  43. package/dist/editor/ai/terminal/components/AgentMemoryPanel.js +131 -0
  44. package/dist/editor/ai/terminal/components/AgentMemoryPanel.js.map +1 -0
  45. package/dist/editor/ai/terminal/components/AgentPromptTrayPopovers.js +10 -2
  46. package/dist/editor/ai/terminal/components/AgentPromptTrayPopovers.js.map +1 -1
  47. package/dist/editor/ai/terminal/components/AgentSettingsContent.d.ts +2 -1
  48. package/dist/editor/ai/terminal/components/AgentSettingsContent.js.map +1 -1
  49. package/dist/editor/ai/terminal/components/AgentSettingsSections.d.ts +2 -1
  50. package/dist/editor/ai/terminal/components/AgentSettingsSections.js +9 -2
  51. package/dist/editor/ai/terminal/components/AgentSettingsSections.js.map +1 -1
  52. package/dist/editor/ai/terminal/components/AgentTerminalContextPanels.js +11 -0
  53. package/dist/editor/ai/terminal/components/AgentTerminalContextPanels.js.map +1 -1
  54. package/dist/editor/ai/terminal/components/AgentTerminalStatusBar.js +27 -20
  55. package/dist/editor/ai/terminal/components/AgentTerminalStatusBar.js.map +1 -1
  56. package/dist/editor/ai/terminal/components/ContextInfoBar.js +4 -3
  57. package/dist/editor/ai/terminal/components/ContextInfoBar.js.map +1 -1
  58. package/dist/editor/ai/terminal/useAgentSettingsData.d.ts +8 -1
  59. package/dist/editor/ai/terminal/useAgentSettingsData.js +44 -5
  60. package/dist/editor/ai/terminal/useAgentSettingsData.js.map +1 -1
  61. package/dist/editor/ai/terminal/useAgentSettingsPanel.d.ts +1 -1
  62. package/dist/editor/bridge/BridgeClient.js +9 -1
  63. package/dist/editor/bridge/BridgeClient.js.map +1 -1
  64. package/dist/editor/bridge/protocol.d.ts +19 -0
  65. package/dist/editor/bridge/protocol.js +1 -1
  66. package/dist/editor/bridge/protocol.js.map +1 -1
  67. package/dist/editor/client/EditorShell.js +163 -67
  68. package/dist/editor/client/EditorShell.js.map +1 -1
  69. package/dist/editor/client/editContext.d.ts +35 -2
  70. package/dist/editor/client/editContext.js +8 -0
  71. package/dist/editor/client/editContext.js.map +1 -1
  72. package/dist/editor/client/fieldModificationStore.js +64 -0
  73. package/dist/editor/client/fieldModificationStore.js.map +1 -1
  74. package/dist/editor/client/itemsRepository.js +95 -3
  75. package/dist/editor/client/itemsRepository.js.map +1 -1
  76. package/dist/editor/client/operations.d.ts +1 -0
  77. package/dist/editor/client/operations.js +166 -13
  78. package/dist/editor/client/operations.js.map +1 -1
  79. package/dist/editor/client/ui/EditorChrome.js +6 -5
  80. package/dist/editor/client/ui/EditorChrome.js.map +1 -1
  81. package/dist/editor/commands/componentCommands.js +2 -1
  82. package/dist/editor/commands/componentCommands.js.map +1 -1
  83. package/dist/editor/commands/keyboardCommands.js +1 -1
  84. package/dist/editor/commands/keyboardCommands.js.map +1 -1
  85. package/dist/editor/componentTreeHelper.js +15 -5
  86. package/dist/editor/componentTreeHelper.js.map +1 -1
  87. package/dist/editor/context-menu/InsertMenu.js +1 -1
  88. package/dist/editor/context-menu/InsertMenu.js.map +1 -1
  89. package/dist/editor/field-types/SingleLineText.js +79 -9
  90. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  91. package/dist/editor/fieldValueDiagnostics.d.ts +79 -0
  92. package/dist/editor/fieldValueDiagnostics.js +133 -0
  93. package/dist/editor/fieldValueDiagnostics.js.map +1 -0
  94. package/dist/editor/menubar/PageSelector.js +1 -1
  95. package/dist/editor/menubar/PageSelector.js.map +1 -1
  96. package/dist/editor/menubar/toolbar-sections/EditControls.js +1 -2
  97. package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
  98. package/dist/editor/menubar/toolbar-sections/ManualBrowser.js +6 -0
  99. package/dist/editor/menubar/toolbar-sections/ManualBrowser.js.map +1 -1
  100. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +5 -5
  101. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  102. package/dist/editor/page-editor-chrome/DragInsertionOverlay.d.ts +8 -1
  103. package/dist/editor/page-editor-chrome/DragInsertionOverlay.js +203 -51
  104. package/dist/editor/page-editor-chrome/DragInsertionOverlay.js.map +1 -1
  105. package/dist/editor/page-editor-chrome/EmptyImageFieldIndicators.d.ts +10 -0
  106. package/dist/editor/page-editor-chrome/EmptyImageFieldIndicators.js +236 -0
  107. package/dist/editor/page-editor-chrome/EmptyImageFieldIndicators.js.map +1 -0
  108. package/dist/editor/page-editor-chrome/FrameMenu.d.ts +2 -0
  109. package/dist/editor/page-editor-chrome/FrameMenu.js +8 -6
  110. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  111. package/dist/editor/page-editor-chrome/FrameMenus.d.ts +3 -1
  112. package/dist/editor/page-editor-chrome/FrameMenus.js +4 -4
  113. package/dist/editor/page-editor-chrome/FrameMenus.js.map +1 -1
  114. package/dist/editor/page-editor-chrome/PageEditorChrome.js +18 -4
  115. package/dist/editor/page-editor-chrome/PageEditorChrome.js.map +1 -1
  116. package/dist/editor/page-editor-chrome/PictureEditorOverlay.d.ts +3 -0
  117. package/dist/editor/page-editor-chrome/PictureEditorOverlay.js +5 -8
  118. package/dist/editor/page-editor-chrome/PictureEditorOverlay.js.map +1 -1
  119. package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +8 -1
  120. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +20 -5
  121. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  122. package/dist/editor/page-editor-chrome/PlaceholderDropZones.d.ts +3 -1
  123. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +2 -2
  124. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
  125. package/dist/editor/page-editor-chrome/overlay/placeholderZones.d.ts +10 -0
  126. package/dist/editor/page-editor-chrome/overlay/placeholderZones.js +61 -1
  127. package/dist/editor/page-editor-chrome/overlay/placeholderZones.js.map +1 -1
  128. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js +121 -48
  129. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js.map +1 -1
  130. package/dist/editor/page-viewer/EditorForm.js +94 -7
  131. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  132. package/dist/editor/page-viewer/MiniMap.js +74 -9
  133. package/dist/editor/page-viewer/MiniMap.js.map +1 -1
  134. package/dist/editor/page-viewer/PageViewer.js +9 -7
  135. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  136. package/dist/editor/page-viewer/PageViewerFrame.js +390 -54
  137. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  138. package/dist/editor/page-viewer/bridgeFieldPatch.js +49 -2
  139. package/dist/editor/page-viewer/bridgeFieldPatch.js.map +1 -1
  140. package/dist/editor/page-viewer/minimapVisibility.d.ts +33 -0
  141. package/dist/editor/page-viewer/minimapVisibility.js +72 -0
  142. package/dist/editor/page-viewer/minimapVisibility.js.map +1 -0
  143. package/dist/editor/page-viewer/pageViewContext.d.ts +1 -0
  144. package/dist/editor/page-viewer/pageViewContext.js +7 -0
  145. package/dist/editor/page-viewer/pageViewContext.js.map +1 -1
  146. package/dist/editor/page-viewer/renderUrl.js +29 -7
  147. package/dist/editor/page-viewer/renderUrl.js.map +1 -1
  148. package/dist/editor/perfTrace.d.ts +46 -0
  149. package/dist/editor/perfTrace.js +213 -0
  150. package/dist/editor/perfTrace.js.map +1 -0
  151. package/dist/editor/reviews/CommentPopover.js +2 -3
  152. package/dist/editor/reviews/CommentPopover.js.map +1 -1
  153. package/dist/editor/reviews/CreateReviewConfirmStep.js +5 -4
  154. package/dist/editor/reviews/CreateReviewConfirmStep.js.map +1 -1
  155. package/dist/editor/reviews/CreateReviewDialog.js +120 -202
  156. package/dist/editor/reviews/CreateReviewDialog.js.map +1 -1
  157. package/dist/editor/reviews/CreateReviewItemsStep.d.ts +18 -0
  158. package/dist/editor/reviews/CreateReviewItemsStep.js +6 -0
  159. package/dist/editor/reviews/CreateReviewItemsStep.js.map +1 -0
  160. package/dist/editor/reviews/CreateReviewLanguagesStep.d.ts +9 -0
  161. package/dist/editor/reviews/CreateReviewLanguagesStep.js +36 -0
  162. package/dist/editor/reviews/CreateReviewLanguagesStep.js.map +1 -0
  163. package/dist/editor/reviews/CreateReviewReviewersStep.d.ts +17 -0
  164. package/dist/editor/reviews/CreateReviewReviewersStep.js +33 -0
  165. package/dist/editor/reviews/CreateReviewReviewersStep.js.map +1 -0
  166. package/dist/editor/reviews/CreateReviewSettingsStep.d.ts +21 -0
  167. package/dist/editor/reviews/CreateReviewSettingsStep.js +33 -0
  168. package/dist/editor/reviews/CreateReviewSettingsStep.js.map +1 -0
  169. package/dist/editor/reviews/MultiReviewManager.js +1 -4
  170. package/dist/editor/reviews/MultiReviewManager.js.map +1 -1
  171. package/dist/editor/reviews/commentAi.js +1 -1
  172. package/dist/editor/reviews/commentAi.js.map +1 -1
  173. package/dist/editor/reviews/openCommentsFeedback.d.ts +2 -6
  174. package/dist/editor/reviews/openCommentsFeedback.js +3 -11
  175. package/dist/editor/reviews/openCommentsFeedback.js.map +1 -1
  176. package/dist/editor/services/agentService.d.ts +38 -0
  177. package/dist/editor/services/agentService.js +42 -0
  178. package/dist/editor/services/agentService.js.map +1 -1
  179. package/dist/editor/services/aiService.d.ts +4 -1
  180. package/dist/editor/services/aiService.js.map +1 -1
  181. package/dist/editor/services/editService.d.ts +1 -1
  182. package/dist/editor/services/editService.js +2 -2
  183. package/dist/editor/services/editService.js.map +1 -1
  184. package/dist/editor/services/reviewsService.d.ts +6 -0
  185. package/dist/editor/services/reviewsService.js +15 -0
  186. package/dist/editor/services/reviewsService.js.map +1 -1
  187. package/dist/editor/settings/panels/AgentProfileConfigPanel.js +2 -1
  188. package/dist/editor/settings/panels/AgentProfileConfigPanel.js.map +1 -1
  189. package/dist/editor/sidebar/ComponentTree.js +31 -10
  190. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  191. package/dist/editor/sidebar/ContentTreePanel.d.ts +2 -2
  192. package/dist/editor/sidebar/ContentTreePanel.js +6 -2
  193. package/dist/editor/sidebar/ContentTreePanel.js.map +1 -1
  194. package/dist/editor/sidebar/MainContentTree.js +3 -2
  195. package/dist/editor/sidebar/MainContentTree.js.map +1 -1
  196. package/dist/editor/tree-indicators/GutterColumns.js +24 -6
  197. package/dist/editor/tree-indicators/GutterColumns.js.map +1 -1
  198. package/dist/editor/ui/ItemCollectionEditor.js +4 -4
  199. package/dist/editor/ui/ItemCollectionEditor.js.map +1 -1
  200. package/dist/editor/ui/PerfectTree.js +5 -5
  201. package/dist/editor/ui/PerfectTree.js.map +1 -1
  202. package/dist/editor/ui/PublishItemDialog.js +1 -1
  203. package/dist/editor/ui/PublishItemDialog.js.map +1 -1
  204. package/dist/editor/ui/PublishRestrictionsDialog.js +179 -148
  205. package/dist/editor/ui/PublishRestrictionsDialog.js.map +1 -1
  206. package/dist/editor/utils/keyboardNavigation.js +9 -2
  207. package/dist/editor/utils/keyboardNavigation.js.map +1 -1
  208. package/dist/editor/views/SingleEditView.js +32 -2
  209. package/dist/editor/views/SingleEditView.js.map +1 -1
  210. package/dist/index.d.ts +3 -1
  211. package/dist/index.js +2 -1
  212. package/dist/index.js.map +1 -1
  213. package/dist/revision.d.ts +2 -2
  214. package/dist/revision.js +2 -2
  215. package/dist/task-board/components/TaskBoardHomeView.js +3 -6
  216. package/dist/task-board/components/TaskBoardHomeView.js.map +1 -1
  217. package/package.json +1 -1
@@ -2,17 +2,18 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
- import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, GripVertical, } from "lucide-react";
5
+ import { Ban, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, GripVertical, } from "lucide-react";
6
6
  import { useEditContext } from "../client/editContext";
7
7
  import { useSlotContext } from "../views/editorSlotContext";
8
8
  import { getAllPlaceholders, getComponentById } from "../componentTreeHelper";
9
+ import { CANVAS_REFRESH_COMPLETED_EVENT, } from "../page-viewer/pageViewContext";
9
10
  import { cn } from "../../lib/utils";
10
11
  import { useReducedMotion } from "../client/hooks/useReducedMotion";
11
12
  import { clientToOverlay, findBridgeComponentTarget, hostViewportToOverlay, FRAME_OUTSET, measureBridgeComponentRect, measureBridgePlaceholderRect, rectEquals, } from "./overlay/geometry";
12
13
  import { IFRAME_OVERLAY_BRIDGE_DRAG_GESTURE_EVENT, useIframeOverlay, } from "./overlay/IframeOverlayProvider";
13
14
  import { useOverlayMeasurement } from "./overlay/useOverlayMeasurement";
14
15
  import { getAutoScrollStep } from "../../lib/pointerDrag";
15
- import { computePlaceholderZones, layoutAwareIndex, placeholderZonesEqual, resolveNearestGap, } from "./overlay/placeholderZones";
16
+ import { computePlaceholderZones, isWrapInsertionGap, layoutAwareIndex, placeholderZonesEqual, resolveNearestGap, } from "./overlay/placeholderZones";
16
17
  import { getBetweenInsertionZoneRect, getDirectionalInsertionHitZoneRect, getDirectionalInsertionZoneRect, getEmptyPlaceholderNeutralZoneRect, InsertionLine, } from "./overlay/InsertionLine";
17
18
  import { buildComponentDragObject } from "./componentDragObject";
18
19
  import { buildInsertTargetLabel, formatPlaceholderDisplayName, } from "./insertTargetLabel";
@@ -31,6 +32,9 @@ const TEMPLATE_THUMBNAIL_WIDTH_PX = 176;
31
32
  const TEMPLATE_THUMBNAIL_IMAGE_HEIGHT_PX = 96;
32
33
  const OFFSCREEN_GUIDE_EDGE_INSET_PX = 18;
33
34
  const OFFSCREEN_GUIDE_CROSS_AXIS_PADDING_PX = 24;
35
+ // Zones start empty for a frame or two while the first overlay measurement
36
+ // runs; only call the drag hopeless after they stay empty this long.
37
+ const NO_DROP_HINT_DELAY_MS = 200;
34
38
  /**
35
39
  * Full-canvas drop affordance shown while a component/template is being dragged.
36
40
  * It drives the continuous {@link InsertionLine} from two sources:
@@ -233,7 +237,7 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
233
237
  }, []);
234
238
  const announce = useCallback((target) => {
235
239
  const displayPlaceholderName = formatPlaceholderDisplayName(target.zone.description || target.zone.placeholder.name);
236
- editContextRef.current?.setStatusMessage(_jsxs("div", { children: ["Drop into placeholder", " ", _jsx("span", { className: "font-bold", children: target.zone.placeholder.parentComponent?.name }), " ", "/", " ", _jsx("span", { className: "font-bold", children: displayPlaceholderName }), " ", "at position", " ", _jsx("span", { className: "font-bold", children: target.adjustedIndex + 1 })] }));
240
+ editContextRef.current?.setStatusMessage(_jsxs("div", { children: ["Drop into placeholder", " ", _jsx("span", { className: "font-bold", children: target.zone.placeholder.parentComponent?.name }), " ", "/ ", _jsx("span", { className: "font-bold", children: displayPlaceholderName }), " at position ", _jsx("span", { className: "font-bold", children: target.adjustedIndex + 1 })] }));
237
241
  }, []);
238
242
  // Ask the host to open a gap (shift siblings) at the resolved zone. Only for
239
243
  // pointer gestures, where we know the moving element + its size.
@@ -277,6 +281,56 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
277
281
  originalPlaceholderPreviewActiveRef.current = false;
278
282
  pageViewContextRef.current?.requestBridgePreviewGap?.({ clear: true });
279
283
  }, []);
284
+ // Pending "keep the original-position placeholder up" hold after a committed
285
+ // move; holds the clear until the canvas repaints (or the backstop fires).
286
+ const previewGapHoldCleanupRef = useRef(null);
287
+ const cancelPreviewGapHold = useCallback(() => {
288
+ previewGapHoldCleanupRef.current?.();
289
+ }, []);
290
+ // After a committed component move, the grayed-out placeholder the bridge
291
+ // draws over the original position must outlive the drag: the operation
292
+ // resolves when the server accepts it, but the canvas repaints later -
293
+ // clearing at drop leaves the stale component fully visible at its old spot
294
+ // until the refresh lands. Mirror holdInsertingUntilCanvasRefresh
295
+ // (EditorShell): clear on the first canvas refresh, with a timeout backstop,
296
+ // or immediately if the operation did not commit.
297
+ const holdPreviewGapUntilCanvasRefresh = useCallback((commit) => {
298
+ previewGapHoldCleanupRef.current?.();
299
+ const detach = () => {
300
+ if (previewGapHoldCleanupRef.current === detach) {
301
+ previewGapHoldCleanupRef.current = null;
302
+ }
303
+ window.removeEventListener(CANVAS_REFRESH_COMPLETED_EVENT, finish);
304
+ window.clearTimeout(timeoutId);
305
+ };
306
+ const finish = () => {
307
+ detach();
308
+ clearPreviewGap();
309
+ };
310
+ const timeoutId = window.setTimeout(finish, 15000);
311
+ window.addEventListener(CANVAS_REFRESH_COMPLETED_EVENT, finish);
312
+ // Cancel (a new drag owns the preview now): stop watching WITHOUT
313
+ // sending a clear that would wipe the new drag's placeholder.
314
+ previewGapHoldCleanupRef.current = detach;
315
+ const isCurrentHold = () => previewGapHoldCleanupRef.current === detach;
316
+ // Promise.resolve guards non-async doubles (test mocks) of
317
+ // droppedInPlaceholder.
318
+ void Promise.resolve(commit)
319
+ .then((result) => {
320
+ if (result?.committed === false && isCurrentHold())
321
+ finish();
322
+ })
323
+ .catch(() => {
324
+ if (isCurrentHold())
325
+ finish();
326
+ });
327
+ }, [clearPreviewGap]);
328
+ useEffect(() => () => {
329
+ // Detach only: unmount tears down the whole chrome (view switch or the
330
+ // refresh itself re-keying the overlay); an unconditional clear here
331
+ // would race a hold that a remounted overlay no longer tracks.
332
+ previewGapHoldCleanupRef.current?.();
333
+ }, []);
280
334
  const startCanceledDragReturnAnimation = useCallback(() => {
281
335
  if (reducedMotionRef.current)
282
336
  return false;
@@ -485,6 +539,32 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
485
539
  window.removeEventListener("keydown", cancelOnEscape, true);
486
540
  };
487
541
  }, [cancelCurrentDrag, isDragging]);
542
+ // No gap anywhere accepts this drag — for a component move that means it
543
+ // already sits in its only possible position (its own adjacent gaps are
544
+ // no-ops and hidden). Surface that instead of leaving the drag mute: a hint
545
+ // rides the drag cursor (gesture drags) or tops the canvas (native drags),
546
+ // and the status line announces it. Delayed so the initial empty measurement
547
+ // doesn't flash the hint at every drag start.
548
+ const noDropPositionMessage = isDragging
549
+ ? editContext?.dragObject?.type === "component"
550
+ ? "Already in its only possible position — nowhere else to move it"
551
+ : "No compatible placeholder on this page"
552
+ : null;
553
+ const zonesEmptyWhileDragging = isDragging && zones.length === 0;
554
+ const [showNoDropHint, setShowNoDropHint] = useState(false);
555
+ useEffect(() => {
556
+ if (!zonesEmptyWhileDragging) {
557
+ setShowNoDropHint(false);
558
+ return;
559
+ }
560
+ const timer = window.setTimeout(() => setShowNoDropHint(true), NO_DROP_HINT_DELAY_MS);
561
+ return () => window.clearTimeout(timer);
562
+ }, [zonesEmptyWhileDragging]);
563
+ useEffect(() => {
564
+ if (showNoDropHint && noDropPositionMessage) {
565
+ editContextRef.current?.setStatusMessage(_jsx("div", { children: noDropPositionMessage }));
566
+ }
567
+ }, [showNoDropHint, noDropPositionMessage]);
488
568
  // Tell the "+" dots which gap is currently targeted so the matching dot can
489
569
  // light up - but only while the insertion LINE is actually visible at that gap,
490
570
  // so the dot highlight and the line appear together. (The resolved gap is set
@@ -571,6 +651,9 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
571
651
  };
572
652
  if (detail.phase === "start") {
573
653
  clearReturnPreview();
654
+ // A pending post-drop hold must stop watching: the new drag owns the
655
+ // original-position placeholder from here on.
656
+ cancelPreviewGapHold();
574
657
  canceledGestureHandledRef.current = false;
575
658
  let component;
576
659
  let dragObject = detail.dragObject;
@@ -581,7 +664,7 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
581
664
  : undefined;
582
665
  if (!component)
583
666
  return;
584
- const selection = ec.selection ?? [];
667
+ const selection = ec.getSelection();
585
668
  inMulti = selection.length > 1 && selection.includes(component.id);
586
669
  const selectedIds = inMulti ? selection : [component.id];
587
670
  dragObject = buildComponentDragObject(component, selectedIds, pvc.page);
@@ -657,9 +740,16 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
657
740
  : null;
658
741
  const target = point ? resolveZoneAtOverlayPoint(point) : null;
659
742
  const commitTarget = !detail.canceled && isDropCommitTarget(target) ? target : null;
660
- clearDragPresentation();
743
+ // On a committed move, keep the original-position placeholder up past
744
+ // the drag - it is cleared when the canvas refresh lands (see
745
+ // holdPreviewGapUntilCanvasRefresh).
746
+ const holdOriginalPreview = !!commitTarget && originalPlaceholderPreviewActiveRef.current;
747
+ clearDragPresentation({ clearPreview: !holdOriginalPreview });
661
748
  if (commitTarget) {
662
- ec.droppedInPlaceholder(commitTarget.zone.placeholder.key, commitTarget.adjustedIndex, undefined, commitTarget.zone.anchor, undefined, commitTarget.zone.position, commitTarget.color);
749
+ const commit = ec.droppedInPlaceholder(commitTarget.zone.placeholder.key, commitTarget.adjustedIndex, undefined, commitTarget.zone.anchor, undefined, commitTarget.zone.position, commitTarget.color);
750
+ if (holdOriginalPreview) {
751
+ holdPreviewGapUntilCanvasRefresh(commit);
752
+ }
663
753
  }
664
754
  ec.dragEnd();
665
755
  }
@@ -670,16 +760,22 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
670
760
  overlay.iframe,
671
761
  resolveZoneAtOverlayPoint,
672
762
  applyResolvedZone,
763
+ cancelPreviewGapHold,
673
764
  clearReturnPreview,
674
765
  clearDragPresentation,
766
+ holdPreviewGapUntilCanvasRefresh,
675
767
  startCanceledDragReturnAnimation,
676
768
  ]);
677
- // Reset when a drag ends.
769
+ // Reset when a drag ends. A pending post-drop hold keeps the original
770
+ // placeholder alive until the canvas refresh, so don't clear the preview
771
+ // out from under it here.
678
772
  useEffect(() => {
679
773
  if (!isDragging) {
680
774
  if (returnPreviewRef.current)
681
775
  return;
682
- clearDragPresentation();
776
+ clearDragPresentation({
777
+ clearPreview: !previewGapHoldCleanupRef.current,
778
+ });
683
779
  }
684
780
  }, [isDragging, clearDragPresentation]);
685
781
  // Never leave a status message lingering after unmount.
@@ -708,8 +804,12 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
708
804
  const dragCursorSnapshotSize = snapshot
709
805
  ? snapshotDisplaySize(snapshot, dragCursorShrinkPx)
710
806
  : null;
711
- const templateThumbnailWidth = shrinkDimension(TEMPLATE_THUMBNAIL_WIDTH_PX, dragCursorShrinkPx);
712
- const templateThumbnailImageHeight = shrinkDimension(TEMPLATE_THUMBNAIL_IMAGE_HEIGHT_PX, dragCursorShrinkPx);
807
+ const templateThumbnailSize = shrinkSizeProportionally({
808
+ width: TEMPLATE_THUMBNAIL_WIDTH_PX,
809
+ height: TEMPLATE_THUMBNAIL_IMAGE_HEIGHT_PX,
810
+ }, dragCursorShrinkPx);
811
+ const templateThumbnailWidth = templateThumbnailSize.width;
812
+ const templateThumbnailImageHeight = templateThumbnailSize.height;
713
813
  const dragCursorSizeTransition = reducedMotion
714
814
  ? undefined
715
815
  : "width 120ms ease-out, height 120ms ease-out";
@@ -775,31 +875,37 @@ export function DragInsertionOverlay({ pageViewContext, onActiveTargetChange, })
775
875
  // of remounting and replaying the fade-in at every gap - that per-gap
776
876
  // re-fade is the "spots quickly flashing" as the cursor sweeps. The
777
877
  // enter fade now plays once when the bar first appears.
778
- , { zone: active.zone, placeholderRect: active.placeholderRect, visualMode: active.visualMode, visualAnchor: active.visualAnchor, visualTargetRect: active.visualTargetRect, color: active.color, label: buildActiveInsertionLabel(active), animate: !reducedMotion, phase: "enter" }, "active-bar")), offscreenGuides.map((guide) => (_jsx(OffscreenInsertionGuidePill, { guide: guide }, guide.direction)))] }), gestureActive &&
878
+ , { zone: active.zone, placeholderRect: active.placeholderRect, visualMode: active.visualMode, visualAnchor: active.visualAnchor, visualTargetRect: active.visualTargetRect, color: active.color, label: buildActiveInsertionLabel(active), animate: !reducedMotion, phase: "enter" }, "active-bar")), offscreenGuides.map((guide) => (_jsx(OffscreenInsertionGuidePill, { guide: guide }, guide.direction))), !gestureActive && showNoDropHint && noDropPositionMessage && (
879
+ // Native (HTML5) drags have no cursor-anchored preview to attach the
880
+ // hint to, so it tops the canvas instead.
881
+ _jsxs("div", { "data-testid": "drag-no-drop-banner", className: "pointer-events-none absolute top-4 left-1/2 flex -translate-x-1/2 items-center gap-1.5 rounded-[4px] bg-neutral-grey-100/90 px-2.5 py-1.5 text-xs leading-none font-medium whitespace-nowrap text-white shadow-md backdrop-blur-sm", style: { zIndex: 900 }, children: [_jsx(Ban, { className: "h-3.5 w-3.5 shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate", children: noDropPositionMessage })] }))] }), gestureActive &&
779
882
  cursorClient &&
780
883
  editContext.dragObject &&
781
884
  createPortal(
782
885
  // Cursor-anchored: the dragged thing (live snapshot, else a chip). The
783
886
  // insertion bar names the target placeholder, so the cursor stays clean.
784
- _jsx("div", { ref: dragCursorPreviewRef, "data-testid": "drag-cursor-preview", className: "pointer-events-none fixed flex flex-col items-start gap-1.5", style: {
887
+ _jsxs("div", { ref: dragCursorPreviewRef, "data-testid": "drag-cursor-preview", className: "pointer-events-none fixed flex flex-col items-start gap-1.5", style: {
785
888
  left: cursorClient.x + 14,
786
889
  top: cursorClient.y + 14,
787
890
  zIndex: 99999,
788
- }, children: snapshot ? (_jsx("div", { "data-testid": "drag-cursor-snapshot", className: "overflow-hidden rounded-md bg-white shadow-lg ring-1 ring-black/10", style: { opacity: 0.96 }, children: _jsx("div", { className: "p-1.5", children: _jsx("img", { src: snapshot.dataUrl, alt: "", "aria-hidden": "true", draggable: false, className: "block rounded-sm", style: {
789
- width: dragCursorSnapshotSize?.width,
790
- height: dragCursorSnapshotSize?.height,
791
- objectFit: "contain",
792
- transition: dragCursorSizeTransition,
793
- } }) }) })) : dragCursorThumbnailUrl ? (_jsxs("div", { "data-testid": "drag-cursor-thumbnail", className: "overflow-hidden rounded-md bg-white shadow-lg ring-1 ring-black/10", style: {
794
- opacity: 0.96,
795
- width: templateThumbnailWidth,
796
- transition: dragCursorSizeTransition,
797
- }, children: [_jsx("div", { "data-testid": "drag-cursor-thumbnail-label", className: "border-border-default truncate border-b px-2 py-1 text-xs leading-4 font-medium text-neutral-grey-100", title: dragCursorLabel, children: dragCursorLabel }), _jsx("div", { className: "p-1.5", children: _jsx("img", { src: dragCursorThumbnailUrl, alt: "", "aria-hidden": "true", draggable: false, className: "block w-full rounded-sm object-contain", style: {
798
- height: templateThumbnailImageHeight,
891
+ }, children: [showNoDropHint && noDropPositionMessage && (
892
+ // Ride ABOVE the dragged preview so the hint sits on top of the
893
+ // snapshot/thumbnail image rather than dangling below it.
894
+ _jsxs("div", { "data-testid": "drag-cursor-no-drop", className: "bg-neutral-grey-100/90 flex items-center gap-1.5 rounded-[4px] px-2 py-1 text-xs leading-none font-medium whitespace-nowrap text-white shadow-md", children: [_jsx(Ban, { className: "h-3.5 w-3.5 shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate", children: noDropPositionMessage })] })), snapshot ? (_jsx("div", { "data-testid": "drag-cursor-snapshot", className: "overflow-hidden rounded-md bg-white shadow-lg ring-1 ring-black/10", style: { opacity: 0.96 }, children: _jsx("div", { className: "p-1.5", children: _jsx("img", { src: snapshot.dataUrl, alt: "", "aria-hidden": "true", draggable: false, className: "block rounded-sm", style: {
895
+ width: dragCursorSnapshotSize?.width,
896
+ height: dragCursorSnapshotSize?.height,
897
+ objectFit: "contain",
799
898
  transition: dragCursorSizeTransition,
800
- } }) })] })) : (_jsxs("div", { "data-testid": "drag-cursor-chip", className: "bg-component-default flex items-center gap-1 rounded px-2 py-1 text-sm whitespace-nowrap text-white shadow-md", children: [_jsx(GripVertical, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), (editContext.dragObject.components?.length ?? 0) > 1
801
- ? `${editContext.dragObject.components.length} components`
802
- : editContext.dragObject.name] })) }), document.body), returnPreviewPortal] }));
899
+ } }) }) })) : dragCursorThumbnailUrl ? (_jsxs("div", { "data-testid": "drag-cursor-thumbnail", className: "overflow-hidden rounded-md bg-white shadow-lg ring-1 ring-black/10", style: {
900
+ opacity: 0.96,
901
+ width: templateThumbnailWidth,
902
+ transition: dragCursorSizeTransition,
903
+ }, children: [_jsx("div", { "data-testid": "drag-cursor-thumbnail-label", className: "border-border-default text-neutral-grey-100 truncate border-b px-2 py-1 text-xs leading-4 font-medium", title: dragCursorLabel, children: dragCursorLabel }), _jsx("div", { className: "p-1.5", children: _jsx("img", { src: dragCursorThumbnailUrl, alt: "", "aria-hidden": "true", draggable: false, className: "block w-full rounded-sm object-contain", style: {
904
+ height: templateThumbnailImageHeight,
905
+ transition: dragCursorSizeTransition,
906
+ } }) })] })) : (_jsxs("div", { "data-testid": "drag-cursor-chip", className: "bg-component-default flex items-center gap-1 rounded px-2 py-1 text-sm whitespace-nowrap text-white shadow-md", children: [_jsx(GripVertical, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), (editContext.dragObject.components?.length ?? 0) > 1
907
+ ? `${editContext.dragObject.components.length} components`
908
+ : editContext.dragObject.name] }))] }), document.body), returnPreviewPortal] }));
803
909
  }
804
910
  function ReturningDragPreview({ preview }) {
805
911
  const [atRest, setAtRest] = useState(false);
@@ -1039,22 +1145,32 @@ function ParentComponentOutline({ component, pageViewContext, color, animate, })
1039
1145
  } }));
1040
1146
  }
1041
1147
  // Size the floating snapshot from 75% of the component's captured width, capped
1042
- // to the viewport. The image uses objectFit:contain, so explicit hover-shrunk
1043
- // width/height boxes do not distort captures with unusual aspect ratios.
1148
+ // to the viewport.
1044
1149
  function snapshotDisplaySize(s, shrinkPx = 0) {
1045
1150
  const viewportWidth = typeof window !== "undefined" ? window.innerWidth : 1280;
1046
1151
  const viewportHeight = typeof window !== "undefined" ? window.innerHeight : 800;
1047
1152
  const width = Math.round(Math.min(Math.max(1, s.width * 0.75), viewportWidth * 0.9));
1048
1153
  const aspectHeight = s.width > 0 ? Math.round((width * Math.max(1, s.height)) / s.width) : width;
1049
1154
  const height = Math.round(Math.min(Math.max(1, aspectHeight), viewportHeight * 0.9));
1155
+ return shrinkSizeProportionally({ width, height }, shrinkPx);
1156
+ }
1157
+ // Shrink both dimensions by ONE scale factor (the larger dimension loses
1158
+ // `shrinkPx`), preserving aspect ratio. Subtracting the pixels per axis
1159
+ // squashed extreme aspect ratios: a flat accordion-row capture lost most of
1160
+ // its height but almost none of its width, so the hover-over-drop-zone shrink
1161
+ // squeezed it into a sliver.
1162
+ export function shrinkSizeProportionally(size, shrinkPx) {
1163
+ const width = Math.max(1, Math.round(size.width));
1164
+ const height = Math.max(1, Math.round(size.height));
1165
+ if (shrinkPx <= 0)
1166
+ return { width, height };
1167
+ const maxDimension = Math.max(width, height);
1168
+ const factor = Math.max(1, maxDimension - shrinkPx) / maxDimension;
1050
1169
  return {
1051
- width: shrinkDimension(width, shrinkPx),
1052
- height: shrinkDimension(height, shrinkPx),
1170
+ width: Math.max(1, Math.round(width * factor)),
1171
+ height: Math.max(1, Math.round(height * factor)),
1053
1172
  };
1054
1173
  }
1055
- function shrinkDimension(value, shrinkPx) {
1056
- return Math.max(1, Math.round(value) - shrinkPx);
1057
- }
1058
1174
  function formatDragCursorLabel(dragObject) {
1059
1175
  const componentCount = dragObject.components?.length ?? 0;
1060
1176
  if (componentCount > 1)
@@ -1170,28 +1286,20 @@ function resolveInsertionZoneVisualTarget(zone, point, placeholderRect, geometry
1170
1286
  }
1171
1287
  : null;
1172
1288
  if (!previousComponent && beforeNext) {
1173
- if (pointInOverlayRect(point, beforeNext.rect)) {
1174
- return pointInDirectionalEdgeBand(point, beforeNext.rect, beforeNext.anchor)
1175
- ? { ...beforeNext, mode: "directional" }
1176
- : {
1177
- ...resolveHiddenInsertionVisual(zone),
1178
- ownerComponentId: beforeNext.ownerComponentId,
1179
- };
1180
- }
1181
- return resolveNeutralInsertionVisual(zone, placeholderRect, point);
1289
+ return resolveSingleEdgeVisual(zone, point, placeholderRect, beforeNext);
1182
1290
  }
1183
1291
  if (!nextComponent && afterPrevious) {
1184
- if (pointInOverlayRect(point, afterPrevious.rect)) {
1185
- return pointInDirectionalEdgeBand(point, afterPrevious.rect, afterPrevious.anchor)
1186
- ? { ...afterPrevious, mode: "directional" }
1187
- : {
1188
- ...resolveHiddenInsertionVisual(zone),
1189
- ownerComponentId: afterPrevious.ownerComponentId,
1190
- };
1191
- }
1192
- return resolveNeutralInsertionVisual(zone, placeholderRect, point);
1292
+ return resolveSingleEdgeVisual(zone, point, placeholderRect, afterPrevious);
1193
1293
  }
1194
1294
  if (afterPrevious && beforeNext) {
1295
+ // A wrap gap (next starts a new row/column of a wrapping grid) has no
1296
+ // shared gutter for a "between" band - the min→max cross span would
1297
+ // stretch the bar across both rows. Treat the gap as the previous
1298
+ // component's trailing edge instead, which is where `zone.position`
1299
+ // (and the "+" dot) already sits.
1300
+ if (isWrapInsertionGap(zone.orientation, afterPrevious.rect, beforeNext.rect)) {
1301
+ return resolveSingleEdgeVisual(zone, point, placeholderRect, afterPrevious);
1302
+ }
1195
1303
  const betweenRect = getBetweenInsertionZoneRect({
1196
1304
  zone,
1197
1305
  previousRect: afterPrevious.rect,
@@ -1225,6 +1333,50 @@ function resolveInsertionZoneVisualTarget(zone, point, placeholderRect, geometry
1225
1333
  }
1226
1334
  return resolveNeutralInsertionVisual(zone, placeholderRect, point);
1227
1335
  }
1336
+ // A gap with a single visual owner: a terminal gap (before the first / after
1337
+ // the last component) or a wrap gap resolved to its previous component. The
1338
+ // component's edge band and the outside band both light the directional bar;
1339
+ // anywhere else falls back to neutral/hidden.
1340
+ function resolveSingleEdgeVisual(zone, point, placeholderRect, edge) {
1341
+ if (pointInOverlayRect(point, edge.rect)) {
1342
+ return pointInDirectionalEdgeBand(point, edge.rect, edge.anchor)
1343
+ ? { ...edge, mode: "directional" }
1344
+ : {
1345
+ ...resolveHiddenInsertionVisual(zone),
1346
+ ownerComponentId: edge.ownerComponentId,
1347
+ };
1348
+ }
1349
+ const outsideEdge = resolveDirectionalOutsideEdgeVisual(zone, point, edge);
1350
+ if (outsideEdge)
1351
+ return outsideEdge;
1352
+ return resolveNeutralInsertionVisual(zone, placeholderRect, point);
1353
+ }
1354
+ // An end gap's "+" dot and insertion band hang OUTSIDE the terminal
1355
+ // component's edge (below the last / above the first component), where the
1356
+ // component-rect checks above can't match. Without this, the first approach
1357
+ // from that side resolved "neutral" (nothing lights up) while the same spot
1358
+ // activated fine once the zone was owned - the pointer had to cross into the
1359
+ // component's edge band once to arm it. Reuse the owned-zone hit rect so the
1360
+ // first approach and the hysteresis accept the exact same region.
1361
+ function resolveDirectionalOutsideEdgeVisual(zone, point, edge) {
1362
+ const hitRect = getDirectionalInsertionHitZoneRect({
1363
+ zone,
1364
+ targetRect: edge.rect,
1365
+ anchor: edge.anchor,
1366
+ });
1367
+ if (!pointInOverlayRect(point, hitRect))
1368
+ return null;
1369
+ const visualDropZoneRect = getDirectionalInsertionZoneRect({
1370
+ zone,
1371
+ targetRect: edge.rect,
1372
+ anchor: edge.anchor,
1373
+ });
1374
+ return {
1375
+ ...edge,
1376
+ mode: "directional",
1377
+ zoneHovered: pointInOverlayRect(point, visualDropZoneRect),
1378
+ };
1379
+ }
1228
1380
  function resolveNeutralInsertionVisual(zone, placeholderRect, point) {
1229
1381
  const targetRect = zone.targetRect ?? placeholderRect;
1230
1382
  const isEmptyPlaceholder = zone.placeholder.components.length === 0;