@beyondwork/docx-react-component 1.0.36 → 1.0.38

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 (107) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +402 -1
  5. package/src/core/commands/index.ts +18 -1
  6. package/src/core/commands/section-layout-commands.ts +58 -0
  7. package/src/core/commands/table-grid.ts +431 -0
  8. package/src/core/commands/table-structure-commands.ts +815 -55
  9. package/src/core/selection/mapping.ts +6 -0
  10. package/src/io/docx-session.ts +24 -9
  11. package/src/io/export/build-app-properties-xml.ts +88 -0
  12. package/src/io/export/serialize-comments.ts +6 -1
  13. package/src/io/export/serialize-footnotes.ts +10 -9
  14. package/src/io/export/serialize-headers-footers.ts +11 -10
  15. package/src/io/export/serialize-main-document.ts +328 -50
  16. package/src/io/export/serialize-numbering.ts +114 -24
  17. package/src/io/export/serialize-tables.ts +87 -11
  18. package/src/io/export/table-properties-xml.ts +174 -20
  19. package/src/io/export/twip.ts +66 -0
  20. package/src/io/normalize/normalize-text.ts +20 -0
  21. package/src/io/ooxml/parse-footnotes.ts +62 -1
  22. package/src/io/ooxml/parse-headers-footers.ts +62 -1
  23. package/src/io/ooxml/parse-main-document.ts +158 -1
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/legal/bookmarks.ts +78 -0
  26. package/src/model/canonical-document.ts +45 -0
  27. package/src/review/store/scope-tag-diff.ts +130 -0
  28. package/src/runtime/document-layout.ts +4 -2
  29. package/src/runtime/document-navigation.ts +2 -306
  30. package/src/runtime/document-runtime.ts +287 -11
  31. package/src/runtime/layout/default-page-format.ts +96 -0
  32. package/src/runtime/layout/docx-font-loader.ts +143 -0
  33. package/src/runtime/layout/index.ts +233 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +59 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +628 -0
  36. package/src/runtime/layout/layout-invalidation.ts +257 -0
  37. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  38. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  39. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  40. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  43. package/src/runtime/layout/page-graph.ts +452 -0
  44. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  45. package/src/runtime/layout/page-story-resolver.ts +195 -0
  46. package/src/runtime/layout/paginated-layout-engine.ts +921 -0
  47. package/src/runtime/layout/project-block-fragments.ts +91 -0
  48. package/src/runtime/layout/public-facet.ts +1398 -0
  49. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  50. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  51. package/src/runtime/layout/table-render-plan.ts +229 -0
  52. package/src/runtime/render/block-fragment-projection.ts +35 -0
  53. package/src/runtime/render/decoration-resolver.ts +189 -0
  54. package/src/runtime/render/index.ts +57 -0
  55. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  56. package/src/runtime/render/render-frame-types.ts +317 -0
  57. package/src/runtime/render/render-kernel.ts +755 -0
  58. package/src/runtime/scope-tag-registry.ts +95 -0
  59. package/src/runtime/surface-projection.ts +1 -0
  60. package/src/runtime/text-ack-range.ts +49 -0
  61. package/src/runtime/view-state.ts +67 -0
  62. package/src/runtime/workflow-markup.ts +1 -5
  63. package/src/runtime/workflow-rail-segments.ts +280 -0
  64. package/src/ui/WordReviewEditor.tsx +99 -15
  65. package/src/ui/editor-runtime-boundary.ts +10 -1
  66. package/src/ui/editor-shell-view.tsx +6 -0
  67. package/src/ui/editor-surface-controller.tsx +3 -0
  68. package/src/ui/headless/chrome-registry.ts +501 -0
  69. package/src/ui/headless/scoped-chrome-policy.ts +183 -0
  70. package/src/ui/headless/selection-tool-context.ts +2 -0
  71. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  75. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  76. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  77. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  78. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  79. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  80. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  81. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  82. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  86. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
  87. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
  88. package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
  90. package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
  91. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  92. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  93. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  94. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
  95. package/src/ui-tailwind/index.ts +33 -0
  96. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  97. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  98. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  99. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  100. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  101. package/src/ui-tailwind/theme/editor-theme.css +505 -144
  102. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  103. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  104. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  105. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  106. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
  107. package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
