@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
@@ -1,15 +1,15 @@
1
1
  import React from "react";
2
2
 
3
- import { BookmarkPlus, ChevronLeft, ChevronRight, MessageSquareText, Rows3 } from "lucide-react";
3
+ import { ChevronLeft, ChevronRight, MessageSquareText, Rows3 } from "lucide-react";
4
4
 
5
5
  import type { ReviewQueueSnapshot } from "../../api/public-types";
6
6
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
7
7
 
8
+ /** @deprecated Use the role action region (TwRoleActionRegion) for queue navigation. */
8
9
  export interface TwReviewQueueBarProps {
9
10
  queue: ReviewQueueSnapshot;
10
11
  onPrevious?: () => void;
11
12
  onNext?: () => void;
12
- onMarkSection?: () => void;
13
13
  }
14
14
 
15
15
  const buttonClass =
@@ -47,18 +47,6 @@ export function TwReviewQueueBar(props: TwReviewQueueBarProps) {
47
47
  Next
48
48
  <ChevronRight className="h-3.5 w-3.5" />
49
49
  </button>
50
- {props.onMarkSection ? (
51
- <button
52
- type="button"
53
- aria-label="Mark section for review"
54
- className={buttonClass}
55
- onMouseDown={preserveEditorSelectionMouseDown}
56
- onClick={props.onMarkSection}
57
- >
58
- <BookmarkPlus className="h-3.5 w-3.5" />
59
- Mark section
60
- </button>
61
- ) : null}
62
50
  <div className="ml-auto flex flex-wrap items-center gap-2 text-xs text-secondary">
63
51
  <span className="inline-flex items-center gap-1 rounded-full bg-canvas px-2.5 py-1 font-medium text-primary">
64
52
  <MessageSquareText className="h-3.5 w-3.5 text-comment" />
@@ -26,16 +26,20 @@ export const ROLE_ACTION_SETS: Record<
26
26
  ReadonlyArray<ToolbarChromeItemId>
27
27
  > = {
28
28
  editor: [
29
- // Posture menu replaces the flat "Mark section" button per spec §6.4.
30
- "editor-scope-posture-menu",
31
- // Insert menu (tables / images / page breaks / section breaks) — only
32
- // meaningful for authoring, so it lives in the editor role's action
33
- // region rather than the base toolbar.
34
- "insert-actions",
35
- "update-actions",
36
- "list-continuation",
29
+ // Comment + inline tracked-changes toggle are the two review-layer
30
+ // actions relevant to authoring. They live in the role region rather
31
+ // than the right cluster so the right cluster stays view-focused.
32
+ "comment",
33
+ "tracked-changes-toggle",
37
34
  ],
38
35
  review: [
36
+ // Optional sidebar panel shortcuts — visible only when the host provides
37
+ // hasSidebarPanelAccess (e.g. the harness). Hidden in base runtime.
38
+ "review-sidebar-tracked-changes",
39
+ "review-sidebar-comments",
40
+ // Inline review actions shared with editor role.
41
+ "comment",
42
+ "tracked-changes-toggle",
39
43
  // Queue navigation + counts, collapsed from the old TwReviewQueueBar.
40
44
  "review-queue-prev",
41
45
  "review-queue-next",
@@ -50,6 +54,8 @@ export const ROLE_ACTION_SETS: Record<
50
54
  "review-markup-mode",
51
55
  ],
52
56
  workflow: [
57
+ // Scoping/posture menu is the primary workflow action (tagging sections).
58
+ "editor-scope-posture-menu",
53
59
  // Work-item navigation (distinct from review-queue nav).
54
60
  "workflow-prev",
55
61
  "workflow-next",
@@ -42,8 +42,8 @@ export interface ResolveSelectionAnchorInput {
42
42
  * - structure(image) → byBlockId when the image has a mediaId that the
43
43
  * engine can map back to a block; else falls through to selection.
44
44
  * - structure(object) → same as image.
45
- * - structure(table) → (byTableCell from sibling plan P4 when shipped;
46
- * today we fall back to bySelection against the selected cells' range).
45
+ * - structure(table) → byTableCell(tableBlockId, currentCell.row, col);
46
+ * blockId from deterministic "table-{tableBlockIndex}" scheme (P4).
47
47
  * - structure(list) → bySelection.
48
48
  *
49
49
  * Returns `null` when the facet has no render kernel installed or the
@@ -152,12 +152,9 @@ function resolveTableAnchor(
152
152
  table: TableStructureContextSnapshot | undefined,
153
153
  ): RenderFrameRect | null {
154
154
  if (!table) return null;
155
- // Sibling plan P4 will expose `byTableCell(tableBlockId, row, col)`
156
- // and `byTableColumnEdge` / `byTableRowEdge` on the anchor index.
157
- // Today we have only `tableBlockIndex` (ordinal), so the best-effort
158
- // fallback is the selection path — callers chain to `anchorForSelection`
159
- // when this returns null.
160
- void frame;
161
- void table;
162
- return null;
155
+ // BlockIds follow the deterministic prefix scheme from surface-projection.ts:
156
+ // table blocks are keyed as "table-{tableBlockIndex}".
157
+ const tableBlockId = `table-${table.tableBlockIndex}`;
158
+ const { rowIndex, columnIndex } = table.currentCell;
159
+ return frame.anchorIndex.byTableCell(tableBlockId, rowIndex, columnIndex);
163
160
  }
@@ -65,6 +65,12 @@ export interface TwSelectionToolHostProps {
65
65
  ) => void;
66
66
  onRestartNumbering?: () => void;
67
67
  onContinueNumbering?: () => void;
68
+ // P6: new table ops
69
+ onToggleRowHeader?: () => void;
70
+ onToggleRowCantSplit?: () => void;
71
+ onDistributeColumnsEvenly?: () => void;
72
+ onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
73
+ onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
68
74
  }
69
75
 
70
76
  export function TwSelectionToolHost(props: TwSelectionToolHostProps) {
@@ -216,6 +222,11 @@ function renderTool(
216
222
  onSetImageFrame={props.onSetImageFrame}
217
223
  onRestartNumbering={props.onRestartNumbering}
218
224
  onContinueNumbering={props.onContinueNumbering}
225
+ onToggleRowHeader={props.onToggleRowHeader}
226
+ onToggleRowCantSplit={props.onToggleRowCantSplit}
227
+ onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
228
+ onSetTableAlignment={props.onSetTableAlignment}
229
+ onSetCellVerticalAlign={props.onSetCellVerticalAlign}
219
230
  />
220
231
  );
221
232
  case "comment-thread":
@@ -29,6 +29,12 @@ export interface TwSelectionToolStructureProps {
29
29
  ) => void;
30
30
  onRestartNumbering?: () => void;
31
31
  onContinueNumbering?: () => void;
32
+ // P6: new table ops
33
+ onToggleRowHeader?: () => void;
34
+ onToggleRowCantSplit?: () => void;
35
+ onDistributeColumnsEvenly?: () => void;
36
+ onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
37
+ onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
32
38
  }
33
39
 
34
40
  export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
@@ -64,6 +70,11 @@ export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
64
70
  onMergeCells={props.onMergeCells}
65
71
  onSplitCell={props.onSplitCell}
66
72
  onSetCellBackground={props.onSetCellBackground}
73
+ onToggleRowHeader={props.onToggleRowHeader}
74
+ onToggleRowCantSplit={props.onToggleRowCantSplit}
75
+ onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
76
+ onSetTableAlignment={props.onSetTableAlignment}
77
+ onSetCellVerticalAlign={props.onSetCellVerticalAlign}
67
78
  />
68
79
  );
69
80
  case "list":
@@ -0,0 +1,245 @@
1
+ /**
2
+ * R4b: floating border picker for the table context toolbar.
3
+ *
4
+ * Opens from a "Borders" button in the toolbar's Cells cluster. Reads the
5
+ * current border state via callbacks into `ref.tables.apply()` (set-cell-borders
6
+ * for selection-scoped edits, set-table-borders for whole-table edits).
7
+ *
8
+ * UI shape — two top-level choices the user makes in sequence:
9
+ * 1. Target family: Outside / Inside / All / None
10
+ * (where "All" applies to every side of every cell in the selection,
11
+ * "Outside" limits to the exterior edges, "Inside" to the interior
12
+ * edges, and "None" clears the selected sides)
13
+ * 2. Style: single / double / dashed / none
14
+ * 3. Size: 0.5pt / 1pt / 1.5pt / 2pt (8ths of a point — OOXML uses `sz` in 8ths)
15
+ * 4. Color: black / primary / secondary / custom
16
+ *
17
+ * The picker itself is a pure UI component — it knows nothing about the
18
+ * canonical document. It emits `onApply(config)` with a structured payload
19
+ * the toolbar hands to `ref.tables.apply({ kind: "set-cell-borders"|"set-table-borders", ... })`.
20
+ */
21
+
22
+ import React, { useState } from "react";
23
+
24
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
25
+
26
+ export type BorderFamily = "outside" | "inside" | "all" | "none";
27
+ export type BorderStyle = "single" | "double" | "dashed" | "none";
28
+ export type BorderSizeEighths = 4 | 8 | 12 | 16; // 0.5pt, 1pt, 1.5pt, 2pt
29
+
30
+ export interface BorderPickerConfig {
31
+ family: BorderFamily;
32
+ style: BorderStyle;
33
+ sizeEighths: BorderSizeEighths;
34
+ colorHex: string;
35
+ }
36
+
37
+ export interface TwTableBorderPickerProps {
38
+ /** When true, the picker is visible. */
39
+ open: boolean;
40
+ /** Disable interaction; applies grey styling. */
41
+ disabled?: boolean;
42
+ /**
43
+ * Called when the user commits a configuration. The toolbar maps this to
44
+ * either `set-table-borders` (T5 selection) or `set-cell-borders` (T2/T3/T4).
45
+ */
46
+ onApply: (config: BorderPickerConfig) => void;
47
+ /** Called when the user dismisses without applying. */
48
+ onDismiss?: () => void;
49
+ }
50
+
51
+ const FAMILIES: ReadonlyArray<{ id: BorderFamily; label: string }> = [
52
+ { id: "outside", label: "Outside" },
53
+ { id: "inside", label: "Inside" },
54
+ { id: "all", label: "All" },
55
+ { id: "none", label: "None" },
56
+ ];
57
+
58
+ const STYLES: ReadonlyArray<{ id: BorderStyle; label: string }> = [
59
+ { id: "single", label: "Single" },
60
+ { id: "double", label: "Double" },
61
+ { id: "dashed", label: "Dashed" },
62
+ { id: "none", label: "None" },
63
+ ];
64
+
65
+ const SIZES: ReadonlyArray<{ id: BorderSizeEighths; label: string }> = [
66
+ { id: 4, label: "0.5 pt" },
67
+ { id: 8, label: "1 pt" },
68
+ { id: 12, label: "1.5 pt" },
69
+ { id: 16, label: "2 pt" },
70
+ ];
71
+
72
+ const COLORS: ReadonlyArray<{ id: string; label: string; hex: string }> = [
73
+ { id: "black", label: "Black", hex: "#000000" },
74
+ { id: "muted", label: "Muted", hex: "#8b8b88" },
75
+ { id: "accent", label: "Accent", hex: "#2563eb" },
76
+ { id: "danger", label: "Danger", hex: "#dc2626" },
77
+ ];
78
+
79
+ /**
80
+ * The picker itself. Lives inside a parent that controls `open` / `onDismiss`;
81
+ * a simple popover today, destined to graduate into a tooltip-positioned
82
+ * overlay once the chrome-overlay floating-tool protocol is stable.
83
+ */
84
+ export function TwTableBorderPicker(props: TwTableBorderPickerProps) {
85
+ const [family, setFamily] = useState<BorderFamily>("all");
86
+ const [style, setStyle] = useState<BorderStyle>("single");
87
+ const [sizeEighths, setSizeEighths] = useState<BorderSizeEighths>(8);
88
+ const [colorHex, setColorHex] = useState<string>("#000000");
89
+
90
+ if (!props.open) return null;
91
+
92
+ const handleApply = () => {
93
+ props.onApply({ family, style, sizeEighths, colorHex });
94
+ };
95
+
96
+ const baseButton =
97
+ "inline-flex h-6 items-center rounded border px-2 text-[11px] font-medium transition-colors";
98
+ const activeButton = "border-accent bg-accent/10 text-accent";
99
+ const idleButton = "border-border text-secondary hover:bg-surface";
100
+
101
+ return (
102
+ <div
103
+ data-testid="table-border-picker"
104
+ role="dialog"
105
+ aria-label="Border picker"
106
+ className="flex flex-col gap-2 rounded-lg border border-border bg-canvas px-3 py-2 shadow-md"
107
+ onMouseDown={preserveEditorSelectionMouseDown}
108
+ >
109
+ {/* Family */}
110
+ <div className="flex items-center gap-1.5">
111
+ <span className="w-14 text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
112
+ Target
113
+ </span>
114
+ {FAMILIES.map((f) => (
115
+ <button
116
+ key={f.id}
117
+ type="button"
118
+ aria-label={`Border target ${f.label}`}
119
+ aria-pressed={family === f.id}
120
+ disabled={props.disabled}
121
+ onClick={() => setFamily(f.id)}
122
+ className={`${baseButton} ${family === f.id ? activeButton : idleButton}`}
123
+ >
124
+ {f.label}
125
+ </button>
126
+ ))}
127
+ </div>
128
+
129
+ {/* Style */}
130
+ <div className="flex items-center gap-1.5">
131
+ <span className="w-14 text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
132
+ Style
133
+ </span>
134
+ {STYLES.map((s) => (
135
+ <button
136
+ key={s.id}
137
+ type="button"
138
+ aria-label={`Border style ${s.label}`}
139
+ aria-pressed={style === s.id}
140
+ disabled={props.disabled}
141
+ onClick={() => setStyle(s.id)}
142
+ className={`${baseButton} ${style === s.id ? activeButton : idleButton}`}
143
+ >
144
+ {s.label}
145
+ </button>
146
+ ))}
147
+ </div>
148
+
149
+ {/* Size */}
150
+ <div className="flex items-center gap-1.5">
151
+ <span className="w-14 text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
152
+ Size
153
+ </span>
154
+ {SIZES.map((s) => (
155
+ <button
156
+ key={s.id}
157
+ type="button"
158
+ aria-label={`Border size ${s.label}`}
159
+ aria-pressed={sizeEighths === s.id}
160
+ disabled={props.disabled}
161
+ onClick={() => setSizeEighths(s.id)}
162
+ className={`${baseButton} ${sizeEighths === s.id ? activeButton : idleButton}`}
163
+ >
164
+ {s.label}
165
+ </button>
166
+ ))}
167
+ </div>
168
+
169
+ {/* Color */}
170
+ <div className="flex items-center gap-1.5">
171
+ <span className="w-14 text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
172
+ Color
173
+ </span>
174
+ {COLORS.map((c) => (
175
+ <button
176
+ key={c.id}
177
+ type="button"
178
+ aria-label={`Border color ${c.label}`}
179
+ aria-pressed={colorHex === c.hex}
180
+ disabled={props.disabled}
181
+ onClick={() => setColorHex(c.hex)}
182
+ className={`h-6 w-6 rounded-full border ${
183
+ colorHex === c.hex ? "border-accent ring-2 ring-accent/40" : "border-border"
184
+ }`}
185
+ style={{ backgroundColor: c.hex }}
186
+ />
187
+ ))}
188
+ </div>
189
+
190
+ {/* Actions */}
191
+ <div className="flex items-center justify-end gap-1.5 pt-1">
192
+ {props.onDismiss ? (
193
+ <button
194
+ type="button"
195
+ aria-label="Dismiss border picker"
196
+ onClick={props.onDismiss}
197
+ disabled={props.disabled}
198
+ className="h-6 rounded border border-border px-2 text-[11px] font-medium text-secondary hover:bg-surface"
199
+ >
200
+ Cancel
201
+ </button>
202
+ ) : null}
203
+ <button
204
+ type="button"
205
+ aria-label="Apply borders"
206
+ data-testid="border-picker-apply"
207
+ onClick={handleApply}
208
+ disabled={props.disabled}
209
+ className="h-6 rounded border border-accent bg-accent px-2 text-[11px] font-medium text-white hover:bg-accent/90 disabled:opacity-40"
210
+ >
211
+ Apply
212
+ </button>
213
+ </div>
214
+ </div>
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Translate the picker's family + style + size + color into the per-side
220
+ * partial that the canonical `set-cell-borders` / `set-table-borders` op
221
+ * expects. Toolbar uses this to build the op payload.
222
+ *
223
+ * The emitted object uses runtime-side border keys:
224
+ * single → "single", double → "double", dashed → "dashed"
225
+ * and a size encoded in 8ths of a point.
226
+ */
227
+ export function configToBordersPatch(
228
+ config: BorderPickerConfig,
229
+ ): {
230
+ applyToFamily: BorderFamily;
231
+ sides: {
232
+ style: BorderStyle;
233
+ sizeEighths: BorderSizeEighths;
234
+ colorHex: string;
235
+ };
236
+ } {
237
+ return {
238
+ applyToFamily: config.family,
239
+ sides: {
240
+ style: config.style,
241
+ sizeEighths: config.sizeEighths,
242
+ colorHex: config.colorHex,
243
+ },
244
+ };
245
+ }
@@ -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 = [
@@ -113,6 +119,24 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
113
119
  <ToolbarBadge tone="accent">Header row</ToolbarBadge>
114
120
  ) : null}
115
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>
136
+ ))}
137
+ </ToolbarSection>
138
+ ) : null}
139
+
116
140
  {/* T5 whole-table: style selector (primary), delete table */}
