@beyondwork/docx-react-component 1.0.38 → 1.0.39

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 (76) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +183 -6
  3. package/src/core/commands/table-structure-commands.ts +31 -2
  4. package/src/core/commands/text-commands.ts +122 -2
  5. package/src/io/docx-session.ts +1 -0
  6. package/src/io/export/serialize-numbering.ts +42 -8
  7. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  8. package/src/io/export/serialize-run-formatting.ts +90 -0
  9. package/src/io/export/serialize-styles.ts +212 -0
  10. package/src/io/ooxml/parse-fields.ts +10 -3
  11. package/src/io/ooxml/parse-numbering.ts +41 -1
  12. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  13. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  14. package/src/io/ooxml/parse-styles.ts +31 -0
  15. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  16. package/src/io/ooxml/xml-element.ts +19 -0
  17. package/src/model/canonical-document.ts +83 -3
  18. package/src/runtime/collab/event-types.ts +165 -0
  19. package/src/runtime/collab/index.ts +22 -0
  20. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  22. package/src/runtime/document-runtime.ts +134 -18
  23. package/src/runtime/layout/index.ts +2 -0
  24. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  25. package/src/runtime/layout/layout-engine-instance.ts +69 -2
  26. package/src/runtime/layout/layout-invalidation.ts +14 -5
  27. package/src/runtime/layout/page-graph.ts +36 -0
  28. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  29. package/src/runtime/layout/paginated-layout-engine.ts +342 -28
  30. package/src/runtime/layout/project-block-fragments.ts +154 -20
  31. package/src/runtime/layout/public-facet.ts +40 -1
  32. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  33. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  34. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  35. package/src/runtime/layout/table-render-plan.ts +21 -1
  36. package/src/runtime/numbering-prefix.ts +5 -0
  37. package/src/runtime/paragraph-style-resolver.ts +194 -0
  38. package/src/runtime/render/render-kernel.ts +5 -1
  39. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  40. package/src/runtime/surface-projection.ts +129 -9
  41. package/src/runtime/table-schema.ts +11 -0
  42. package/src/ui/WordReviewEditor.tsx +285 -5
  43. package/src/ui/editor-command-bag.ts +4 -0
  44. package/src/ui/editor-runtime-boundary.ts +16 -0
  45. package/src/ui/editor-shell-view.tsx +4 -0
  46. package/src/ui/editor-surface-controller.tsx +9 -1
  47. package/src/ui/headless/chrome-registry.ts +34 -5
  48. package/src/ui/headless/scoped-chrome-policy.ts +29 -0
  49. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  50. package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
  51. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
  52. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
  53. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  54. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  55. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
  56. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  57. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
  58. package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
  59. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
  60. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  61. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
  62. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  63. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  64. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  65. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  66. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  67. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
  68. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  69. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  70. package/src/ui-tailwind/index.ts +1 -5
  71. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
  72. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
  73. package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
  74. package/src/runtime/collab-review-sync.ts +0 -254
  75. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
  76. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -0,0 +1,284 @@