@@ -32,161 +32,297 @@ const CELL_COLORS = [
32
32
  "#fce7f3",
33
33
  ] as const;
34
34
 
35
+ /**
36
+ * Table tier (R2.4, spec §6.4 / plan table tier matrix).
37
+ *
38
+ * - `caret-in-cell` (T2) — single-cell selection. Minimal inline set:
39
+ * row +/−, column +/−. Anything structural (merge/split/fill/delete
40
+ * table) lives behind "More" to keep the panel ~180px wide.
41
+ * - `multi-cell` (T3) — >1 cell but not a full row/column/table. Adds
42
+ * merge/split/fill palette.
43
+ * - `row-selected` (T4a) — selection spans exactly one full row
44
+ * (`selectedCellCount === columnCount` + currentCell hint). Row delete
45
+ * + insert + fill palette.
46
+ * - `column-selected` (T4b) — selection spans exactly one full column
47
+ * (`selectedCellCount === rowCount`). Column delete + insert.
48
+ * - `whole-table` (T5) — selection covers every cell OR the user picked
49
+ * the table-select handle (future). Style + delete + table-level
50
+ * formatting.
51
+ *
52
+ * The tier resolver is a pure function of the capability snapshot so
53
+ * tests can drive it from seeded snapshots without a DOM.
54
+ */
55
+ export type TableTier =
56
+ | "caret-in-cell"
57
+ | "multi-cell"
58
+ | "row-selected"
59
+ | "column-selected"
60
+ | "whole-table";
61
+
62
+ export function resolveTableTier(
63
+ ctx: TableStructureContextSnapshot,
64
+ ): TableTier {
65
+ const totalCells = ctx.rowCount * ctx.columnCount;
66
+ if (ctx.selectedCellCount >= totalCells && totalCells > 0) {
67
+ return "whole-table";
68
+ }
69
+ if (
70
+ ctx.selectedCellCount > 1 &&
71
+ ctx.selectedCellCount === ctx.columnCount &&
72
+ ctx.columnCount > 0
73
+ ) {
74
+ return "row-selected";
75
+ }
76
+ if (
77
+ ctx.selectedCellCount > 1 &&
78
+ ctx.selectedCellCount === ctx.rowCount &&
79
+ ctx.rowCount > 0
80
+ ) {
81
+ return "column-selected";
82
+ }
83
+ if (ctx.selectedCellCount > 1) {
84
+ return "multi-cell";
85
+ }
86
+ return "caret-in-cell";
87
+ }
88
+
35
89
  export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
36
90
  const tableContext = props.tableContext;
37
- const tableSizeLabel = tableContext ? `${tableContext.rowCount} x ${tableContext.columnCount}` : null;
38
- const selectionLabel = tableContext
39
- ? tableContext.selectedCellCount > 1
40
- ? `${tableContext.selectedCellCount} cells`
41
- : `R${tableContext.currentCell.rowIndex + 1} C${tableContext.currentCell.columnIndex + 1}`
91
+ const tier = tableContext ? resolveTableTier(tableContext) : "caret-in-cell";
92
+ const tableSizeLabel = tableContext
93
+ ? `${tableContext.rowCount} x ${tableContext.columnCount}`
42
94
  : null;
95
+ const selectionLabel = tableContext ? formatSelectionLabel(tableContext, tier) : null;
96
+
97
+ // Tier-specific width caps. Progressive: T2 ≤ 18rem, T3 ≤ 22rem, T4
98
+ // ≤ 24rem, T5 ≤ 28rem. Down from the old flat 30rem always.
99
+ const widthCap = tierWidthCap(tier);
43
100
 
