@beyondwork/docx-react-component 1.0.36 → 1.0.38
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 +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +402 -1
- package/src/core/commands/index.ts +18 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +328 -50
- package/src/io/export/serialize-numbering.ts +114 -24
- package/src/io/export/serialize-tables.ts +87 -11
- package/src/io/export/table-properties-xml.ts +174 -20
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +20 -0
- package/src/io/ooxml/parse-footnotes.ts +62 -1
- package/src/io/ooxml/parse-headers-footers.ts +62 -1
- package/src/io/ooxml/parse-main-document.ts +158 -1
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +45 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +2 -306
- package/src/runtime/document-runtime.ts +287 -11
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +233 -0
- package/src/runtime/layout/inert-layout-facet.ts +59 -0
- package/src/runtime/layout/layout-engine-instance.ts +628 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +452 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +921 -0
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +1398 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +99 -15
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +501 -0
- package/src/ui/headless/scoped-chrome-policy.ts +183 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +505 -144
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useCallback, type CSSProperties, type FocusEventHandler, type Ref } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import type {
|
|
4
|
+
ChromePinsState,
|
|
5
|
+
ChromePinSurface,
|
|
6
|
+
PinState,
|
|
7
|
+
RuntimeContextAnalyticsSnapshot,
|
|
8
|
+
} from "../../api/public-types";
|
|
6
9
|
import type { ActiveSelectionToolModel } from "../../ui/headless/selection-tool-types";
|
|
7
|
-
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
8
10
|
import { TwContextAnalyticsSummary } from "./tw-context-analytics-summary";
|
|
11
|
+
import { TwDetachHandle } from "./tw-detach-handle";
|
|
9
12
|
import { TwSelectionToolBlocked } from "./tw-selection-tool-blocked";
|
|
10
13
|
import { TwSelectionToolComment } from "./tw-selection-tool-comment";
|
|
11
14
|
import { TwSelectionToolFormatting } from "./tw-selection-tool-formatting";
|
|
@@ -22,6 +25,13 @@ export interface TwSelectionToolHostProps {
|
|
|
22
25
|
tool: ActiveSelectionToolModel | null;
|
|
23
26
|
contextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
24
27
|
placement: SelectionToolPlacement | null;
|
|
28
|
+
/**
|
|
29
|
+
* Chrome pin state (R2.3) — selection-tier detach/float persistence
|
|
30
|
+
* rides ViewState.chromePins.selectionTier so pinned tools survive
|
|
31
|
+
* snapshot rebuilds. When absent, the host renders non-pinnable.
|
|
32
|
+
*/
|
|
33
|
+
chromePins?: ChromePinsState;
|
|
34
|
+
onChromePinChange?: (surface: ChromePinSurface, pin: PinState | null) => void;
|
|
25
35
|
rootRef?: Ref<HTMLDivElement>;
|
|
26
36
|
onFocusCapture?: FocusEventHandler<HTMLDivElement>;
|
|
27
37
|
onBlurCapture?: FocusEventHandler<HTMLDivElement>;
|
|
@@ -62,48 +72,22 @@ export function TwSelectionToolHost(props: TwSelectionToolHostProps) {
|
|
|
62
72
|
return null;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
setIsDetached(false);
|
|
77
|
-
setDetachedOffset({ x: 0, y: 0 });
|
|
78
|
-
dragStateRef.current = null;
|
|
79
|
-
}, [props.tool.kind]);
|
|
80
|
-
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (!supportsTopMenuControls || typeof window === "undefined") {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const handleMouseMove = (event: MouseEvent) => {
|
|
87
|
-
if (!dragStateRef.current) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
setDetachedOffset({
|
|
91
|
-
x: dragStateRef.current.originX + (event.clientX - dragStateRef.current.startX),
|
|
92
|
-
y: dragStateRef.current.originY + (event.clientY - dragStateRef.current.startY),
|
|
93
|
-
});
|
|
94
|
-
};
|
|
75
|
+
// R2.3: pin state now rides ViewState.chromePins.selectionTier so
|
|
76
|
+
// pinned tools survive snapshot rebuilds. When the host doesn't pass
|
|
77
|
+
// chromePins/onChromePinChange, the detach handle is suppressed (same
|
|
78
|
+
// as the pre-R2 behavior for tool kinds without the handle).
|
|
79
|
+
const pin = props.chromePins?.selectionTier;
|
|
80
|
+
const isDetached = pin?.detached ?? false;
|
|
81
|
+
const detachedOffset = pin?.offset ?? { x: 0, y: 0 };
|
|
82
|
+
const supportsDetach =
|
|
83
|
+
supportsDetachForKind(props.tool.kind) && Boolean(props.onChromePinChange);
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return () => {
|
|
103
|
-
window.removeEventListener("mousemove", handleMouseMove);
|
|
104
|
-
window.removeEventListener("mouseup", handleMouseUp);
|
|
105
|
-
};
|
|
106
|
-
}, [supportsTopMenuControls]);
|
|
85
|
+
const handlePinChange = useCallback(
|
|
86
|
+
(surface: ChromePinSurface, next: PinState | null) => {
|
|
87
|
+
props.onChromePinChange?.(surface, next);
|
|
88
|
+
},
|
|
89
|
+
[props.onChromePinChange], // eslint-disable-line react-hooks/exhaustive-deps
|
|
90
|
+
);
|
|
107
91
|
|
|
108
92
|
const overlayTestId = getOverlayTestId(props.tool.kind, Boolean(props.placement));
|
|
109
93
|
const toolContent = renderTool(props, props.tool);
|
|
@@ -124,56 +108,25 @@ export function TwSelectionToolHost(props: TwSelectionToolHostProps) {
|
|
|
124
108
|
{toolContent}
|
|
125
109
|
</div>
|
|
126
110
|
) : null;
|
|
127
|
-
const wrappedContent =
|
|
128
|
-
|
|
129
|
-
<div className="
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
dragStateRef.current = {
|
|
139
|
-
startX: event.clientX,
|
|
140
|
-
startY: event.clientY,
|
|
141
|
-
originX: isDetached ? detachedOffset.x : 0,
|
|
142
|
-
originY: isDetached ? detachedOffset.y : 0,
|
|
143
|
-
};
|
|
144
|
-
}}
|
|
145
|
-
>
|
|
146
|
-
<GripHorizontal className="h-3 w-3" />
|
|
147
|
-
</button>
|
|
148
|
-
<span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
149
|
-
{getTopMenuLabel(props.tool.kind)}
|
|
150
|
-
</span>
|
|
151
|
-
<span className="rounded-full bg-surface px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-[0.08em] text-secondary">
|
|
152
|
-
{isDetached ? "Floating" : "Docked"}
|
|
153
|
-
</span>
|
|
154
|
-
<button
|
|
155
|
-
type="button"
|
|
156
|
-
aria-label={isDetached ? "Dock menu" : "Float menu"}
|
|
157
|
-
aria-pressed={isDetached}
|
|
158
|
-
data-testid="selection-tool-attach-toggle"
|
|
159
|
-
className="inline-flex h-6 items-center rounded-md border border-border/60 px-2 text-[10px] font-medium text-secondary transition-colors hover:bg-surface hover:text-primary"
|
|
160
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
161
|
-
onClick={() => {
|
|
162
|
-
setIsDetached((current) => !current);
|
|
163
|
-
}}
|
|
164
|
-
>
|
|
165
|
-
{isDetached ? "Dock menu" : "Float menu"}
|
|
166
|
-
</button>
|
|
111
|
+
const wrappedContent =
|
|
112
|
+
content && supportsDetach ? (
|
|
113
|
+
<div className="flex flex-col gap-1">
|
|
114
|
+
<TwDetachHandle
|
|
115
|
+
surface="selectionTier"
|
|
116
|
+
pin={pin}
|
|
117
|
+
onChange={handlePinChange}
|
|
118
|
+
label={getTopMenuLabel(props.tool.kind)}
|
|
119
|
+
data-testid={`selection-tool-detach-${props.tool.kind}`}
|
|
120
|
+
/>
|
|
121
|
+
{content}
|
|
167
122
|
</div>
|
|
168
|
-
|
|
169
|
-
</div>
|
|
170
|
-
) : content;
|
|
123
|
+
) : content;
|
|
171
124
|
|
|
172
125
|
if (!wrappedContent) {
|
|
173
126
|
return null;
|
|
174
127
|
}
|
|
175
128
|
|
|
176
|
-
if (isDetached) {
|
|
129
|
+
if (isDetached && supportsDetach) {
|
|
177
130
|
return (
|
|
178
131
|
<div className="pointer-events-none absolute inset-0 z-20" data-testid={overlayTestId}>
|
|
179
132
|
<div
|
|
@@ -285,8 +238,14 @@ function getOverlayTestId(kind: ActiveSelectionToolModel["kind"], hasPlacement:
|
|
|
285
238
|
}
|
|
286
239
|
}
|
|
287
240
|
|
|
288
|
-
|
|
289
|
-
|
|
241
|
+
/**
|
|
242
|
+
* R2.3: the detach affordance lands on every selection tier that has
|
|
243
|
+
* interactive chrome. `blocked-explainer` remains non-detachable —
|
|
244
|
+
* it's a status-only surface that should dismiss when the selection
|
|
245
|
+
* changes, not get pinned.
|
|
246
|
+
*/
|
|
247
|
+
function supportsDetachForKind(kind: ActiveSelectionToolModel["kind"]): boolean {
|
|
248
|
+
return kind !== "blocked-explainer";
|
|
290
249
|
}
|
|
291
250
|
|
|
292
251
|
function getTopMenuLabel(kind: ActiveSelectionToolModel["kind"]): string {
|
|
@@ -297,6 +256,12 @@ function getTopMenuLabel(kind: ActiveSelectionToolModel["kind"]): string {
|
|
|
297
256
|
return "Suggestion";
|
|
298
257
|
case "workflow-task":
|
|
299
258
|
return "Workflow";
|
|
259
|
+
case "structure-context":
|
|
260
|
+
return "Structure";
|
|
261
|
+
case "comment-thread":
|
|
262
|
+
return "Comment";
|
|
263
|
+
case "blocked-explainer":
|
|
264
|
+
return "Menu";
|
|
300
265
|
default:
|
|
301
266
|
return "Menu";
|
|
302
267
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selection-tool placement helper — pure function that decides where a
|
|
3
|
+
* selection-anchored floating chrome panel renders relative to its
|
|
4
|
+
* anchor rect.
|
|
5
|
+
*
|
|
6
|
+
* Extracts the right→left→above/below decision that lived in
|
|
7
|
+
* `tw-review-workspace.tsx::resolveSelectionToolbarPlacement` so
|
|
8
|
+
* consumers can feed it render-frame rects instead of DOM rects (R2.1
|
|
9
|
+
* owns the rect production; this helper is zoom-agnostic).
|
|
10
|
+
*
|
|
11
|
+
* The output is a `SelectionToolPlacement` compatible with
|
|
12
|
+
* `tw-selection-tool-host`'s existing contract so the rewire is
|
|
13
|
+
* additive — callers that already use `SelectionToolPlacement` keep
|
|
14
|
+
* working.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { CSSProperties } from "react";
|
|
18
|
+
|
|
19
|
+
import type { RenderFrameRect } from "../../api/public-types";
|
|
20
|
+
|
|
21
|
+
export interface SelectionToolPlacementDecision {
|
|
22
|
+
placement: "right" | "left" | "above" | "below";
|
|
23
|
+
style: CSSProperties;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResolveSelectionToolPlacementInput {
|
|
27
|
+
/** Anchor rect in overlay coordinate space. */
|
|
28
|
+
anchor: RenderFrameRect;
|
|
29
|
+
/** Container size in overlay coordinate space. */
|
|
30
|
+
container: { widthPx: number; heightPx: number };
|
|
31
|
+
/**
|
|
32
|
+
* Estimated toolbar width in px. Defaults to a reasonable bounded
|
|
33
|
+
* window (`clamp(168, 32% of container, 260)`) so the helper can run
|
|
34
|
+
* before the toolbar measures its own width.
|
|
35
|
+
*/
|
|
36
|
+
estimatedToolbarWidthPx?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Estimated toolbar height in px. Defaults to 44 (matches the
|
|
39
|
+
* legacy DOM-based math).
|
|
40
|
+
*/
|
|
41
|
+
estimatedToolbarHeightPx?: number;
|
|
42
|
+
/** Edge padding in px. Defaults to 16. */
|
|
43
|
+
edgePaddingPx?: number;
|
|
44
|
+
/** Gap between anchor and toolbar in px. Defaults to 12. */
|
|
45
|
+
gapPx?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_EDGE_PADDING = 16;
|
|
49
|
+
const DEFAULT_GAP = 12;
|
|
50
|
+
const DEFAULT_TOOLBAR_HEIGHT = 44;
|
|
51
|
+
const TOOLBAR_WIDTH_MIN = 168;
|
|
52
|
+
const TOOLBAR_WIDTH_MAX = 260;
|
|
53
|
+
const TOOLBAR_WIDTH_CONTAINER_FRACTION = 0.32;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decide where to render the selection-tool panel relative to its
|
|
57
|
+
* anchor. Prefers `right` when there is clearance, falls back to
|
|
58
|
+
* `left`, then `above` / `below` based on vertical room.
|
|
59
|
+
*
|
|
60
|
+
* Returns `null` when the container is zero-sized (the caller should
|
|
61
|
+
* then skip rendering entirely).
|
|
62
|
+
*/
|
|
63
|
+
export function resolveSelectionToolPlacement(
|
|
64
|
+
input: ResolveSelectionToolPlacementInput,
|
|
65
|
+
): SelectionToolPlacementDecision | null {
|
|
66
|
+
const { anchor, container } = input;
|
|
67
|
+
if (container.widthPx <= 0 || container.heightPx <= 0) return null;
|
|
68
|
+
|
|
69
|
+
const edgePadding = input.edgePaddingPx ?? DEFAULT_EDGE_PADDING;
|
|
70
|
+
const gapPx = input.gapPx ?? DEFAULT_GAP;
|
|
71
|
+
const toolbarHeight = input.estimatedToolbarHeightPx ?? DEFAULT_TOOLBAR_HEIGHT;
|
|
72
|
+
const toolbarWidth =
|
|
73
|
+
input.estimatedToolbarWidthPx ??
|
|
74
|
+
Math.min(
|
|
75
|
+
TOOLBAR_WIDTH_MAX,
|
|
76
|
+
Math.max(
|
|
77
|
+
TOOLBAR_WIDTH_MIN,
|
|
78
|
+
container.widthPx * TOOLBAR_WIDTH_CONTAINER_FRACTION,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const anchorLeft = anchor.leftPx;
|
|
83
|
+
const anchorRight = anchor.leftPx + anchor.widthPx;
|
|
84
|
+
const anchorTop = anchor.topPx;
|
|
85
|
+
const anchorBottom = anchor.topPx + anchor.heightPx;
|
|
86
|
+
const centerX = anchorLeft + anchor.widthPx / 2;
|
|
87
|
+
const centerY = anchorTop + anchor.heightPx / 2;
|
|
88
|
+
|
|
89
|
+
const rightClearance = container.widthPx - anchorRight - gapPx - edgePadding;
|
|
90
|
+
const leftClearance = anchorLeft - gapPx - edgePadding;
|
|
91
|
+
|
|
92
|
+
const clampedCenterX = Math.max(
|
|
93
|
+
edgePadding,
|
|
94
|
+
Math.min(
|
|
95
|
+
centerX,
|
|
96
|
+
Math.max(edgePadding, container.widthPx - edgePadding),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
const clampedCenterY = Math.max(
|
|
100
|
+
edgePadding + toolbarHeight / 2,
|
|
101
|
+
Math.min(
|
|
102
|
+
centerY,
|
|
103
|
+
Math.max(
|
|
104
|
+
edgePadding + toolbarHeight / 2,
|
|
105
|
+
container.heightPx - edgePadding - toolbarHeight / 2,
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
const maxWidthPx = Math.max(220, container.widthPx - edgePadding * 2);
|
|
110
|
+
|
|
111
|
+
if (rightClearance >= toolbarWidth) {
|
|
112
|
+
return {
|
|
113
|
+
placement: "right",
|
|
114
|
+
style: {
|
|
115
|
+
left: `${anchorRight}px`,
|
|
116
|
+
top: `${clampedCenterY}px`,
|
|
117
|
+
maxWidth: `${maxWidthPx}px`,
|
|
118
|
+
transform: `translate(${gapPx}px, -50%)`,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (leftClearance >= toolbarWidth) {
|
|
124
|
+
return {
|
|
125
|
+
placement: "left",
|
|
126
|
+
style: {
|
|
127
|
+
left: `${anchorLeft}px`,
|
|
128
|
+
top: `${clampedCenterY}px`,
|
|
129
|
+
maxWidth: `${maxWidthPx}px`,
|
|
130
|
+
transform: `translate(calc(-100% - ${gapPx}px), -50%)`,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const placement: "above" | "below" =
|
|
136
|
+
anchorTop < toolbarHeight + gapPx + edgePadding ? "below" : "above";
|
|
137
|
+
return {
|
|
138
|
+
placement,
|
|
139
|
+
style: {
|
|
140
|
+
left: `${clampedCenterX}px`,
|
|
141
|
+
top: `${placement === "above" ? anchorTop : anchorBottom}px`,
|
|
142
|
+
maxWidth: `${maxWidthPx}px`,
|
|
143
|
+
transform:
|
|
144
|
+
placement === "above"
|
|
145
|
+
? `translate(-50%, calc(-100% - ${gapPx}px))`
|
|
146
|
+
: `translate(-50%, ${gapPx}px)`,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -22,6 +22,15 @@ export interface TwSelectionToolbarProps {
|
|
|
22
22
|
const focusRingClass =
|
|
23
23
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Fallback colors for the selection-toolbar one-click apply buttons.
|
|
27
|
+
* The model can override via `textColorDefault` / `highlightColorDefault`
|
|
28
|
+
* which R2.5 plumbs through from `formattingState` so the apply button
|
|
29
|
+
* reflects the user's recent color pick.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_TEXT_COLOR = "#1660a8";
|
|
32
|
+
const DEFAULT_HIGHLIGHT_COLOR = "#ffff00";
|
|
33
|
+
|
|
25
34
|
export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarProps>(function TwSelectionToolbar(props, ref) {
|
|
26
35
|
const { model } = props;
|
|
27
36
|
const addCommentDisabled = !model.canAddComment;
|
|
@@ -64,17 +73,19 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
64
73
|
/>
|
|
65
74
|
<ToolbarActionButton
|
|
66
75
|
icon={<Baseline className="h-3.5 w-3.5" />}
|
|
67
|
-
label=
|
|
76
|
+
label={`Apply ${model.textColorDefault ?? DEFAULT_TEXT_COLOR}`}
|
|
68
77
|
pressed={false}
|
|
69
78
|
disabled={formattingDisabled}
|
|
70
|
-
onClick={() => props.onSetTextColor?.(
|
|
79
|
+
onClick={() => props.onSetTextColor?.(model.textColorDefault ?? DEFAULT_TEXT_COLOR)}
|
|
71
80
|
/>
|
|
72
81
|
<ToolbarActionButton
|
|
73
82
|
icon={<Highlighter className="h-3.5 w-3.5" />}
|
|
74
|
-
label=
|
|
83
|
+
label={`Apply ${model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR} highlight`}
|
|
75
84
|
pressed={false}
|
|
76
85
|
disabled={formattingDisabled}
|
|
77
|
-
onClick={() =>
|
|
86
|
+
onClick={() =>
|
|
87
|
+
props.onSetHighlightColor?.(model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR)
|
|
88
|
+
}
|
|
78
89
|
/>
|
|
79
90
|
|
|
80
91
|
<div className="mx-0.5 h-4 w-px bg-border" />
|