@beyondwork/docx-react-component 1.0.58 → 1.0.59
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/README.md +2 -2
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +978 -10
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +72 -42
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +159 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +476 -34
- package/src/runtime/document-search.ts +115 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +5 -8
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +290 -21
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* fill; `noLine: true` → `stroke: "none"`.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { EMU_PER_PX } from "../../runtime/units";
|
|
20
|
+
import { EMU_PER_PX, ROTATION_UNITS_PER_DEGREE } from "../../runtime/units";
|
|
21
21
|
|
|
22
22
|
const SUPPORTED_GEOMETRIES = new Set(["rect", "ellipse", "roundRect"]);
|
|
23
23
|
|
|
@@ -81,9 +81,18 @@ export function resolveLineCss(line: ShapeLine | undefined): ResolvedLineCss {
|
|
|
81
81
|
return { stroke, strokeWidth };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export type GradientFill = {
|
|
85
|
+
kind: "gradient";
|
|
86
|
+
stops: Array<{ pos: number; color: string; colorType: "srgbClr" | "schemeClr" }>;
|
|
87
|
+
direction:
|
|
88
|
+
| { kind: "linear"; angle: number; scaled?: boolean }
|
|
89
|
+
| { kind: "path"; path: "circle" | "rect" | "shape" };
|
|
90
|
+
rotWithShape?: boolean;
|
|
91
|
+
};
|
|
92
|
+
|
|
84
93
|
export interface ShapeSegmentLike {
|
|
85
94
|
geometry?: string;
|
|
86
|
-
fill?: ShapeFill;
|
|
95
|
+
fill?: ShapeFill | GradientFill;
|
|
87
96
|
line?: ShapeLine;
|
|
88
97
|
}
|
|
89
98
|
|
|
@@ -94,6 +103,44 @@ export interface ShapeSegmentLike {
|
|
|
94
103
|
*/
|
|
95
104
|
export type SvgSpec = readonly [string, Record<string, string>, ...SvgSpec[]];
|
|
96
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Build an SVG `<defs>` element containing a `<linearGradient>` or
|
|
108
|
+
* `<radialGradient>` for the given gradient fill. `id` is the gradient
|
|
109
|
+
* element ID referenced via `fill="url(#id)"` on the geometry element.
|
|
110
|
+
*
|
|
111
|
+
* OOXML linear gradient angle: 60000ths of a degree, clockwise from north.
|
|
112
|
+
* Converted to SVG objectBoundingBox coordinates via:
|
|
113
|
+
* x1 = 0.5 − 0.5·sin(θ), y1 = 0.5 + 0.5·cos(θ)
|
|
114
|
+
* x2 = 0.5 + 0.5·sin(θ), y2 = 0.5 − 0.5·cos(θ)
|
|
115
|
+
* where θ is in radians.
|
|
116
|
+
*
|
|
117
|
+
* OOXML stop pos: 0–100000 (= 0–100%).
|
|
118
|
+
*/
|
|
119
|
+
function renderGradientDefs(fill: GradientFill, id: string): SvgSpec {
|
|
120
|
+
const stopEls: SvgSpec[] = fill.stops.map(
|
|
121
|
+
(s): SvgSpec => [
|
|
122
|
+
"stop",
|
|
123
|
+
{
|
|
124
|
+
offset: `${(s.pos / 1000).toFixed(2)}%`,
|
|
125
|
+
"stop-color": s.colorType === "srgbClr" ? `#${s.color}` : "currentColor",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
let gradEl: SvgSpec;
|
|
131
|
+
if (fill.direction.kind === "linear") {
|
|
132
|
+
const rad = (fill.direction.angle / ROTATION_UNITS_PER_DEGREE) * (Math.PI / 180);
|
|
133
|
+
const x1 = (0.5 - 0.5 * Math.sin(rad)).toFixed(4);
|
|
134
|
+
const y1 = (0.5 + 0.5 * Math.cos(rad)).toFixed(4);
|
|
135
|
+
const x2 = (0.5 + 0.5 * Math.sin(rad)).toFixed(4);
|
|
136
|
+
const y2 = (0.5 - 0.5 * Math.cos(rad)).toFixed(4);
|
|
137
|
+
gradEl = ["linearGradient", { id, x1, y1, x2, y2, gradientUnits: "objectBoundingBox" }, ...stopEls];
|
|
138
|
+
} else {
|
|
139
|
+
gradEl = ["radialGradient", { id, cx: "50%", cy: "50%", r: "50%", gradientUnits: "objectBoundingBox" }, ...stopEls];
|
|
140
|
+
}
|
|
141
|
+
return ["defs", {}, gradEl];
|
|
142
|
+
}
|
|
143
|
+
|
|
97
144
|
/**
|
|
98
145
|
* Render a supported geometry into a PM-compatible DOMOutputSpec tree
|
|
99
146
|
* for an inline `<svg>`. Returns `null` when the geometry is unsupported
|
|
@@ -101,6 +148,9 @@ export type SvgSpec = readonly [string, Record<string, string>, ...SvgSpec[]];
|
|
|
101
148
|
*
|
|
102
149
|
* The SVG is sized 1:1 to its container; the wrapper span owns the
|
|
103
150
|
* outer `width:Xpx; height:Ypx`.
|
|
151
|
+
*
|
|
152
|
+
* Gradient fills: emits `<defs><linearGradient>` / `<radialGradient>` and
|
|
153
|
+
* references it via `fill="url(#g0)"` on the geometry element.
|
|
104
154
|
*/
|
|
105
155
|
export function renderShapeSvg(
|
|
106
156
|
segment: ShapeSegmentLike,
|
|
@@ -111,7 +161,19 @@ export function renderShapeSvg(
|
|
|
111
161
|
if (!segment.geometry || !SUPPORTED_GEOMETRIES.has(segment.geometry)) {
|
|
112
162
|
return null;
|
|
113
163
|
}
|
|
114
|
-
|
|
164
|
+
|
|
165
|
+
let fillAttr: string;
|
|
166
|
+
let gradDefs: SvgSpec | null = null;
|
|
167
|
+
if (segment.fill && segment.fill.kind === "gradient") {
|
|
168
|
+
gradDefs = renderGradientDefs(segment.fill, "g0");
|
|
169
|
+
fillAttr = "url(#g0)";
|
|
170
|
+
} else {
|
|
171
|
+
fillAttr = resolveFillCss(segment.fill as ShapeFill | undefined).fill;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Keep resolveFillCss call for non-gradient so the isSchemePlaceholder
|
|
175
|
+
// warning path (future) still fires when needed.
|
|
176
|
+
const fillCss = { fill: fillAttr, isSchemePlaceholder: false };
|
|
115
177
|
const lineCss = resolveLineCss(segment.line);
|
|
116
178
|
const sw = lineCss.strokeWidth;
|
|
117
179
|
// Inset the geometry by half the stroke so the stroke paints inside
|
|
@@ -184,17 +246,17 @@ export function renderShapeSvg(
|
|
|
184
246
|
// `xmlns` *attribute* after createElement() is meaningless — the
|
|
185
247
|
// resulting node is HTMLUnknownElement and won't paint as SVG.
|
|
186
248
|
// Children inherit the namespace, so the geometry tag stays bare.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
];
|
|
249
|
+
const svgAttrs = {
|
|
250
|
+
viewBox: `0 0 ${widthPx} ${heightPx}`,
|
|
251
|
+
width: String(widthPx),
|
|
252
|
+
height: String(heightPx),
|
|
253
|
+
preserveAspectRatio: "none",
|
|
254
|
+
"aria-hidden": "true",
|
|
255
|
+
};
|
|
256
|
+
if (gradDefs) {
|
|
257
|
+
return ["http://www.w3.org/2000/svg svg", svgAttrs, gradDefs, geometryEl];
|
|
258
|
+
}
|
|
259
|
+
return ["http://www.w3.org/2000/svg svg", svgAttrs, geometryEl];
|
|
198
260
|
}
|
|
199
261
|
|
|
200
262
|
/**
|
|
@@ -71,6 +71,20 @@ export function resolveMarkerJustificationCss(raw: string | undefined): string {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function resolveMarkerAlignCss(raw: string | undefined): React.CSSProperties["textAlign"] {
|
|
75
|
+
switch (raw) {
|
|
76
|
+
case "left":
|
|
77
|
+
return "left";
|
|
78
|
+
case "center":
|
|
79
|
+
return "center";
|
|
80
|
+
case "right":
|
|
81
|
+
case "both":
|
|
82
|
+
case "distribute":
|
|
83
|
+
default:
|
|
84
|
+
return "right";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
/** Build CSSProperties for a paragraph block from spacing/indent/alignment. */
|
|
75
89
|
export function buildParagraphStyle(
|
|
76
90
|
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
@@ -144,11 +158,12 @@ export function buildMarkerStyle(
|
|
|
144
158
|
suffix: "tab" | "space" | "nothing" | undefined,
|
|
145
159
|
markerRunProperties: CanonicalRunFormatting | undefined,
|
|
146
160
|
markerWidth: number | undefined,
|
|
161
|
+
markerStart: number | undefined,
|
|
147
162
|
markerJustification: string | undefined,
|
|
148
163
|
): React.CSSProperties {
|
|
149
164
|
const style: React.CSSProperties = {
|
|
150
165
|
fontVariantNumeric: "tabular-nums",
|
|
151
|
-
|
|
166
|
+
textAlign: resolveMarkerAlignCss(markerJustification),
|
|
152
167
|
};
|
|
153
168
|
|
|
154
169
|
if (markerRunProperties) {
|
|
@@ -180,8 +195,10 @@ export function buildMarkerStyle(
|
|
|
180
195
|
style.width = `${markerWidthPt}pt`;
|
|
181
196
|
style.minWidth = `${markerWidthPt}pt`;
|
|
182
197
|
style.flexBasis = `${markerWidthPt}pt`;
|
|
198
|
+
style.marginLeft = `-${markerWidthPt}pt`;
|
|
183
199
|
style.marginRight = 0;
|
|
184
200
|
style.overflow = "visible";
|
|
201
|
+
void markerStart; // consumed via paragraph padding-left geometry
|
|
185
202
|
} else {
|
|
186
203
|
const fallbackMinWidth = Math.min(Math.max(prefix.length + 1, 4), 14);
|
|
187
204
|
const fallbackMarginRight =
|
|
@@ -142,6 +142,7 @@ function ParagraphBlock({
|
|
|
142
142
|
const resolvedNumbering = block.resolvedNumbering;
|
|
143
143
|
const markerRunProperties = resolvedNumbering?.markerRunProperties;
|
|
144
144
|
const markerWidth = resolvedNumbering?.geometry?.markerLane?.width;
|
|
145
|
+
const markerStart = resolvedNumbering?.geometry?.markerLane?.start;
|
|
145
146
|
const markerJustification = resolvedNumbering?.geometry?.markerJustification;
|
|
146
147
|
|
|
147
148
|
const prefixSpan =
|
|
@@ -164,6 +165,7 @@ function ParagraphBlock({
|
|
|
164
165
|
numberingSuffix,
|
|
165
166
|
markerRunProperties,
|
|
166
167
|
markerWidth,
|
|
168
|
+
markerStart,
|
|
167
169
|
markerJustification,
|
|
168
170
|
)}
|
|
169
171
|
>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Node as PMNode } from "prosemirror-model";
|
|
12
12
|
import type { NodeViewConstructor, ViewMutationRecord } from "prosemirror-view";
|
|
13
|
+
import { PERCENTAGE_PARTS } from "../../runtime/units.ts";
|
|
13
14
|
|
|
14
15
|
// R2c: band class styles live in ./tw-table-bands.module.css. Consumers import
|
|
15
16
|
// that stylesheet through their build pipeline (same pattern as editor-theme.css).
|
|
@@ -325,7 +326,7 @@ function applyTableAttrs(table: HTMLTableElement, node: PMNode): void {
|
|
|
325
326
|
const tableWidthType = node.attrs.tableWidthType as string | null | undefined;
|
|
326
327
|
let baseClasses = "border-collapse w-full my-2 text-sm";
|
|
327
328
|
if (tableWidthType === "pct" && typeof tableWidth === "number") {
|
|
328
|
-
// OOXML pct widths are fiftieths of a percent (
|
|
329
|
+
// OOXML pct widths are fiftieths of a percent (PERCENTAGE_PARTS = 100%).
|
|
329
330
|
table.style.width = `${tableWidth / 50}%`;
|
|
330
331
|
baseClasses = "border-collapse my-2 text-sm";
|
|
331
332
|
} else if (tableWidthType === "dxa" && typeof tableWidth === "number") {
|
|
@@ -406,6 +407,14 @@ function syncColgroup(table: HTMLTableElement, node: PMNode): void {
|
|
|
406
407
|
const gridColumns = Array.isArray(node.attrs.gridColumns)
|
|
407
408
|
? (node.attrs.gridColumns as number[])
|
|
408
409
|
: [];
|
|
410
|
+
// SOW gap G1 — percent widths win when the table itself is sized in
|
|
411
|
+
// percent. The relative array sums to 100 and comes from
|
|
412
|
+
// `computeRelativeGridColumns` in surface-projection so the column
|
|
413
|
+
// proportions track the container instead of the absolute `pt` widths
|
|
414
|
+
// sliding against it. `null` (the default) keeps the legacy pt path.
|
|
415
|
+
const gridColumnsRelative = Array.isArray(node.attrs.gridColumnsRelative)
|
|
416
|
+
? (node.attrs.gridColumnsRelative as number[])
|
|
417
|
+
: null;
|
|
409
418
|
const existing = Array.from(table.children).find(
|
|
410
419
|
(child): child is HTMLTableColElement =>
|
|
411
420
|
child instanceof (table.ownerDocument?.defaultView?.HTMLTableColElement ??
|
|
@@ -429,12 +438,19 @@ function syncColgroup(table: HTMLTableElement, node: PMNode): void {
|
|
|
429
438
|
while (colgroup.childElementCount > desired) {
|
|
430
439
|
colgroup.lastElementChild?.remove();
|
|
431
440
|
}
|
|
441
|
+
const usePct =
|
|
442
|
+
gridColumnsRelative !== null && gridColumnsRelative.length === desired;
|
|
432
443
|
for (let i = 0; i < desired; i += 1) {
|
|
433
444
|
const col = colgroup.children[i] as HTMLTableColElement;
|
|
434
445
|
const twips = gridColumns[i] ?? 0;
|
|
435
446
|
col.setAttribute("data-col-index", String(i));
|
|
436
447
|
col.setAttribute("data-col-twips", String(twips));
|
|
437
|
-
|
|
448
|
+
if (usePct) {
|
|
449
|
+
const pct = gridColumnsRelative[i] ?? 0;
|
|
450
|
+
col.style.width = pct > 0 ? `${pct.toFixed(4)}%` : "";
|
|
451
|
+
} else {
|
|
452
|
+
col.style.width = twips > 0 ? `${twips / 20}pt` : "";
|
|
453
|
+
}
|
|
438
454
|
}
|
|
439
455
|
|
|
440
456
|
if (!existing) {
|
package/src/ui-tailwind/index.ts
CHANGED
|
@@ -67,6 +67,15 @@ export {
|
|
|
67
67
|
type CommandPaletteItem,
|
|
68
68
|
type TwCommandPaletteProps,
|
|
69
69
|
} from "./chrome/tw-command-palette";
|
|
70
|
+
export {
|
|
71
|
+
TwCommandPaletteMount,
|
|
72
|
+
type TwCommandPaletteMountProps,
|
|
73
|
+
} from "./chrome/tw-command-palette-mount";
|
|
74
|
+
export {
|
|
75
|
+
useContainerBreakpoint,
|
|
76
|
+
resolveBreakpoint,
|
|
77
|
+
type BreakpointMap,
|
|
78
|
+
} from "./chrome/use-container-breakpoint";
|
|
70
79
|
|
|
71
80
|
// Collab chrome (P9) — mount when chromePreset === "collab"; each
|
|
72
81
|
// component is pure presentational and takes snapshots + callbacks.
|
|
@@ -3,6 +3,16 @@ import type {
|
|
|
3
3
|
PageLayoutSnapshot,
|
|
4
4
|
SurfaceBlockSnapshot,
|
|
5
5
|
} from "../api/public-types.ts";
|
|
6
|
+
import { findPageForOffset } from "../runtime/document-navigation.ts";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP,
|
|
9
|
+
estimateBlockHeight,
|
|
10
|
+
estimateParagraphLineCount,
|
|
11
|
+
estimateParagraphLineHeight,
|
|
12
|
+
getUsableColumnWidth,
|
|
13
|
+
} from "../runtime/page-layout-estimation.ts";
|
|
14
|
+
|
|
15
|
+
const DOCUMENT_CONTENT_TOP_PADDING_PX = 40;
|
|
6
16
|
|
|
7
17
|
export interface LineMarker {
|
|
8
18
|
id: string;
|
|
@@ -14,14 +24,76 @@ export function computeLineMarkersIfEnabled(input: {
|
|
|
14
24
|
pageLayout: PageLayoutSnapshot | undefined;
|
|
15
25
|
surfaceBlocks: readonly SurfaceBlockSnapshot[];
|
|
16
26
|
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>;
|
|
17
|
-
buildLineNumberMarkers: (
|
|
18
|
-
blocks: readonly SurfaceBlockSnapshot[],
|
|
19
|
-
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
|
|
20
|
-
) => LineMarker[];
|
|
21
27
|
}): LineMarker[] {
|
|
22
28
|
if (!input.pageLayout?.lineNumbering) {
|
|
23
29
|
return [];
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
return
|
|
32
|
+
return buildLineNumberMarkers(input.surfaceBlocks, input.pages);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildLineNumberMarkers(
|
|
36
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
37
|
+
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
|
|
38
|
+
): LineMarker[] {
|
|
39
|
+
const markers: LineMarker[] = [];
|
|
40
|
+
if (pages.length === 0) {
|
|
41
|
+
return markers;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let currentTopTwips = 0;
|
|
45
|
+
let lineNumber = 1;
|
|
46
|
+
let lastPageIndex = -1;
|
|
47
|
+
let lastSectionIndex = -1;
|
|
48
|
+
|
|
49
|
+
for (const block of blocks) {
|
|
50
|
+
const pageIndex = findPageForOffset(pages, block.from);
|
|
51
|
+
const page = pages[pageIndex];
|
|
52
|
+
if (!page) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lineNumbering = page.layout.lineNumbering;
|
|
57
|
+
const restartMode = lineNumbering?.restart ?? "newPage";
|
|
58
|
+
const restartStart = lineNumbering?.start ?? 1;
|
|
59
|
+
const countBy = Math.max(1, lineNumbering?.countBy ?? 1);
|
|
60
|
+
const columnWidth = getUsableColumnWidth(page.layout);
|
|
61
|
+
|
|
62
|
+
if (pageIndex !== lastPageIndex) {
|
|
63
|
+
if (restartMode === "newPage" || lastPageIndex === -1) {
|
|
64
|
+
lineNumber = restartStart;
|
|
65
|
+
}
|
|
66
|
+
lastPageIndex = pageIndex;
|
|
67
|
+
}
|
|
68
|
+
if (page.sectionIndex !== lastSectionIndex) {
|
|
69
|
+
if (restartMode === "newSection" || lastSectionIndex === -1) {
|
|
70
|
+
lineNumber = restartStart;
|
|
71
|
+
}
|
|
72
|
+
lastSectionIndex = page.sectionIndex;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (block.kind === "paragraph" && lineNumbering) {
|
|
76
|
+
const lineCount = estimateParagraphLineCount(block, columnWidth);
|
|
77
|
+
const lineHeight = estimateParagraphLineHeight(block);
|
|
78
|
+
const suppress = block.suppressLineNumbers === true;
|
|
79
|
+
for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) {
|
|
80
|
+
if (!suppress && (lineNumber - restartStart) % countBy === 0) {
|
|
81
|
+
markers.push({
|
|
82
|
+
id: `${block.blockId}-${lineIndex}`,
|
|
83
|
+
label: String(lineNumber),
|
|
84
|
+
topPx:
|
|
85
|
+
DOCUMENT_CONTENT_TOP_PADDING_PX +
|
|
86
|
+
(currentTopTwips + lineIndex * lineHeight) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!suppress) {
|
|
90
|
+
lineNumber += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
currentTopTwips += estimateBlockHeight(block, columnWidth);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return markers;
|
|
27
99
|
}
|
|
@@ -147,7 +147,7 @@ export interface TwPageStackChromeLayerProps {
|
|
|
147
147
|
"data-testid"?: string;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
151
151
|
facet,
|
|
152
152
|
scrollRoot,
|
|
153
153
|
renderFrameRevision,
|
|
@@ -409,4 +409,59 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
409
409
|
);
|
|
410
410
|
};
|
|
411
411
|
|
|
412
|
+
function storyTargetEqual(
|
|
413
|
+
a: TwPageStackChromeLayerProps["activeStory"],
|
|
414
|
+
b: TwPageStackChromeLayerProps["activeStory"],
|
|
415
|
+
): boolean {
|
|
416
|
+
if (a.kind !== b.kind) return false;
|
|
417
|
+
if (a.kind === "main") return true;
|
|
418
|
+
if (a.kind === "footnote" || a.kind === "endnote") {
|
|
419
|
+
// TS narrows a to { noteId: string }; b shares the same kind (guard above)
|
|
420
|
+
return a.noteId === (b as Extract<typeof b, { noteId: string }>).noteId;
|
|
421
|
+
}
|
|
422
|
+
if (a.kind === "header" || a.kind === "footer") {
|
|
423
|
+
const bh = b as Extract<typeof b, { kind: "header" | "footer" }>;
|
|
424
|
+
return (
|
|
425
|
+
a.relationshipId === bh.relationshipId &&
|
|
426
|
+
a.variant === bh.variant &&
|
|
427
|
+
a.sectionIndex === bh.sectionIndex
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function rangeEqual(
|
|
434
|
+
a: { start: number; end: number } | null | undefined,
|
|
435
|
+
b: { start: number; end: number } | null | undefined,
|
|
436
|
+
): boolean {
|
|
437
|
+
if (a == null && b == null) return true;
|
|
438
|
+
if (a == null || b == null) return false;
|
|
439
|
+
return a.start === b.start && a.end === b.end;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function propsAreEqual(
|
|
443
|
+
prev: TwPageStackChromeLayerProps,
|
|
444
|
+
next: TwPageStackChromeLayerProps,
|
|
445
|
+
): boolean {
|
|
446
|
+
return (
|
|
447
|
+
prev.facet === next.facet &&
|
|
448
|
+
prev.scrollRoot === next.scrollRoot &&
|
|
449
|
+
prev.renderFrameRevision === next.renderFrameRevision &&
|
|
450
|
+
storyTargetEqual(prev.activeStory, next.activeStory) &&
|
|
451
|
+
prev.onOpenStory === next.onOpenStory &&
|
|
452
|
+
prev.pmSurfaceElement === next.pmSurfaceElement &&
|
|
453
|
+
prev.pmView === next.pmView &&
|
|
454
|
+
rangeEqual(prev.visiblePageIndexRange, next.visiblePageIndexRange) &&
|
|
455
|
+
prev["data-testid"] === next["data-testid"]
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export const TwPageStackChromeLayer = React.memo(
|
|
460
|
+
TwPageStackChromeLayerInner,
|
|
461
|
+
propsAreEqual,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
/** Exported for unit testing only. */
|
|
465
|
+
export { propsAreEqual as _propsAreEqualForTest };
|
|
466
|
+
|
|
412
467
|
export default TwPageStackChromeLayer;
|
|
@@ -140,6 +140,7 @@ function RegionParagraph({
|
|
|
140
140
|
const resolvedNumbering = block.resolvedNumbering;
|
|
141
141
|
const markerRunProperties = resolvedNumbering?.markerRunProperties;
|
|
142
142
|
const markerWidth = resolvedNumbering?.geometry?.markerLane?.width;
|
|
143
|
+
const markerStart = resolvedNumbering?.geometry?.markerLane?.start;
|
|
143
144
|
const markerJustification = resolvedNumbering?.geometry?.markerJustification;
|
|
144
145
|
|
|
145
146
|
const prefixSpan =
|
|
@@ -164,6 +165,7 @@ function RegionParagraph({
|
|
|
164
165
|
numberingSuffix,
|
|
165
166
|
markerRunProperties,
|
|
166
167
|
markerWidth,
|
|
168
|
+
markerStart,
|
|
167
169
|
markerJustification,
|
|
168
170
|
)}
|
|
169
171
|
>
|