@beyondwork/docx-react-component 1.0.58 → 1.0.60
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 +980 -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 +4 -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/workflow-payload.ts +6 -1
- 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 +5 -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 +153 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +230 -0
- package/src/runtime/document-runtime.ts +821 -54
- 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 +108 -10
- 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
|
@@ -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
|
>
|
|
@@ -157,6 +157,16 @@ function CommentThreadCard(props: {
|
|
|
157
157
|
const canEdit = isOwnComment && thread.status === "open" && props.onEditBody != null;
|
|
158
158
|
const hasNoBody = isEmptyCommentBody(leadEntry?.body);
|
|
159
159
|
const showExcerpt = Boolean(thread.excerpt) && !isDraftThread && thread.excerpt !== "Empty thread";
|
|
160
|
+
const threadCardClassName = [
|
|
161
|
+
"rounded-lg bg-surface/90 transition-colors ring-1 ring-border",
|
|
162
|
+
isActive
|
|
163
|
+
? "bg-accent-soft/40 ring-accent/25 shadow-[var(--shadow-soft)]"
|
|
164
|
+
: "hover:bg-surface",
|
|
165
|
+
thread.status === "detached"
|
|
166
|
+
? "border-l-[3px] border-[var(--color-semantic-warning)] opacity-70"
|
|
167
|
+
: "",
|
|
168
|
+
].join(" ");
|
|
169
|
+
const threadContentPaddingClass = thread.status === "detached" ? "pl-2.5 pr-3" : "px-3";
|
|
160
170
|
|
|
161
171
|
const scrollRef = useCallback(
|
|
162
172
|
(node: HTMLButtonElement | null) => {
|
|
@@ -168,130 +178,123 @@ function CommentThreadCard(props: {
|
|
|
168
178
|
);
|
|
169
179
|
|
|
170
180
|
return (
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"w-full
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
: "",
|
|
185
|
-
].join(" ")}
|
|
186
|
-
onClick={() => props.onOpenComment?.(thread)}
|
|
187
|
-
>
|
|
188
|
-
{/* Header row: avatar + author + date + status */}
|
|
189
|
-
<div className="mb-1.5 flex items-center gap-1.5">
|
|
190
|
-
<span className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-subtle text-[8px] font-semibold text-secondary">
|
|
191
|
-
{thread.createdBy.charAt(0).toUpperCase()}
|
|
192
|
-
</span>
|
|
193
|
-
<span className="truncate text-[10px] font-medium text-primary">{thread.createdBy}</span>
|
|
194
|
-
{thread.status === "detached" && (
|
|
195
|
-
<span
|
|
196
|
-
data-comment-thread-detached-chip="true"
|
|
197
|
-
className="inline-flex items-center rounded-full bg-[var(--color-semantic-warning-soft)] text-[var(--color-semantic-warning)] text-[9px] font-semibold uppercase tracking-[0.08em] px-1.5 py-0.5 ml-1.5"
|
|
198
|
-
>
|
|
199
|
-
Detached
|
|
181
|
+
<div data-comment-thread-card={thread.commentId} className={threadCardClassName}>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
ref={scrollRef}
|
|
185
|
+
data-comment-thread-id={thread.commentId}
|
|
186
|
+
data-comment-thread-status={thread.status}
|
|
187
|
+
className={["w-full cursor-pointer pb-1 pt-2.5 text-left", threadContentPaddingClass, focusRingClass].join(" ")}
|
|
188
|
+
onClick={() => props.onOpenComment?.(thread)}
|
|
189
|
+
>
|
|
190
|
+
{/* Header row: avatar + author + date + status */}
|
|
191
|
+
<div className="mb-1.5 flex items-center gap-1.5">
|
|
192
|
+
<span className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-subtle text-[8px] font-semibold text-secondary">
|
|
193
|
+
{thread.createdBy.charAt(0).toUpperCase()}
|
|
200
194
|
</span>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
195
|
+
<span className="truncate text-[10px] font-medium text-primary">{thread.createdBy}</span>
|
|
196
|
+
{thread.status === "detached" && (
|
|
197
|
+
<span
|
|
198
|
+
data-comment-thread-detached-chip="true"
|
|
199
|
+
className="ml-1.5 inline-flex items-center rounded-full bg-[var(--color-semantic-warning-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[var(--color-semantic-warning)]"
|
|
200
|
+
>
|
|
201
|
+
Detached
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
<span data-comment-thread-created-at="true" className="text-[9px] text-tertiary">
|
|
205
|
+
{formatCommentDate(thread.createdAt)}
|
|
206
|
+
</span>
|
|
207
|
+
<span className="flex-1" />
|
|
208
|
+
{isDraftThread ? <StatusBadge label="draft" tone="draft" /> : null}
|
|
209
|
+
{thread.status === "resolved" ? <StatusBadge label="resolved" tone="resolved" /> : null}
|
|
210
|
+
</div>
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
{/* Excerpt — anchored text from document */}
|
|
213
|
+
{showExcerpt ? (
|
|
214
|
+
<p className="mb-1.5 rounded-md bg-comment-soft px-2 py-1 text-[9px] italic leading-4 text-secondary whitespace-pre-wrap break-words line-clamp-2">
|
|
215
|
+
{thread.excerpt}
|
|
216
|
+
</p>
|
|
217
|
+
) : null}
|
|
216
218
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
219
|
+
{/* Comment body */}
|
|
220
|
+
{canEdit && (isActive || hasNoBody) ? (
|
|
221
|
+
<InlineEditableBody
|
|
222
|
+
body={leadEntry?.body ?? ""}
|
|
223
|
+
autoFocus={isActive && hasNoBody}
|
|
224
|
+
onSave={(newBody) => props.onEditBody?.(thread.commentId, newBody)}
|
|
225
|
+
label={isDraftThread ? "New comment" : undefined}
|
|
226
|
+
/>
|
|
227
|
+
) : presentation ? (
|
|
228
|
+
<CommentMarkdownRenderer
|
|
229
|
+
body={presentation.body}
|
|
230
|
+
mentions={presentation.mentions}
|
|
231
|
+
attachments={presentation.attachments}
|
|
232
|
+
resolveAttachmentHref={resolveAttachmentHref}
|
|
233
|
+
className="text-[10px] leading-[1.1rem] text-secondary break-words"
|
|
234
|
+
/>
|
|
235
|
+
) : leadEntry?.body ? (
|
|
236
|
+
<p
|
|
237
|
+
className="text-[10px] leading-[1.1rem] text-secondary whitespace-pre-wrap break-words line-clamp-4"
|
|
238
|
+
data-comment-thread-body="true"
|
|
239
|
+
>
|
|
240
|
+
{leadEntry.body}
|
|
241
|
+
</p>
|
|
242
|
+
) : canEdit ? (
|
|
243
|
+
<p
|
|
244
|
+
className="cursor-text text-[10px] italic text-tertiary"
|
|
245
|
+
onClick={(e) => {
|
|
246
|
+
e.stopPropagation();
|
|
247
|
+
props.onOpenComment?.(thread);
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
New comment
|
|
251
|
+
</p>
|
|
252
|
+
) : null}
|
|
251
253
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
{/* Reply entries (compact) */}
|
|
255
|
+
{thread.entries.slice(1).map((entry) => {
|
|
256
|
+
const replyPresentation = replyPresentationByEntryId.get(entry.entryId);
|
|
257
|
+
return (
|
|
258
|
+
<div key={entry.entryId} className="mt-2 ml-4 border-l border-border pl-2.5">
|
|
259
|
+
<div className="mb-0.5 flex items-center gap-1">
|
|
260
|
+
<span className="text-[9px] font-medium text-secondary">{entry.authorId}</span>
|
|
261
|
+
<span className="text-[9px] text-tertiary">{formatCommentDate(entry.createdAt)}</span>
|
|
262
|
+
</div>
|
|
263
|
+
{replyPresentation ? (
|
|
264
|
+
<CommentMarkdownRenderer
|
|
265
|
+
body={replyPresentation.body}
|
|
266
|
+
mentions={presentation?.mentions}
|
|
267
|
+
attachments={presentation?.attachments}
|
|
268
|
+
resolveAttachmentHref={resolveAttachmentHref}
|
|
269
|
+
className="text-[10px] leading-4 text-secondary break-words"
|
|
270
|
+
/>
|
|
271
|
+
) : (
|
|
272
|
+
<p
|
|
273
|
+
className="text-[10px] leading-4 text-secondary whitespace-pre-wrap break-words line-clamp-3"
|
|
274
|
+
data-comment-reply-body="true"
|
|
275
|
+
>
|
|
276
|
+
{entry.body}
|
|
277
|
+
</p>
|
|
278
|
+
)}
|
|
260
279
|
</div>
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
body={replyPresentation.body}
|
|
264
|
-
mentions={presentation?.mentions}
|
|
265
|
-
attachments={presentation?.attachments}
|
|
266
|
-
resolveAttachmentHref={resolveAttachmentHref}
|
|
267
|
-
className="text-[10px] leading-4 text-secondary break-words"
|
|
268
|
-
/>
|
|
269
|
-
) : (
|
|
270
|
-
<p
|
|
271
|
-
className="text-[10px] leading-4 text-secondary whitespace-pre-wrap break-words line-clamp-3"
|
|
272
|
-
data-comment-reply-body="true"
|
|
273
|
-
>
|
|
274
|
-
{entry.body}
|
|
275
|
-
</p>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
);
|
|
279
|
-
})}
|
|
280
|
+
);
|
|
281
|
+
})}
|
|
280
282
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
{thread.entryCount > thread.entries.length ? (
|
|
284
|
+
<p className="mt-1 text-[9px] text-tertiary">
|
|
285
|
+
+{thread.entryCount - thread.entries.length} more
|
|
286
|
+
</p>
|
|
287
|
+
) : null}
|
|
288
|
+
</button>
|
|
286
289
|
|
|
287
290
|
{/* Inline actions — compact, horizontal */}
|
|
288
|
-
<div className="mt-2 flex items-center gap-1">
|
|
291
|
+
<div className={["mt-2 flex items-center gap-1 pb-2.5", threadContentPaddingClass].join(" ")}>
|
|
289
292
|
{thread.status === "open" && (
|
|
290
293
|
<>
|
|
291
294
|
<button
|
|
292
295
|
type="button"
|
|
293
296
|
className="inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[9px] font-medium text-accent hover:bg-accent-soft transition-colors"
|
|
294
|
-
onClick={(
|
|
297
|
+
onClick={() => props.onResolveComment?.(thread.commentId)}
|
|
295
298
|
>
|
|
296
299
|
<Check className="h-2 w-2" /> Resolve
|
|
297
300
|
</button>
|
|
@@ -305,7 +308,7 @@ function CommentThreadCard(props: {
|
|
|
305
308
|
type="button"
|
|
306
309
|
className="inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[9px] font-medium text-secondary hover:bg-surface-hover transition-colors"
|
|
307
310
|
data-comment-thread-action="reopen"
|
|
308
|
-
onClick={(
|
|
311
|
+
onClick={() => props.onReopenComment?.(thread.commentId)}
|
|
309
312
|
>
|
|
310
313
|
<RotateCcw className="h-2 w-2" /> Reopen
|
|
311
314
|
</button>
|
|
@@ -314,7 +317,7 @@ function CommentThreadCard(props: {
|
|
|
314
317
|
<span className="text-[9px] text-comment">Detached</span>
|
|
315
318
|
)}
|
|
316
319
|
</div>
|
|
317
|
-
</
|
|
320
|
+
</div>
|
|
318
321
|
);
|
|
319
322
|
}
|
|
320
323
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { HelpCircle, Search } from "lucide-react";
|
|
3
|
+
import { FOCUS_RING_CLASSES } from "../theme/tokens";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Thin pinned footer rendered at the bottom of the review rail. The footer
|
|
@@ -14,8 +15,7 @@ export interface TwReviewRailFooterProps {
|
|
|
14
15
|
searchLabel?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const focusRingClass =
|
|
18
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-canvas";
|
|
18
|
+
const focusRingClass = FOCUS_RING_CLASSES;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Accept only http(s) and mailto help links. Rejects javascript:, data:,
|
|
@@ -273,6 +273,20 @@ export const HOST_LOCKED_TOKENS = [
|
|
|
273
273
|
export type HostOverridableToken = (typeof HOST_OVERRIDABLE_TOKENS)[number];
|
|
274
274
|
export type HostLockedToken = (typeof HOST_LOCKED_TOKENS)[number];
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Canonical focus-visible ring class string (designsystem §4.7 / §7.2).
|
|
278
|
+
*
|
|
279
|
+
* Every interactive chrome surface that renders a custom focus indicator
|
|
280
|
+
* MUST import this constant rather than inline the Tailwind utilities.
|
|
281
|
+
* The invariant is enforced by `test/ui-tailwind/focus-ring-canonical.test.ts`.
|
|
282
|
+
*
|
|
283
|
+
* Exceptions: input / textarea elements that use a 1-px border ring as the
|
|
284
|
+
* idle-state affordance (e.g. `tw-comment-sidebar` reply composer) are
|
|
285
|
+
* a distinct pattern and not covered by this utility.
|
|
286
|
+
*/
|
|
287
|
+
export const FOCUS_RING_CLASSES =
|
|
288
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
289
|
+
|
|
276
290
|
/** Returns true if `path` is in the locked set (must not be overridden by hosts). */
|
|
277
291
|
export function isTokenPathLocked(path: string): boolean {
|
|
278
292
|
return (HOST_LOCKED_TOKENS as readonly string[]).includes(path);
|
|
@@ -131,6 +131,11 @@ export function TwShellHeader(props: TwShellHeaderProps): React.ReactElement {
|
|
|
131
131
|
key={mode.id}
|
|
132
132
|
value={mode.id}
|
|
133
133
|
disabled={mode.disabled}
|
|
134
|
+
onClick={() => {
|
|
135
|
+
if (!mode.disabled) {
|
|
136
|
+
props.onModeChange?.(mode.id);
|
|
137
|
+
}
|
|
138
|
+
}}
|
|
134
139
|
className={`wre-rail-tab ${focusRingClass}`}
|
|
135
140
|
data-testid={`tw-shell-header__mode-${mode.id}`}
|
|
136
141
|
>
|