117
141
  {tier === "whole-table" ? (
118
142
  <ToolbarSection label="Style">
@@ -162,15 +186,34 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
162
186
  Below
163
187
  </ToolbarButton>
164
188
  {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>
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
+ </>
174
217
  ) : null}
175
218
  </ToolbarSection>
176
219
  ) : null}
@@ -195,15 +238,25 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
195
238
  Right
196
239
  </ToolbarButton>
197
240
  {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>
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
+ </>
207
260
  ) : null}
208
261
  </ToolbarSection>
209
262
  ) : null}
@@ -257,6 +310,29 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
257
310
  </ToolbarSection>
258
311
  ) : null}
259
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>
332
+ ))}
333
+ </ToolbarSection>
334
+ ) : null}
335
+
260
336
  {/* T5 only: delete table (danger) */}
261
337
  {tier === "whole-table" ? (
262
338
  <ToolbarSection label="Table">
@@ -362,6 +438,7 @@ function ToolbarButton(props: {
362
438
  danger?: boolean;
363
439
  disabled: boolean;
364
440
  onClick?: () => void;
441
+ active?: boolean;
365
442
  }) {
366
443
  const capabilityEnabled = props.capability?.enabled ?? true;
367
444
  const title = !capabilityEnabled ? props.capability?.reason : undefined;
@@ -369,14 +446,17 @@ function ToolbarButton(props: {
369
446
  <button
370
447
  type="button"
371
448
  aria-label={props.ariaLabel}
449
+ aria-pressed={props.active}
372
450
  disabled={props.disabled || !props.onClick || !capabilityEnabled}
373
451
  onMouseDown={preserveEditorSelectionMouseDown}
374
452
  onClick={props.onClick}
375
453
  title={title}
376
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 ${
377
- props.danger
378
- ? "text-danger hover:bg-danger/10"
379
- : "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"
380
460
  }`}
381
461
  >
382
462
  {props.children}