@beyondwork/docx-react-component 1.0.56 → 1.0.58
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 +1 -1
- package/package.json +1 -1
- package/src/api/public-types.ts +330 -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 +158 -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 +421 -3
- package/src/runtime/chart/chart-model-store.ts +73 -10
- package/src/runtime/document-runtime.ts +760 -41
- package/src/runtime/document-search.ts +61 -0
- 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/query-scopes.ts +186 -0
- package/src/runtime/resolved-numbering-geometry.ts +12 -0
- package/src/runtime/scope-resolver.ts +60 -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 +192 -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 +239 -11
- package/src/ui/editor-runtime-boundary.ts +97 -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 +24 -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-object-selection-overlay.tsx +157 -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 +214 -11
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +32 -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/ui-tailwind/tw-review-workspace.tsx +23 -0
- package/src/validation/compatibility-engine.ts +2 -0
- package/src/validation/docx-comment-proof.ts +12 -3
|
@@ -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;
|
|
@@ -87,6 +87,15 @@ export const BRAND_TOKENS = {
|
|
|
87
87
|
comment: "#E8F4EC",
|
|
88
88
|
selection: "#DDF1E4",
|
|
89
89
|
},
|
|
90
|
+
highlight: {
|
|
91
|
+
// Text-highlighter yellow. User-driven content color (like cell fills
|
|
92
|
+
// and author avatars) — intentionally stable across themes so highlights
|
|
93
|
+
// persist when the document is re-opened under a different theme.
|
|
94
|
+
// Light mode picks Material Amber-A100 for contrast on white canvas;
|
|
95
|
+
// dark mode dims the same chroma (~#B8A829) to avoid glare on dark
|
|
96
|
+
// canvas while keeping the mark recognizably "yellow".
|
|
97
|
+
default: "#FFF59D",
|
|
98
|
+
},
|
|
90
99
|
scopeTint: {
|
|
91
100
|
blocked: "#FBE3E6",
|
|
92
101
|
inScope: "#E2F2E8",
|
|
@@ -170,6 +179,7 @@ export type BrandTokenPath =
|
|
|
170
179
|
| `color.status.${keyof BrandTokens["color"]["status"]}`
|
|
171
180
|
| `color.comment.${keyof BrandTokens["color"]["comment"]}`
|
|
172
181
|
| `color.change.${keyof BrandTokens["color"]["change"]}`
|
|
182
|
+
| `color.highlight.${keyof BrandTokens["color"]["highlight"]}`
|
|
173
183
|
| `color.scopeTint.${keyof BrandTokens["color"]["scopeTint"]}`
|
|
174
184
|
| `color.chart.categorical.${keyof BrandTokens["color"]["chart"]["categorical"]}`
|
|
175
185
|
| `color.chart.sequential.${keyof BrandTokens["color"]["chart"]["sequential"]}`
|
|
@@ -198,6 +198,8 @@ export interface TwReviewWorkspaceProps {
|
|
|
198
198
|
selectionContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
199
199
|
currentScopeContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
200
200
|
commands: EditorCommandBag;
|
|
201
|
+
/** N6 — release the grabbed image/shape. Wired to `runtime.deselectObject()` by the host. */
|
|
202
|
+
onDeselectObject?: () => void;
|
|
201
203
|
activeSelectionTool?: ActiveSelectionToolModel | null;
|
|
202
204
|
selectionToolAnchor?: SelectionToolAnchor | null;
|
|
203
205
|
documentNavigation?: DocumentNavigationSnapshot;
|
|
@@ -680,6 +682,23 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
680
682
|
// above bumps `renderFrameRevision` on the same kinds; including it
|
|
681
683
|
// in the dependency list re-runs this memo without a separate
|
|
682
684
|
// subscription.
|
|
685
|
+
// N6 — resolve grabbed-object segment offsets from the surface so the
|
|
686
|
+
// selection overlay can query the anchor index without a full surface walk.
|
|
687
|
+
const grabbedSegmentOffsets = useMemo(() => {
|
|
688
|
+
const objectId = snapshot.grabbedObjectId ?? null;
|
|
689
|
+
if (!objectId || !snapshot.surface) return null;
|
|
690
|
+
for (const block of snapshot.surface.blocks) {
|
|
691
|
+
if (!("segments" in block)) continue;
|
|
692
|
+
for (const seg of (block as { segments?: unknown[] }).segments ?? []) {
|
|
693
|
+
const s = seg as { kind?: string; mediaId?: string; from?: number; to?: number };
|
|
694
|
+
if ((s.kind === "image" || s.kind === "shape") && s.mediaId === objectId && s.from != null) {
|
|
695
|
+
return { from: s.from, to: s.to ?? s.from + 1 };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}, [snapshot.grabbedObjectId, snapshot.surface]);
|
|
701
|
+
|
|
683
702
|
const statusBarPageFacts = useMemo(() => {
|
|
684
703
|
const facet = props.layoutFacet;
|
|
685
704
|
if (!facet) {
|
|
@@ -1662,6 +1681,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1662
1681
|
{props.layoutFacet ? (
|
|
1663
1682
|
<TwChromeOverlay
|
|
1664
1683
|
facet={props.layoutFacet}
|
|
1684
|
+
grabbedObjectId={snapshot.grabbedObjectId ?? null}
|
|
1685
|
+
grabbedObjectFromOffset={grabbedSegmentOffsets?.from ?? null}
|
|
1686
|
+
grabbedObjectToOffset={grabbedSegmentOffsets?.to ?? null}
|
|
1687
|
+
onDeselectObject={props.onDeselectObject}
|
|
1665
1688
|
tableContext={props.tableContext}
|
|
1666
1689
|
onSetColumnWidth={props.onSetColumnWidth}
|
|
1667
1690
|
onSetRowHeight={props.onSetRowHeight}
|
|
@@ -444,6 +444,7 @@ function measureInlineNode(
|
|
|
444
444
|
flags.hyperlinks = true;
|
|
445
445
|
return node.children.reduce((size, child) => size + measureInlineNode(child, flags), 0);
|
|
446
446
|
case "image":
|
|
447
|
+
case "drawing_frame":
|
|
447
448
|
flags.images = true;
|
|
448
449
|
flags.runs = true;
|
|
449
450
|
return 1;
|
|
@@ -778,6 +779,7 @@ function collectLossyInlineContent(
|
|
|
778
779
|
case "field":
|
|
779
780
|
case "bookmark_start":
|
|
780
781
|
case "bookmark_end":
|
|
782
|
+
case "drawing_frame":
|
|
781
783
|
case "shape":
|
|
782
784
|
case "wordart":
|
|
783
785
|
case "vml_shape":
|
|
@@ -70,6 +70,7 @@ export type ClosureValidationCheck =
|
|
|
70
70
|
| { type: "minCommentThreads"; count: number }
|
|
71
71
|
| { type: "lockedFragmentCountAtLeast"; count: number }
|
|
72
72
|
| { type: "surfaceBlockKind"; kind: string; count?: number }
|
|
73
|
+
| { type: "segmentLabelPrefix"; value: string; kind?: string }
|
|
73
74
|
| { type: "opaqueBlockLabelPrefix"; value: string }
|
|
74
75
|
| { type: "opaqueInlineLabelPrefix"; value: string }
|
|
75
76
|
| {
|
|
@@ -238,6 +239,12 @@ export function evaluateClosureCheck(
|
|
|
238
239
|
(check.count ?? 1),
|
|
239
240
|
reason: `expected at least ${check.count ?? 1} surface blocks of kind ${check.kind}`,
|
|
240
241
|
};
|
|
242
|
+
case "segmentLabelPrefix":
|
|
243
|
+
return {
|
|
244
|
+
type: check.type,
|
|
245
|
+
passed: hasSegmentLabelPrefix(context.surface, check.value, check.kind),
|
|
246
|
+
reason: `expected a ${check.kind ?? "surface"} segment label starting with ${check.value}`,
|
|
247
|
+
};
|
|
241
248
|
case "opaqueBlockLabelPrefix":
|
|
242
249
|
return {
|
|
243
250
|
type: check.type,
|
|
@@ -253,7 +260,7 @@ export function evaluateClosureCheck(
|
|
|
253
260
|
case "opaqueInlineLabelPrefix":
|
|
254
261
|
return {
|
|
255
262
|
type: check.type,
|
|
256
|
-
passed:
|
|
263
|
+
passed: hasSegmentLabelPrefix(context.surface, check.value, "opaque_inline"),
|
|
257
264
|
reason: `expected an opaque inline label starting with ${check.value}`,
|
|
258
265
|
};
|
|
259
266
|
case "trackedChangeMatch": {
|
|
@@ -458,9 +465,10 @@ export function extractDocxCommentProof(bytes: Uint8Array): DocxCommentProof {
|
|
|
458
465
|
};
|
|
459
466
|
}
|
|
460
467
|
|
|
461
|
-
function
|
|
468
|
+
function hasSegmentLabelPrefix(
|
|
462
469
|
surface: RuntimeRenderSnapshot["surface"],
|
|
463
470
|
prefix: string,
|
|
471
|
+
segmentKind?: string,
|
|
464
472
|
): boolean {
|
|
465
473
|
if (!surface) {
|
|
466
474
|
return false;
|
|
@@ -474,7 +482,8 @@ function hasOpaqueInlineLabelPrefix(
|
|
|
474
482
|
if (
|
|
475
483
|
block.segments.some(
|
|
476
484
|
(segment) =>
|
|
477
|
-
segment.kind ===
|
|
485
|
+
(segmentKind === undefined || segment.kind === segmentKind) &&
|
|
486
|
+
"label" in segment &&
|
|
478
487
|
typeof segment.label === "string" &&
|
|
479
488
|
segment.label.startsWith(prefix),
|
|
480
489
|
)
|