@beyondwork/docx-react-component 1.0.37 → 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 (116) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +496 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +845 -56
  6. package/src/core/commands/text-commands.ts +122 -2
  7. package/src/io/docx-session.ts +1 -0
  8. package/src/io/export/serialize-main-document.ts +2 -11
  9. package/src/io/export/serialize-numbering.ts +43 -10
  10. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  11. package/src/io/export/serialize-run-formatting.ts +90 -0
  12. package/src/io/export/serialize-styles.ts +212 -0
  13. package/src/io/export/serialize-tables.ts +74 -0
  14. package/src/io/export/table-properties-xml.ts +139 -4
  15. package/src/io/normalize/normalize-text.ts +15 -0
  16. package/src/io/ooxml/parse-fields.ts +10 -3
  17. package/src/io/ooxml/parse-footnotes.ts +60 -0
  18. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  19. package/src/io/ooxml/parse-main-document.ts +137 -0
  20. package/src/io/ooxml/parse-numbering.ts +41 -1
  21. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  22. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  23. package/src/io/ooxml/parse-styles.ts +31 -0
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  26. package/src/io/ooxml/xml-element.ts +19 -0
  27. package/src/model/canonical-document.ts +117 -3
  28. package/src/runtime/collab/event-types.ts +165 -0
  29. package/src/runtime/collab/index.ts +22 -0
  30. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  31. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  32. package/src/runtime/document-layout.ts +4 -2
  33. package/src/runtime/document-navigation.ts +1 -1
  34. package/src/runtime/document-runtime.ts +248 -18
  35. package/src/runtime/layout/default-page-format.ts +96 -0
  36. package/src/runtime/layout/index.ts +47 -0
  37. package/src/runtime/layout/inert-layout-facet.ts +16 -0
  38. package/src/runtime/layout/layout-engine-instance.ts +100 -23
  39. package/src/runtime/layout/layout-invalidation.ts +14 -5
  40. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-graph.ts +55 -0
  43. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  44. package/src/runtime/layout/paginated-layout-engine.ts +484 -37
  45. package/src/runtime/layout/project-block-fragments.ts +225 -0
  46. package/src/runtime/layout/public-facet.ts +748 -16
  47. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  48. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  49. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  50. package/src/runtime/layout/table-render-plan.ts +249 -0
  51. package/src/runtime/numbering-prefix.ts +5 -0
  52. package/src/runtime/paragraph-style-resolver.ts +194 -0
  53. package/src/runtime/render/block-fragment-projection.ts +35 -0
  54. package/src/runtime/render/decoration-resolver.ts +189 -0
  55. package/src/runtime/render/index.ts +57 -0
  56. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  57. package/src/runtime/render/render-frame-types.ts +317 -0
  58. package/src/runtime/render/render-kernel.ts +759 -0
  59. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  60. package/src/runtime/surface-projection.ts +129 -9
  61. package/src/runtime/table-schema.ts +11 -0
  62. package/src/runtime/view-state.ts +67 -0
  63. package/src/runtime/workflow-markup.ts +1 -5
  64. package/src/runtime/workflow-rail-segments.ts +280 -0
  65. package/src/ui/WordReviewEditor.tsx +368 -19
  66. package/src/ui/editor-command-bag.ts +4 -0
  67. package/src/ui/editor-runtime-boundary.ts +16 -0
  68. package/src/ui/editor-shell-view.tsx +10 -0
  69. package/src/ui/editor-surface-controller.tsx +9 -1
  70. package/src/ui/headless/chrome-registry.ts +310 -15
  71. package/src/ui/headless/scoped-chrome-policy.ts +49 -1
  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/review-queue-bar.tsx +2 -14
  75. package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
  76. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  77. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
  78. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
  79. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  80. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  81. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  82. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  83. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
  84. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  85. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
  86. package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
  88. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  89. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  90. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  91. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  92. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  93. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
  94. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  95. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  96. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  97. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  98. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  99. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  100. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
  101. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  102. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  103. package/src/ui-tailwind/index.ts +29 -0
  104. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  105. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  106. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  107. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  108. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  109. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  110. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
  111. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  112. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  113. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
  114. package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
  115. package/src/runtime/collab-review-sync.ts +0 -254
  116. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -21,6 +21,12 @@ export interface TwTableContextToolbarProps {
21
21
  onSplitCell?: () => void;
22
22
  onSetCellBackground?: (color: string) => void;
23
23
  onDeleteTable?: () => void;
24
+ // P6: new ops surfaced from P2 capability flags
25
+ onToggleRowHeader?: () => void;
26
+ onToggleRowCantSplit?: () => void;
27
+ onDistributeColumnsEvenly?: () => void;
28
+ onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
29
+ onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
24
30
  }
