@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwCommandPaletteMount — ready-to-use mount adapter that wires
|
|
3
|
+
* `TwCommandPalette` + `isCommandPaletteOpenShortcut` together so a
|
|
4
|
+
* host only needs to supply the command graph.
|
|
5
|
+
*
|
|
6
|
+
* design-close-chrome Phase 3 / designsystem §6.25.
|
|
7
|
+
*
|
|
8
|
+
* Why a separate mount component:
|
|
9
|
+
* - `TwCommandPalette` is intentionally pure — no global state, no
|
|
10
|
+
* key subscription. That keeps it testable and composable.
|
|
11
|
+
* - Most hosts want the Ctrl+K / Cmd+K binding, open-state
|
|
12
|
+
* management, and click-off-to-close plumbing out of the box.
|
|
13
|
+
* - This adapter pairs them with a single `groups` prop. Opt-out
|
|
14
|
+
* is trivial: use `TwCommandPalette` directly with your own open
|
|
15
|
+
* state + key handler.
|
|
16
|
+
*
|
|
17
|
+
* The adapter:
|
|
18
|
+
* - Subscribes to window keydown with `isCommandPaletteOpenShortcut`
|
|
19
|
+
* (only when not already open, to let the palette own Escape/Enter).
|
|
20
|
+
* - Calls `event.preventDefault()` so Ctrl+K does not bubble to the
|
|
21
|
+
* browser (avoids the URL-bar focus on macOS Chrome).
|
|
22
|
+
* - Honors a `disabled` flag so hosts can suppress the binding while
|
|
23
|
+
* another modal captures input.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React, { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
TwCommandPalette,
|
|
30
|
+
isCommandPaletteOpenShortcut,
|
|
31
|
+
type CommandPaletteGroup,
|
|
32
|
+
type TwCommandPaletteProps,
|
|
33
|
+
} from "./tw-command-palette";
|
|
34
|
+
|
|
35
|
+
export interface TwCommandPaletteMountProps
|
|
36
|
+
extends Omit<TwCommandPaletteProps, "open" | "onOpenChange"> {
|
|
37
|
+
/** Command graph supplied by the host. */
|
|
38
|
+
groups: readonly CommandPaletteGroup[];
|
|
39
|
+
/**
|
|
40
|
+
* When `true`, the global Ctrl+K / Cmd+K listener is suppressed so
|
|
41
|
+
* a higher-priority modal keeps keyboard focus. Defaults to `false`.
|
|
42
|
+
*/
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Optional: fires when the palette's open state transitions. Callers
|
|
46
|
+
* can use this to emit analytics or to coordinate focus with other
|
|
47
|
+
* chrome surfaces.
|
|
48
|
+
*/
|
|
49
|
+
onOpenChange?: (open: boolean) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Optional: the initial open state. Defaults to `false`. Primarily
|
|
52
|
+
* useful for Storybook / tests.
|
|
53
|
+
*/
|
|
54
|
+
defaultOpen?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function TwCommandPaletteMount(props: TwCommandPaletteMountProps): ReactNode {
|
|
58
|
+
const { disabled = false, defaultOpen = false, onOpenChange, ...paletteProps } = props;
|
|
59
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
60
|
+
// Ref mirrors `open` so the keydown handler can toggle without reading
|
|
61
|
+
// React state (which React 18 strict mode double-invokes inside
|
|
62
|
+
// `setState` updaters). Fire `onOpenChange` exactly once per Ctrl+K.
|
|
63
|
+
const openRef = useRef(defaultOpen);
|
|
64
|
+
|
|
65
|
+
const handleOpenChange = useCallback(
|
|
66
|
+
(next: boolean) => {
|
|
67
|
+
openRef.current = next;
|
|
68
|
+
setOpen(next);
|
|
69
|
+
onOpenChange?.(next);
|
|
70
|
+
},
|
|
71
|
+
[onOpenChange],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (disabled) return;
|
|
76
|
+
if (typeof window === "undefined") return;
|
|
77
|
+
const handler = (event: KeyboardEvent): void => {
|
|
78
|
+
if (!isCommandPaletteOpenShortcut(event)) return;
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
// Toggle: Ctrl+K with palette open dismisses it. That matches
|
|
81
|
+
// VSCode / most IDE palettes. Side effect (onOpenChange) lives
|
|
82
|
+
// here — NOT inside a setState updater — so React 18 strict mode
|
|
83
|
+
// cannot fire it twice for a single keypress.
|
|
84
|
+
const next = !openRef.current;
|
|
85
|
+
openRef.current = next;
|
|
86
|
+
setOpen(next);
|
|
87
|
+
onOpenChange?.(next);
|
|
88
|
+
};
|
|
89
|
+
window.addEventListener("keydown", handler);
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener("keydown", handler);
|
|
92
|
+
};
|
|
93
|
+
}, [disabled, onOpenChange]);
|
|
94
|
+
|
|
95
|
+
return <TwCommandPalette {...paletteProps} open={open} onOpenChange={handleOpenChange} />;
|
|
96
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as React from "react";
|
|
12
12
|
|
|
13
13
|
import { TwShortcutHint, type ShortcutKey } from "./tw-shortcut-hint";
|
|
14
|
+
import { FOCUS_RING_CLASSES } from "../theme/tokens";
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Dedupe context
|
|
@@ -222,7 +223,7 @@ function ContextMenuRow({ item, platform }: ContextMenuRowProps): React.JSX.Elem
|
|
|
222
223
|
"text-[13px] text-[var(--color-text-primary)] text-left",
|
|
223
224
|
"hover:bg-[var(--color-bg-hover)]",
|
|
224
225
|
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
225
|
-
|
|
226
|
+
FOCUS_RING_CLASSES,
|
|
226
227
|
].join(" ")}
|
|
227
228
|
>
|
|
228
229
|
<span className="flex items-center gap-2 min-w-0">
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
4
4
|
import type { ActiveImageContext } from "../../ui/headless/selection-tool-types";
|
|
5
|
+
import { EMU_PER_INCH } from "../../runtime/units.ts";
|
|
5
6
|
|
|
6
7
|
export interface TwImageContextToolbarProps {
|
|
7
8
|
activeImage: ActiveImageContext;
|
|
@@ -17,10 +18,10 @@ export interface TwImageContextToolbarProps {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const IMAGE_SIZE_PRESETS = [
|
|
20
|
-
{ label: "Small image", widthEmu:
|
|
21
|
-
{ label: "Medium image", widthEmu:
|
|
22
|
-
{ label: "Large image", widthEmu:
|
|
23
|
-
]
|
|
21
|
+
{ label: "Small image", widthEmu: 2 * EMU_PER_INCH, heightEmu: 1 * EMU_PER_INCH },
|
|
22
|
+
{ label: "Medium image", widthEmu: 3 * EMU_PER_INCH, heightEmu: 1.5 * EMU_PER_INCH },
|
|
23
|
+
{ label: "Large image", widthEmu: 4 * EMU_PER_INCH, heightEmu: 2 * EMU_PER_INCH },
|
|
24
|
+
];
|
|
24
25
|
|
|
25
26
|
const NUDGE_EMU = 228600;
|
|
26
27
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, { type ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
import { FOCUS_RING_CLASSES } from "../theme/tokens";
|
|
4
|
+
|
|
3
5
|
export interface TwModeDockAction {
|
|
4
6
|
id: string;
|
|
5
7
|
label: string;
|
|
@@ -15,8 +17,10 @@ export interface TwModeDockProps {
|
|
|
15
17
|
className?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
// TwModeDock is a deprecated harness-only surface per designsystem §6.26,
|
|
21
|
+
// but its focus ring still participates in the canonical §4.7 contract —
|
|
22
|
+
// import the shared token constant so a future ring update cascades here.
|
|
23
|
+
const focusRingClass = FOCUS_RING_CLASSES;
|
|
20
24
|
|
|
21
25
|
export function TwModeDock(props: TwModeDockProps) {
|
|
22
26
|
const actions = (props.actions ?? []).slice(0, 3);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useContainerBreakpoint — rAF-coalesced ResizeObserver hook that
|
|
3
|
+
* resolves to a named breakpoint based on a monitored element's
|
|
4
|
+
* `contentRect.width`.
|
|
5
|
+
*
|
|
6
|
+
* Motivation: the shipped `responsive-chrome.ts` compact-mode hook is
|
|
7
|
+
* viewport-driven (single threshold), so a toolbar inside a narrow
|
|
8
|
+
* split-pane renders at full width even when its container is small.
|
|
9
|
+
* Container-driven breakpoints fix this — the toolbar responds to its
|
|
10
|
+
* own box, not the page.
|
|
11
|
+
*
|
|
12
|
+
* Perf discipline (CLAUDE.md §Performance Invariants #1 + #7):
|
|
13
|
+
* - The ResizeObserver callback MUST NOT call getBoundingClientRect
|
|
14
|
+
* or any offset/client read. It only reads `entry.contentRect.width`
|
|
15
|
+
* (which is already laid-out and cached by the browser's observer
|
|
16
|
+
* implementation).
|
|
17
|
+
* - State updates are coalesced inside `requestAnimationFrame` so
|
|
18
|
+
* multiple rapid resizes collapse into one render, and the new
|
|
19
|
+
* measurement never drives a synchronous paint.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
*
|
|
23
|
+
* const ref = useRef<HTMLDivElement | null>(null);
|
|
24
|
+
* const bp = useContainerBreakpoint(ref, { compact: 0, wide: 720 });
|
|
25
|
+
* return <div ref={ref} data-breakpoint={bp}>...</div>;
|
|
26
|
+
*
|
|
27
|
+
* Thresholds map `width` to the highest-named breakpoint whose
|
|
28
|
+
* threshold is ≤ width. Every map MUST include `0` as one of its
|
|
29
|
+
* thresholds so every width resolves to some name.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
33
|
+
|
|
34
|
+
export type BreakpointMap = Readonly<Record<string, number>>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a width to the highest-named breakpoint whose threshold is
|
|
38
|
+
* ≤ width. Exported for unit testing and for callers that already own
|
|
39
|
+
* the width and just want the mapping logic.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveBreakpoint<T extends BreakpointMap>(
|
|
42
|
+
width: number,
|
|
43
|
+
thresholds: T,
|
|
44
|
+
): keyof T | null {
|
|
45
|
+
let best: keyof T | null = null;
|
|
46
|
+
let bestThreshold = -Infinity;
|
|
47
|
+
for (const name of Object.keys(thresholds) as (keyof T)[]) {
|
|
48
|
+
const t = thresholds[name] as number;
|
|
49
|
+
if (width >= t && t > bestThreshold) {
|
|
50
|
+
best = name;
|
|
51
|
+
bestThreshold = t;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return best;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useContainerBreakpoint<T extends BreakpointMap>(
|
|
58
|
+
ref: React.RefObject<HTMLElement | null>,
|
|
59
|
+
thresholds: T,
|
|
60
|
+
initial: keyof T | null = null,
|
|
61
|
+
): keyof T | null {
|
|
62
|
+
const [breakpoint, setBreakpoint] = useState<keyof T | null>(initial);
|
|
63
|
+
const rafRef = useRef<number | null>(null);
|
|
64
|
+
const latestWidthRef = useRef<number | null>(null);
|
|
65
|
+
|
|
66
|
+
// R8 — stringify thresholds into a stable deps key so inline
|
|
67
|
+
// literals (e.g. `{ compact: 0, wide: 720 }` declared in JSX) do
|
|
68
|
+
// not destroy + recreate the ResizeObserver on every render. The
|
|
69
|
+
// stringify cost is a single pass over a ~3-key object when the
|
|
70
|
+
// caller's identity changes, which is what we want.
|
|
71
|
+
const depsKey = useMemo(() => JSON.stringify(thresholds), [thresholds]);
|
|
72
|
+
// Thresholds snapshot that the effect reads — captured at subscription
|
|
73
|
+
// time so the observer callback uses the values that corresponded to
|
|
74
|
+
// the current subscription.
|
|
75
|
+
const thresholdsRef = useRef(thresholds);
|
|
76
|
+
thresholdsRef.current = thresholds;
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const el = ref.current;
|
|
80
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
81
|
+
|
|
82
|
+
const observer = new ResizeObserver((entries) => {
|
|
83
|
+
// Read-only access to contentRect; no layout-triggering calls.
|
|
84
|
+
const entry = entries[entries.length - 1];
|
|
85
|
+
if (!entry) return;
|
|
86
|
+
latestWidthRef.current = entry.contentRect.width;
|
|
87
|
+
if (rafRef.current !== null) return;
|
|
88
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
89
|
+
rafRef.current = null;
|
|
90
|
+
const w = latestWidthRef.current;
|
|
91
|
+
if (w === null) return;
|
|
92
|
+
const next = resolveBreakpoint(w, thresholdsRef.current);
|
|
93
|
+
setBreakpoint((prev) => (prev === next ? prev : next));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
observer.observe(el);
|
|
98
|
+
return () => {
|
|
99
|
+
observer.disconnect();
|
|
100
|
+
if (rafRef.current !== null) {
|
|
101
|
+
cancelAnimationFrame(rafRef.current);
|
|
102
|
+
rafRef.current = null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
// depsKey is the stable projection of `thresholds`; `ref` is an
|
|
106
|
+
// object reference that callers typically hold stable via useRef.
|
|
107
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
|
+
}, [ref, depsKey]);
|
|
109
|
+
|
|
110
|
+
return breakpoint;
|
|
111
|
+
}
|
|
@@ -25,7 +25,6 @@ import type {
|
|
|
25
25
|
} from "../../api/public-types";
|
|
26
26
|
import { TwScopeRailLayer } from "./tw-scope-rail-layer";
|
|
27
27
|
import { TwScopeCardLayer } from "./tw-scope-card-layer";
|
|
28
|
-
import { TwPageStackOverlayLayer } from "./tw-page-stack-overlay-layer";
|
|
29
28
|
import { TwPageStackChromeLayer, type PmPortalView } from "../page-stack/tw-page-stack-chrome-layer";
|
|
30
29
|
import { TwTableGripLayer } from "../chrome/tw-table-grip-layer";
|
|
31
30
|
import { TwObjectSelectionOverlay } from "./tw-object-selection-overlay";
|
|
@@ -219,14 +218,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
219
218
|
data-testid={testId ?? "chrome-overlay"}
|
|
220
219
|
role="presentation"
|
|
221
220
|
>
|
|
222
|
-
{pageStackScrollRoot !== undefined ? (
|
|
223
|
-
<TwPageStackOverlayLayer
|
|
224
|
-
facet={facet}
|
|
225
|
-
scrollRoot={pageStackScrollRoot}
|
|
226
|
-
renderFrameRevision={renderFrameRevision ?? 0}
|
|
227
|
-
visiblePageIndexRange={visiblePageIndexRange ?? null}
|
|
228
|
-
/>
|
|
229
|
-
) : null}
|
|
230
221
|
{pageStackScrollRoot !== undefined ? (
|
|
231
222
|
<TwPageStackChromeLayer
|
|
232
223
|
facet={facet}
|
|
@@ -628,13 +628,12 @@ export const TwPageStackOverlayLayer: React.FC<TwPageStackOverlayLayerProps> = (
|
|
|
628
628
|
height: `${rect.heightPx}px`,
|
|
629
629
|
left: 0,
|
|
630
630
|
right: 0,
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
backgroundColor: "transparent",
|
|
631
|
+
// N1 (L8 Phase D): this component is placed at z-0 BEFORE
|
|
632
|
+
// the z-10 PM wrapper inside `wre-page-surface`, so an opaque
|
|
633
|
+
// page background here sits behind PM text rather than on top
|
|
634
|
+
// of it. White card + border + shadow gives the 'N distinct
|
|
635
|
+
// papers on a gray canvas' appearance.
|
|
636
|
+
backgroundColor: "var(--color-page-bg, white)",
|
|
638
637
|
border: "1px solid var(--color-page-border, rgba(148,163,184,0.2))",
|
|
639
638
|
borderRadius: "var(--radius-page, 4px)",
|
|
640
639
|
boxShadow:
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
isSupportedShapeGeometry,
|
|
10
10
|
renderShapeSvg,
|
|
11
|
+
type GradientFill,
|
|
11
12
|
type ShapeFill,
|
|
12
13
|
type ShapeLine,
|
|
13
14
|
} from "./shape-renderer.ts";
|
|
@@ -83,6 +84,16 @@ function safeHexColor(raw: string | null | undefined): string | null {
|
|
|
83
84
|
return HEX_COLOR_RE.test(raw) ? `#${raw}` : null;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/** Strict CSS hex validator for inline style sinks. Accepts 3/4/6/8-digit hex with optional leading #. */
|
|
88
|
+
function safeFilterHexColor(raw: string | null | undefined): string | null {
|
|
89
|
+
if (!raw || raw === "auto") return null;
|
|
90
|
+
const trimmed = raw.trim();
|
|
91
|
+
if (!/^#?(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(trimmed)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return `#${trimmed.replace(/^#/, "").toUpperCase()}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
/** Validate a CSS color value (may already include #). Returns the value or null. */
|
|
87
98
|
function safeCssColor(raw: string | null | undefined): string | null {
|
|
88
99
|
if (!raw) return null;
|
|
@@ -124,6 +135,20 @@ function resolveMarkerJustificationCss(raw: string | null): string {
|
|
|
124
135
|
}
|
|
125
136
|
}
|
|
126
137
|
|
|
138
|
+
function resolveMarkerAlignCss(raw: string | null): string {
|
|
139
|
+
switch (raw) {
|
|
140
|
+
case "left":
|
|
141
|
+
return "left";
|
|
142
|
+
case "center":
|
|
143
|
+
return "center";
|
|
144
|
+
case "right":
|
|
145
|
+
case "both":
|
|
146
|
+
case "distribute":
|
|
147
|
+
default:
|
|
148
|
+
return "right";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
127
152
|
/**
|
|
128
153
|
* ProseMirror schema for the supported live surface slice.
|
|
129
154
|
*
|
|
@@ -147,8 +172,10 @@ export const editorSchema = new Schema({
|
|
|
147
172
|
numberingPrefix: { default: null },
|
|
148
173
|
numberingSuffix: { default: null },
|
|
149
174
|
numberingMarkerWidth: { default: null },
|
|
175
|
+
numberingMarkerStart: { default: null },
|
|
150
176
|
numberingMarkerJustification: { default: null },
|
|
151
177
|
numberingMarkerRunProperties: { default: null },
|
|
178
|
+
numberingPicBulletSrc: { default: null },
|
|
152
179
|
alignment: { default: null },
|
|
153
180
|
spacingBefore: { default: null },
|
|
154
181
|
spacingAfter: { default: null },
|
|
@@ -217,7 +244,7 @@ export const editorSchema = new Schema({
|
|
|
217
244
|
else if (lineSpacing && lineRule === "exact") styles.push(`line-height: ${lineSpacing / 20}pt`);
|
|
218
245
|
else if (lineSpacing && lineRule === "atLeast") styles.push(`min-height: ${lineSpacing / 20}pt`);
|
|
219
246
|
const indentLeft = node.attrs.indentLeft as number | null;
|
|
220
|
-
if (indentLeft) styles.push(`padding-left: ${indentLeft / 20}pt`);
|
|
247
|
+
if (indentLeft !== null) styles.push(`padding-left: ${indentLeft / 20}pt`);
|
|
221
248
|
const indentRight = node.attrs.indentRight as number | null;
|
|
222
249
|
if (indentRight) styles.push(`padding-right: ${indentRight / 20}pt`);
|
|
223
250
|
const indentFirstLine = node.attrs.indentFirstLine as number | null;
|
|
@@ -271,7 +298,9 @@ export const editorSchema = new Schema({
|
|
|
271
298
|
const numberingLevel = node.attrs.numberingLevel as number | null;
|
|
272
299
|
const numberingSuffix = node.attrs.numberingSuffix as "tab" | "space" | "nothing" | null;
|
|
273
300
|
const numberingMarkerWidth = node.attrs.numberingMarkerWidth as number | null;
|
|
301
|
+
const numberingMarkerStart = node.attrs.numberingMarkerStart as number | null;
|
|
274
302
|
const numberingMarkerJustification = node.attrs.numberingMarkerJustification as string | null;
|
|
303
|
+
const numberingPicBulletSrc = node.attrs.numberingPicBulletSrc as string | null;
|
|
275
304
|
const children: Array<string | number | readonly unknown[]> = [];
|
|
276
305
|
if (pageBreak) {
|
|
277
306
|
children.push([
|
|
@@ -285,10 +314,10 @@ export const editorSchema = new Schema({
|
|
|
285
314
|
"Page break",
|
|
286
315
|
]);
|
|
287
316
|
}
|
|
288
|
-
if (numberingPrefix) {
|
|
317
|
+
if (numberingPrefix || numberingPicBulletSrc) {
|
|
289
318
|
const hasResolvedMarkerWidth =
|
|
290
319
|
typeof numberingMarkerWidth === "number" && numberingMarkerWidth > 0;
|
|
291
|
-
const fallbackMinWidth = Math.min(Math.max(numberingPrefix
|
|
320
|
+
const fallbackMinWidth = Math.min(Math.max((numberingPrefix?.length ?? 1) + 1, 4), 14);
|
|
292
321
|
const fallbackMarginRight =
|
|
293
322
|
numberingSuffix === "nothing"
|
|
294
323
|
? "0.25rem"
|
|
@@ -315,7 +344,7 @@ export const editorSchema = new Schema({
|
|
|
315
344
|
|
|
316
345
|
const prefixStyles = [
|
|
317
346
|
`font-variant-numeric: tabular-nums`,
|
|
318
|
-
`
|
|
347
|
+
`text-align: ${resolveMarkerAlignCss(numberingMarkerJustification)}`,
|
|
319
348
|
];
|
|
320
349
|
|
|
321
350
|
if (markerRunProperties) {
|
|
@@ -344,9 +373,11 @@ export const editorSchema = new Schema({
|
|
|
344
373
|
`width: ${markerWidthPt}pt`,
|
|
345
374
|
`min-width: ${markerWidthPt}pt`,
|
|
346
375
|
`flex-basis: ${markerWidthPt}pt`,
|
|
376
|
+
`margin-left: -${markerWidthPt}pt`,
|
|
347
377
|
`margin-right: 0`,
|
|
348
378
|
`overflow: visible`,
|
|
349
379
|
);
|
|
380
|
+
void numberingMarkerStart; // consumed via paragraph padding-left geometry
|
|
350
381
|
} else {
|
|
351
382
|
prefixStyles.push(
|
|
352
383
|
`min-width: ${fallbackMinWidth}ch`,
|
|
@@ -359,14 +390,16 @@ export const editorSchema = new Schema({
|
|
|
359
390
|
{
|
|
360
391
|
class: baseClasses.join(" "),
|
|
361
392
|
contenteditable: "false",
|
|
362
|
-
"data-numbering-prefix": numberingPrefix,
|
|
393
|
+
"data-numbering-prefix": numberingPicBulletSrc ? "" : (numberingPrefix ?? ""),
|
|
363
394
|
...(typeof numberingLevel === "number"
|
|
364
395
|
? { "data-numbering-level": String(numberingLevel) }
|
|
365
396
|
: {}),
|
|
366
397
|
...(numberingSuffix ? { "data-numbering-suffix": numberingSuffix } : {}),
|
|
367
398
|
style: prefixStyles.join("; "),
|
|
368
399
|
},
|
|
369
|
-
|
|
400
|
+
numberingPicBulletSrc
|
|
401
|
+
? (["img", { src: numberingPicBulletSrc, alt: "", "aria-hidden": "true", style: "max-width:100%;max-height:100%;object-fit:contain;display:block;" }] as readonly unknown[])
|
|
402
|
+
: (numberingPrefix ?? ""),
|
|
370
403
|
]);
|
|
371
404
|
}
|
|
372
405
|
children.push([
|
|
@@ -465,6 +498,8 @@ export const editorSchema = new Schema({
|
|
|
465
498
|
wrapMode: { default: null },
|
|
466
499
|
distMargins: { default: null },
|
|
467
500
|
positionH: { default: null },
|
|
501
|
+
// Lane 6d N9.b — polygon clip for tight/through wrap.
|
|
502
|
+
wrapPolygon: { default: null },
|
|
468
503
|
// Lane 6d N11.b — CSS filter effects (soft-edge, outer shadow, glow).
|
|
469
504
|
softEdgeRadius: { default: null },
|
|
470
505
|
outerShadow: { default: null },
|
|
@@ -504,22 +539,46 @@ export const editorSchema = new Schema({
|
|
|
504
539
|
const softEdgeRadius = node.attrs.softEdgeRadius as number | null;
|
|
505
540
|
const outerShadow = node.attrs.outerShadow as {
|
|
506
541
|
blurRad: number; dist: number; dir: number; color: string;
|
|
542
|
+
colorType: "srgbClr" | "schemeClr";
|
|
543
|
+
} | null;
|
|
544
|
+
const glow = node.attrs.glow as {
|
|
545
|
+
radius: number; color: string;
|
|
546
|
+
colorType: "srgbClr" | "schemeClr";
|
|
507
547
|
} | null;
|
|
508
|
-
const glow = node.attrs.glow as { radius: number; color: string } | null;
|
|
509
548
|
const filterParts: string[] = [];
|
|
510
549
|
if (softEdgeRadius) {
|
|
511
550
|
filterParts.push(`blur(${(softEdgeRadius / EMU_PER_PX).toFixed(2)}px)`);
|
|
512
551
|
}
|
|
552
|
+
// Defense in depth: even though parse-picture.ts validates
|
|
553
|
+
// srgbClr@val against a strict hex allowlist, re-validate here at
|
|
554
|
+
// the CSS sink so a future parser refactor or a bypass that lands
|
|
555
|
+
// attacker-controlled text in node.attrs cannot escape
|
|
556
|
+
// `drop-shadow(#…)` into arbitrary CSS (e.g. `FF0000) url(…)/*`).
|
|
557
|
+
// safeFilterHexColor returns `#RRGGBB` on valid hex input and
|
|
558
|
+
// empty string otherwise, so schemeClr tokens (e.g. "accent1")
|
|
559
|
+
// naturally skip this branch until a theme resolver runs.
|
|
560
|
+
const safeFilterHexColor = (raw: unknown): string => {
|
|
561
|
+
return typeof raw === "string" &&
|
|
562
|
+
/^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(raw)
|
|
563
|
+
? `#${raw.toUpperCase()}`
|
|
564
|
+
: "";
|
|
565
|
+
};
|
|
513
566
|
if (glow) {
|
|
514
|
-
|
|
567
|
+
const glowColor = safeFilterHexColor(glow.color);
|
|
568
|
+
if (glowColor) {
|
|
569
|
+
filterParts.push(`drop-shadow(0 0 ${(glow.radius / EMU_PER_PX).toFixed(2)}px ${glowColor})`);
|
|
570
|
+
}
|
|
515
571
|
}
|
|
516
572
|
if (outerShadow) {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
573
|
+
const shadowColor = safeFilterHexColor(outerShadow.color);
|
|
574
|
+
if (shadowColor) {
|
|
575
|
+
const blurPx = (outerShadow.blurRad / EMU_PER_PX).toFixed(2);
|
|
576
|
+
const distPx = outerShadow.dist / EMU_PER_PX;
|
|
577
|
+
const dirRad = (outerShadow.dir / ROTATION_UNITS_PER_DEGREE) * (Math.PI / 180);
|
|
578
|
+
const dx = (distPx * Math.cos(dirRad)).toFixed(2);
|
|
579
|
+
const dy = (distPx * Math.sin(dirRad)).toFixed(2);
|
|
580
|
+
filterParts.push(`drop-shadow(${dx}px ${dy}px ${blurPx}px ${shadowColor})`);
|
|
581
|
+
}
|
|
523
582
|
}
|
|
524
583
|
// N9 float-wrap → CSS float + shape-outside on the wrapper span.
|
|
525
584
|
const wrapMode = node.attrs.wrapMode as string | null;
|
|
@@ -527,8 +586,16 @@ export const editorSchema = new Schema({
|
|
|
527
586
|
const distMargins = node.attrs.distMargins as
|
|
528
587
|
| { top?: number; bottom?: number; left?: number; right?: number }
|
|
529
588
|
| null;
|
|
589
|
+
const wrapPolygon = node.attrs.wrapPolygon as Array<{ x: number; y: number }> | null;
|
|
530
590
|
const wrapperStyleParts: string[] = [];
|
|
531
|
-
if (isFloating && wrapMode === "
|
|
591
|
+
if (isFloating && (wrapMode === "tight" || wrapMode === "through") && wrapPolygon?.length) {
|
|
592
|
+
// N9.b — polygon clip: OOXML wrapPolygon coords are in 21600ths-of-image units.
|
|
593
|
+
const floatSide = positionH?.align === "right" ? "right" : "left";
|
|
594
|
+
const pts = wrapPolygon
|
|
595
|
+
.map((p) => `${(p.x / 21600 * 100).toFixed(2)}% ${(p.y / 21600 * 100).toFixed(2)}%`)
|
|
596
|
+
.join(", ");
|
|
597
|
+
wrapperStyleParts.push(`float:${floatSide}`, `shape-outside:polygon(${pts})`);
|
|
598
|
+
} else if (isFloating && wrapMode === "square") {
|
|
532
599
|
const floatSide = positionH?.align === "right" ? "right" : "left";
|
|
533
600
|
wrapperStyleParts.push(
|
|
534
601
|
`float:${floatSide}`,
|
|
@@ -898,14 +965,7 @@ export const editorSchema = new Schema({
|
|
|
898
965
|
const geometry = node.attrs.geometry as string | null;
|
|
899
966
|
const fill = node.attrs.fill as
|
|
900
967
|
| ShapeFill
|
|
901
|
-
|
|
|
902
|
-
kind: "gradient";
|
|
903
|
-
stops: Array<{ pos: number; color: string; colorType: "srgbClr" | "schemeClr" }>;
|
|
904
|
-
direction:
|
|
905
|
-
| { kind: "linear"; angle: number; scaled?: boolean }
|
|
906
|
-
| { kind: "path"; path: "circle" | "rect" | "shape" };
|
|
907
|
-
rotWithShape?: boolean;
|
|
908
|
-
}
|
|
968
|
+
| GradientFill
|
|
909
969
|
| {
|
|
910
970
|
kind: "pattern";
|
|
911
971
|
preset: string;
|
|
@@ -917,11 +977,13 @@ export const editorSchema = new Schema({
|
|
|
917
977
|
const heightEmu = node.attrs.heightEmu as number | null;
|
|
918
978
|
const widthPx = widthEmu ? Math.max(8, Math.round(widthEmu / EMU_PER_PX)) : null;
|
|
919
979
|
const heightPx = heightEmu ? Math.max(8, Math.round(heightEmu / EMU_PER_PX)) : null;
|
|
980
|
+
// N10.b — gradient fills pass through to renderShapeSvg (SVG defs path).
|
|
981
|
+
// Pattern fills remain unsupported → chip fallback.
|
|
920
982
|
const svgFill =
|
|
921
983
|
fill === undefined || fill === null
|
|
922
984
|
? undefined
|
|
923
|
-
: fill.kind === "solid" || fill.kind === "none"
|
|
924
|
-
? fill
|
|
985
|
+
: fill.kind === "solid" || fill.kind === "none" || fill.kind === "gradient"
|
|
986
|
+
? (fill as ShapeFill | GradientFill)
|
|
925
987
|
: undefined;
|
|
926
988
|
// N10 — try SVG render path for supported geometries with extent.
|
|
927
989
|
if (
|
|
@@ -374,8 +374,14 @@ function buildParagraph(
|
|
|
374
374
|
paragraphLayout.indentation.firstLine < 0
|
|
375
375
|
? Math.abs(paragraphLayout.indentation.firstLine)
|
|
376
376
|
: null),
|
|
377
|
+
numberingMarkerStart: paragraphLayout.markerLane?.start ?? null,
|
|
377
378
|
numberingMarkerJustification: paragraphLayout.markerJustification ?? null,
|
|
378
379
|
numberingMarkerRunProperties: block.resolvedNumbering?.markerRunProperties ?? null,
|
|
380
|
+
numberingPicBulletSrc: (() => {
|
|
381
|
+
const mediaId = block.resolvedNumbering?.picBulletMediaId;
|
|
382
|
+
if (!mediaId) return null;
|
|
383
|
+
return mediaPreviews[mediaId]?.src ?? null;
|
|
384
|
+
})(),
|
|
379
385
|
shadingFill: block.shading?.fill ?? cascade?.shading?.fill ?? null,
|
|
380
386
|
borderTop: (block.borders as Record<string, unknown>)?.top ?? cascadeBorders?.top ?? null,
|
|
381
387
|
borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? cascadeBorders?.bottom ?? null,
|
|
@@ -464,6 +470,8 @@ function buildInlineContent(
|
|
|
464
470
|
wrapMode: segment.anchor?.wrapMode ?? null,
|
|
465
471
|
distMargins: segment.anchor?.distMargins ?? null,
|
|
466
472
|
positionH: segment.anchor?.positionH ?? null,
|
|
473
|
+
// Lane 6d N9.b — polygon clip.
|
|
474
|
+
wrapPolygon: segment.anchor?.wrapPolygon ?? null,
|
|
467
475
|
// Lane 6d N11.b — filter effects.
|
|
468
476
|
softEdgeRadius: segment.pictureEffects?.softEdgeRadius ?? null,
|
|
469
477
|
outerShadow: segment.pictureEffects?.outerShadow ?? null,
|
|
@@ -587,6 +595,7 @@ function buildTable(
|
|
|
587
595
|
{
|
|
588
596
|
styleId: block.styleId ?? null,
|
|
589
597
|
gridColumns: block.gridColumns,
|
|
598
|
+
gridColumnsRelative: block.gridColumnsRelative ?? null,
|
|
590
599
|
alignment: block.alignment ?? null,
|
|
591
600
|
tblLookFirstRow: block.tblLook?.firstRow ?? false,
|
|
592
601
|
tblLookLastRow: block.tblLook?.lastRow ?? false,
|