@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
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import * as React from "react";
|
|
23
|
+
import { X } from "lucide-react";
|
|
23
24
|
import type {
|
|
24
25
|
EditorRole,
|
|
25
26
|
IssueMetadataValue,
|
|
@@ -148,13 +149,25 @@ export const TwScopeCard: React.FC<TwScopeCardProps> = ({
|
|
|
148
149
|
const headerId = React.useId();
|
|
149
150
|
const liveRegionId = React.useId();
|
|
150
151
|
|
|
151
|
-
//
|
|
152
|
+
// Cached focusable list — populated on mount and on subtree mutations so
|
|
153
|
+
// Tab handler never calls getFocusable() per-keystroke (perf §7).
|
|
154
|
+
const focusablesRef = React.useRef<HTMLElement[]>([]);
|
|
155
|
+
|
|
156
|
+
// --- Focus management + focusable cache init ----------------------------
|
|
152
157
|
React.useEffect(() => {
|
|
153
158
|
const root = rootRef.current;
|
|
154
159
|
if (!root) return undefined;
|
|
155
|
-
|
|
160
|
+
|
|
161
|
+
const refresh = () => { focusablesRef.current = getFocusable(root); };
|
|
162
|
+
refresh();
|
|
163
|
+
|
|
164
|
+
const observer = new MutationObserver(refresh);
|
|
165
|
+
observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["disabled", "hidden", "inert"] });
|
|
166
|
+
|
|
167
|
+
const first = focusablesRef.current[0];
|
|
156
168
|
first?.focus();
|
|
157
|
-
|
|
169
|
+
|
|
170
|
+
return () => observer.disconnect();
|
|
158
171
|
}, []);
|
|
159
172
|
|
|
160
173
|
// --- Escape + click-outside ---------------------------------------------
|
|
@@ -183,18 +196,17 @@ export const TwScopeCard: React.FC<TwScopeCardProps> = ({
|
|
|
183
196
|
};
|
|
184
197
|
}, [onClose, pinned]);
|
|
185
198
|
|
|
186
|
-
// --- Focus trap
|
|
199
|
+
// --- Focus trap (reads cached list — no DOM query per Tab) --------------
|
|
187
200
|
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
188
201
|
if (event.key !== "Tab") return;
|
|
189
|
-
const
|
|
190
|
-
if (!root) return;
|
|
191
|
-
const focusables = getFocusable(root);
|
|
202
|
+
const focusables = focusablesRef.current;
|
|
192
203
|
if (focusables.length === 0) return;
|
|
193
204
|
const first = focusables[0];
|
|
194
205
|
const last = focusables[focusables.length - 1];
|
|
195
206
|
const active = document.activeElement;
|
|
207
|
+
const root = rootRef.current;
|
|
196
208
|
if (event.shiftKey) {
|
|
197
|
-
if (active === first || !root
|
|
209
|
+
if (active === first || !root?.contains(active)) {
|
|
198
210
|
event.preventDefault();
|
|
199
211
|
last.focus();
|
|
200
212
|
}
|
|
@@ -266,7 +278,7 @@ export const TwScopeCard: React.FC<TwScopeCardProps> = ({
|
|
|
266
278
|
onClick={onClose}
|
|
267
279
|
data-testid="scope-card-close"
|
|
268
280
|
>
|
|
269
|
-
|
|
281
|
+
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
|
270
282
|
</button>
|
|
271
283
|
</div>
|
|
272
284
|
</div>
|
|
@@ -614,9 +626,17 @@ function formatRelative(isoString: string): string {
|
|
|
614
626
|
function getFocusable(root: HTMLElement): HTMLElement[] {
|
|
615
627
|
const selector =
|
|
616
628
|
'button:not([disabled]), a[href], [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])';
|
|
617
|
-
return Array.from(root.querySelectorAll<HTMLElement>(selector)).filter(
|
|
618
|
-
(el
|
|
619
|
-
|
|
629
|
+
return Array.from(root.querySelectorAll<HTMLElement>(selector)).filter((el) => {
|
|
630
|
+
if (el.hasAttribute("inert")) return false;
|
|
631
|
+
// Walk the ancestor chain for visibility/display — tolerates display:contents.
|
|
632
|
+
let node: HTMLElement | null = el;
|
|
633
|
+
while (node && node !== root) {
|
|
634
|
+
const s = getComputedStyle(node);
|
|
635
|
+
if (s.display === "none" || s.visibility === "hidden") return false;
|
|
636
|
+
node = node.parentElement;
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
});
|
|
620
640
|
}
|
|
621
641
|
|
|
622
642
|
function posturePresentationLabel(posture: ScopeCardModel["posture"]): string {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane 6d — Slice N8: TOC outline sidebar (P11.8).
|
|
3
|
+
*
|
|
4
|
+
* Renders the formal TOC field entries from `getTocSnapshot()` as a
|
|
5
|
+
* collapsible left panel. Unlike the heading-based nav panel (which
|
|
6
|
+
* uses `DocumentNavigationSnapshot.headings`), this sidebar reflects the
|
|
7
|
+
* actual `<w:sdt>` / `TOC` field content — complete with the document
|
|
8
|
+
* author's chosen heading levels, custom styles, and page numbers.
|
|
9
|
+
*
|
|
10
|
+
* Navigation: clicking an entry calls `onNavigate(entry)`. The host
|
|
11
|
+
* is responsible for resolving `entry.anchor` or `entry.bookmarkName`
|
|
12
|
+
* into a runtime scroll (typically via `runtime.setSelection` or
|
|
13
|
+
* `onNavigateHeading(entry.headingId)`).
|
|
14
|
+
*
|
|
15
|
+
* `status === "stale"` shows a banner so the reviewer knows the TOC
|
|
16
|
+
* needs a refresh before page numbers are reliable.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import React from "react";
|
|
20
|
+
import type { TocSnapshot, TocEntrySnapshot } from "../../api/public-types";
|
|
21
|
+
|
|
22
|
+
export interface TwTocOutlineSidebarProps {
|
|
23
|
+
/** `null` when the document has no TOC field. */
|
|
24
|
+
tocSnapshot: TocSnapshot | null;
|
|
25
|
+
/** Called when the user clicks a TOC entry. */
|
|
26
|
+
onNavigate: (entry: TocEntrySnapshot) => void;
|
|
27
|
+
/** Whether the sidebar is currently open. */
|
|
28
|
+
open: boolean;
|
|
29
|
+
/** Called when the close button is pressed. */
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Indentation step per outline level (px). Levels start at 1 so
|
|
35
|
+
* level-1 gets 0 extra indentation; level-2 gets 12 px, etc.
|
|
36
|
+
*/
|
|
37
|
+
const INDENT_PX_PER_LEVEL = 12;
|
|
38
|
+
|
|
39
|
+
export function TwTocOutlineSidebar({
|
|
40
|
+
tocSnapshot,
|
|
41
|
+
onNavigate,
|
|
42
|
+
open,
|
|
43
|
+
onClose,
|
|
44
|
+
}: TwTocOutlineSidebarProps): React.ReactElement | null {
|
|
45
|
+
if (!open) return null;
|
|
46
|
+
|
|
47
|
+
const entries = tocSnapshot?.entries ?? [];
|
|
48
|
+
const isStale = tocSnapshot?.status === "stale";
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<aside
|
|
52
|
+
aria-label="Table of contents"
|
|
53
|
+
data-toc-sidebar=""
|
|
54
|
+
className="flex h-full w-56 shrink-0 flex-col border-r border-border bg-surface"
|
|
55
|
+
>
|
|
56
|
+
{/* Header */}
|
|
57
|
+
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
|
58
|
+
<span className="text-xs font-medium uppercase tracking-wider text-secondary">
|
|
59
|
+
Table of Contents
|
|
60
|
+
</span>
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
aria-label="Close table of contents"
|
|
64
|
+
data-toc-close=""
|
|
65
|
+
onClick={onClose}
|
|
66
|
+
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface-hover"
|
|
67
|
+
>
|
|
68
|
+
{/* × */}
|
|
69
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
70
|
+
<path d="M1 1l10 10M11 1L1 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Stale banner */}
|
|
76
|
+
{isStale ? (
|
|
77
|
+
<div
|
|
78
|
+
role="status"
|
|
79
|
+
data-toc-stale=""
|
|
80
|
+
className="border-b border-border bg-[var(--color-warning-soft,theme(colors.amber.50))] px-3 py-1.5 text-[11px] text-[var(--color-warning,theme(colors.amber.700))]"
|
|
81
|
+
>
|
|
82
|
+
Table of contents may be out of date.
|
|
83
|
+
</div>
|
|
84
|
+
) : null}
|
|
85
|
+
|
|
86
|
+
{/* Entry list */}
|
|
87
|
+
<nav
|
|
88
|
+
className="flex-1 overflow-y-auto px-2 py-2"
|
|
89
|
+
aria-label="Document outline"
|
|
90
|
+
>
|
|
91
|
+
{entries.length > 0 ? (
|
|
92
|
+
<ul className="space-y-px">
|
|
93
|
+
{entries.map((entry) => {
|
|
94
|
+
const indent = (entry.level - 1) * INDENT_PX_PER_LEVEL;
|
|
95
|
+
return (
|
|
96
|
+
<li key={entry.tocEntryId}>
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
data-toc-entry-id={entry.tocEntryId}
|
|
100
|
+
data-toc-level={entry.level}
|
|
101
|
+
className="flex w-full items-baseline gap-1 rounded-md px-2 py-1 text-left text-xs text-primary transition-colors hover:bg-surface-hover"
|
|
102
|
+
style={{ paddingLeft: `${8 + indent}px` }}
|
|
103
|
+
onClick={() => onNavigate(entry)}
|
|
104
|
+
>
|
|
105
|
+
<span className="flex-1 truncate">{entry.text}</span>
|
|
106
|
+
{entry.pageIndex != null ? (
|
|
107
|
+
<span
|
|
108
|
+
aria-label={`page ${entry.pageIndex + 1}`}
|
|
109
|
+
className="shrink-0 tabular-nums text-tertiary"
|
|
110
|
+
>
|
|
111
|
+
{entry.pageIndex + 1}
|
|
112
|
+
</span>
|
|
113
|
+
) : null}
|
|
114
|
+
</button>
|
|
115
|
+
</li>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</ul>
|
|
119
|
+
) : tocSnapshot === null ? (
|
|
120
|
+
<p className="px-2 py-4 text-xs text-tertiary">
|
|
121
|
+
No table of contents found in this document.
|
|
122
|
+
</p>
|
|
123
|
+
) : (
|
|
124
|
+
<p className="px-2 py-4 text-xs text-tertiary">
|
|
125
|
+
The table of contents is empty.
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
</nav>
|
|
129
|
+
</aside>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default TwTocOutlineSidebar;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import React from "react";
|
|
15
|
+
import { flushSync } from "react-dom";
|
|
15
16
|
import { createRoot, type Root } from "react-dom/client";
|
|
16
17
|
import type { Node as PMNode } from "prosemirror-model";
|
|
17
18
|
import type { NodeViewConstructor } from "prosemirror-view";
|
|
@@ -24,6 +25,7 @@ const DEFAULT_HEIGHT = 336;
|
|
|
24
25
|
class ChartNodeViewInstance {
|
|
25
26
|
readonly dom: HTMLElement;
|
|
26
27
|
private _root: Root | null = null;
|
|
28
|
+
private _mountedChartId: string | null = null;
|
|
27
29
|
|
|
28
30
|
constructor(node: PMNode) {
|
|
29
31
|
this.dom = document.createElement("span");
|
|
@@ -33,41 +35,78 @@ class ChartNodeViewInstance {
|
|
|
33
35
|
this._mount(node);
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Mount or update the React root to render the chart whose id lives in
|
|
40
|
+
* `node.attrs.parsedChartId`. Transitions handled explicitly:
|
|
41
|
+
*
|
|
42
|
+
* - id present, entry in store → create/reuse root, render ChartSurface.
|
|
43
|
+
* - id present, entry missing → unmount any prior root (don't leave a
|
|
44
|
+
* stale chart from a previous id mounted).
|
|
45
|
+
* - id null / undefined → unmount any prior root (no chart).
|
|
46
|
+
*
|
|
47
|
+
* The previous implementation handled only the first case and silently
|
|
48
|
+
* left the prior React tree mounted on null / missing-entry transitions.
|
|
49
|
+
*/
|
|
36
50
|
private _mount(node: PMNode): void {
|
|
37
|
-
const parsedChartId = node.attrs.parsedChartId as string | null;
|
|
38
|
-
|
|
51
|
+
const parsedChartId = (node.attrs.parsedChartId as string | null) ?? null;
|
|
52
|
+
|
|
53
|
+
if (!parsedChartId) {
|
|
54
|
+
this._unmountRoot();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
39
57
|
|
|
40
58
|
const entry = chartModelStore.get(parsedChartId);
|
|
41
|
-
if (!entry)
|
|
59
|
+
if (!entry) {
|
|
60
|
+
this._unmountRoot();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
42
63
|
|
|
43
64
|
const width = (node.attrs.widthPx as number | null) ?? entry.widthPx ?? DEFAULT_WIDTH;
|
|
44
65
|
const height = (node.attrs.heightPx as number | null) ?? entry.heightPx ?? DEFAULT_HEIGHT;
|
|
66
|
+
const previewMediaId = node.attrs.previewMediaId as string | null;
|
|
67
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
45
68
|
|
|
46
69
|
const el = React.createElement(ChartSurface, {
|
|
47
70
|
model: entry.model,
|
|
48
71
|
width,
|
|
49
72
|
height,
|
|
50
73
|
theme: entry.theme,
|
|
74
|
+
...(previewMediaId ? { previewMediaId } : {}),
|
|
75
|
+
...(previewMediaId && previewSrc
|
|
76
|
+
? {
|
|
77
|
+
resolveMediaUrl: (mediaId: string) =>
|
|
78
|
+
mediaId === previewMediaId ? previewSrc : undefined,
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
51
81
|
});
|
|
52
82
|
|
|
53
83
|
if (!this._root) {
|
|
54
84
|
this._root = createRoot(this.dom);
|
|
55
85
|
}
|
|
56
|
-
|
|
86
|
+
flushSync(() => {
|
|
87
|
+
this._root?.render(el);
|
|
88
|
+
});
|
|
89
|
+
this._mountedChartId = parsedChartId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private _unmountRoot(): void {
|
|
93
|
+
if (this._root) {
|
|
94
|
+
this._root.unmount();
|
|
95
|
+
this._root = null;
|
|
96
|
+
}
|
|
97
|
+
this._mountedChartId = null;
|
|
57
98
|
}
|
|
58
99
|
|
|
59
100
|
update(node: PMNode): boolean {
|
|
60
|
-
|
|
61
|
-
|
|
101
|
+
// Handle every `parsedChartId` transition in-place so PM doesn't
|
|
102
|
+
// destroy/recreate this NodeView on attr-only changes. Returning true
|
|
103
|
+
// tells PM "I handled this update."
|
|
62
104
|
this._mount(node);
|
|
63
105
|
return true;
|
|
64
106
|
}
|
|
65
107
|
|
|
66
108
|
destroy(): void {
|
|
67
|
-
|
|
68
|
-
this._root.unmount();
|
|
69
|
-
this._root = null;
|
|
70
|
-
}
|
|
109
|
+
this._unmountRoot();
|
|
71
110
|
}
|
|
72
111
|
|
|
73
112
|
stopEvent(): boolean {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane 6d — Slice N9 (P7.1 + P7.3): pure float-wrap resolver.
|
|
3
|
+
*
|
|
4
|
+
* Maps `SurfaceDrawingAnchor` (the V2c.4-projected DrawingFrame anchor) to
|
|
5
|
+
* a CSS-style descriptor that the `image_atom` / `shape_atom` toDOM can
|
|
6
|
+
* stamp onto the wrapper span. Pure — no DOM access, no React, no schema
|
|
7
|
+
* dependency.
|
|
8
|
+
*
|
|
9
|
+
* v1 coverage (per the lane-6d MVP plan):
|
|
10
|
+
* - `square` → CSS float + `shape-outside: margin-box`
|
|
11
|
+
* - `topAndBottom` → block-level `clear: both`
|
|
12
|
+
* - `none` + `behindDoc` → absolute-positioned, `z-index: -1` (behind text)
|
|
13
|
+
* - `tight` / `through` → fall back to `square` and emit a one-shot
|
|
14
|
+
* warn so telemetry can flag uncovered shapes. v2 will consume
|
|
15
|
+
* `wrapPolygon` data once Lane 3b V2c.x extends `AnchorGeometry`.
|
|
16
|
+
*
|
|
17
|
+
* EMU constant: 9525 EMU = 1 px at 96 dpi (OOXML standard).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { SurfaceDrawingAnchor } from "../../api/public-types";
|
|
21
|
+
import { EMU_PER_PX } from "../../runtime/units";
|
|
22
|
+
|
|
23
|
+
export interface FloatWrapStyle {
|
|
24
|
+
/** CSS `float` value when the anchor wraps inline text. */
|
|
25
|
+
float?: "left" | "right";
|
|
26
|
+
/** CSS `shape-outside` for `square` wrap so adjacent text honors the
|
|
27
|
+
* picture's margin box. */
|
|
28
|
+
shapeOutside?: string;
|
|
29
|
+
/** CSS `clear` — set on `topAndBottom` so the picture renders on its
|
|
30
|
+
* own line. */
|
|
31
|
+
clear?: "both";
|
|
32
|
+
/** Block-level signal for `topAndBottom` so the wrapper switches from
|
|
33
|
+
* inline-block to block. */
|
|
34
|
+
display?: "block";
|
|
35
|
+
/** Absolute positioning — used for `behindDoc` shapes that sit beneath
|
|
36
|
+
* content. */
|
|
37
|
+
positionAbsolute?: { topPx: number; leftPx: number; zIndex: number };
|
|
38
|
+
/** When the resolver fell back to a default for an unsupported wrap
|
|
39
|
+
* mode, this carries the original mode for telemetry. */
|
|
40
|
+
fallbackFromMode?: "tight" | "through";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const warnedFallbacks = new Set<string>();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Emit a one-shot console warning per wrap mode so noisy CCEP corpora
|
|
47
|
+
* don't flood the console but coverage gaps still surface. Skips SSR
|
|
48
|
+
* (no `window`) so server-render passes don't spam logs.
|
|
49
|
+
*/
|
|
50
|
+
function warnFallbackOnce(mode: "tight" | "through"): void {
|
|
51
|
+
if (warnedFallbacks.has(mode)) return;
|
|
52
|
+
warnedFallbacks.add(mode);
|
|
53
|
+
if (typeof window === "undefined") return;
|
|
54
|
+
// Quiet by default — only fires once per mode per session.
|
|
55
|
+
console.warn(
|
|
56
|
+
`[lane-6d/N9] wrapMode="${mode}" rendered as "square" v1 fallback (polygon clipping pending Lane 3b V2c.x).`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reset the fallback warn dedup map. Test-only helper — production code
|
|
62
|
+
* never calls this.
|
|
63
|
+
*/
|
|
64
|
+
export function _resetFloatWrapWarnings(): void {
|
|
65
|
+
warnedFallbacks.clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pure resolver: anchor → CSS-style descriptor. Returns `null` when
|
|
70
|
+
* the anchor is inline (no float treatment needed) or has no wrapMode.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveFloatStyle(
|
|
73
|
+
anchor: SurfaceDrawingAnchor | undefined,
|
|
74
|
+
): FloatWrapStyle | null {
|
|
75
|
+
if (!anchor) return null;
|
|
76
|
+
if (anchor.display !== "floating") return null;
|
|
77
|
+
|
|
78
|
+
switch (anchor.wrapMode) {
|
|
79
|
+
case "square": {
|
|
80
|
+
const float = anchor.positionH?.align === "right" ? "right" : "left";
|
|
81
|
+
return { float, shapeOutside: "margin-box" };
|
|
82
|
+
}
|
|
83
|
+
case "topAndBottom":
|
|
84
|
+
return { clear: "both", display: "block" };
|
|
85
|
+
case "tight":
|
|
86
|
+
warnFallbackOnce("tight");
|
|
87
|
+
return {
|
|
88
|
+
float: anchor.positionH?.align === "right" ? "right" : "left",
|
|
89
|
+
shapeOutside: "margin-box",
|
|
90
|
+
fallbackFromMode: "tight",
|
|
91
|
+
};
|
|
92
|
+
case "through":
|
|
93
|
+
warnFallbackOnce("through");
|
|
94
|
+
return {
|
|
95
|
+
float: anchor.positionH?.align === "right" ? "right" : "left",
|
|
96
|
+
shapeOutside: "margin-box",
|
|
97
|
+
fallbackFromMode: "through",
|
|
98
|
+
};
|
|
99
|
+
case "none": {
|
|
100
|
+
// `none` + behindDoc → absolute positioned underneath the text.
|
|
101
|
+
// `none` + !behindDoc → above text (in front). Either way we use
|
|
102
|
+
// absolute positioning when offsets are present.
|
|
103
|
+
if (
|
|
104
|
+
anchor.positionH?.offset !== undefined ||
|
|
105
|
+
anchor.positionV?.offset !== undefined
|
|
106
|
+
) {
|
|
107
|
+
return {
|
|
108
|
+
positionAbsolute: {
|
|
109
|
+
topPx: Math.round((anchor.positionV?.offset ?? 0) / EMU_PER_PX),
|
|
110
|
+
leftPx: Math.round((anchor.positionH?.offset ?? 0) / EMU_PER_PX),
|
|
111
|
+
zIndex: anchor.behindDoc ? -1 : 1,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// `none` with no offsets is rare — treat as inline-overlap.
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|