25
31
 
26
32
  const CELL_COLORS = [
@@ -32,161 +38,367 @@ const CELL_COLORS = [
32
38
  "#fce7f3",
33
39
  ] as const;
34
40
 
41
+ /**
42
+ * Table tier (R2.4, spec §6.4 / plan table tier matrix).
43
+ *
44
+ * - `caret-in-cell` (T2) — single-cell selection. Minimal inline set:
45
+ * row +/−, column +/−. Anything structural (merge/split/fill/delete
46
+ * table) lives behind "More" to keep the panel ~180px wide.
47
+ * - `multi-cell` (T3) — >1 cell but not a full row/column/table. Adds
48
+ * merge/split/fill palette.
49
+ * - `row-selected` (T4a) — selection spans exactly one full row
50
+ * (`selectedCellCount === columnCount` + currentCell hint). Row delete
51
+ * + insert + fill palette.
52
+ * - `column-selected` (T4b) — selection spans exactly one full column
53
+ * (`selectedCellCount === rowCount`). Column delete + insert.
54
+ * - `whole-table` (T5) — selection covers every cell OR the user picked
55
+ * the table-select handle (future). Style + delete + table-level
56
+ * formatting.
57
+ *
58
+ * The tier resolver is a pure function of the capability snapshot so
59
+ * tests can drive it from seeded snapshots without a DOM.
60
+ */
61
+ export type TableTier =
62
+ | "caret-in-cell"
63
+ | "multi-cell"
64
+ | "row-selected"
65
+ | "column-selected"
66
+ | "whole-table";
67
+
68
+ export function resolveTableTier(
69
+ ctx: TableStructureContextSnapshot,
70
+ ): TableTier {
71
+ const totalCells = ctx.rowCount * ctx.columnCount;
72
+ if (ctx.selectedCellCount >= totalCells && totalCells > 0) {
73
+ return "whole-table";
74
+ }
75
+ if (
76
+ ctx.selectedCellCount > 1 &&
77
+ ctx.selectedCellCount === ctx.columnCount &&
78
+ ctx.columnCount > 0
79
+ ) {
80
+ return "row-selected";
81
+ }
82
+ if (
83
+ ctx.selectedCellCount > 1 &&
84
+ ctx.selectedCellCount === ctx.rowCount &&
85
+ ctx.rowCount > 0
86
+ ) {
87
+ return "column-selected";
88
+ }
89
+ if (ctx.selectedCellCount > 1) {
90
+ return "multi-cell";
91
+ }
92
+ return "caret-in-cell";
93
+ }
94
+
35
95
  export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
36
96
  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}`
97
+ const tier = tableContext ? resolveTableTier(tableContext) : "caret-in-cell";
98
+ const tableSizeLabel = tableContext
99
+ ? `${tableContext.rowCount} x ${tableContext.columnCount}`
42
100
  : null;
101
+ const selectionLabel = tableContext ? formatSelectionLabel(tableContext, tier) : null;
102
+
103
+ // Tier-specific width caps. Progressive: T2 ≤ 18rem, T3 ≤ 22rem, T4
104
+ // ≤ 24rem, T5 ≤ 28rem. Down from the old flat 30rem always.
105
+ const widthCap = tierWidthCap(tier);
43
106
 
44
107
  return (
45
108
  <div
46
109
  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"
110
+ data-tier={tier}
111
+ 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
112
  >
49
113
  <span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
50
- Table
114
+ {tierLabel(tier)}
51
115
  </span>
52
116
  {tableSizeLabel ? <ToolbarBadge>{tableSizeLabel}</ToolbarBadge> : null}
53
117
  {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}
75
- </option>
118
+ {tableContext?.currentCell.isHeader ? (
119
+ <ToolbarBadge tone="accent">Header row</ToolbarBadge>
120
+ ) : null}
121
+
122
+ {/* T5 whole-table: table alignment */}
123
+ {tier === "whole-table" ? (
124
+ <ToolbarSection label="Align">
125
+ {(["left", "center", "right"] as const).map((align) => (
126
+ <ToolbarButton
127
+ key={align}
128
+ ariaLabel={`Align table ${align}`}
129
+ capability={tableContext?.operations.setTableAlignment}
130
+ disabled={props.disabled}
131
+ onClick={() => props.onSetTableAlignment?.(align)}
132
+ active={tableContext?.currentCell != null && align === "left"}
133
+ >
134
+ {align[0]!.toUpperCase()}
135
+ </ToolbarButton>
76
136
  ))}
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
- />
137
+ </ToolbarSection>
138
+ ) : null}
139
+
140
+ {/* T5 whole-table: style selector (primary), delete table */}
141
+ {tier === "whole-table" ? (
142
+ <ToolbarSection label="Style">
143
+ <select
144
+ aria-label="Table style"
145
+ className="h-7 min-w-[9rem] rounded-md border border-border bg-canvas px-2 text-[11px] text-primary disabled:opacity-40"
146
+ disabled={
147
+ props.disabled ||
148
+ props.tableStyles.length === 0 ||
149
+ !props.onSetTableStyle ||
150
+ !tableContext?.operations.setTableStyle.enabled
151
+ }
152
+ onMouseDown={preserveEditorSelectionMouseDown}
153
+ onChange={(event) => props.onSetTableStyle?.(event.target.value)}
154
+ value={tableContext?.currentStyleId ?? ""}
155
+ title={tableContext?.operations.setTableStyle.reason}
156
+ >
157
+ <option value="" disabled>
158
+ Table style
159
+ </option>
160
+ {props.tableStyles.map((style) => (
161
+ <option key={style.styleId} value={style.styleId}>
162
+ {style.displayName}
163
+ </option>
164
+ ))}
165
+ </select>
166
+ </ToolbarSection>
167
+ ) : null}
168
+
169
+ {/* T2 / T4a row-selected: row ops */}
170
+ {(tier === "caret-in-cell" || tier === "row-selected") ? (
171
+ <ToolbarSection label="Rows">
172
+ <ToolbarButton
173
+ ariaLabel="Add row above"
174
+ capability={tableContext?.operations.addRowBefore}
175
+ disabled={props.disabled}
176
+ onClick={props.onAddRowBefore}
177
+ >
178
+ Above
179
+ </ToolbarButton>
180
+ <ToolbarButton
181
+ ariaLabel="Add row below"
182
+ capability={tableContext?.operations.addRowAfter}
183
+ disabled={props.disabled}
184
+ onClick={props.onAddRowAfter}
185
+ >
186
+ Below
187
+ </ToolbarButton>
188
+ {tier === "row-selected" ? (
189
+ <>
190
+ <ToolbarButton
191
+ ariaLabel="Delete row"
192
+ capability={tableContext?.operations.deleteRow}
193
+ disabled={props.disabled}
194
+ onClick={props.onDeleteRow}
195
+ danger
196
+ >
197
+ Delete row
198
+ </ToolbarButton>
199
+ <ToolbarButton
200
+ ariaLabel="Toggle header row"
201
+ capability={tableContext?.operations.setRowIsHeader}
202
+ disabled={props.disabled}
203
+ onClick={props.onToggleRowHeader}
204
+ active={tableContext?.currentCell.isHeader}
205
+ >
206
+ Header
207
+ </ToolbarButton>
208
+ <ToolbarButton
209
+ ariaLabel="Toggle row can't split"
210
+ capability={tableContext?.operations.setRowCantSplit}
211
+ disabled={props.disabled}
212
+ onClick={props.onToggleRowCantSplit}
213
+ >
214
+ No break
215
+ </ToolbarButton>
216
+ </>
217
+ ) : null}
218
+ </ToolbarSection>
219
+ ) : null}
220
+
221
+ {/* T2 / T4b column-selected: column ops */}
222
+ {(tier === "caret-in-cell" || tier === "column-selected") ? (
223
+ <ToolbarSection label="Columns">
224
+ <ToolbarButton
225
+ ariaLabel="Add column left"
226
+ capability={tableContext?.operations.addColumnBefore}
227
+ disabled={props.disabled}
228
+ onClick={props.onAddColumnBefore}
229
+ >
230
+ Left
231
+ </ToolbarButton>
232
+ <ToolbarButton
233
+ ariaLabel="Add column right"
234
+ capability={tableContext?.operations.addColumnAfter}
235
+ disabled={props.disabled}
236
+ onClick={props.onAddColumnAfter}
237
+ >
238
+ Right
239
+ </ToolbarButton>
240
+ {tier === "column-selected" ? (
241
+ <>
242
+ <ToolbarButton
243
+ ariaLabel="Delete column"
244
+ capability={tableContext?.operations.deleteColumn}
245
+ disabled={props.disabled}
246
+ onClick={props.onDeleteColumn}
247
+ danger
248
+ >
249
+ Delete column
250
+ </ToolbarButton>
251
+ <ToolbarButton
252
+ ariaLabel="Distribute columns evenly"
253
+ capability={tableContext?.operations.distributeColumnsEvenly}
254
+ disabled={props.disabled}
255
+ onClick={props.onDistributeColumnsEvenly}
256
+ >
257
+ Distribute
258
+ </ToolbarButton>
259
+ </>
260
+ ) : null}
261
+ </ToolbarSection>
262
+ ) : null}
263
+
264
+ {/* T3 multi-cell: merge/split */}
265
+ {tier === "multi-cell" ||
266
+ tier === "row-selected" ||
267
+ tier === "column-selected" ? (
268
+ <ToolbarSection label="Cells">
269
+ <ToolbarButton
270
+ ariaLabel="Merge cells"
271
+ capability={tableContext?.operations.mergeCells}
272
+ disabled={props.disabled}
273
+ onClick={props.onMergeCells}
274
+ >
275
+ Merge
276
+ </ToolbarButton>
277
+ <ToolbarButton
278
+ ariaLabel="Split cell"
279
+ capability={tableContext?.operations.splitCell}
280
+ disabled={props.disabled}
281
+ onClick={props.onSplitCell}
282
+ >
283
+ Split
284
+ </ToolbarButton>
285
+ </ToolbarSection>
286
+ ) : null}
287
+
288
+ {/* Fill palette: multi-cell + full-row/column + whole-table */}
289
+ {tier !== "caret-in-cell" ? (
290
+ <ToolbarSection label="Fill">
291
+ <div className="flex items-center gap-1">
292
+ {CELL_COLORS.map((color) => (
293
+ <button
294
+ key={color}
295
+ type="button"
296
+ aria-label={`Set cell fill ${color}`}
297
+ disabled={
298
+ props.disabled ||
299
+ !props.onSetCellBackground ||
300
+ !tableContext?.operations.setCellBackground.enabled
301
+ }
302
+ onMouseDown={preserveEditorSelectionMouseDown}
303
+ onClick={() => props.onSetCellBackground?.(color)}
304
+ className="h-5 w-5 rounded border border-border disabled:opacity-40"
305
+ style={{ backgroundColor: color }}
306
+ title={tableContext?.operations.setCellBackground.reason}
307
+ />
308
+ ))}
309
+ </div>
310
+ </ToolbarSection>
311
+ ) : null}
312
+
313
+ {/* Cell vertical alignment (caret-in-cell + multi-cell) */}
314
+ {(tier === "caret-in-cell" || tier === "multi-cell") ? (
315
+ <ToolbarSection label="V-Align">
316
+ {(
317
+ [
318
+ ["top", "Top"],
319
+ ["center", "Mid"],
320
+ ["bottom", "Bot"],
321
+ ] as const
322
+ ).map(([align, label]) => (
323
+ <ToolbarButton
324
+ key={align}
325
+ ariaLabel={`Cell vertical align ${align}`}
326
+ capability={tableContext?.operations.setCellVerticalAlign}
327
+ disabled={props.disabled}
328
+ onClick={() => props.onSetCellVerticalAlign?.(align)}
329
+ >
330
+ {label}
331
+ </ToolbarButton>
171
332
  ))}
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>
333
+ </ToolbarSection>
334
+ ) : null}
335
+
336
+ {/* T5 only: delete table (danger) */}
337
+ {tier === "whole-table" ? (
338
+ <ToolbarSection label="Table">
339
+ <ToolbarButton
340
+ ariaLabel="Delete table"
341
+ capability={tableContext?.operations.deleteTable}
342
+ danger
343
+ disabled={props.disabled}
344
+ onClick={props.onDeleteTable}
345
+ >
346
+ Delete table
347
+ </ToolbarButton>
348
+ </ToolbarSection>
349
+ ) : null}
186
350
  </div>
187
351
  );
188
352
  }
189
353
 
354
+ function formatSelectionLabel(
355
+ ctx: TableStructureContextSnapshot,
356
+ tier: TableTier,
357
+ ): string {
358
+ if (tier === "caret-in-cell") {
359
+ return `R${ctx.currentCell.rowIndex + 1} C${ctx.currentCell.columnIndex + 1}`;
360
+ }
361
+ if (tier === "row-selected") {
362
+ return `Row ${ctx.currentCell.rowIndex + 1}`;
363
+ }
364
+ if (tier === "column-selected") {
365
+ return `Col ${ctx.currentCell.columnIndex + 1}`;
366
+ }
367
+ if (tier === "whole-table") {
368
+ return "Whole table";
369
+ }
370
+ return `${ctx.selectedCellCount} cells`;
371
+ }
372
+
373
+ function tierLabel(tier: TableTier): string {
374
+ switch (tier) {
375
+ case "caret-in-cell":
376
+ return "Cell";
377
+ case "multi-cell":
378
+ return "Cells";
379
+ case "row-selected":
380
+ return "Row";
381
+ case "column-selected":
382
+ return "Column";
383
+ case "whole-table":
384
+ return "Table";
385
+ }
386
+ }
387
+
388
+ function tierWidthCap(tier: TableTier): string {
389
+ switch (tier) {
390
+ case "caret-in-cell":
391
+ return "max-w-[min(18rem,calc(100vw-1.5rem))]";
392
+ case "multi-cell":
393
+ return "max-w-[min(22rem,calc(100vw-1.5rem))]";
394
+ case "row-selected":
395
+ case "column-selected":
396
+ return "max-w-[min(24rem,calc(100vw-1.5rem))]";
397
+ case "whole-table":
398
+ return "max-w-[min(28rem,calc(100vw-1.5rem))]";
399
+ }
400
+ }
401
+
190
402
  function ToolbarBadge(props: {
191
403
  children: React.ReactNode;
192
404
  tone?: "neutral" | "accent";
@@ -226,6 +438,7 @@ function ToolbarButton(props: {
226
438
  danger?: boolean;
227
439
  disabled: boolean;
228
440
  onClick?: () => void;
441
+ active?: boolean;
229
442
  }) {
230
443
  const capabilityEnabled = props.capability?.enabled ?? true;
231
444
  const title = !capabilityEnabled ? props.capability?.reason : undefined;
@@ -233,14 +446,17 @@ function ToolbarButton(props: {
233
446
  <button
234
447
  type="button"
235
448
  aria-label={props.ariaLabel}
449
+ aria-pressed={props.active}
236
450
  disabled={props.disabled || !props.onClick || !capabilityEnabled}
237
451
  onMouseDown={preserveEditorSelectionMouseDown}
238
452
  onClick={props.onClick}
239
453
  title={title}
240
454
  className={`inline-flex h-7 items-center rounded-md px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
241
- props.danger
242
- ? "text-danger hover:bg-danger/10"
243
- : "text-primary hover:bg-surface"
455
+ props.active
456
+ ? "bg-accent/15 text-accent"
457
+ : props.danger
458
+ ? "text-danger hover:bg-danger/10"
459
+ : "text-primary hover:bg-surface"
244
460
  }`}
245
461
  >
246
462
  {props.children}