@beyondwork/docx-react-component 1.0.106 → 1.0.109

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 (190) hide show
  1. package/package.json +19 -5
  2. package/src/api/geometry-overlay-rects.ts +5 -0
  3. package/src/api/package-version.ts +1 -1
  4. package/src/api/page-anchor-id.ts +5 -0
  5. package/src/api/public-types.ts +16 -9
  6. package/src/api/table-node-specs.ts +6 -0
  7. package/src/api/v3/_create.ts +2 -1
  8. package/src/api/v3/_page-anchor-id.ts +52 -0
  9. package/src/api/v3/_runtime-handle.ts +92 -1
  10. package/src/api/v3/ai/_audit-time.ts +5 -0
  11. package/src/api/v3/ai/_pe2-evidence.ts +38 -0
  12. package/src/api/v3/ai/attach.ts +7 -2
  13. package/src/api/v3/ai/replacement.ts +101 -18
  14. package/src/api/v3/ai/resolve.ts +2 -2
  15. package/src/api/v3/ai/review.ts +177 -3
  16. package/src/api/v3/index.ts +1 -0
  17. package/src/api/v3/runtime/collab.ts +462 -0
  18. package/src/api/v3/runtime/document.ts +503 -20
  19. package/src/api/v3/runtime/geometry.ts +97 -0
  20. package/src/api/v3/runtime/layout.ts +744 -0
  21. package/src/api/v3/runtime/perf-probe.ts +14 -0
  22. package/src/api/v3/runtime/viewport.ts +9 -8
  23. package/src/api/v3/ui/_types.ts +149 -55
  24. package/src/api/v3/ui/chrome-preset-model.ts +5 -5
  25. package/src/api/v3/ui/debug.ts +115 -2
  26. package/src/api/v3/ui/index.ts +13 -0
  27. package/src/api/v3/ui/overlays.ts +0 -8
  28. package/src/api/v3/ui/surface.ts +56 -0
  29. package/src/api/v3/ui/viewport.ts +22 -9
  30. package/src/core/commands/image-commands.ts +1 -0
  31. package/src/core/commands/index.ts +6 -0
  32. package/src/core/schema/text-schema.ts +43 -5
  33. package/src/core/selection/mapping.ts +8 -1
  34. package/src/core/selection/review-anchors.ts +5 -1
  35. package/src/core/state/text-transaction.ts +8 -2
  36. package/src/io/export/serialize-revisions.ts +149 -1
  37. package/src/io/normalize/normalize-text.ts +6 -0
  38. package/src/io/ooxml/parse-bookmark-references.ts +55 -0
  39. package/src/io/ooxml/parse-fields.ts +24 -2
  40. package/src/io/ooxml/parse-headers-footers.ts +38 -5
  41. package/src/io/ooxml/parse-main-document.ts +153 -9
  42. package/src/io/ooxml/parse-numbering.ts +20 -0
  43. package/src/io/ooxml/parse-revisions.ts +19 -8
  44. package/src/io/opc/package-reader.ts +98 -8
  45. package/src/model/anchor.ts +4 -3
  46. package/src/model/canonical-document.ts +220 -2
  47. package/src/model/canonical-hash.ts +221 -0
  48. package/src/model/canonical-layout-inputs.ts +245 -6
  49. package/src/model/layout/index.ts +1 -0
  50. package/src/model/layout/page-graph-types.ts +118 -1
  51. package/src/model/review/revision-types.ts +14 -3
  52. package/src/preservation/store.ts +20 -4
  53. package/src/review/README.md +1 -1
  54. package/src/review/store/revision-actions.ts +14 -2
  55. package/src/runtime/collab/event-types.ts +67 -1
  56. package/src/runtime/collab/runtime-collab-sync.ts +177 -5
  57. package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
  58. package/src/runtime/document-heading-outline.ts +147 -0
  59. package/src/runtime/document-navigation.ts +8 -243
  60. package/src/runtime/document-runtime.ts +240 -97
  61. package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
  62. package/src/runtime/formatting/layout-inputs.ts +38 -5
  63. package/src/runtime/formatting/numbering/geometry.ts +28 -2
  64. package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
  65. package/src/runtime/geometry/caret-geometry.ts +5 -6
  66. package/src/runtime/geometry/geometry-facet.ts +60 -10
  67. package/src/runtime/geometry/geometry-index.ts +591 -20
  68. package/src/runtime/geometry/geometry-types.ts +59 -0
  69. package/src/runtime/geometry/hit-test.ts +11 -1
  70. package/src/runtime/geometry/overlay-rects.ts +5 -3
  71. package/src/runtime/geometry/project-anchors.ts +1 -1
  72. package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
  73. package/src/runtime/layout/index.ts +6 -0
  74. package/src/runtime/layout/layout-engine-instance.ts +6 -1
  75. package/src/runtime/layout/layout-engine-version.ts +181 -16
  76. package/src/runtime/layout/layout-facet-types.ts +6 -0
  77. package/src/runtime/layout/page-graph.ts +21 -4
  78. package/src/runtime/layout/paginated-layout-engine.ts +139 -15
  79. package/src/runtime/layout/project-block-fragments.ts +265 -7
  80. package/src/runtime/layout/public-facet.ts +78 -24
  81. package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
  82. package/src/runtime/layout/table-row-split.ts +92 -35
  83. package/src/runtime/prerender/cache-envelope.ts +2 -2
  84. package/src/runtime/prerender/cache-key.ts +5 -4
  85. package/src/runtime/prerender/customxml-cache.ts +0 -1
  86. package/src/runtime/render/render-kernel.ts +1 -1
  87. package/src/runtime/revision-runtime.ts +112 -10
  88. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  89. package/src/runtime/scopes/action-validation.ts +22 -2
  90. package/src/runtime/scopes/capabilities.ts +316 -0
  91. package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
  92. package/src/runtime/scopes/compiler-service.ts +108 -4
  93. package/src/runtime/scopes/content-control-evidence.ts +79 -0
  94. package/src/runtime/scopes/create-issue.ts +5 -5
  95. package/src/runtime/scopes/evidence.ts +91 -0
  96. package/src/runtime/scopes/formatting/apply.ts +2 -0
  97. package/src/runtime/scopes/geometry-evidence.ts +130 -0
  98. package/src/runtime/scopes/index.ts +54 -0
  99. package/src/runtime/scopes/issue-lifecycle.ts +224 -0
  100. package/src/runtime/scopes/layout-evidence.ts +374 -0
  101. package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
  102. package/src/runtime/scopes/preservation-boundary.ts +7 -1
  103. package/src/runtime/scopes/replacement/apply.ts +97 -34
  104. package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
  105. package/src/runtime/scopes/semantic-scope-types.ts +242 -3
  106. package/src/runtime/scopes/visualization.ts +28 -0
  107. package/src/runtime/surface-projection.ts +44 -5
  108. package/src/runtime/telemetry/perf-probe.ts +216 -0
  109. package/src/runtime/virtualized-rendering.ts +36 -1
  110. package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
  111. package/src/runtime/workflow/coordinator.ts +39 -11
  112. package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
  113. package/src/runtime/workflow/index.ts +3 -0
  114. package/src/runtime/workflow/overlay-lane-types.ts +58 -0
  115. package/src/runtime/workflow/overlay-lanes.ts +168 -10
  116. package/src/runtime/workflow/overlay-store.ts +2 -2
  117. package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
  118. package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
  119. package/src/session/_sync-legacy.ts +17 -27
  120. package/src/session/import/loader.ts +6 -4
  121. package/src/session/import/source-package-evidence.ts +186 -2
  122. package/src/session/index.ts +5 -6
  123. package/src/session/session.ts +30 -56
  124. package/src/session/types.ts +8 -13
  125. package/src/shell/session-bootstrap.ts +155 -81
  126. package/src/ui/WordReviewEditor.tsx +520 -12
  127. package/src/ui/editor-shell-view.tsx +14 -4
  128. package/src/ui/editor-surface-controller.tsx +5 -3
  129. package/src/ui/headless/selection-tool-resolver.ts +1 -2
  130. package/src/ui/presence-overlay-lane.ts +0 -1
  131. package/src/ui/ui-controller-factory.ts +7 -0
  132. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
  133. package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
  134. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
  135. package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
  136. package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
  137. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
  138. package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
  139. package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
  140. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
  141. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
  142. package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
  143. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
  144. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
  145. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
  146. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  147. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
  148. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
  149. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
  150. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
  151. package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
  152. package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
  153. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
  154. package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
  155. package/src/ui-tailwind/debug/README.md +4 -1
  156. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
  157. package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
  158. package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
  159. package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
  160. package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
  161. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
  162. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
  163. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
  164. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
  165. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
  166. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
  167. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
  168. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
  169. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
  170. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
  171. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
  172. package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
  173. package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
  174. package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
  175. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
  176. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
  177. package/src/ui-tailwind/review-workspace/types.ts +26 -12
  178. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
  179. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
  180. package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
  181. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
  182. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
  183. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
  184. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
  185. package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
  186. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
  187. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
  188. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
  189. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
  190. package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
