@beyondwork/docx-react-component 1.0.57 → 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 +1 -1
- 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 +1149 -8
- 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 +120 -39
- 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 +165 -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 +544 -35
- package/src/runtime/document-search.ts +176 -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 +183 -0
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/scope-resolver.ts +60 -0
- 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 +293 -18
- 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 +258 -44
- package/src/ui/editor-runtime-boundary.ts +13 -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 +23 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +158 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +105 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -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 +52 -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,9 +25,9 @@ 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";
|
|
30
|
+
import { TwObjectSelectionOverlay } from "./tw-object-selection-overlay";
|
|
31
31
|
|
|
32
32
|
export interface TwChromeOverlayProps {
|
|
33
33
|
/** Layout facet the overlay layers read from. */
|
|
@@ -93,6 +93,16 @@ export interface TwChromeOverlayProps {
|
|
|
93
93
|
/** Optional extra children (e.g., future comment balloon layer). */
|
|
94
94
|
children?: React.ReactNode;
|
|
95
95
|
|
|
96
|
+
// Object selection overlay (N6) ----------------------------------------
|
|
97
|
+
/** R.3 — grabbed image/shape id, or null. When set, the selection overlay renders. */
|
|
98
|
+
grabbedObjectId?: string | null;
|
|
99
|
+
/** Document `from` offset of the grabbed segment (for anchor-index lookup). */
|
|
100
|
+
grabbedObjectFromOffset?: number | null;
|
|
101
|
+
/** Document `to` offset of the grabbed segment. */
|
|
102
|
+
grabbedObjectToOffset?: number | null;
|
|
103
|
+
/** Called when the user clicks outside the selection box to deselect. */
|
|
104
|
+
onDeselectObject?: () => void;
|
|
105
|
+
|
|
96
106
|
// Table grip props (P6) -----------------------------------------------
|
|
97
107
|
/** Active table context — when present, column/row resize grips are shown. */
|
|
98
108
|
tableContext?: TableStructureContextSnapshot | null;
|
|
@@ -187,6 +197,10 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
187
197
|
scopeCardScopeTagEditor,
|
|
188
198
|
"data-testid": testId,
|
|
189
199
|
children,
|
|
200
|
+
grabbedObjectId,
|
|
201
|
+
grabbedObjectFromOffset,
|
|
202
|
+
grabbedObjectToOffset,
|
|
203
|
+
onDeselectObject,
|
|
190
204
|
tableContext,
|
|
191
205
|
onSetColumnWidth,
|
|
192
206
|
onSetRowHeight,
|
|
@@ -204,14 +218,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
204
218
|
data-testid={testId ?? "chrome-overlay"}
|
|
205
219
|
role="presentation"
|
|
206
220
|
>
|
|
207
|
-
{pageStackScrollRoot !== undefined ? (
|
|
208
|
-
<TwPageStackOverlayLayer
|
|
209
|
-
facet={facet}
|
|
210
|
-
scrollRoot={pageStackScrollRoot}
|
|
211
|
-
renderFrameRevision={renderFrameRevision ?? 0}
|
|
212
|
-
visiblePageIndexRange={visiblePageIndexRange ?? null}
|
|
213
|
-
/>
|
|
214
|
-
) : null}
|
|
215
221
|
{pageStackScrollRoot !== undefined ? (
|
|
216
222
|
<TwPageStackChromeLayer
|
|
217
223
|
facet={facet}
|
|
@@ -251,6 +257,14 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
251
257
|
onSetColumnWidth={onSetColumnWidth}
|
|
252
258
|
onSetRowHeight={onSetRowHeight}
|
|
253
259
|
/>
|
|
260
|
+
<TwObjectSelectionOverlay
|
|
261
|
+
grabbedObjectId={grabbedObjectId ?? null}
|
|
262
|
+
grabbedObjectFromOffset={grabbedObjectFromOffset ?? null}
|
|
263
|
+
grabbedObjectToOffset={grabbedObjectToOffset ?? null}
|
|
264
|
+
facet={facet}
|
|
265
|
+
space={space}
|
|
266
|
+
onDeselect={onDeselectObject}
|
|
267
|
+
/>
|
|
254
268
|
{children}
|
|
255
269
|
</div>
|
|
256
270
|
);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane 6d — N6 P11.1–P11.5: object selection chrome overlay.
|
|
3
|
+
*
|
|
4
|
+
* Renders a selection box with 8 resize handles and a rotate grip around the
|
|
5
|
+
* grabbed image or shape. Purely visual chrome — no resize/rotate mutations
|
|
6
|
+
* in this slice; handle interaction is deferred to the follow-up N6.b slice.
|
|
7
|
+
*
|
|
8
|
+
* Positioning: uses `RenderAnchorIndex.byRuntimeOffset(from)` + `bySelection`
|
|
9
|
+
* to compute the object rect in overlay-coordinate space, then paints over it.
|
|
10
|
+
*
|
|
11
|
+
* Dismissal: clicking outside the overlay calls `onDeselect()` which routes
|
|
12
|
+
* to `runtime.deselectObject()` in the workspace.
|
|
13
|
+
*
|
|
14
|
+
* v1 scope:
|
|
15
|
+
* - rect, ellipse, roundRect shapes and inline/floating images.
|
|
16
|
+
* - Resize handles are visual only (pointer-events-none on each handle).
|
|
17
|
+
* - Rotate grip is visual only.
|
|
18
|
+
* - Anchor drag indicator omitted until N6.b.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as React from "react";
|
|
22
|
+
import { useEffect, useRef } from "react";
|
|
23
|
+
import type { WordReviewEditorLayoutFacet } from "../../api/public-types";
|
|
24
|
+
import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
|
|
25
|
+
import { projectRectToOverlay } from "./chrome-overlay-projector";
|
|
26
|
+
|
|
27
|
+
/** The 8 corner/edge handle positions. */
|
|
28
|
+
const HANDLE_POSITIONS = [
|
|
29
|
+
"nw", "n", "ne",
|
|
30
|
+
"w", "e",
|
|
31
|
+
"sw", "s", "se",
|
|
32
|
+
] as const;
|
|
33
|
+
type HandlePosition = typeof HANDLE_POSITIONS[number];
|
|
34
|
+
|
|
35
|
+
const CURSOR_MAP: Record<HandlePosition, string> = {
|
|
36
|
+
nw: "nw-resize", n: "n-resize", ne: "ne-resize",
|
|
37
|
+
w: "w-resize", e: "e-resize",
|
|
38
|
+
sw: "sw-resize", s: "s-resize", se: "se-resize",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface TwObjectSelectionOverlayProps {
|
|
42
|
+
/** Stable id of the grabbed image/shape (mediaId or shapeId), or null. */
|
|
43
|
+
grabbedObjectId: string | null;
|
|
44
|
+
/** Document offset (`from`) of the grabbed object's inline segment. Null when no object grabbed. */
|
|
45
|
+
grabbedObjectFromOffset: number | null;
|
|
46
|
+
/** Document offset (`to`) of the grabbed object's inline segment. Null when no object grabbed. */
|
|
47
|
+
grabbedObjectToOffset: number | null;
|
|
48
|
+
/** Layout facet for render-frame + anchor-index access. */
|
|
49
|
+
facet: WordReviewEditorLayoutFacet;
|
|
50
|
+
/** Optional overlay coordinate-space override. */
|
|
51
|
+
space?: OverlayCoordinateSpace;
|
|
52
|
+
/** Called when the user clicks outside the selection box. */
|
|
53
|
+
onDeselect?: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function TwObjectSelectionOverlay({
|
|
57
|
+
grabbedObjectId,
|
|
58
|
+
grabbedObjectFromOffset,
|
|
59
|
+
grabbedObjectToOffset,
|
|
60
|
+
facet,
|
|
61
|
+
space,
|
|
62
|
+
onDeselect,
|
|
63
|
+
}: TwObjectSelectionOverlayProps) {
|
|
64
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
|
|
66
|
+
// Click-outside to deselect.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!grabbedObjectId || !onDeselect) return;
|
|
69
|
+
function handlePointerDown(e: PointerEvent) {
|
|
70
|
+
if (overlayRef.current && !overlayRef.current.contains(e.target as Node)) {
|
|
71
|
+
onDeselect!();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
document.addEventListener("pointerdown", handlePointerDown, { capture: true });
|
|
75
|
+
return () => document.removeEventListener("pointerdown", handlePointerDown, { capture: true });
|
|
76
|
+
}, [grabbedObjectId, onDeselect]);
|
|
77
|
+
|
|
78
|
+
if (!grabbedObjectId || grabbedObjectFromOffset == null) return null;
|
|
79
|
+
|
|
80
|
+
const frame = typeof facet.getRenderFrame === "function" ? facet.getRenderFrame() : null;
|
|
81
|
+
if (!frame) return null;
|
|
82
|
+
|
|
83
|
+
const rawRect = grabbedObjectToOffset != null
|
|
84
|
+
? frame.anchorIndex.bySelection(grabbedObjectFromOffset, grabbedObjectToOffset)
|
|
85
|
+
: frame.anchorIndex.byRuntimeOffset(grabbedObjectFromOffset);
|
|
86
|
+
if (!rawRect) return null;
|
|
87
|
+
|
|
88
|
+
const rect = projectRectToOverlay(rawRect, space);
|
|
89
|
+
|
|
90
|
+
const boxStyle: React.CSSProperties = {
|
|
91
|
+
position: "absolute",
|
|
92
|
+
left: rect.left,
|
|
93
|
+
top: rect.top,
|
|
94
|
+
width: rect.width,
|
|
95
|
+
height: rect.height,
|
|
96
|
+
outline: "2px solid var(--color-accent-primary)",
|
|
97
|
+
boxSizing: "border-box",
|
|
98
|
+
pointerEvents: "auto",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
ref={overlayRef}
|
|
104
|
+
style={boxStyle}
|
|
105
|
+
data-chrome-overlay=""
|
|
106
|
+
data-object-selection=""
|
|
107
|
+
data-object-id={grabbedObjectId}
|
|
108
|
+
aria-label="Selected object"
|
|
109
|
+
>
|
|
110
|
+
{HANDLE_POSITIONS.map((pos) => (
|
|
111
|
+
<ObjectHandle key={pos} position={pos} />
|
|
112
|
+
))}
|
|
113
|
+
<RotateGrip />
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ObjectHandle({ position }: { position: HandlePosition }) {
|
|
119
|
+
const HANDLE_PX = 8;
|
|
120
|
+
const half = HANDLE_PX / 2;
|
|
121
|
+
const pos = position;
|
|
122
|
+
|
|
123
|
+
const style: React.CSSProperties = {
|
|
124
|
+
position: "absolute",
|
|
125
|
+
width: HANDLE_PX,
|
|
126
|
+
height: HANDLE_PX,
|
|
127
|
+
background: "white",
|
|
128
|
+
border: "1.5px solid var(--color-accent-primary)",
|
|
129
|
+
borderRadius: 1,
|
|
130
|
+
boxSizing: "border-box",
|
|
131
|
+
cursor: CURSOR_MAP[pos],
|
|
132
|
+
// Visual only in v1 — pointer events disabled so clicks fall through to
|
|
133
|
+
// the click-outside listener which deselectObjects.
|
|
134
|
+
pointerEvents: "none",
|
|
135
|
+
...(pos.includes("w") ? { left: -half } : pos.includes("e") ? { right: -half } : { left: "50%", transform: "translateX(-50%)" }),
|
|
136
|
+
...(pos.includes("n") ? { top: -half } : pos.includes("s") ? { bottom: -half } : { top: "50%", transform: `${pos === "w" || pos === "e" ? "translateY(-50%)" : "translateX(-50%) translateY(-50%)"}` }),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return <div style={style} data-handle={pos} aria-hidden="true" />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function RotateGrip() {
|
|
143
|
+
const style: React.CSSProperties = {
|
|
144
|
+
position: "absolute",
|
|
145
|
+
width: 10,
|
|
146
|
+
height: 10,
|
|
147
|
+
borderRadius: "50%",
|
|
148
|
+
background: "white",
|
|
149
|
+
border: "1.5px solid var(--color-accent-primary)",
|
|
150
|
+
top: -24,
|
|
151
|
+
left: "50%",
|
|
152
|
+
transform: "translateX(-50%)",
|
|
153
|
+
cursor: "grab",
|
|
154
|
+
pointerEvents: "none",
|
|
155
|
+
boxSizing: "border-box",
|
|
156
|
+
};
|
|
157
|
+
return <div style={style} data-handle="rotate" aria-hidden="true" />;
|
|
158
|
+
}
|
|
@@ -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:
|