44
101
  return (
45
102
  <div
46
103
  data-testid="table-context-toolbar"
47
- className="flex max-w-[min(30rem,calc(100vw-1.5rem))] flex-wrap items-start gap-1.5 rounded-lg border border-border bg-canvas px-2.5 py-1.5 shadow-sm"
104
+ data-tier={tier}
105
+ className={`flex ${widthCap} flex-wrap items-start gap-1.5 rounded-lg border border-border bg-canvas px-2.5 py-1.5 shadow-sm`}
48
106
  >
49
107
  <span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
50
- Table
108
+ {tierLabel(tier)}
51
109
  </span>
52
110
  {tableSizeLabel ? <ToolbarBadge>{tableSizeLabel}</ToolbarBadge> : null}
53
111
  {selectionLabel ? <ToolbarBadge>{selectionLabel}</ToolbarBadge> : null}
54
- {tableContext?.currentCell.isHeader ? <ToolbarBadge tone="accent">Header row</ToolbarBadge> : null}
55
-
56
- <ToolbarSection label="Style">
57
- <select
58
- aria-label="Table style"
59
- className="h-7 min-w-[9rem] rounded-md border border-border bg-canvas px-2 text-[11px] text-primary disabled:opacity-40"
60
- disabled={
61
- props.disabled ||
62
- props.tableStyles.length === 0 ||
63
- !props.onSetTableStyle ||
64
- !tableContext?.operations.setTableStyle.enabled
65
- }
66
- onMouseDown={preserveEditorSelectionMouseDown}
67
- onChange={(event) => props.onSetTableStyle?.(event.target.value)}
68
- value={tableContext?.currentStyleId ?? ""}
69
- title={tableContext?.operations.setTableStyle.reason}
70
- >
71
- <option value="" disabled>Table style</option>
72
- {props.tableStyles.map((style) => (
73
- <option key={style.styleId} value={style.styleId}>
74
- {style.displayName}
112
+ {tableContext?.currentCell.isHeader ? (
113
+ <ToolbarBadge tone="accent">Header row</ToolbarBadge>
114
+ ) : null}
115
+
116
+ {/* T5 whole-table: style selector (primary), delete table */}
117
+ {tier === "whole-table" ? (
118
+ <ToolbarSection label="Style">
119
+ <select
120
+ aria-label="Table style"
121
+ className="h-7 min-w-[9rem] rounded-md border border-border bg-canvas px-2 text-[11px] text-primary disabled:opacity-40"
122
+ disabled={
123
+ props.disabled ||
124
+ props.tableStyles.length === 0 ||
125
+ !props.onSetTableStyle ||
126
+ !tableContext?.operations.setTableStyle.enabled
127
+ }
128
+ onMouseDown={preserveEditorSelectionMouseDown}
129
+ onChange={(event) => props.onSetTableStyle?.(event.target.value)}
130
+ value={tableContext?.currentStyleId ?? ""}
131
+ title={tableContext?.operations.setTableStyle.reason}
132
+ >
133
+ <option value="" disabled>
134
+ Table style
75
135
  </option>
76
- ))}
77
- </select>
78
- </ToolbarSection>
79
-
80
- <ToolbarSection label="Rows">
81
- <ToolbarButton
82
- ariaLabel="Add row above"
83
- capability={tableContext?.operations.addRowBefore}
84
- disabled={props.disabled}
85
- onClick={props.onAddRowBefore}
86
- >
87
- Above
88
- </ToolbarButton>
89
- <ToolbarButton
90
- ariaLabel="Add row below"
91
- capability={tableContext?.operations.addRowAfter}
92
- disabled={props.disabled}
93
- onClick={props.onAddRowAfter}
94
- >
95
- Below
96
- </ToolbarButton>
97
- <ToolbarButton
98
- ariaLabel="Delete row"
99
- capability={tableContext?.operations.deleteRow}
100
- disabled={props.disabled}
101
- onClick={props.onDeleteRow}
102
- >
103
- Delete row
104
- </ToolbarButton>
105
- </ToolbarSection>
106
-
107
- <ToolbarSection label="Columns">
108
- <ToolbarButton
109
- ariaLabel="Add column left"
110
- capability={tableContext?.operations.addColumnBefore}
111
- disabled={props.disabled}
112
- onClick={props.onAddColumnBefore}
113
- >
114
- Left
115
- </ToolbarButton>
116
- <ToolbarButton
117
- ariaLabel="Add column right"
118
- capability={tableContext?.operations.addColumnAfter}
119
- disabled={props.disabled}
120
- onClick={props.onAddColumnAfter}
121
- >
122
- Right
123
- </ToolbarButton>
124
- <ToolbarButton
125
- ariaLabel="Delete column"
126
- capability={tableContext?.operations.deleteColumn}
127
- disabled={props.disabled}
128
- onClick={props.onDeleteColumn}
129
- >
130
- Delete column
131
- </ToolbarButton>
132
- </ToolbarSection>
133
-
134
- <ToolbarSection label="Cells">
135
- <ToolbarButton
136
- ariaLabel="Merge cells"
137
- capability={tableContext?.operations.mergeCells}
138
- disabled={props.disabled}
139
- onClick={props.onMergeCells}
140
- >
141
- Merge
142
- </ToolbarButton>
143
- <ToolbarButton
144
- ariaLabel="Split cell"
145
- capability={tableContext?.operations.splitCell}
146
- disabled={props.disabled}
147
- onClick={props.onSplitCell}
148
- >
149
- Split
150
- </ToolbarButton>
151
- </ToolbarSection>
152
-
153
- <ToolbarSection label="Fill">
154
- <div className="flex items-center gap-1">
155
- {CELL_COLORS.map((color) => (
156
- <button
157
- key={color}
158
- type="button"
159
- aria-label={`Set cell fill ${color}`}
160
- disabled={
161
- props.disabled ||
162
- !props.onSetCellBackground ||
163
- !tableContext?.operations.setCellBackground.enabled
164
- }
165
- onMouseDown={preserveEditorSelectionMouseDown}
166
- onClick={() => props.onSetCellBackground?.(color)}
167
- className="h-5 w-5 rounded border border-border disabled:opacity-40"
168
- style={{ backgroundColor: color }}
169
- title={tableContext?.operations.setCellBackground.reason}
170
- />
171
- ))}
172
- </div>
173
- </ToolbarSection>
174
-
175
- <ToolbarSection label="Table">
176
- <ToolbarButton
177
- ariaLabel="Delete table"
178
- capability={tableContext?.operations.deleteTable}
179
- danger
180
- disabled={props.disabled}
181
- onClick={props.onDeleteTable}
182
- >
183
- Delete table
184
- </ToolbarButton>
185
- </ToolbarSection>
136
+ {props.tableStyles.map((style) => (
137
+ <option key={style.styleId} value={style.styleId}>
138
+ {style.displayName}
139
+ </option>
140
+ ))}
141
+ </select>
142
+ </ToolbarSection>
143
+ ) : null}
144
+
145
+ {/* T2 / T4a row-selected: row ops */}
146
+ {(tier === "caret-in-cell" || tier === "row-selected") ? (
147
+ <ToolbarSection label="Rows">
148
+ <ToolbarButton
149
+ ariaLabel="Add row above"
150
+ capability={tableContext?.operations.addRowBefore}
151
+ disabled={props.disabled}
152
+ onClick={props.onAddRowBefore}
153
+ >
154
+ Above
155
+ </ToolbarButton>
156
+ <ToolbarButton
157
+ ariaLabel="Add row below"
158
+ capability={tableContext?.operations.addRowAfter}
159
+ disabled={props.disabled}
160
+ onClick={props.onAddRowAfter}
161
+ >
162
+ Below
163
+ </ToolbarButton>
164
+ {tier === "row-selected" ? (
165
+ <ToolbarButton
166
+ ariaLabel="Delete row"
167
+ capability={tableContext?.operations.deleteRow}
168
+ disabled={props.disabled}
169
+ onClick={props.onDeleteRow}
170
+ danger
171
+ >
172
+ Delete row
173
+ </ToolbarButton>
174
+ ) : null}
175
+ </ToolbarSection>
176
+ ) : null}
177
+
178
+ {/* T2 / T4b column-selected: column ops */}
179
+ {(tier === "caret-in-cell" || tier === "column-selected") ? (
180
+ <ToolbarSection label="Columns">
181
+ <ToolbarButton
182
+ ariaLabel="Add column left"
183
+ capability={tableContext?.operations.addColumnBefore}
184
+ disabled={props.disabled}
185
+ onClick={props.onAddColumnBefore}
186
+ >
187
+ Left
188
+ </ToolbarButton>
189
+ <ToolbarButton
190
+ ariaLabel="Add column right"
191
+ capability={tableContext?.operations.addColumnAfter}
192
+ disabled={props.disabled}
193
+ onClick={props.onAddColumnAfter}
194
+ >
195
+ Right
196
+ </ToolbarButton>
197
+ {tier === "column-selected" ? (
198
+ <ToolbarButton
199
+ ariaLabel="Delete column"
200
+ capability={tableContext?.operations.deleteColumn}
201
+ disabled={props.disabled}
202
+ onClick={props.onDeleteColumn}
203
+ danger
204
+ >
205
+ Delete column
206
+ </ToolbarButton>
207
+ ) : null}
208
+ </ToolbarSection>
209
+ ) : null}
210
+
211
+ {/* T3 multi-cell: merge/split */}
212
+ {tier === "multi-cell" ||
213
+ tier === "row-selected" ||
214
+ tier === "column-selected" ? (
215
+ <ToolbarSection label="Cells">
216
+ <ToolbarButton
217
+ ariaLabel="Merge cells"
218
+ capability={tableContext?.operations.mergeCells}
219
+ disabled={props.disabled}
220
+ onClick={props.onMergeCells}
221
+ >
222
+ Merge
223
+ </ToolbarButton>
224
+ <ToolbarButton
225
+ ariaLabel="Split cell"
226
+ capability={tableContext?.operations.splitCell}
227
+ disabled={props.disabled}
228
+ onClick={props.onSplitCell}
229
+ >
230
+ Split
231
+ </ToolbarButton>
232
+ </ToolbarSection>
233
+ ) : null}
234
+
235
+ {/* Fill palette: multi-cell + full-row/column + whole-table */}
236
+ {tier !== "caret-in-cell" ? (
237
+ <ToolbarSection label="Fill">
238
+ <div className="flex items-center gap-1">
239
+ {CELL_COLORS.map((color) => (
240
+ <button
241
+ key={color}
242
+ type="button"
243
+ aria-label={`Set cell fill ${color}`}
244
+ disabled={
245
+ props.disabled ||
246
+ !props.onSetCellBackground ||
247
+ !tableContext?.operations.setCellBackground.enabled
248
+ }
249
+ onMouseDown={preserveEditorSelectionMouseDown}
250
+ onClick={() => props.onSetCellBackground?.(color)}
251
+ className="h-5 w-5 rounded border border-border disabled:opacity-40"
252
+ style={{ backgroundColor: color }}
253
+ title={tableContext?.operations.setCellBackground.reason}
254
+ />
255
+ ))}
256
+ </div>
257
+ </ToolbarSection>
258
+ ) : null}
259
+
260
+ {/* T5 only: delete table (danger) */}
261
+ {tier === "whole-table" ? (
262
+ <ToolbarSection label="Table">
263
+ <ToolbarButton
264
+ ariaLabel="Delete table"
265
+ capability={tableContext?.operations.deleteTable}
266
+ danger
267
+ disabled={props.disabled}
268
+ onClick={props.onDeleteTable}
269
+ >
270
+ Delete table
271
+ </ToolbarButton>
272
+ </ToolbarSection>
273
+ ) : null}
186
274
  </div>
187
275
  );