@@ -66,6 +66,7 @@ import {
66
66
  */
67
67
  function createStableHostProxy(
68
68
  hostRef: { current: EditorActionHostCallbacks },
69
+ fallbackRef?: { current: EditorActionHostCallbacks },
69
70
  ): EditorActionHostCallbacks {
70
71
  // Proxy construction is ~free and happens exactly once per component
71
72
  // lifetime (see the empty-dep useMemo call site below). The `get`
@@ -76,11 +77,20 @@ function createStableHostProxy(
76
77
  // `typeof host[key] === "function"` checks elsewhere behave
77
78
  // predictably.
78
79
  if (typeof prop !== "string") return undefined;
79
- return (hostRef.current as Record<string, unknown>)[prop];
80
+ const hostValue = (hostRef.current as Record<string, unknown>)[prop];
81
+ if (hostValue !== undefined) return hostValue;
82
+ return fallbackRef
83
+ ? (fallbackRef.current as Record<string, unknown>)[prop]
84
+ : undefined;
80
85
  },
81
86
  has: (_target, prop) => {
82
87
  if (typeof prop !== "string") return false;
83
- return prop in (hostRef.current as Record<string, unknown>);
88
+ return (
89
+ prop in (hostRef.current as Record<string, unknown>) ||
90
+ (fallbackRef
91
+ ? prop in (fallbackRef.current as Record<string, unknown>)
92
+ : false)
93
+ );
84
94
  },
85
95
  // `ownKeys` + `getOwnPropertyDescriptor` left default — iteration
86
96
  // over the proxy isn't part of the registry contract.
@@ -184,7 +194,22 @@ export function TwWorkspaceChromeHost(
184
194
  // `[mode]` so wholesale-snapshot re-renders don't rebuild groups.
185
195
  const hostRef = useRef<EditorActionHostCallbacks>(editorActionHost);
186
196
  hostRef.current = editorActionHost;
187
- const stableHost = useMemo(() => createStableHostProxy(hostRef), []);
197
+ // Phase E trust/detail action fallback. Registry rows remain callback-
198
+ // driven, but the chrome host can satisfy product-safe detail requests
199
+ // through the existing rail-tab channel it already owns. Hosts still
200
+ // override these by supplying explicit callbacks in `editorActionHost`.
201
+ const railActionHostRef = useRef<EditorActionHostCallbacks>({});
202
+ const openRailTabRef = useRef<
203
+ ((tab: EditorRailTab, itemId?: string) => void) | undefined
204
+ >(onOpenRailTab);
205
+ openRailTabRef.current = onOpenRailTab;
206
+ railActionHostRef.current = {
207
+ onOpenTemplateSlotInfo: () => openRailTabRef.current?.("workflow"),
208
+ };
209
+ const stableHost = useMemo(
210
+ () => createStableHostProxy(hostRef, railActionHostRef),
211
+ [],
212
+ );
188
213
 
189
214
  const controller = useContextMenuController({
190
215
  mode,
@@ -203,15 +228,6 @@ export function TwWorkspaceChromeHost(
203
228
  requestWithKindsRef.current = controller.requestWithKinds;
204
229
  const dismissRef = useRef(controller.dismiss);
205
230
  dismissRef.current = controller.dismiss;
206
- // Phase E.0 — ref-sync the rail-tab callback so the imperative handle
207
- // identity stays stable while still dispatching to the freshest host
208
- // closure on every call. `undefined` is kept in the ref when the host
209
- // omits the prop so `openRailTab` becomes a safe no-op.
210
- const openRailTabRef = useRef<
211
- ((tab: EditorRailTab, itemId?: string) => void) | undefined
212
- >(onOpenRailTab);
213
- openRailTabRef.current = onOpenRailTab;
214
-
215
231
  useImperativeHandle(
216
232
  controllerRef,
217
233
  () => ({
@@ -18,15 +18,24 @@ import type { ScopeRailSegment } from "../../api/public-types.ts";
18
18
  import type {
19
19
  EditorRole,
20
20
  EditorStoryTarget,
21
+ GeometryFacet,
21
22
  ScopeIssueAction,
22
23
  TableStructureContextSnapshot,
23
24
  WordReviewEditorLayoutFacet,
25
+ WorkflowFacet,
24
26
  WorkflowScopeMode,
25
27
  } from "../../api/public-types";
28
+ import type {
29
+ UiObjectDragSession,
30
+ UiObjectDragStartInput,
31
+ } from "../../api/v3/ui/_types.ts";
26
32
  import { TwScopeCardLayer } from "./tw-scope-card-layer";
27
33
  import { TwPageStackChromeLayer, type PmPortalView } from "../page-stack/tw-page-stack-chrome-layer";
28
34
  import { TwTableGripLayer } from "../chrome/tw-table-grip-layer";
29
35
  import { TwObjectSelectionOverlay } from "./tw-object-selection-overlay";
36
+ import { TwPresenceOverlayLane } from "./tw-presence-overlay-lane";
37
+ import { TwReviewOverlayLaneMarkers } from "./tw-review-overlay-lane-markers";
38
+ import { TwSubstrateOverlayLanes } from "./tw-substrate-overlay-lanes";
30
39
 
31
40
  export interface TwChromeOverlayProps {
32
41
  /** Layout facet the overlay layers read from (layout-semantic data). */
@@ -36,13 +45,13 @@ export interface TwChromeOverlayProps {
36
45
  * `facet.getRenderFrame` / `.getRenderZoom` in refactor/05
37
46
  * cross-lane-coord §8.4.
38
47
  */
39
- geometryFacet: import("../../runtime/geometry/index.ts").GeometryFacet;
48
+ geometryFacet: GeometryFacet;
40
49
  /**
41
50
  * Workflow facet — no-provider fallback for scope rail/card reads.
42
51
  * Mounted editor paths prefer `api.ui.scope.*`; pass `null` when no
43
52
  * runtime is attached (e.g., during initial mount).
44
53
  */
45
- workflowFacet: import("../../runtime/workflow/rail/types.ts").WorkflowFacet | null;
54
+ workflowFacet: WorkflowFacet | null;
46
55
  /** Optional coordinate space override. Defaults to the overlay origin. */
47
56
  space?: OverlayCoordinateSpace;
48
57
  /** Active scope id (for emphasis + rail tab sync). */
@@ -111,6 +120,8 @@ export interface TwChromeOverlayProps {
111
120
  grabbedObjectToOffset?: number | null;
112
121
  /** Called when the user clicks outside the selection box to deselect. */
113
122
  onDeselectObject?: () => void;
123
+ /** Object handle drag lifecycle; routed through the mounted UI API seam. */
124
+ onBeginObjectDrag?: (input: UiObjectDragStartInput) => UiObjectDragSession;
114
125
 
115
126
  // Table grip props (P6) -----------------------------------------------
116
127
  /** Active table context — when present, column/row resize grips are shown. */
@@ -200,6 +211,15 @@ export interface TwChromeOverlayProps {
200
211
  > | null;
201
212
  }
202
213
 
214
+ export function resolveTableGripContext(
215
+ tableContext: TableStructureContextSnapshot | null | undefined,
216
+ ): TableStructureContextSnapshot | null {
217
+ if (!tableContext || tableContext.selectionKind !== "cell") {
218
+ return null;
219
+ }
220
+ return tableContext;
221
+ }
222
+
203
223
  /**
204
224
  * Placement contract:
205
225
  * - The overlay is an absolutely positioned `div` that fills its parent.
@@ -231,6 +251,7 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
231
251
  grabbedObjectFromOffset,
232
252
  grabbedObjectToOffset,
233
253
  onDeselectObject,
254
+ onBeginObjectDrag,
234
255
  tableContext,
235
256
  onSetColumnWidth,
236
257
  onSetRowHeight,
@@ -245,6 +266,8 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
245
266
  mediaPreviews,
246
267
  activeBandRibbonProps,
247
268
  }) => {
269
+ const tableGripContext = resolveTableGripContext(tableContext);
270
+
248
271
  return (
249
272
  <div
250
273
  className="wre-chrome-overlay pointer-events-none absolute inset-0 z-30"
@@ -284,7 +307,7 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
284
307
  <TwTableGripLayer
285
308
  facet={facet}
286
309
  geometryFacet={geometryFacet}
287
- tableContext={tableContext ?? null}
310
+ tableContext={tableGripContext}
288
311
  space={space}
289
312
  onSetColumnWidth={onSetColumnWidth}
290
313
  onSetRowHeight={onSetRowHeight}
@@ -296,7 +319,11 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
296
319
  geometryFacet={geometryFacet}
297
320
  space={space}
298
321
  onDeselect={onDeselectObject}
322
+ onBeginObjectDrag={onBeginObjectDrag}
299
323
  />
324
+ <TwSubstrateOverlayLanes space={space} />
325
+ <TwReviewOverlayLaneMarkers space={space} />
326
+ <TwPresenceOverlayLane />
300
327
  {children}
301
328
  </div>
302
329
  );
@@ -21,6 +21,11 @@
21
21
  import * as React from "react";
22
22
  import { useEffect, useRef } from "react";
23
23
  import type { GeometryFacet } from "../../api/public-types";
24
+ import type {
25
+ UiObjectDragHandle,
26
+ UiObjectDragSession,
27
+ UiObjectDragStartInput,
28
+ } from "../../api/v3/ui/_types.ts";
24
29
  import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
25
30
  import { projectRectToOverlay } from "./chrome-overlay-projector";
26
31
 
@@ -57,6 +62,8 @@ export interface TwObjectSelectionOverlayProps {
57
62
  space?: OverlayCoordinateSpace;
58
63
  /** Called when the user clicks outside the selection box. */
59
64
  onDeselect?: () => void;
65
+ /** Starts the mounted object-drag lifecycle for move/resize/rotate handles. */
66
+ onBeginObjectDrag?: (input: UiObjectDragStartInput) => UiObjectDragSession;
60
67
  }
61
68
 
62
69
  export function TwObjectSelectionOverlay({
@@ -66,8 +73,10 @@ export function TwObjectSelectionOverlay({
66
73
  geometryFacet,
67
74
  space,
68
75
  onDeselect,
76
+ onBeginObjectDrag,
69
77
  }: TwObjectSelectionOverlayProps) {
70
78
  const overlayRef = useRef<HTMLDivElement>(null);
79
+ const dragRef = useRef<{ session: UiObjectDragSession } | null>(null);
71
80
 
72
81
  // Click-outside to deselect.
73
82
  useEffect(() => {
@@ -81,6 +90,42 @@ export function TwObjectSelectionOverlay({
81
90
  return () => document.removeEventListener("pointerdown", handlePointerDown, { capture: true });
82
91
  }, [grabbedObjectId, onDeselect]);
83
92
 
93
+ useEffect(() => {
94
+ if (!onBeginObjectDrag) return;
95
+ function handlePointerMove(e: PointerEvent) {
96
+ const active = dragRef.current;
97
+ if (!active) return;
98
+ active.session.update({
99
+ clientX: e.clientX,
100
+ clientY: e.clientY,
101
+ });
102
+ }
103
+ function handlePointerUp(e: PointerEvent) {
104
+ const active = dragRef.current;
105
+ if (!active) return;
106
+ dragRef.current = null;
107
+ active.session.commit({
108
+ clientX: e.clientX,
109
+ clientY: e.clientY,
110
+ });
111
+ }
112
+ function handleKeyDown(e: KeyboardEvent) {
113
+ if (e.key !== "Escape") return;
114
+ const active = dragRef.current;
115
+ if (!active) return;
116
+ dragRef.current = null;
117
+ active.session.cancel();
118
+ }
119
+ document.addEventListener("pointermove", handlePointerMove, { capture: true });
120
+ document.addEventListener("pointerup", handlePointerUp, { capture: true });
121
+ document.addEventListener("keydown", handleKeyDown, { capture: true });
122
+ return () => {
123
+ document.removeEventListener("pointermove", handlePointerMove, { capture: true });
124
+ document.removeEventListener("pointerup", handlePointerUp, { capture: true });
125
+ document.removeEventListener("keydown", handleKeyDown, { capture: true });
126
+ };
127
+ }, [onBeginObjectDrag]);
128
+
84
129
  if (!grabbedObjectId || grabbedObjectFromOffset == null) return null;
85
130
 
86
131
  const frame = geometryFacet.getRenderFrame();
@@ -92,6 +137,14 @@ export function TwObjectSelectionOverlay({
92
137
  if (!rawRect) return null;
93
138
 
94
139
  const rect = projectRectToOverlay(rawRect, space);
140
+ const coordinateSpace = space ?? { originLeftPx: 0, originTopPx: 0 };
141
+ const dragInitialRect = {
142
+ leftPx: rawRect.leftPx - coordinateSpace.originLeftPx,
143
+ topPx: rawRect.topPx - coordinateSpace.originTopPx,
144
+ widthPx: rawRect.widthPx,
145
+ heightPx: rawRect.heightPx,
146
+ space: "overlay" as const,
147
+ };
95
148
 
96
149
  const boxStyle: React.CSSProperties = {
97
150
  position: "absolute",
@@ -104,6 +157,20 @@ export function TwObjectSelectionOverlay({
104
157
  pointerEvents: "auto",
105
158
  };
106
159
 
160
+ const beginDrag = (handle: UiObjectDragHandle, event: React.PointerEvent<HTMLElement>) => {
161
+ if (!onBeginObjectDrag) return;
162
+ event.preventDefault();
163
+ event.stopPropagation();
164
+ const session = onBeginObjectDrag({
165
+ objectId: grabbedObjectId,
166
+ handle,
167
+ clientX: event.clientX,
168
+ clientY: event.clientY,
169
+ initialRect: dragInitialRect,
170
+ });
171
+ dragRef.current = { session };
172
+ };
173
+
107
174
  return (
108
175
  <div
109
176
  ref={overlayRef}
@@ -112,16 +179,37 @@ export function TwObjectSelectionOverlay({
112
179
  data-object-selection=""
113
180
  data-object-id={grabbedObjectId}
114
181
  aria-label="Selected object"
182
+ onPointerDown={(event) => {
183
+ if (event.target === event.currentTarget) {
184
+ beginDrag("move", event);
185
+ }
186
+ }}
115
187
  >
116
188
  {HANDLE_POSITIONS.map((pos) => (
117
- <ObjectHandle key={pos} position={pos} />
189
+ <ObjectHandle
190
+ key={pos}
191
+ position={pos}
192
+ interactive={Boolean(onBeginObjectDrag)}
193
+ onPointerDown={(event) => beginDrag(pos, event)}
194
+ />
118
195
  ))}
119
- <RotateGrip />
196
+ <RotateGrip
197
+ interactive={Boolean(onBeginObjectDrag)}
198
+ onPointerDown={(event) => beginDrag("rotate", event)}
199
+ />
120
200
  </div>
121
201
  );
122
202
  }
123
203
 
124
- function ObjectHandle({ position }: { position: HandlePosition }) {
204
+ function ObjectHandle({
205
+ position,
206
+ interactive,
207
+ onPointerDown,
208
+ }: {
209
+ position: HandlePosition;
210
+ interactive: boolean;
211
+ onPointerDown?: React.PointerEventHandler<HTMLDivElement>;
212
+ }) {
125
213
  const HANDLE_PX = 8;
126
214
  const half = HANDLE_PX / 2;
127
215
  const pos = position;
@@ -135,17 +223,28 @@ function ObjectHandle({ position }: { position: HandlePosition }) {
135
223
  borderRadius: 1,
136
224
  boxSizing: "border-box",
137
225
  cursor: CURSOR_MAP[pos],
138
- // Visual only in v1 — pointer events disabled so clicks fall through to
139
- // the click-outside listener which deselectObjects.
140
- pointerEvents: "none",
226
+ pointerEvents: interactive ? "auto" : "none",
141
227
  ...(pos.includes("w") ? { left: -half } : pos.includes("e") ? { right: -half } : { left: "50%", transform: "translateX(-50%)" }),
142
228
  ...(pos.includes("n") ? { top: -half } : pos.includes("s") ? { bottom: -half } : { top: "50%", transform: `${pos === "w" || pos === "e" ? "translateY(-50%)" : "translateX(-50%) translateY(-50%)"}` }),
143
229
  };
144
230
 
145
- return <div style={style} data-handle={pos} aria-hidden="true" />;
231
+ return (
232
+ <div
233
+ style={style}
234
+ data-handle={pos}
235
+ aria-hidden="true"
236
+ onPointerDown={onPointerDown}
237
+ />
238
+ );
146
239
  }
147
240
 
148
- function RotateGrip() {
241
+ function RotateGrip({
242
+ interactive,
243
+ onPointerDown,
244
+ }: {
245
+ interactive: boolean;
246
+ onPointerDown?: React.PointerEventHandler<HTMLDivElement>;
247
+ }) {
149
248
  const style: React.CSSProperties = {
150
249
  position: "absolute",
151
250
  width: 10,
@@ -157,8 +256,15 @@ function RotateGrip() {
157
256
  left: "50%",
158
257
  transform: "translateX(-50%)",
159
258
  cursor: "grab",
160
- pointerEvents: "none",
259
+ pointerEvents: interactive ? "auto" : "none",
161
260
  boxSizing: "border-box",
162
261
  };
163
- return <div style={style} data-handle="rotate" aria-hidden="true" />;
262
+ return (
263
+ <div
264
+ style={style}
265
+ data-handle="rotate"
266
+ aria-hidden="true"
267
+ onPointerDown={onPointerDown}
268
+ />
269
+ );
164
270
  }