@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.
- package/package.json +41 -31
- package/src/api/public-types.ts +183 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +134 -18
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +40 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/ui/WordReviewEditor.tsx +285 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +4 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +1 -5
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- 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 {
|
|
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
|
-
//
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
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) → (
|
|
46
|
-
*
|
|
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
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
378
|
-
? "
|
|
379
|
-
:
|
|
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}
|