@beyondwork/docx-react-component 1.0.56 → 1.0.57
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 +1 -1
- package/src/api/public-types.ts +157 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +17 -11
- package/src/core/selection/mapping.ts +18 -1
- package/src/core/selection/review-anchors.ts +29 -18
- package/src/io/chart-preview-resolver.ts +175 -41
- package/src/io/docx-session.ts +57 -2
- package/src/io/export/serialize-main-document.ts +82 -0
- package/src/io/export/serialize-styles.ts +61 -3
- package/src/io/export/table-properties-xml.ts +19 -4
- package/src/io/normalize/normalize-text.ts +33 -0
- package/src/io/ooxml/parse-anchor.ts +182 -0
- package/src/io/ooxml/parse-drawing.ts +319 -0
- package/src/io/ooxml/parse-fields.ts +115 -2
- package/src/io/ooxml/parse-fill.ts +215 -0
- package/src/io/ooxml/parse-font-table.ts +190 -0
- package/src/io/ooxml/parse-footnotes.ts +52 -1
- package/src/io/ooxml/parse-main-document.ts +241 -1
- package/src/io/ooxml/parse-numbering.ts +96 -0
- package/src/io/ooxml/parse-picture.ts +107 -0
- package/src/io/ooxml/parse-settings.ts +34 -0
- package/src/io/ooxml/parse-shapes.ts +87 -0
- package/src/io/ooxml/parse-solid-fill.ts +11 -0
- package/src/io/ooxml/parse-styles.ts +74 -1
- package/src/io/ooxml/parse-theme.ts +60 -0
- package/src/io/paste/html-clipboard.ts +449 -0
- package/src/io/paste/word-clipboard.ts +5 -1
- package/src/legal/_document-root.ts +26 -0
- package/src/legal/bookmarks.ts +4 -3
- package/src/legal/cross-references.ts +3 -2
- package/src/legal/defined-terms.ts +2 -1
- package/src/legal/signature-blocks.ts +2 -1
- package/src/model/canonical-document.ts +415 -3
- package/src/runtime/chart/chart-model-store.ts +73 -10
- package/src/runtime/document-runtime.ts +693 -41
- package/src/runtime/edit-ops/index.ts +129 -0
- package/src/runtime/event-refresh-hints.ts +7 -0
- package/src/runtime/field-resolver.ts +341 -0
- package/src/runtime/footnote-resolver.ts +55 -0
- package/src/runtime/hyperlink-color-resolver.ts +13 -10
- package/src/runtime/object-grab/index.ts +51 -0
- package/src/runtime/paragraph-style-resolver.ts +105 -0
- package/src/runtime/resolved-numbering-geometry.ts +12 -0
- package/src/runtime/selection/cursor-ops.ts +186 -15
- package/src/runtime/selection/index.ts +17 -1
- package/src/runtime/structure-ops/index.ts +77 -0
- package/src/runtime/styles-cascade.ts +33 -0
- package/src/runtime/surface-projection.ts +186 -12
- package/src/runtime/theme-color-resolver.ts +189 -44
- package/src/runtime/units.ts +46 -0
- package/src/runtime/view-state.ts +13 -2
- package/src/ui/WordReviewEditor.tsx +168 -10
- package/src/ui/editor-runtime-boundary.ts +94 -1
- package/src/ui/editor-shell-view.tsx +1 -1
- package/src/ui/runtime-shortcut-dispatch.ts +17 -3
- package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
- package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
- package/src/ui-tailwind/chart/render/area.tsx +22 -4
- package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
- package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
- package/src/ui-tailwind/chart/render/combo.tsx +37 -4
- package/src/ui-tailwind/chart/render/line.tsx +28 -5
- package/src/ui-tailwind/chart/render/pie.tsx +36 -16
- package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
- package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
- package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
- package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
- package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
- package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
- package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
- package/src/ui-tailwind/editor-surface/pm-schema.ts +188 -11
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +28 -2
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
- package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
- package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
- package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
- package/src/ui-tailwind/theme/editor-theme.css +1 -0
- package/src/ui-tailwind/theme/tokens.css +6 -0
- package/src/ui-tailwind/theme/tokens.ts +10 -0
- package/src/validation/compatibility-engine.ts +2 -0
- package/src/validation/docx-comment-proof.ts +12 -3
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane 6d — Slice N10 (P12.1 + P12.2): pure SVG shape renderer.
|
|
3
|
+
*
|
|
4
|
+
* Maps the V2c.5 `kind: "shape"` surface segment onto inline SVG markup
|
|
5
|
+
* for the most common geometries (rect / ellipse / roundRect). Pure —
|
|
6
|
+
* no DOM access, no React; returns a plain markup string the toDOM can
|
|
7
|
+
* embed via attrs. Returns `null` for unsupported geometries so the
|
|
8
|
+
* caller falls back to the existing chip render.
|
|
9
|
+
*
|
|
10
|
+
* v1 fill coverage: solid (srgbClr) → `#${hex}`; solid (schemeClr) →
|
|
11
|
+
* `currentColor` placeholder until Lane 6a theme resolver wires through;
|
|
12
|
+
* none → `none`. Gradient + pattern fills are dropped at the surface
|
|
13
|
+
* boundary (V2c.5 surface only carries solid/none) — they remain on the
|
|
14
|
+
* canonical model for a future renderer extension.
|
|
15
|
+
*
|
|
16
|
+
* v1 line coverage: width = `widthEmu / 9525` px; color same handling as
|
|
17
|
+
* fill; `noLine: true` → `stroke: "none"`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { EMU_PER_PX } from "../../runtime/units";
|
|
21
|
+
|
|
22
|
+
const SUPPORTED_GEOMETRIES = new Set(["rect", "ellipse", "roundRect"]);
|
|
23
|
+
|
|
24
|
+
export type ShapeFill =
|
|
25
|
+
| { kind: "solid"; color: string; colorType: "srgbClr" | "schemeClr" }
|
|
26
|
+
| { kind: "none" };
|
|
27
|
+
|
|
28
|
+
export type ShapeLine = {
|
|
29
|
+
color?: string;
|
|
30
|
+
widthEmu?: number;
|
|
31
|
+
noLine?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface ResolvedFillCss {
|
|
35
|
+
fill: string;
|
|
36
|
+
/** True when caller should warn about unresolved scheme color. */
|
|
37
|
+
isSchemePlaceholder: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ResolvedLineCss {
|
|
41
|
+
stroke: string;
|
|
42
|
+
strokeWidth: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a shape fill into a CSS color string. schemeClr falls back
|
|
47
|
+
* to `currentColor` until Lane 6a's theme resolver is wired here —
|
|
48
|
+
* `isSchemePlaceholder` flags the placeholder so callers (or follow-up
|
|
49
|
+
* slices) can later substitute the resolved theme color.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveFillCss(fill: ShapeFill | undefined): ResolvedFillCss {
|
|
52
|
+
if (!fill || fill.kind === "none") {
|
|
53
|
+
return { fill: "none", isSchemePlaceholder: false };
|
|
54
|
+
}
|
|
55
|
+
if (fill.colorType === "schemeClr") {
|
|
56
|
+
return { fill: "currentColor", isSchemePlaceholder: true };
|
|
57
|
+
}
|
|
58
|
+
// srgbClr — color is uppercase hex without leading `#`.
|
|
59
|
+
return { fill: `#${fill.color}`, isSchemePlaceholder: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a shape line into stroke + stroke-width values. `noLine: true`
|
|
64
|
+
* collapses to `stroke: "none"` per OOXML semantics. Default stroke
|
|
65
|
+
* width is 1 px when widthEmu is absent or zero.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveLineCss(line: ShapeLine | undefined): ResolvedLineCss {
|
|
68
|
+
if (!line || line.noLine) {
|
|
69
|
+
return { stroke: "none", strokeWidth: 0 };
|
|
70
|
+
}
|
|
71
|
+
// Default stroke (no widthEmu) matches the floor used when widthEmu
|
|
72
|
+
// is set — otherwise a `widthEmu: 0` shape would get a fatter stroke
|
|
73
|
+
// (1 px) than a `widthEmu: 1` shape (0.5 px from the Math.max floor).
|
|
74
|
+
const strokeWidth = line.widthEmu && line.widthEmu > 0
|
|
75
|
+
? Math.max(0.5, line.widthEmu / EMU_PER_PX)
|
|
76
|
+
: 0.5;
|
|
77
|
+
// line.color uses OOXML hex without `#`; "auto" → currentColor.
|
|
78
|
+
const stroke = !line.color || line.color === "auto"
|
|
79
|
+
? "currentColor"
|
|
80
|
+
: `#${line.color}`;
|
|
81
|
+
return { stroke, strokeWidth };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ShapeSegmentLike {
|
|
85
|
+
geometry?: string;
|
|
86
|
+
fill?: ShapeFill;
|
|
87
|
+
line?: ShapeLine;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* PM `DOMOutputSpec`-style array tree for an SVG element. Each inner
|
|
92
|
+
* tuple is `[tag, attrs, ...children]`. Used by ProseMirror's
|
|
93
|
+
* `toDOM` to construct DOM nodes without string parsing.
|
|
94
|
+
*/
|
|
95
|
+
export type SvgSpec = readonly [string, Record<string, string>, ...SvgSpec[]];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render a supported geometry into a PM-compatible DOMOutputSpec tree
|
|
99
|
+
* for an inline `<svg>`. Returns `null` when the geometry is unsupported
|
|
100
|
+
* or the caller did not supply pixel dimensions (a chip-fallback signal).
|
|
101
|
+
*
|
|
102
|
+
* The SVG is sized 1:1 to its container; the wrapper span owns the
|
|
103
|
+
* outer `width:Xpx; height:Ypx`.
|
|
104
|
+
*/
|
|
105
|
+
export function renderShapeSvg(
|
|
106
|
+
segment: ShapeSegmentLike,
|
|
107
|
+
widthPx: number,
|
|
108
|
+
heightPx: number,
|
|
109
|
+
): SvgSpec | null {
|
|
110
|
+
if (!widthPx || !heightPx) return null;
|
|
111
|
+
if (!segment.geometry || !SUPPORTED_GEOMETRIES.has(segment.geometry)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const fillCss = resolveFillCss(segment.fill);
|
|
115
|
+
const lineCss = resolveLineCss(segment.line);
|
|
116
|
+
const sw = lineCss.strokeWidth;
|
|
117
|
+
// Inset the geometry by half the stroke so the stroke paints inside
|
|
118
|
+
// the bounding box (browsers default to centered stroke).
|
|
119
|
+
const insetX = sw / 2;
|
|
120
|
+
const insetY = sw / 2;
|
|
121
|
+
const innerW = Math.max(0, widthPx - sw);
|
|
122
|
+
const innerH = Math.max(0, heightPx - sw);
|
|
123
|
+
|
|
124
|
+
let geometryEl: SvgSpec;
|
|
125
|
+
switch (segment.geometry) {
|
|
126
|
+
case "rect":
|
|
127
|
+
geometryEl = [
|
|
128
|
+
"rect",
|
|
129
|
+
{
|
|
130
|
+
x: String(insetX),
|
|
131
|
+
y: String(insetY),
|
|
132
|
+
width: String(innerW),
|
|
133
|
+
height: String(innerH),
|
|
134
|
+
fill: fillCss.fill,
|
|
135
|
+
stroke: lineCss.stroke,
|
|
136
|
+
"stroke-width": String(sw),
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
break;
|
|
140
|
+
case "roundRect": {
|
|
141
|
+
// Default OOXML preset uses ~10% of the shorter side for corner radius.
|
|
142
|
+
const r = Math.min(innerW, innerH) * 0.1;
|
|
143
|
+
geometryEl = [
|
|
144
|
+
"rect",
|
|
145
|
+
{
|
|
146
|
+
x: String(insetX),
|
|
147
|
+
y: String(insetY),
|
|
148
|
+
width: String(innerW),
|
|
149
|
+
height: String(innerH),
|
|
150
|
+
rx: String(r),
|
|
151
|
+
ry: String(r),
|
|
152
|
+
fill: fillCss.fill,
|
|
153
|
+
stroke: lineCss.stroke,
|
|
154
|
+
"stroke-width": String(sw),
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "ellipse": {
|
|
160
|
+
const cx = widthPx / 2;
|
|
161
|
+
const cy = heightPx / 2;
|
|
162
|
+
const rx = Math.max(0, (widthPx - sw) / 2);
|
|
163
|
+
const ry = Math.max(0, (heightPx - sw) / 2);
|
|
164
|
+
geometryEl = [
|
|
165
|
+
"ellipse",
|
|
166
|
+
{
|
|
167
|
+
cx: String(cx),
|
|
168
|
+
cy: String(cy),
|
|
169
|
+
rx: String(rx),
|
|
170
|
+
ry: String(ry),
|
|
171
|
+
fill: fillCss.fill,
|
|
172
|
+
stroke: lineCss.stroke,
|
|
173
|
+
"stroke-width": String(sw),
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// PM's DOMSerializer parses the tag string for an SVG namespace prefix:
|
|
183
|
+
// "http://www.w3.org/2000/svg svg" → createElementNS(). Setting an
|
|
184
|
+
// `xmlns` *attribute* after createElement() is meaningless — the
|
|
185
|
+
// resulting node is HTMLUnknownElement and won't paint as SVG.
|
|
186
|
+
// Children inherit the namespace, so the geometry tag stays bare.
|
|
187
|
+
return [
|
|
188
|
+
"http://www.w3.org/2000/svg svg",
|
|
189
|
+
{
|
|
190
|
+
viewBox: `0 0 ${widthPx} ${heightPx}`,
|
|
191
|
+
width: String(widthPx),
|
|
192
|
+
height: String(heightPx),
|
|
193
|
+
preserveAspectRatio: "none",
|
|
194
|
+
"aria-hidden": "true",
|
|
195
|
+
},
|
|
196
|
+
geometryEl,
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Whether N10 v1 supports rendering this geometry as SVG. Useful for
|
|
202
|
+
* caller-side branching when the chip fallback should fire.
|
|
203
|
+
*/
|
|
204
|
+
export function isSupportedShapeGeometry(geometry: string | undefined): boolean {
|
|
205
|
+
return geometry !== undefined && SUPPORTED_GEOMETRIES.has(geometry);
|
|
206
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R.4 SurfaceLayer — named shell for the outermost input surface layer.
|
|
3
|
+
* See `docs/plans/lane-1-editing-foundation.md` §R.4.
|
|
4
|
+
*
|
|
5
|
+
* Analogous to LibreOffice `SwWrtShell`. Owns: keyboard resolution, paste/drop
|
|
6
|
+
* routing decisions, composition state. Does NOT own: PM view construction,
|
|
7
|
+
* DOM manipulation, visible selection chrome — those stay in
|
|
8
|
+
* `pm-command-bridge.ts` as a thin plugin factory that forwards events here.
|
|
9
|
+
*
|
|
10
|
+
* The R.4 scope shipped in this first extraction is the TYPED RESULT SHAPE +
|
|
11
|
+
* a testable dispatch seam for the keyboard path. Tests can now exercise
|
|
12
|
+
* "Ctrl+F without React render" by calling `surfaceLayer.resolveKeyDown`
|
|
13
|
+
* directly — no PM boot, no DOM.
|
|
14
|
+
*
|
|
15
|
+
* Follow-up work (not in this slice): migrate `pm-command-bridge.ts`
|
|
16
|
+
* `handleKeyDown` / `handlePaste` / `handleDrop` to call into
|
|
17
|
+
* `surfaceLayer.dispatchPaste` / `surfaceLayer.dispatchDrop` so the bridge
|
|
18
|
+
* becomes a plugin factory with zero dispatch logic.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
resolveSurfaceShortcut,
|
|
23
|
+
resolveShellShortcut,
|
|
24
|
+
type ShellShortcutContext,
|
|
25
|
+
type ShellShortcutResolution,
|
|
26
|
+
type ShortcutKeyInput,
|
|
27
|
+
type SurfaceShortcutContext,
|
|
28
|
+
type SurfaceShortcutResolution,
|
|
29
|
+
} from "../../ui/runtime-shortcut-dispatch";
|
|
30
|
+
|
|
31
|
+
export type SurfaceResult =
|
|
32
|
+
| { kind: "dispatched"; surface: SurfaceShortcutResolution }
|
|
33
|
+
| { kind: "shell"; shell: ShellShortcutResolution }
|
|
34
|
+
| { kind: "pass-through" };
|
|
35
|
+
|
|
36
|
+
export interface SurfaceLayer {
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a keydown event. Consults the shell-level dispatcher first
|
|
39
|
+
* (Ctrl+F / Ctrl+Shift+E / F5 etc.) and falls back to the surface-level
|
|
40
|
+
* dispatcher (typing, arrows, Tab). Returns a typed `SurfaceResult` so
|
|
41
|
+
* callers can switch on the kind without reaching into either
|
|
42
|
+
* dispatcher's specific resolution types.
|
|
43
|
+
*/
|
|
44
|
+
resolveKeyDown(
|
|
45
|
+
input: ShortcutKeyInput,
|
|
46
|
+
shell: ShellShortcutContext,
|
|
47
|
+
surface: SurfaceShortcutContext,
|
|
48
|
+
): SurfaceResult;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default stateless SurfaceLayer instance. Safe to share across runtimes.
|
|
53
|
+
*/
|
|
54
|
+
export const surfaceLayer: SurfaceLayer = {
|
|
55
|
+
resolveKeyDown(input, shell, surface) {
|
|
56
|
+
const shellResolution = resolveShellShortcut(input, shell);
|
|
57
|
+
if (shellResolution.kind !== "none") {
|
|
58
|
+
return { kind: "shell", shell: shellResolution };
|
|
59
|
+
}
|
|
60
|
+
const surfaceResolution = resolveSurfaceShortcut(input, surface);
|
|
61
|
+
if (surfaceResolution.kind !== "none") {
|
|
62
|
+
return { kind: "dispatched", surface: surfaceResolution };
|
|
63
|
+
}
|
|
64
|
+
return { kind: "pass-through" };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -93,6 +93,35 @@ export function TwInlineToken(props: TwInlineTokenProps) {
|
|
|
93
93
|
);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// V2c.5 — shape segment (rect/ellipse/text-box/etc). Lane 6d N10 will
|
|
97
|
+
// upgrade this to proper SVG/HTML shape rendering; for now we render a
|
|
98
|
+
// selectable chip so reviewers can see the shape exists in the doc.
|
|
99
|
+
if (segment.kind === "shape") {
|
|
100
|
+
const isTextBox = Boolean(segment.isTextBox);
|
|
101
|
+
const label = isTextBox && segment.txbxText ? segment.txbxText : segment.label;
|
|
102
|
+
return (
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
tabIndex={-1}
|
|
106
|
+
onMouseDown={(e) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
props.onSelectionChange?.(createSelectionSnapshot(segment.from, segment.to));
|
|
109
|
+
}}
|
|
110
|
+
className={`inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs border-none cursor-pointer ${commentClass} text-secondary bg-surface ${
|
|
111
|
+
selected ? "ring-1 ring-accent/30" : ""
|
|
112
|
+
} ${focusRingClass}`}
|
|
113
|
+
title={segment.detail || segment.label}
|
|
114
|
+
data-segment-kind="shape"
|
|
115
|
+
data-shape-geometry={segment.geometry ?? ""}
|
|
116
|
+
>
|
|
117
|
+
{renderTwCaret(selection, segment.from)}
|
|
118
|
+
<span>{isTextBox ? "□" : "◇"}</span>
|
|
119
|
+
{label}
|
|
120
|
+
{renderTwCaret(selection, segment.to)}
|
|
121
|
+
</button>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
// opaque_inline
|
|
97
126
|
if (segment.kind === "opaque_inline") {
|
|
98
127
|
if (segment.presentation === "quiet-marker") {
|
|
@@ -23,7 +23,13 @@ export function TwSegmentView(props: TwSegmentViewProps) {
|
|
|
23
23
|
const { segment, selection, markupDisplay } = props;
|
|
24
24
|
|
|
25
25
|
// Non-text segments delegate to TwInlineToken
|
|
26
|
-
if (
|
|
26
|
+
if (
|
|
27
|
+
segment.kind === "tab" ||
|
|
28
|
+
segment.kind === "hard_break" ||
|
|
29
|
+
segment.kind === "image" ||
|
|
30
|
+
segment.kind === "opaque_inline" ||
|
|
31
|
+
segment.kind === "shape"
|
|
32
|
+
) {
|
|
27
33
|
return (
|
|
28
34
|
<TwInlineToken
|
|
29
35
|
segment={segment}
|
|
@@ -73,6 +73,7 @@ import type {
|
|
|
73
73
|
WordReviewEditorLayoutFacet,
|
|
74
74
|
} from "../../api/public-types.ts";
|
|
75
75
|
import {
|
|
76
|
+
measureWidgetsViaOffsetChain,
|
|
76
77
|
measureWidgetsViaBoundingRect,
|
|
77
78
|
resolvePageOverlayRects,
|
|
78
79
|
type PageOverlayRect,
|
|
@@ -176,7 +177,10 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
176
177
|
const origin = overlayRootRef.current;
|
|
177
178
|
const pageCount = facet.getPageCount();
|
|
178
179
|
if (origin) {
|
|
179
|
-
const widgets = measureWidgetsViaBoundingRect(scrollRoot, origin
|
|
180
|
+
const widgets = measureWidgetsViaBoundingRect(scrollRoot, origin, {
|
|
181
|
+
pageCount,
|
|
182
|
+
visiblePageIndexRange,
|
|
183
|
+
});
|
|
180
184
|
const originRect = origin.getBoundingClientRect();
|
|
181
185
|
// jsdom + SSR never populate `origin.clientHeight` (no layout
|
|
182
186
|
// pass). Fall back to `originRect.height`, and if that's also
|
|
@@ -194,12 +198,24 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
194
198
|
widgets,
|
|
195
199
|
pageCount,
|
|
196
200
|
scrollHeight,
|
|
201
|
+
visiblePageIndexRange,
|
|
197
202
|
}),
|
|
198
203
|
);
|
|
199
204
|
} else {
|
|
200
|
-
|
|
205
|
+
const widgets = measureWidgetsViaOffsetChain(scrollRoot, {
|
|
206
|
+
pageCount,
|
|
207
|
+
visiblePageIndexRange,
|
|
208
|
+
});
|
|
209
|
+
setRects(
|
|
210
|
+
resolvePageOverlayRects({
|
|
211
|
+
widgets,
|
|
212
|
+
pageCount,
|
|
213
|
+
scrollHeight: scrollRoot.clientHeight,
|
|
214
|
+
visiblePageIndexRange,
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
201
217
|
}
|
|
202
|
-
}, [facet, scrollRoot]);
|
|
218
|
+
}, [facet, scrollRoot, visiblePageIndexRange]);
|
|
203
219
|
|
|
204
220
|
const refreshRects = React.useCallback(() => {
|
|
205
221
|
if (!scrollRoot) {
|
|
@@ -371,14 +387,14 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
371
387
|
}
|
|
372
388
|
style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
|
|
373
389
|
>
|
|
374
|
-
{rects.map((rect
|
|
375
|
-
const page = facet.getPage(pageIndex);
|
|
390
|
+
{rects.map((rect) => {
|
|
391
|
+
const page = facet.getPage(rect.pageIndex);
|
|
376
392
|
if (!page) return null;
|
|
377
393
|
return (
|
|
378
394
|
<TwPageChromeEntry
|
|
379
395
|
key={`page-chrome-${rect.pageId}`}
|
|
380
396
|
rect={rect}
|
|
381
|
-
pageIndex={pageIndex}
|
|
397
|
+
pageIndex={rect.pageIndex}
|
|
382
398
|
page={page}
|
|
383
399
|
facet={facet}
|
|
384
400
|
activeStory={activeStory}
|
|
@@ -159,7 +159,7 @@ function CommentThreadCard(props: {
|
|
|
159
159
|
const showExcerpt = Boolean(thread.excerpt) && !isDraftThread && thread.excerpt !== "Empty thread";
|
|
160
160
|
|
|
161
161
|
const scrollRef = useCallback(
|
|
162
|
-
(node:
|
|
162
|
+
(node: HTMLButtonElement | null) => {
|
|
163
163
|
if (node && isActive && typeof node.scrollIntoView === "function") {
|
|
164
164
|
node.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
165
165
|
}
|
|
@@ -168,14 +168,13 @@ function CommentThreadCard(props: {
|
|
|
168
168
|
);
|
|
169
169
|
|
|
170
170
|
return (
|
|
171
|
-
<
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
172
173
|
ref={scrollRef}
|
|
173
174
|
data-comment-thread-id={thread.commentId}
|
|
174
175
|
data-comment-thread-status={thread.status}
|
|
175
|
-
role="button"
|
|
176
|
-
tabIndex={0}
|
|
177
176
|
className={[
|
|
178
|
-
"cursor-pointer rounded-lg bg-surface/90 px-3 py-2.5 transition-colors ring-1 ring-border
|
|
177
|
+
"w-full text-left cursor-pointer rounded-lg bg-surface/90 px-3 py-2.5 transition-colors ring-1 ring-border",
|
|
179
178
|
focusRingClass,
|
|
180
179
|
isActive
|
|
181
180
|
? "bg-accent-soft/40 ring-accent/25 shadow-[var(--shadow-soft)]"
|
|
@@ -185,12 +184,6 @@ function CommentThreadCard(props: {
|
|
|
185
184
|
: "",
|
|
186
185
|
].join(" ")}
|
|
187
186
|
onClick={() => props.onOpenComment?.(thread)}
|
|
188
|
-
onKeyDown={(event) => {
|
|
189
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
190
|
-
event.preventDefault();
|
|
191
|
-
props.onOpenComment?.(thread);
|
|
192
|
-
}
|
|
193
|
-
}}
|
|
194
187
|
>
|
|
195
188
|
{/* Header row: avatar + author + date + status */}
|
|
196
189
|
<div className="mb-1.5 flex items-center gap-1.5">
|
|
@@ -260,7 +253,7 @@ function CommentThreadCard(props: {
|
|
|
260
253
|
{thread.entries.slice(1).map((entry) => {
|
|
261
254
|
const replyPresentation = replyPresentationByEntryId.get(entry.entryId);
|
|
262
255
|
return (
|
|
263
|
-
<div key={entry.entryId} className="mt-2 ml-4 border-l border-border
|
|
256
|
+
<div key={entry.entryId} className="mt-2 ml-4 border-l border-border pl-2.5">
|
|
264
257
|
<div className="mb-0.5 flex items-center gap-1">
|
|
265
258
|
<span className="text-[9px] font-medium text-secondary">{entry.authorId}</span>
|
|
266
259
|
<span className="text-[9px] text-tertiary">{formatCommentDate(entry.createdAt)}</span>
|
|
@@ -321,7 +314,7 @@ function CommentThreadCard(props: {
|
|
|
321
314
|
<span className="text-[9px] text-comment">Detached</span>
|
|
322
315
|
)}
|
|
323
316
|
</div>
|
|
324
|
-
</
|
|
317
|
+
</button>
|
|
325
318
|
);
|
|
326
319
|
}
|
|
327
320
|
|
|
@@ -338,7 +331,8 @@ function InlineEditableBody(props: {
|
|
|
338
331
|
useEffect(() => {
|
|
339
332
|
if (isEditing && textareaRef.current) {
|
|
340
333
|
textareaRef.current.focus();
|
|
341
|
-
textareaRef.current.
|
|
334
|
+
const len = textareaRef.current.value.length;
|
|
335
|
+
textareaRef.current.setSelectionRange(len, len);
|
|
342
336
|
}
|
|
343
337
|
}, [isEditing]);
|
|
344
338
|
|
|
@@ -367,7 +361,7 @@ function InlineEditableBody(props: {
|
|
|
367
361
|
) : null}
|
|
368
362
|
<textarea
|
|
369
363
|
ref={textareaRef}
|
|
370
|
-
className="w-full resize-none rounded-md bg-surface px-2 py-1.5 text-[10px] leading-4 text-primary placeholder:text-tertiary focus:outline-none focus:ring-1 focus:ring-accent ring-1 ring-border
|
|
364
|
+
className="w-full resize-none rounded-md bg-surface px-2 py-1.5 text-[10px] leading-4 text-primary placeholder:text-tertiary focus:outline-none focus:ring-1 focus:ring-accent ring-1 ring-border"
|
|
371
365
|
rows={2}
|
|
372
366
|
value={draft}
|
|
373
367
|
placeholder="Type your comment..."
|
|
@@ -428,7 +422,7 @@ function ReplyInput(props: { commentId: string; onAddReply: (commentId: string,
|
|
|
428
422
|
<div className="w-full mt-1.5" onClick={(e) => e.stopPropagation()}>
|
|
429
423
|
<textarea
|
|
430
424
|
ref={inputRef}
|
|
431
|
-
className="w-full resize-none rounded bg-surface px-2 py-1 text-[11px] text-primary placeholder:text-tertiary focus:outline-none focus:ring-1 focus:ring-accent ring-1 ring-border
|
|
425
|
+
className="w-full resize-none rounded bg-surface px-2 py-1 text-[11px] text-primary placeholder:text-tertiary focus:outline-none focus:ring-1 focus:ring-accent ring-1 ring-border"
|
|
432
426
|
rows={2}
|
|
433
427
|
placeholder="Reply..."
|
|
434
428
|
value={body}
|
|
@@ -187,28 +187,3 @@ export function TwHealthPanel(props: TwHealthPanelProps) {
|
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
function FeatureClassBadge(props: { featureClass: CompatibilityFeatureEntry["featureClass"] }) {
|
|
191
|
-
const styles: Record<string, string> = {
|
|
192
|
-
"supported-roundtrip":
|
|
193
|
-
"text-[var(--color-semantic-success)] bg-[var(--color-semantic-success-soft)]",
|
|
194
|
-
"preserve-only":
|
|
195
|
-
"text-[var(--color-semantic-warning)] bg-[var(--color-semantic-warning-soft)]",
|
|
196
|
-
"unsupported-fatal":
|
|
197
|
-
"text-[var(--color-semantic-error)] bg-[var(--color-semantic-error-soft)]",
|
|
198
|
-
};
|
|
199
|
-
const labels: Record<string, string> = {
|
|
200
|
-
"supported-roundtrip": "supported",
|
|
201
|
-
"preserve-only": "preserve-only",
|
|
202
|
-
"unsupported-fatal": "blocked",
|
|
203
|
-
};
|
|
204
|
-
return (
|
|
205
|
-
<span
|
|
206
|
-
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${styles[props.featureClass] ?? ""}`}
|
|
207
|
-
>
|
|
208
|
-
{labels[props.featureClass]}
|
|
209
|
-
</span>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Keep FeatureClassBadge exported for potential external use
|
|
214
|
-
export { FeatureClassBadge };
|
|
@@ -23,6 +23,13 @@ export type RailCardTone =
|
|
|
23
23
|
|
|
24
24
|
export interface RailCardAvatar {
|
|
25
25
|
initials: string;
|
|
26
|
+
/**
|
|
27
|
+
* Author-specific avatar background color. Caller-provided CSS color string
|
|
28
|
+
* (hex, rgb, hsl, or named). Intentionally bypasses the design token system:
|
|
29
|
+
* author identity colors must remain stable across light/dark themes and
|
|
30
|
+
* across workspace re-brands. Callers should provide a color with sufficient
|
|
31
|
+
* contrast against white `initials` text (WCAG AA 4.5:1).
|
|
32
|
+
*/
|
|
26
33
|
color?: string;
|
|
27
34
|
alt?: string;
|
|
28
35
|
}
|
|
@@ -80,28 +87,14 @@ export function TwRailCard(props: TwRailCardProps) {
|
|
|
80
87
|
} = props;
|
|
81
88
|
|
|
82
89
|
const handleClick = onClick || onSelect;
|
|
83
|
-
const tag: "article" | "button" = handleClick ? "button" : "article";
|
|
84
90
|
|
|
85
91
|
const clamped = progress
|
|
86
92
|
? Math.max(0, Math.min(1, progress.total && progress.total > 0 ? progress.value / progress.total : progress.value))
|
|
87
93
|
: 0;
|
|
88
94
|
|
|
89
|
-
const
|
|
90
|
-
className: `wre-rail-card block w-full text-left${isFocused ? " ring-1 ring-[var(--color-accent-primary)]/30" : ""}`,
|
|
91
|
-
"data-tone": tone,
|
|
92
|
-
"data-active": isActive ? "true" : "false",
|
|
93
|
-
"data-focused": isFocused ? "true" : undefined,
|
|
94
|
-
"data-testid": dataTestId,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
if (handleClick) {
|
|
98
|
-
commonProps.onClick = handleClick;
|
|
99
|
-
commonProps.type = "button";
|
|
100
|
-
}
|
|
95
|
+
const sharedClassName = `wre-rail-card block w-full text-left${isFocused ? " ring-1 ring-[var(--color-accent-primary)]/30" : ""}`;
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
tag,
|
|
104
|
-
commonProps,
|
|
97
|
+
const cardChildren = (
|
|
105
98
|
<>
|
|
106
99
|
{counter ? (
|
|
107
100
|
<span className="wre-rail-card__counter" aria-label={counter.label} title={counter.label}>
|
|
@@ -153,6 +146,34 @@ export function TwRailCard(props: TwRailCardProps) {
|
|
|
153
146
|
/>
|
|
154
147
|
</span>
|
|
155
148
|
) : null}
|
|
156
|
-
|
|
149
|
+
</>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (handleClick) {
|
|
153
|
+
return (
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
className={sharedClassName}
|
|
157
|
+
data-tone={tone}
|
|
158
|
+
data-active={isActive ? "true" : "false"}
|
|
159
|
+
data-focused={isFocused ? "true" : undefined}
|
|
160
|
+
data-testid={dataTestId}
|
|
161
|
+
onClick={handleClick}
|
|
162
|
+
>
|
|
163
|
+
{cardChildren}
|
|
164
|
+
</button>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<article
|
|
170
|
+
className={sharedClassName}
|
|
171
|
+
data-tone={tone}
|
|
172
|
+
data-active={isActive ? "true" : "false"}
|
|
173
|
+
data-focused={isFocused ? "true" : undefined}
|
|
174
|
+
data-testid={dataTestId}
|
|
175
|
+
>
|
|
176
|
+
{cardChildren}
|
|
177
|
+
</article>
|
|
157
178
|
);
|
|
158
179
|
}
|
|
@@ -157,8 +157,8 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
157
157
|
<Tabs.List
|
|
158
158
|
className={
|
|
159
159
|
editorial
|
|
160
|
-
? "flex shrink-0 items-center gap-2 border-b border-border
|
|
161
|
-
: "flex shrink-0 border-b border-border
|
|
160
|
+
? "flex shrink-0 items-center gap-2 border-b border-border px-2"
|
|
161
|
+
: "flex shrink-0 border-b border-border px-3 py-2"
|
|
162
162
|
}
|
|
163
163
|
style={
|
|
164
164
|
editorial
|
|
@@ -120,18 +120,11 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
120
120
|
const isActive = activeRevisionId === rev.revisionId;
|
|
121
121
|
|
|
122
122
|
return (
|
|
123
|
-
<
|
|
123
|
+
<button
|
|
124
124
|
key={rev.revisionId}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
className={`flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border/40 ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
125
|
+
type="button"
|
|
126
|
+
className={`w-full text-left flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
128
127
|
onClick={() => props.onOpenRevision?.(rev)}
|
|
129
|
-
onKeyDown={(event) => {
|
|
130
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
131
|
-
event.preventDefault();
|
|
132
|
-
props.onOpenRevision?.(rev);
|
|
133
|
-
}
|
|
134
|
-
}}
|
|
135
128
|
>
|
|
136
129
|
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
137
130
|
rev.kind === "insertion" ? "bg-insert"
|
|
@@ -189,12 +182,12 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
189
182
|
)}
|
|
190
183
|
</div>
|
|
191
184
|
</div>
|
|
192
|
-
</
|
|
185
|
+
</button>
|
|
193
186
|
);
|
|
194
187
|
})}
|
|
195
188
|
</div>
|
|
196
189
|
) : (
|
|
197
|
-
<p className="rounded-lg bg-surface px-3 py-4 text-xs text-tertiary ring-1 ring-border
|
|
190
|
+
<p className="rounded-lg bg-surface px-3 py-4 text-xs text-tertiary ring-1 ring-border">
|
|
198
191
|
{trackedChanges.totalCount > 0
|
|
199
192
|
? (visibleRevisions.length > 0
|
|
200
193
|
? "No revisions match the current filter."
|
|
@@ -54,7 +54,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
54
54
|
if (uniqueSegments.length === 0) {
|
|
55
55
|
return (
|
|
56
56
|
<div
|
|
57
|
-
className="wre-workflow-tab-empty rounded-md border border-dashed border-border
|
|
57
|
+
className="wre-workflow-tab-empty rounded-md border border-dashed border-border bg-canvas/50 p-4 text-[11px] text-tertiary"
|
|
58
58
|
data-testid="workflow-tab-empty"
|
|
59
59
|
>
|
|
60
60
|
<div className="font-semibold uppercase tracking-[0.1em] text-secondary">
|
|
@@ -82,7 +82,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
82
82
|
<button
|
|
83
83
|
key={segment.scopeId}
|
|
84
84
|
type="button"
|
|
85
|
-
className={`wre-workflow-card flex flex-col gap-1 rounded-md border border-border
|
|
85
|
+
className={`wre-workflow-card flex flex-col gap-1 rounded-md border border-border bg-canvas p-3 text-left transition-shadow hover:shadow-md ${
|
|
86
86
|
isActive ? "ring-1 ring-accent/60" : ""
|
|
87
87
|
}`}
|
|
88
88
|
onClick={() => {
|
|
@@ -91,6 +91,9 @@
|
|
|
91
91
|
--color-change-comment: #E8F4EC;
|
|
92
92
|
--color-change-selection: #DDF1E4;
|
|
93
93
|
|
|
94
|
+
/* Highlight (user-driven content, stable across themes — §3 cell-fill + text-highlight family) */
|
|
95
|
+
--color-highlight-default: #FFF59D;
|
|
96
|
+
|
|
94
97
|
/* Chart — categorical */
|
|
95
98
|
--color-chart-categorical-1: #1F6B4F;
|
|
96
99
|
--color-chart-categorical-2: #72D6AE;
|
|
@@ -236,6 +239,9 @@
|
|
|
236
239
|
--color-change-comment: #21342A;
|
|
237
240
|
--color-change-selection: #294235;
|
|
238
241
|
|
|
242
|
+
/* Highlight (dimmed to avoid glare; same chroma family as light-mode yellow) */
|
|
243
|
+
--color-highlight-default: #B8A829;
|
|
244
|
+
|
|
239
245
|
/* Chart — categorical */
|
|
240
246
|
--color-chart-categorical-1: #53B487;
|
|
241
247
|
--color-chart-categorical-2: #9AE7C7;
|