1
+ /**
2
+ * TwTableGripLayer — column-resize and row-resize grips for the active table.
3
+ *
4
+ * Grips are positioned via the render-kernel anchor index (P4):
5
+ * - Column grips: `anchorIndex.byTableColumnEdge(blockId, colIndex)`
6
+ * - Row grips: `anchorIndex.byTableRowEdge(blockId, rowIndex)`
7
+ *
8
+ * This is a pure overlay consumer: no DOM measurement, no selection mutation,
9
+ * no PM transactions. Drag deltas are converted from px → twips using the
10
+ * kernel's `pxPerTwip` ratio and dispatched through `onSetColumnWidth` /
11
+ * `onSetRowHeight` callbacks (which route to `ref.tables.apply(op)`).
12
+ *
13
+ * Drag state lives in `useRef` so moves don't trigger re-renders mid-drag.
14
+ */
15
+
16
+ import React, { useCallback, useEffect, useRef } from "react";
17
+
18
+ import type {
19
+ TableStructureContextSnapshot,
20
+ WordReviewEditorLayoutFacet,
21
+ } from "../../api/public-types";
22
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
23
+ import type { OverlayCoordinateSpace } from "../chrome-overlay/chrome-overlay-projector";
24
+ import { projectRectToOverlay } from "../chrome-overlay/chrome-overlay-projector";
25
+ import { DEFAULT_PX_PER_TWIP } from "../../runtime/render/render-frame-types";
26
+
27
+ const GRIP_PX = 6;
28
+ const MIN_COLUMN_TWIPS = 720;
29
+ const MIN_ROW_TWIPS = 120;
30
+
31
+ export interface TwTableGripLayerProps {
32
+ facet: WordReviewEditorLayoutFacet;
33
+ tableContext: TableStructureContextSnapshot | null;
34
+ space?: OverlayCoordinateSpace;
35
+ disabled?: boolean;
36
+ onSetColumnWidth?: (columnIndex: number, twips: number) => void;
37
+ onSetRowHeight?: (
38
+ rowIndex: number,
39
+ twips: number,
40
+ rule: "auto" | "atLeast" | "exact",
41
+ ) => void;
42
+ }
43
+
44
+ export function TwTableGripLayer({
45
+ facet,
46
+ tableContext,
47
+ space,
48
+ disabled,
49
+ onSetColumnWidth,
50
+ onSetRowHeight,
51
+ }: TwTableGripLayerProps) {
52
+ if (!tableContext) return null;
53
+
54
+ const frame =
55
+ typeof facet.getRenderFrame === "function"
56
+ ? (facet.getRenderFrame() ?? null)
57
+ : null;
58
+ if (!frame) return null;
59
+
60
+ const blockId = `table-${tableContext.tableBlockIndex}`;
61
+ const pageIndex = facet.getFirstPageIndexForBlock(blockId) ?? 0;
62
+ const plan = facet.getTableRenderPlan(blockId, pageIndex);
63
+ if (!plan) return null;
64
+
65
+ const pxPerTwip =
66
+ typeof facet.getRenderZoom === "function"
67
+ ? (facet.getRenderZoom()?.pxPerTwip ?? DEFAULT_PX_PER_TWIP)
68
+ : DEFAULT_PX_PER_TWIP;
69
+
70
+ return (
71
+ <>
72
+ {plan.columnResizeHandles.map((handle) => {
73
+ const rect = frame.anchorIndex.byTableColumnEdge(
74
+ blockId,
75
+ handle.columnIndex,
76
+ );
77
+ if (!rect) return null;
78
+ const pos = projectRectToOverlay(rect, space);
79
+ return (
80
+ <ColResizeGrip
81
+ key={`col-${blockId}-${handle.columnIndex}`}
82
+ pos={pos}
83
+ colIndex={handle.columnIndex}
84
+ originalTwips={plan.columnsTwips[handle.columnIndex] ?? 720}
85
+ pxPerTwip={pxPerTwip}
86
+ disabled={!!disabled || !onSetColumnWidth}
87
+ onCommit={onSetColumnWidth}
88
+ />
89
+ );
90
+ })}
91
+ {Array.from({ length: tableContext.rowCount }, (_, rowIndex) => {
92
+ const rect = frame.anchorIndex.byTableRowEdge(blockId, rowIndex);
93
+ if (!rect) return null;
94
+ const pos = projectRectToOverlay(rect, space);
95
+ return (
96
+ <RowResizeGrip
97
+ key={`row-${blockId}-${rowIndex}`}
98
+ pos={pos}
99
+ rowIndex={rowIndex}
100
+ pxPerTwip={pxPerTwip}
101
+ disabled={!!disabled || !onSetRowHeight}
102
+ onCommit={onSetRowHeight}
103
+ />
104
+ );
105
+ })}
106
+ </>
107
+ );
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Column resize grip
112
+ // ---------------------------------------------------------------------------
113
+
114
+ interface ColResizeGripProps {
115
+ pos: { left: string; top: string; width: string; height: string };
116
+ colIndex: number;
117
+ originalTwips: number;
118
+ pxPerTwip: number;
119
+ disabled: boolean;
120
+ onCommit?: (columnIndex: number, twips: number) => void;
121
+ }
122
+
123
+ function ColResizeGrip({
124
+ pos,
125
+ colIndex,
126
+ originalTwips,
127
+ pxPerTwip,
128
+ disabled,
129
+ onCommit,
130
+ }: ColResizeGripProps) {
131
+ const dragRef = useRef<{
132
+ startX: number;
133
+ originalTwips: number;
134
+ } | null>(null);
135
+
136
+ const handleMouseDown = useCallback(
137
+ (e: React.MouseEvent<HTMLElement>) => {
138
+ if (disabled) return;
139
+ preserveEditorSelectionMouseDown(e);
140
+ dragRef.current = { startX: e.clientX, originalTwips };
141
+ },
142
+ [disabled, originalTwips],
143
+ );
144
+
145
+ useEffect(() => {
146
+ const handleMove = (e: MouseEvent) => {
147
+ if (!dragRef.current) return;
148
+ e.preventDefault();
149
+ };
150
+ const handleUp = (e: MouseEvent) => {
151
+ if (!dragRef.current) return;
152
+ const deltaX = e.clientX - dragRef.current.startX;
153
+ const deltaTwips = deltaX / pxPerTwip;
154
+ const newTwips = Math.max(
155
+ MIN_COLUMN_TWIPS,
156
+ Math.round(dragRef.current.originalTwips + deltaTwips),
157
+ );
158
+ dragRef.current = null;
159
+ onCommit?.(colIndex, newTwips);
160
+ };
161
+ window.addEventListener("mousemove", handleMove);
162
+ window.addEventListener("mouseup", handleUp);
163
+ return () => {
164
+ window.removeEventListener("mousemove", handleMove);
165
+ window.removeEventListener("mouseup", handleUp);
166
+ };
167
+ }, [colIndex, pxPerTwip, onCommit]);
168
+
169
+ return (
170
+ <div
171
+ role="separator"
172
+ aria-orientation="vertical"
173
+ aria-label={`Resize column ${colIndex + 1}`}
174
+ data-testid={`col-resize-grip-${colIndex}`}
175
+ className={[
176
+ "pointer-events-auto absolute",
177
+ disabled
178
+ ? "cursor-default opacity-0"
179
+ : "cursor-col-resize opacity-0 hover:opacity-100",
180
+ "transition-opacity",
181
+ "bg-accent",
182
+ ].join(" ")}
183
+ style={{
184
+ left: `calc(${pos.left} - ${GRIP_PX / 2}px)`,
185
+ top: pos.top,
186
+ width: `${GRIP_PX}px`,
187
+ height: pos.height,
188
+ }}
189
+ onMouseDown={handleMouseDown}
190
+ />
191
+ );
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Row resize grip
196
+ // ---------------------------------------------------------------------------
197
+
198
+ interface RowResizeGripProps {
199
+ pos: { left: string; top: string; width: string; height: string };
200
+ rowIndex: number;
201
+ pxPerTwip: number;
202
+ disabled: boolean;
203
+ onCommit?: (
204
+ rowIndex: number,
205
+ twips: number,
206
+ rule: "auto" | "atLeast" | "exact",
207
+ ) => void;
208
+ }
209
+
210
+ function RowResizeGrip({
211
+ pos,
212
+ rowIndex,
213
+ pxPerTwip,
214
+ disabled,
215
+ onCommit,
216
+ }: RowResizeGripProps) {
217
+ const dragRef = useRef<{
218
+ startY: number;
219
+ startHeightPx: number;
220
+ } | null>(null);
221
+
222
+ const handleMouseDown = useCallback(
223
+ (e: React.MouseEvent<HTMLElement>) => {
224
+ if (disabled) return;
225
+ preserveEditorSelectionMouseDown(e);
226
+ // Start height is the current visible row height from the grip rect.
227
+ // We use the grip's own top position vs. previous row edge as a proxy.
228
+ dragRef.current = { startY: e.clientY, startHeightPx: 0 };
229
+ },
230
+ [disabled],
231
+ );
232
+
233
+ useEffect(() => {
234
+ const handleMove = (e: MouseEvent) => {
235
+ if (!dragRef.current) return;
236
+ e.preventDefault();
237
+ };
238
+ const handleUp = (e: MouseEvent) => {
239
+ if (!dragRef.current) return;
240
+ const deltaY = e.clientY - dragRef.current.startY;
241
+ if (Math.abs(deltaY) < 2) {
242
+ dragRef.current = null;
243
+ return;
244
+ }
245
+ const deltaTwips = deltaY / pxPerTwip;
246
+ // We don't know the current row height from the grip alone; use atLeast
247
+ // so that a positive drag expands and a negative drag collapses to auto.
248
+ const newTwips = Math.max(MIN_ROW_TWIPS, Math.round(deltaTwips));
249
+ const rule = newTwips > 0 ? "atLeast" : "auto";
250
+ dragRef.current = null;
251
+ onCommit?.(rowIndex, newTwips, rule as "atLeast");
252
+ };
253
+ window.addEventListener("mousemove", handleMove);
254
+ window.addEventListener("mouseup", handleUp);
255
+ return () => {
256
+ window.removeEventListener("mousemove", handleMove);
257
+ window.removeEventListener("mouseup", handleUp);
258
+ };
259
+ }, [rowIndex, pxPerTwip, onCommit]);
260
+
261
+ return (
262
+ <div
263
+ role="separator"
264
+ aria-orientation="horizontal"
265
+ aria-label={`Resize row ${rowIndex + 1}`}
266
+ data-testid={`row-resize-grip-${rowIndex}`}
267
+ className={[
268
+ "pointer-events-auto absolute",
269
+ disabled
270
+ ? "cursor-default opacity-0"
271
+ : "cursor-row-resize opacity-0 hover:opacity-100",
272
+ "transition-opacity",
273
+ "bg-accent",
274
+ ].join(" ")}
275
+ style={{
276
+ left: pos.left,
277
+ top: `calc(${pos.top} - ${GRIP_PX / 2}px)`,
278
+ width: pos.width,
279
+ height: `${GRIP_PX}px`,
280
+ }}
281
+ onMouseDown={handleMouseDown}
282
+ />
283
+ );
284
+ }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { RenderFrameRect } from "../../runtime/render/index.ts";
12
+ import { recordPerfSample } from "../editor-surface/perf-probe.ts";
12
13
 
13
14
  export interface OverlayCoordinateSpace {
14
15
  /** Top-left of the overlay in the document column's coordinate space. */
@@ -33,12 +34,15 @@ export function projectRectToOverlay(
33
34
  rect: RenderFrameRect,
34
35
  space: OverlayCoordinateSpace = { originLeftPx: 0, originTopPx: 0 },
35
36
  ): CSSRect {
36
- return {
37
+ const t0 = typeof performance !== "undefined" ? performance.now() : 0;
38
+ const result: CSSRect = {
37
39
  left: `${rect.leftPx - space.originLeftPx}px`,
38
40
  top: `${rect.topPx - space.originTopPx}px`,
39
41
  width: `${rect.widthPx}px`,
40
42
  height: `${rect.heightPx}px`,
41
43
  };
44
+ if (t0 > 0) recordPerfSample("chrome.overlay_reposition", performance.now() - t0);
45
+ return result;
42
46
  }
43
47
 
44
48
  /**
@@ -7,12 +7,6 @@
7
7
 
8
8
  export { TwChromeOverlay, type TwChromeOverlayProps } from "./tw-chrome-overlay";
9
9
  export { TwScopeRailLayer, type TwScopeRailLayerProps } from "./tw-scope-rail-layer";
10
- export {
11
- TwWorkspaceViewSwitcher,
12
- type TwWorkspaceViewSwitcherProps,
13
- type WorkspaceView,
14
- type WorkspaceViewAction,
15
- } from "./tw-workspace-view-switcher";
16
10
  export {
17
11
  inflateRect,
18
12
  projectRectToOverlay,
@@ -15,9 +15,12 @@
15
15
  import * as React from "react";
16
16
  import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
17
17
  import type { ScopeRailSegment } from "../../runtime/layout";
18
- import type { WordReviewEditorLayoutFacet } from "../../runtime/layout";
18
+ import type {
19
+ TableStructureContextSnapshot,
20
+ WordReviewEditorLayoutFacet,
21
+ } from "../../api/public-types";
19
22
  import { TwScopeRailLayer } from "./tw-scope-rail-layer";
20
- import { TwWorkspaceViewSwitcher, type WorkspaceView } from "./tw-workspace-view-switcher";
23
+ import { TwTableGripLayer } from "../chrome/tw-table-grip-layer";
21
24
 
22
25
  export interface TwChromeOverlayProps {
23
26
  /** Layout facet the overlay layers read from. */
@@ -28,16 +31,22 @@ export interface TwChromeOverlayProps {
28
31
  activeScopeId?: string | null;
29
32
  /** Click handler the rail layer forwards to consumers. */
30
33
  onScopeSegmentClick?: (segment: ScopeRailSegment) => void;
31
- /** Currently active workspace view preset (draft / layout / review / workflow). */
32
- activeWorkspaceView?: WorkspaceView;
33
- /** Handler that fires when the user picks a workspace view. */
34
- onWorkspaceViewChange?: (view: WorkspaceView) => void;
35
- /** Show the bottom workspace view-switcher dock. Default: true. */
36
- showWorkspaceDock?: boolean;
37
34
  /** Test id applied to the overlay root. */
38
35
  "data-testid"?: string;
39
36
  /** Optional extra children (e.g., future comment balloon layer). */
40
37
  children?: React.ReactNode;
38
+
39
+ // Table grip props (P6) -----------------------------------------------
40
+ /** Active table context — when present, column/row resize grips are shown. */
41
+ tableContext?: TableStructureContextSnapshot | null;
42
+ /** Fires when a column grip drag completes. */
43
+ onSetColumnWidth?: (columnIndex: number, twips: number) => void;
44
+ /** Fires when a row grip drag completes. */
45
+ onSetRowHeight?: (
46
+ rowIndex: number,
47
+ twips: number,
48
+ rule: "auto" | "atLeast" | "exact",
49
+ ) => void;
41
50
  }
42
51
 
43
52
  /**
@@ -54,11 +63,11 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
54
63
  space,
55
64
  activeScopeId,
56
65
  onScopeSegmentClick,
57
- activeWorkspaceView,
58
- onWorkspaceViewChange,
59
- showWorkspaceDock = true,
60
66
  "data-testid": testId,
61
67
  children,
68
+ tableContext,
69
+ onSetColumnWidth,
70
+ onSetRowHeight,
62
71
  }) => {
63
72
  return (
64
73
  <div
@@ -72,13 +81,14 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
72
81
  activeScopeId={activeScopeId}
73
82
  onSegmentClick={onScopeSegmentClick}
74
83
  />
84
+ <TwTableGripLayer
85
+ facet={facet}
86
+ tableContext={tableContext ?? null}
87
+ space={space}
88
+ onSetColumnWidth={onSetColumnWidth}
89
+ onSetRowHeight={onSetRowHeight}
90
+ />
75
91
  {children}
76
- {showWorkspaceDock ? (
77
- <TwWorkspaceViewSwitcher
78
- activeView={activeWorkspaceView ?? "review"}
79
- onViewChange={onWorkspaceViewChange}
80
- />
81
- ) : null}
82
92
  </div>
83
93
  );
84
94
  };
@@ -0,0 +1,15 @@
1
+ import type { DocumentPageSnapshot, SurfaceBlockSnapshot } from "../../api/public-types.ts";
2
+
3
+ /**
4
+ * Filter surface blocks to those that overlap a page's character-offset range.
5
+ * A block overlaps [startOffset, endOffset) when block.from < endOffset AND block.to > startOffset.
6
+ * Blocks that straddle a page boundary appear on both pages.
7
+ */
8
+ export function sliceBlocksForPage(
9
+ blocks: SurfaceBlockSnapshot[],
10
+ page: Pick<DocumentPageSnapshot, "startOffset" | "endOffset">,
11
+ ): SurfaceBlockSnapshot[] {
12
+ return blocks.filter(
13
+ (b) => b.from < page.endOffset && b.to > page.startOffset,
14
+ );
15
+ }
@@ -23,6 +23,8 @@ export interface CommandBridgeCallbacks extends SelectionSyncCallbacks {
23
23
  onInsertHardBreak: () => void;
24
24
  onInsertTab: () => void;
25
25
  onOutdentTab?: () => void;
26
+ onListIndent?: () => void;
27
+ onListOutdent?: () => void;
26
28
  onUndo: () => void;
27
29
  onRedo: () => void;
28
30
  onBlockedInput?: (command: "paste" | "drop", message: string) => void;
@@ -77,8 +79,14 @@ export function createSelectionSyncPlugin(
77
79
  });
78
80
  }
79
81
 
82
+ export interface CommandBridgePluginOptions {
83
+ /** P6: when true, omit prosemirror-tables columnResizing() — chrome overlay grips take over. */
84
+ useChromeColumnResizing?: boolean;
85
+ }
86
+
80
87
  export function createCommandBridgePlugins(
81
88
  callbacks: CommandBridgeCallbacks,
89
+ options?: CommandBridgePluginOptions,
82
90
  ): Plugin[] {
83
91
  let isComposing = false;
84
92
 
@@ -145,6 +153,18 @@ export function createCommandBridgePlugins(
145
153
  return false;
146
154
  }
147
155
 
156
+ // Alt+Shift+Right → list indent; Alt+Shift+Left → list outdent (Word keyboard behavior)
157
+ if (event.altKey && event.shiftKey) {
158
+ if (event.key === "ArrowRight") {
159
+ callbacks.onListIndent?.();
160
+ return true;
161
+ }
162
+ if (event.key === "ArrowLeft") {
163
+ callbacks.onListOutdent?.();
164
+ return true;
165
+ }
166
+ }
167
+
148
168
  const resolution = resolveSurfaceShortcut(
149
169
  {
150
170
  key: event.key,
@@ -195,7 +215,12 @@ export function createCommandBridgePlugins(
195
215
  // selection handles. Doc-changing table transactions (new rows, etc.) are
196
216
  // filtered by the runtime filter above; navigation-only steps pass through.
197
217
  const tablePlugin = tableEditing();
198
- const columnResizingPlugin = columnResizing();
199
-
200
- return [filterPlugin, selectionPlugin, inputPlugin, keydownPlugin, tablePlugin, columnResizingPlugin];
218
+ // P6: chrome overlay grips replace prosemirror-tables column resizing.
219
+ // useChromeColuumnResizing=true removes the PM plugin; set to false to fall
220
+ // back to the legacy prosemirror-tables drag handle for one release cycle.
221
+ const plugins: Plugin[] = [filterPlugin, selectionPlugin, inputPlugin, keydownPlugin, tablePlugin];
222
+ if (!options?.useChromeColumnResizing) {
223
+ plugins.push(columnResizing());
224
+ }
225
+ return plugins;
201
226
  }