188
276
  }
189
277
 
278
+ function formatSelectionLabel(
279
+ ctx: TableStructureContextSnapshot,
280
+ tier: TableTier,
281
+ ): string {
282
+ if (tier === "caret-in-cell") {
283
+ return `R${ctx.currentCell.rowIndex + 1} C${ctx.currentCell.columnIndex + 1}`;
284
+ }
285
+ if (tier === "row-selected") {
286
+ return `Row ${ctx.currentCell.rowIndex + 1}`;
287
+ }
288
+ if (tier === "column-selected") {
289
+ return `Col ${ctx.currentCell.columnIndex + 1}`;
290
+ }
291
+ if (tier === "whole-table") {
292
+ return "Whole table";
293
+ }
294
+ return `${ctx.selectedCellCount} cells`;
295
+ }
296
+
297
+ function tierLabel(tier: TableTier): string {
298
+ switch (tier) {
299
+ case "caret-in-cell":
300
+ return "Cell";
301
+ case "multi-cell":
302
+ return "Cells";
303
+ case "row-selected":
304
+ return "Row";
305
+ case "column-selected":
306
+ return "Column";
307
+ case "whole-table":
308
+ return "Table";
309
+ }
310
+ }
311
+
312
+ function tierWidthCap(tier: TableTier): string {
313
+ switch (tier) {
314
+ case "caret-in-cell":
315
+ return "max-w-[min(18rem,calc(100vw-1.5rem))]";
316
+ case "multi-cell":
317
+ return "max-w-[min(22rem,calc(100vw-1.5rem))]";
318
+ case "row-selected":
319
+ case "column-selected":
320
+ return "max-w-[min(24rem,calc(100vw-1.5rem))]";
321
+ case "whole-table":
322
+ return "max-w-[min(28rem,calc(100vw-1.5rem))]";
323
+ }
324
+ }
325
+
190
326
  function ToolbarBadge(props: {
191
327
  children: React.ReactNode;
192
328
  tone?: "neutral" | "accent";
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Chrome-overlay projector — maps `RenderFrameRect` values into CSS-absolute
3
+ * positions under a chrome overlay's own coordinate space.
4
+ *
5
+ * Per runtime-rendering-and-chrome-phase.md §6.2, every overlay child reads
6
+ * the same rect math so rails, balloons, toolbars, and handles stay aligned
7
+ * on scroll, zoom, and resize. The projector is a tiny pure helper so the
8
+ * rect math can be unit-tested and reused.
9
+ */
10
+
11
+ import type { RenderFrameRect } from "../../runtime/render/index.ts";
12
+
13
+ export interface OverlayCoordinateSpace {
14
+ /** Top-left of the overlay in the document column's coordinate space. */
15
+ originLeftPx: number;
16
+ originTopPx: number;
17
+ }
18
+
19
+ export interface CSSRect {
20
+ left: string;
21
+ top: string;
22
+ width: string;
23
+ height: string;
24
+ }
25
+
26
+ /**
27
+ * Convert a RenderFrameRect (which is relative to the document column's
28
+ * top-left) into CSS absolute-position values relative to the overlay's own
29
+ * coordinate space. Output uses `px` units so consumers can pass the
30
+ * result straight to inline `style={{...}}`.
31
+ */
32
+ export function projectRectToOverlay(
33
+ rect: RenderFrameRect,
34
+ space: OverlayCoordinateSpace = { originLeftPx: 0, originTopPx: 0 },
35
+ ): CSSRect {
36
+ return {
37
+ left: `${rect.leftPx - space.originLeftPx}px`,
38
+ top: `${rect.topPx - space.originTopPx}px`,
39
+ width: `${rect.widthPx}px`,
40
+ height: `${rect.heightPx}px`,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Expand a page-interior rect outward by `padLeftPx` / `padRightPx` so a
46
+ * decoration can spill into the page margin (scope rail gutter). The
47
+ * output rect width is adjusted accordingly.
48
+ */
49
+ export function inflateRect(
50
+ rect: RenderFrameRect,
51
+ pad: {
52
+ leftPx?: number;
53
+ rightPx?: number;
54
+ topPx?: number;
55
+ bottomPx?: number;
56
+ },
57
+ ): RenderFrameRect {
58
+ const padLeft = pad.leftPx ?? 0;
59
+ const padRight = pad.rightPx ?? 0;
60
+ const padTop = pad.topPx ?? 0;
61
+ const padBottom = pad.bottomPx ?? 0;
62
+ return {
63
+ leftPx: rect.leftPx - padLeft,
64
+ topPx: rect.topPx - padTop,
65
+ widthPx: rect.widthPx + padLeft + padRight,
66
+ heightPx: rect.heightPx + padTop + padBottom,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Union of two rects; returns `null` if either input is nullish so callers
72
+ * can safely chain lookups.
73
+ */
74
+ export function unionRect(
75
+ a: RenderFrameRect | null | undefined,
76
+ b: RenderFrameRect | null | undefined,
77
+ ): RenderFrameRect | null {
78
+ if (!a) return b ?? null;
79
+ if (!b) return a;
80
+ const left = Math.min(a.leftPx, b.leftPx);
81
+ const top = Math.min(a.topPx, b.topPx);
82
+ const right = Math.max(a.leftPx + a.widthPx, b.leftPx + b.widthPx);
83
+ const bottom = Math.max(a.topPx + a.heightPx, b.topPx + b.heightPx);
84
+ return {
85
+ leftPx: left,
86
+ topPx: top,
87
+ widthPx: right - left,
88
+ heightPx: bottom - top,
89
+ };
90
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ChromeOverlay public module entry.
3
+ *
4
+ * Importers should pull the overlay + layer components from here; the
5
+ * internal layer files may be reorganized as the chrome phase continues.
6
+ */
7
+
8
+ export { TwChromeOverlay, type TwChromeOverlayProps } from "./tw-chrome-overlay";
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
+ export {
17
+ inflateRect,
18
+ projectRectToOverlay,
19
+ unionRect,
20
+ type CSSRect,
21
+ type OverlayCoordinateSpace,
22
+ } from "./chrome-overlay-projector";
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ChromeOverlay — the single absolute-positioned overlay plane that hosts
3
+ * every over-document surface (scope rail, comment balloons, revision
4
+ * margin bars, object handles, workspace view-switcher dock).
5
+ *
6
+ * Per runtime-rendering-and-chrome-phase.md §6.2, every overlay child
7
+ * receives `ref.layout` and reads its position from the same render-frame
8
+ * anchor index — not DOM rects, not selection rects — so the chrome stays
9
+ * in place across scroll, zoom, and relayout.
10
+ *
11
+ * This component owns only the plane and the shared coordinate space.
12
+ * Each layer it composes is a pure consumer of the facet.
13
+ */
14
+
15
+ import * as React from "react";
16
+ import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
17
+ import type { ScopeRailSegment } from "../../runtime/layout";
18
+ import type { WordReviewEditorLayoutFacet } from "../../runtime/layout";
19
+ import { TwScopeRailLayer } from "./tw-scope-rail-layer";
20
+ import { TwWorkspaceViewSwitcher, type WorkspaceView } from "./tw-workspace-view-switcher";
21
+
22
+ export interface TwChromeOverlayProps {
23
+ /** Layout facet the overlay layers read from. */
24
+ facet: WordReviewEditorLayoutFacet;
25
+ /** Optional coordinate space override. Defaults to the overlay origin. */
26
+ space?: OverlayCoordinateSpace;
27
+ /** Active scope id (for emphasis + rail tab sync). */
28
+ activeScopeId?: string | null;
29
+ /** Click handler the rail layer forwards to consumers. */
30
+ 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
+ /** Test id applied to the overlay root. */
38
+ "data-testid"?: string;
39
+ /** Optional extra children (e.g., future comment balloon layer). */
40
+ children?: React.ReactNode;
41
+ }
42
+
43
+ /**
44
+ * Placement contract:
45
+ * - The overlay is an absolutely positioned `div` that fills its parent.
46
+ * - The parent must be `position: relative` so the overlay anchors to
47
+ * the document column (not the viewport).
48
+ * - Pointer events are disabled on the root so the editor surface under
49
+ * the overlay continues to receive input; individual layers opt in to
50
+ * pointer events on their interactive elements (buttons, handles).
51
+ */
52
+ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
53
+ facet,
54
+ space,
55
+ activeScopeId,
56
+ onScopeSegmentClick,
57
+ activeWorkspaceView,
58
+ onWorkspaceViewChange,
59
+ showWorkspaceDock = true,
60
+ "data-testid": testId,
61
+ children,
62
+ }) => {
63
+ return (
64
+ <div
65
+ className="wre-chrome-overlay pointer-events-none absolute inset-0 z-30"
66
+ data-testid={testId ?? "chrome-overlay"}
67
+ role="presentation"
68
+ >
69
+ <TwScopeRailLayer
70
+ facet={facet}
71
+ space={space}
72
+ activeScopeId={activeScopeId}
73
+ onSegmentClick={onScopeSegmentClick}
74
+ />
75
+ {children}
76
+ {showWorkspaceDock ? (
77
+ <TwWorkspaceViewSwitcher
78
+ activeView={activeWorkspaceView ?? "review"}
79
+ onViewChange={onWorkspaceViewChange}
80
+ />
81
+ ) : null}
82
+ </div>
83
+ );
84
+ };
85
+
86
+ export default TwChromeOverlay;