@beyondwork/docx-react-component 1.0.56 → 1.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/api/public-types.ts +330 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +17 -11
- package/src/core/selection/mapping.ts +18 -1
- package/src/core/selection/review-anchors.ts +29 -18
- package/src/io/chart-preview-resolver.ts +175 -41
- package/src/io/docx-session.ts +57 -2
- package/src/io/export/serialize-main-document.ts +82 -0
- package/src/io/export/serialize-styles.ts +61 -3
- package/src/io/export/table-properties-xml.ts +19 -4
- package/src/io/normalize/normalize-text.ts +33 -0
- package/src/io/ooxml/parse-anchor.ts +182 -0
- package/src/io/ooxml/parse-drawing.ts +319 -0
- package/src/io/ooxml/parse-fields.ts +115 -2
- package/src/io/ooxml/parse-fill.ts +215 -0
- package/src/io/ooxml/parse-font-table.ts +190 -0
- package/src/io/ooxml/parse-footnotes.ts +52 -1
- package/src/io/ooxml/parse-main-document.ts +241 -1
- package/src/io/ooxml/parse-numbering.ts +96 -0
- package/src/io/ooxml/parse-picture.ts +158 -0
- package/src/io/ooxml/parse-settings.ts +34 -0
- package/src/io/ooxml/parse-shapes.ts +87 -0
- package/src/io/ooxml/parse-solid-fill.ts +11 -0
- package/src/io/ooxml/parse-styles.ts +74 -1
- package/src/io/ooxml/parse-theme.ts +60 -0
- package/src/io/paste/html-clipboard.ts +449 -0
- package/src/io/paste/word-clipboard.ts +5 -1
- package/src/legal/_document-root.ts +26 -0
- package/src/legal/bookmarks.ts +4 -3
- package/src/legal/cross-references.ts +3 -2
- package/src/legal/defined-terms.ts +2 -1
- package/src/legal/signature-blocks.ts +2 -1
- package/src/model/canonical-document.ts +421 -3
- package/src/runtime/chart/chart-model-store.ts +73 -10
- package/src/runtime/document-runtime.ts +760 -41
- package/src/runtime/document-search.ts +61 -0
- package/src/runtime/edit-ops/index.ts +129 -0
- package/src/runtime/event-refresh-hints.ts +7 -0
- package/src/runtime/field-resolver.ts +341 -0
- package/src/runtime/footnote-resolver.ts +55 -0
- package/src/runtime/hyperlink-color-resolver.ts +13 -10
- package/src/runtime/object-grab/index.ts +51 -0
- package/src/runtime/paragraph-style-resolver.ts +105 -0
- package/src/runtime/query-scopes.ts +186 -0
- package/src/runtime/resolved-numbering-geometry.ts +12 -0
- package/src/runtime/scope-resolver.ts +60 -0
- package/src/runtime/selection/cursor-ops.ts +186 -15
- package/src/runtime/selection/index.ts +17 -1
- package/src/runtime/structure-ops/index.ts +77 -0
- package/src/runtime/styles-cascade.ts +33 -0
- package/src/runtime/surface-projection.ts +192 -12
- package/src/runtime/theme-color-resolver.ts +189 -44
- package/src/runtime/units.ts +46 -0
- package/src/runtime/view-state.ts +13 -2
- package/src/ui/WordReviewEditor.tsx +239 -11
- package/src/ui/editor-runtime-boundary.ts +97 -1
- package/src/ui/editor-shell-view.tsx +1 -1
- package/src/ui/runtime-shortcut-dispatch.ts +17 -3
- package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
- package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
- package/src/ui-tailwind/chart/render/area.tsx +22 -4
- package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
- package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
- package/src/ui-tailwind/chart/render/combo.tsx +37 -4
- package/src/ui-tailwind/chart/render/line.tsx +28 -5
- package/src/ui-tailwind/chart/render/pie.tsx +36 -16
- package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
- package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
- package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
- package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +24 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
- package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +157 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
- package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
- package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
- package/src/ui-tailwind/editor-surface/pm-schema.ts +214 -11
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +32 -2
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
- package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
- package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
- package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
- package/src/ui-tailwind/theme/editor-theme.css +1 -0
- package/src/ui-tailwind/theme/tokens.css +6 -0
- package/src/ui-tailwind/theme/tokens.ts +10 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +23 -0
- package/src/validation/compatibility-engine.ts +2 -0
- package/src/validation/docx-comment-proof.ts +12 -3
|
@@ -52,6 +52,7 @@ import type {
|
|
|
52
52
|
SurfaceBlockSnapshot,
|
|
53
53
|
SurfaceInlineSegment,
|
|
54
54
|
TableOp,
|
|
55
|
+
CanonicalDocumentFragment,
|
|
55
56
|
TableOpResult,
|
|
56
57
|
PublicTableEvent,
|
|
57
58
|
PublicTableRenderPlan,
|
|
@@ -150,7 +151,7 @@ import {
|
|
|
150
151
|
} from "../io/source-package-provenance.ts";
|
|
151
152
|
import { readOpcPackage } from "../io/opc/package-reader.ts";
|
|
152
153
|
import { deriveCapabilities } from "../runtime/session-capabilities";
|
|
153
|
-
import { searchDocument } from "../runtime/document-search.ts";
|
|
154
|
+
import { findTextMatches, searchDocument } from "../runtime/document-search.ts";
|
|
154
155
|
import {
|
|
155
156
|
resolveCurrentContextAnalyticsQuery,
|
|
156
157
|
runtimeContextAnalyticsSnapshotsEqual,
|
|
@@ -480,6 +481,50 @@ function collectChartSnapshots(doc: CanonicalDocType): import("../api/public-typ
|
|
|
480
481
|
return results;
|
|
481
482
|
}
|
|
482
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Walk the canonical document, compute each chart_preview's stableChartId,
|
|
486
|
+
* and project a snapshot for the first matching id. Short-circuits on
|
|
487
|
+
* match so the happy path is O(k) in blocks-until-match rather than the
|
|
488
|
+
* O(N) the `collect().find()` fallback incurred for every ref call. For
|
|
489
|
+
* hosts that call `getChartSnapshot` in a tight loop over many chartIds,
|
|
490
|
+
* this reduces the cost from O(N²) to O(N·k).
|
|
491
|
+
*/
|
|
492
|
+
function lookupChartSnapshot(
|
|
493
|
+
doc: CanonicalDocType,
|
|
494
|
+
chartId: string,
|
|
495
|
+
): import("../api/public-types").ChartSnapshot | null {
|
|
496
|
+
return lookupChartSnapshotInBlocks(doc.content.children, chartId);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function lookupChartSnapshotInBlocks(
|
|
500
|
+
blocks: CanonicalDocType["content"]["children"],
|
|
501
|
+
chartId: string,
|
|
502
|
+
): import("../api/public-types").ChartSnapshot | null {
|
|
503
|
+
for (const block of blocks) {
|
|
504
|
+
if (block.type === "paragraph") {
|
|
505
|
+
for (const inline of block.children) {
|
|
506
|
+
if (inline.type === "chart_preview" && inline.parsedData) {
|
|
507
|
+
const id = stableChartId(inline.rawXml);
|
|
508
|
+
if (id === chartId) {
|
|
509
|
+
return projectChartSnapshot(id, inline.parsedData);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} else if (block.type === "table") {
|
|
514
|
+
for (const row of block.rows) {
|
|
515
|
+
for (const cell of row.cells) {
|
|
516
|
+
const found = lookupChartSnapshotInBlocks(cell.children, chartId);
|
|
517
|
+
if (found) return found;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} else if (block.type === "sdt" || block.type === "custom_xml") {
|
|
521
|
+
const found = lookupChartSnapshotInBlocks(block.children, chartId);
|
|
522
|
+
if (found) return found;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
483
528
|
function collectChartSnapshotsFromBlocks(
|
|
484
529
|
blocks: CanonicalDocType["content"]["children"],
|
|
485
530
|
results: import("../api/public-types").ChartSnapshot[],
|
|
@@ -528,6 +573,16 @@ export function __createWordReviewEditorRefBridge(
|
|
|
528
573
|
redo: () => runtime.redo(),
|
|
529
574
|
replaceText: (text, target, formatting) => runtime.replaceText(text, target, formatting),
|
|
530
575
|
insertFragment: (fragment, target) => runtime.insertFragment(fragment, target),
|
|
576
|
+
copy: (target) => runtime.copy(target),
|
|
577
|
+
cut: (target) => runtime.cut(target),
|
|
578
|
+
getClipboardBuffer: () => runtime.getClipboardBuffer(),
|
|
579
|
+
getClipboardWireFormats: () => runtime.getClipboardWireFormats(),
|
|
580
|
+
selectObject: (objectId) => runtime.selectObject(objectId),
|
|
581
|
+
deselectObject: () => runtime.deselectObject(),
|
|
582
|
+
getGrabbedObject: () => runtime.getGrabbedObject(),
|
|
583
|
+
startAction: (name) => runtime.startAction(name),
|
|
584
|
+
endAction: () => runtime.endAction(),
|
|
585
|
+
isInAction: () => runtime.isInAction(),
|
|
531
586
|
addComment: (params) => runtime.addComment(params),
|
|
532
587
|
openComment: (commentId) => runtime.openComment(commentId),
|
|
533
588
|
resolveComment: (commentId) => runtime.resolveComment(commentId),
|
|
@@ -554,11 +609,7 @@ export function __createWordReviewEditorRefBridge(
|
|
|
554
609
|
getRenderSnapshot: () => clonePublicValue(runtime.getRenderSnapshot()),
|
|
555
610
|
getCompatibilityReport: () => runtime.getCompatibilityReport(),
|
|
556
611
|
getWarnings: () => runtime.getWarnings(),
|
|
557
|
-
getChartSnapshot: (chartId) =>
|
|
558
|
-
return collectChartSnapshots(runtime.getCanonicalDocument()).find(
|
|
559
|
-
(s) => s.chartId === chartId,
|
|
560
|
-
) ?? null;
|
|
561
|
-
},
|
|
612
|
+
getChartSnapshot: (chartId) => lookupChartSnapshot(runtime.getCanonicalDocument(), chartId),
|
|
562
613
|
getChartSnapshots: () => collectChartSnapshots(runtime.getCanonicalDocument()),
|
|
563
614
|
getCommentSidebarSnapshot: () =>
|
|
564
615
|
clonePublicValue(runtime.getRenderSnapshot().comments),
|
|
@@ -858,6 +909,34 @@ export function __createWordReviewEditorRefBridge(
|
|
|
858
909
|
getWorkflowMetadataSnapshot: () => {
|
|
859
910
|
return clonePublicValue(runtime.getWorkflowMetadataSnapshot());
|
|
860
911
|
},
|
|
912
|
+
queryScopes: (filter) => {
|
|
913
|
+
return clonePublicValue(runtime.queryScopes(filter));
|
|
914
|
+
},
|
|
915
|
+
findScopesAt: (position, options) => {
|
|
916
|
+
return clonePublicValue(runtime.findScopesAt(position, options));
|
|
917
|
+
},
|
|
918
|
+
findScopesIntersecting: (range, options) => {
|
|
919
|
+
return clonePublicValue(runtime.findScopesIntersecting(range, options));
|
|
920
|
+
},
|
|
921
|
+
findFirstText: (query, opts) => {
|
|
922
|
+
const hits = findTextMatchesForRuntime(runtime, query, opts);
|
|
923
|
+
return hits.length > 0 ? (hits[0] ?? null) : null;
|
|
924
|
+
},
|
|
925
|
+
findAllText: (query, opts) => {
|
|
926
|
+
return findTextMatchesForRuntime(runtime, query, opts);
|
|
927
|
+
},
|
|
928
|
+
selectFirstText: (query, opts) => {
|
|
929
|
+
const hits = findTextMatchesForRuntime(runtime, query, opts);
|
|
930
|
+
if (hits.length === 0) return false;
|
|
931
|
+
applyRuntimeSelection(runtime, createSelectionFromAnchor(hits[0]!));
|
|
932
|
+
return true;
|
|
933
|
+
},
|
|
934
|
+
selectAllText: (query, opts) => {
|
|
935
|
+
const hits = findTextMatchesForRuntime(runtime, query, opts);
|
|
936
|
+
if (hits.length === 0) return 0;
|
|
937
|
+
applyRuntimeSelection(runtime, createSelectionFromAnchor(hits[0]!));
|
|
938
|
+
return hits.length;
|
|
939
|
+
},
|
|
861
940
|
// P17 — metadata persistence toggle + convert methods.
|
|
862
941
|
setMetadataPersistenceMode: (mode) => {
|
|
863
942
|
if (mode === "external" && !(options?.resolverRef?.current ?? null)) {
|
|
@@ -1528,6 +1607,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1528
1607
|
redo: () => activeRuntime.redo(),
|
|
1529
1608
|
replaceText: (text, target, formatting) => activeRuntime.replaceText(text, target, formatting),
|
|
1530
1609
|
insertFragment: (fragment, target) => activeRuntime.insertFragment(fragment, target),
|
|
1610
|
+
copy: (target) => activeRuntime.copy(target),
|
|
1611
|
+
cut: (target) => activeRuntime.cut(target),
|
|
1612
|
+
getClipboardBuffer: () => activeRuntime.getClipboardBuffer(),
|
|
1613
|
+
getClipboardWireFormats: () => activeRuntime.getClipboardWireFormats(),
|
|
1614
|
+
selectObject: (objectId) => activeRuntime.selectObject(objectId),
|
|
1615
|
+
deselectObject: () => activeRuntime.deselectObject(),
|
|
1616
|
+
getGrabbedObject: () => activeRuntime.getGrabbedObject(),
|
|
1617
|
+
startAction: (name) => activeRuntime.startAction(name),
|
|
1618
|
+
endAction: () => activeRuntime.endAction(),
|
|
1619
|
+
isInAction: () => activeRuntime.isInAction(),
|
|
1531
1620
|
addComment: (params) =>
|
|
1532
1621
|
activeRuntime.addComment({
|
|
1533
1622
|
...params,
|
|
@@ -1584,9 +1673,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1584
1673
|
getCompatibilityReport: () => activeRuntime.getCompatibilityReport(),
|
|
1585
1674
|
getWarnings: () => activeRuntime.getWarnings(),
|
|
1586
1675
|
getChartSnapshot: (chartId) =>
|
|
1587
|
-
|
|
1588
|
-
(s) => s.chartId === chartId,
|
|
1589
|
-
) ?? null,
|
|
1676
|
+
lookupChartSnapshot(activeRuntime.getCanonicalDocument(), chartId),
|
|
1590
1677
|
getChartSnapshots: () => collectChartSnapshots(activeRuntime.getCanonicalDocument()),
|
|
1591
1678
|
getCommentSidebarSnapshot: () =>
|
|
1592
1679
|
clonePublicValue(activeRuntime.getRenderSnapshot().comments),
|
|
@@ -1917,6 +2004,34 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1917
2004
|
getWorkflowMetadataSnapshot: () => {
|
|
1918
2005
|
return clonePublicValue(activeRuntime.getWorkflowMetadataSnapshot());
|
|
1919
2006
|
},
|
|
2007
|
+
queryScopes: (filter) => {
|
|
2008
|
+
return clonePublicValue(activeRuntime.queryScopes(filter));
|
|
2009
|
+
},
|
|
2010
|
+
findScopesAt: (position, options) => {
|
|
2011
|
+
return clonePublicValue(activeRuntime.findScopesAt(position, options));
|
|
2012
|
+
},
|
|
2013
|
+
findScopesIntersecting: (range, options) => {
|
|
2014
|
+
return clonePublicValue(activeRuntime.findScopesIntersecting(range, options));
|
|
2015
|
+
},
|
|
2016
|
+
findFirstText: (query, opts) => {
|
|
2017
|
+
const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
|
|
2018
|
+
return hits.length > 0 ? (hits[0] ?? null) : null;
|
|
2019
|
+
},
|
|
2020
|
+
findAllText: (query, opts) => {
|
|
2021
|
+
return findTextMatchesForRuntime(activeRuntime, query, opts);
|
|
2022
|
+
},
|
|
2023
|
+
selectFirstText: (query, opts) => {
|
|
2024
|
+
const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
|
|
2025
|
+
if (hits.length === 0) return false;
|
|
2026
|
+
applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(hits[0]!));
|
|
2027
|
+
return true;
|
|
2028
|
+
},
|
|
2029
|
+
selectAllText: (query, opts) => {
|
|
2030
|
+
const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
|
|
2031
|
+
if (hits.length === 0) return 0;
|
|
2032
|
+
applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(hits[0]!));
|
|
2033
|
+
return hits.length;
|
|
2034
|
+
},
|
|
1920
2035
|
// P17 — metadata persistence toggle + convert methods.
|
|
1921
2036
|
setMetadataPersistenceMode: (mode) => {
|
|
1922
2037
|
if (mode === "external" && scopeMetadataResolverRef.current === null) {
|
|
@@ -2777,10 +2892,84 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2777
2892
|
onBlur: handleSurfaceBlur,
|
|
2778
2893
|
onSelectionChange: dispatchSelection,
|
|
2779
2894
|
onInsertText: (text: string) => dispatchTextCommand(activeRuntime, { type: "insert-text", text }, DISPATCH_CONTEXT),
|
|
2780
|
-
|
|
2781
|
-
|
|
2895
|
+
// v5 follow-up — Delete/Backspace when a grab is active (R.3
|
|
2896
|
+
// ObjectGrabLayer). LO reference: `SwFEShell::DeleteSelection()` is
|
|
2897
|
+
// grab-aware; without grab-awareness, Delete would run
|
|
2898
|
+
// paragraph-delete on whichever paragraph the text caret happened to
|
|
2899
|
+
// be in — a silent data-loss footgun.
|
|
2900
|
+
//
|
|
2901
|
+
// Minimum safe behavior until a proper `object.delete` runtime
|
|
2902
|
+
// command + Lane 6 P11 chrome land: swallow the key (don't run
|
|
2903
|
+
// paragraph-delete) and deselect the grab so the user can see that
|
|
2904
|
+
// the key was recognized. The actual object removal follows when
|
|
2905
|
+
// chrome wires resize/delete drag handles; this gate just prevents
|
|
2906
|
+
// the paragraph-delete footgun.
|
|
2907
|
+
onDeleteBackward: () => {
|
|
2908
|
+
const grabbed = activeRuntime.getGrabbedObject();
|
|
2909
|
+
if (grabbed) {
|
|
2910
|
+
activeRuntime.deselectObject();
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
dispatchTextCommand(activeRuntime, { type: "delete-backward" }, DISPATCH_CONTEXT);
|
|
2914
|
+
},
|
|
2915
|
+
onDeleteForward: () => {
|
|
2916
|
+
const grabbed = activeRuntime.getGrabbedObject();
|
|
2917
|
+
if (grabbed) {
|
|
2918
|
+
activeRuntime.deselectObject();
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
dispatchTextCommand(activeRuntime, { type: "delete-forward" }, DISPATCH_CONTEXT);
|
|
2922
|
+
},
|
|
2782
2923
|
onInsertTab: () => dispatchTextCommand(activeRuntime, { type: "insert-tab" }, DISPATCH_CONTEXT),
|
|
2783
2924
|
onOutdentTab: () => dispatchTextCommand(activeRuntime, { type: "outdent-tab" }, DISPATCH_CONTEXT),
|
|
2925
|
+
// I3 widening tail — Tab at last cell: dispatch `add-row-after` via the
|
|
2926
|
+
// same table-structure-operation path every other row-insert caller uses
|
|
2927
|
+
// (toolbar, context menu, ref method), so track-changes + collab replay
|
|
2928
|
+
// stay consistent. See src/ui-tailwind/editor-surface/pm-command-bridge.ts
|
|
2929
|
+
// `isAtLastCellOfTable` for the detection criterion.
|
|
2930
|
+
onTableInsertRowBelow: () =>
|
|
2931
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
2932
|
+
type: "add-row-after",
|
|
2933
|
+
}),
|
|
2934
|
+
// I2 Tier B Slice 5 — drag-to-move / drag-to-copy.
|
|
2935
|
+
// v5 close-out: bridge now emits `dragstart`/`dragend` so same-editor
|
|
2936
|
+
// drag-to-move gets `sourceRange` populated. On move-effect drops we
|
|
2937
|
+
// insert the fragment AND delete the source range — the full Word
|
|
2938
|
+
// behavior. Both ops are wrapped in one action bracket via the
|
|
2939
|
+
// auto-bracketing in insertFragment + our explicit startAction around
|
|
2940
|
+
// the delete, so the undo history sees a single atomic action.
|
|
2941
|
+
onDropFragment: (meta: {
|
|
2942
|
+
fragment: CanonicalDocumentFragment;
|
|
2943
|
+
effect: "move" | "copy";
|
|
2944
|
+
sourceRange?: { from: number; to: number };
|
|
2945
|
+
}) => {
|
|
2946
|
+
if (meta.effect === "move" && meta.sourceRange) {
|
|
2947
|
+
// Delete source FIRST, then insert at the drop position. This order
|
|
2948
|
+
// is correctness-critical: if the drop was AFTER the source, the
|
|
2949
|
+
// insertion would shift the source offsets forward and a subsequent
|
|
2950
|
+
// delete would target the wrong (post-insertion) positions. By
|
|
2951
|
+
// deleting first and letting `insertFragment` land on the current
|
|
2952
|
+
// selection, both orderings behave correctly. The runtime's
|
|
2953
|
+
// selection validator (R.5.b) clamps the drop-site selection if
|
|
2954
|
+
// the source delete shifted it. Word degenerates move to copy
|
|
2955
|
+
// when drop is inside source; we pass through unchanged.
|
|
2956
|
+
activeRuntime.startAction("drag-move");
|
|
2957
|
+
try {
|
|
2958
|
+
activeRuntime.replaceText("", {
|
|
2959
|
+
kind: "range",
|
|
2960
|
+
from: meta.sourceRange.from,
|
|
2961
|
+
to: meta.sourceRange.to,
|
|
2962
|
+
assoc: { start: -1, end: 1 },
|
|
2963
|
+
});
|
|
2964
|
+
activeRuntime.insertFragment(meta.fragment);
|
|
2965
|
+
} finally {
|
|
2966
|
+
activeRuntime.endAction();
|
|
2967
|
+
}
|
|
2968
|
+
} else {
|
|
2969
|
+
// Copy-effect OR external drag (no sourceRange) — just insert.
|
|
2970
|
+
activeRuntime.insertFragment(meta.fragment);
|
|
2971
|
+
}
|
|
2972
|
+
},
|
|
2784
2973
|
onInsertHardBreak: () => dispatchTextCommand(activeRuntime, { type: "insert-hard-break" }, DISPATCH_CONTEXT),
|
|
2785
2974
|
onSplitParagraph: () => dispatchTextCommand(activeRuntime, { type: "split-paragraph" }, DISPATCH_CONTEXT),
|
|
2786
2975
|
onUndo: () => activeRuntime.undo(),
|
|
@@ -2803,6 +2992,31 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2803
2992
|
source: meta.source,
|
|
2804
2993
|
});
|
|
2805
2994
|
},
|
|
2995
|
+
// v5 close-out: wire rich-paste fragment routing to insertFragment so
|
|
2996
|
+
// Office / HTML clipboard payloads land in the mounted editor. The
|
|
2997
|
+
// insertFragment auto-bracket (R.5.a Phase 2) makes the paste a
|
|
2998
|
+
// single-undo action.
|
|
2999
|
+
onPasteFragment: (meta: {
|
|
3000
|
+
fragment: CanonicalDocumentFragment;
|
|
3001
|
+
source: "wordml" | "html";
|
|
3002
|
+
}) => {
|
|
3003
|
+
activeRuntime.insertFragment(meta.fragment);
|
|
3004
|
+
},
|
|
3005
|
+
// v5 close-out: image paste via existing insertImage ref method.
|
|
3006
|
+
// Width/height are omitted so the renderer picks sensible defaults
|
|
3007
|
+
// based on the decoded bitmap dimensions (existing insertImage
|
|
3008
|
+
// behavior); hosts that need specific dimensions can intercept this
|
|
3009
|
+
// callback on their own CommandBridgeCallbacks override.
|
|
3010
|
+
onPasteImage: (meta: {
|
|
3011
|
+
data: Uint8Array;
|
|
3012
|
+
mimeType: string;
|
|
3013
|
+
source: "paste" | "drop";
|
|
3014
|
+
}) => {
|
|
3015
|
+
applyRuntimeInsertImage(activeRuntime, {
|
|
3016
|
+
data: meta.data,
|
|
3017
|
+
mimeType: meta.mimeType,
|
|
3018
|
+
});
|
|
3019
|
+
},
|
|
2806
3020
|
};
|
|
2807
3021
|
|
|
2808
3022
|
const reviewCallbacks = {
|
|
@@ -4898,6 +5112,20 @@ function clonePublicValue<T>(value: T): T {
|
|
|
4898
5112
|
return structuredClone(value);
|
|
4899
5113
|
}
|
|
4900
5114
|
|
|
5115
|
+
function findTextMatchesForRuntime(
|
|
5116
|
+
runtime: WordReviewEditorRuntime,
|
|
5117
|
+
query: string,
|
|
5118
|
+
options: SearchOptions | undefined,
|
|
5119
|
+
): EditorAnchorProjection[] {
|
|
5120
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
5121
|
+
return findTextMatches(
|
|
5122
|
+
runtime.getSessionState().canonicalDocument,
|
|
5123
|
+
snapshot.selection,
|
|
5124
|
+
query,
|
|
5125
|
+
options ?? {},
|
|
5126
|
+
);
|
|
5127
|
+
}
|
|
5128
|
+
|
|
4901
5129
|
/**
|
|
4902
5130
|
* Open the correct header/footer story for a specific page. The page's
|
|
4903
5131
|
* resolved `stories.header` / `stories.footer` already carries the
|
|
@@ -122,6 +122,12 @@ export interface WordReviewEditorRuntime extends DocumentRuntime {
|
|
|
122
122
|
): void;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
type InternalWordReviewEditorRuntime = WordReviewEditorRuntime & {
|
|
126
|
+
hydrateChartPreviews?: (
|
|
127
|
+
resolvedDocument: EditorSessionState["canonicalDocument"],
|
|
128
|
+
) => boolean;
|
|
129
|
+
};
|
|
130
|
+
|
|
125
131
|
type PackageBackedDocxSession = ReturnType<typeof loadDocxEditorSession>;
|
|
126
132
|
|
|
127
133
|
interface SnapshotExportBarrier {
|
|
@@ -313,6 +319,9 @@ export function useEditorRuntimeBoundary(
|
|
|
313
319
|
progressiveSurfaceRef.current = progressiveSurface;
|
|
314
320
|
const runtimeRef = useRef<WordReviewEditorRuntime | null>(null);
|
|
315
321
|
const pendingReadySourceRef = useRef<"docx" | "session" | "snapshot" | null>(null);
|
|
322
|
+
const activeLoadTokenRef = useRef<symbol | null>(null);
|
|
323
|
+
const pendingChartPreviewDocRef =
|
|
324
|
+
useRef<EditorSessionState["canonicalDocument"] | null>(null);
|
|
316
325
|
const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
317
326
|
const lastSavedRevisionTokenRef = useRef<string | null>(null);
|
|
318
327
|
// Fastload P6: scheduler used by the DOM-side async docx loader. Held
|
|
@@ -386,6 +395,9 @@ export function useEditorRuntimeBoundary(
|
|
|
386
395
|
async function loadRuntime(): Promise<void> {
|
|
387
396
|
setLoadError(null);
|
|
388
397
|
pendingReadySourceRef.current = null;
|
|
398
|
+
pendingChartPreviewDocRef.current = null;
|
|
399
|
+
const loadToken = Symbol(documentId);
|
|
400
|
+
activeLoadTokenRef.current = loadToken;
|
|
389
401
|
if (autosaveTimerRef.current) {
|
|
390
402
|
clearTimeout(autosaveTimerRef.current);
|
|
391
403
|
autosaveTimerRef.current = null;
|
|
@@ -448,12 +460,34 @@ export function useEditorRuntimeBoundary(
|
|
|
448
460
|
return;
|
|
449
461
|
}
|
|
450
462
|
recordPerfSample("loadSession.laycacheProbe");
|
|
463
|
+
const handleChartPreviewsReady = (
|
|
464
|
+
resolvedDocument: EditorSessionState["canonicalDocument"],
|
|
465
|
+
): void => {
|
|
466
|
+
if (cancelled || activeLoadTokenRef.current !== loadToken) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
pendingChartPreviewDocRef.current = resolvedDocument;
|
|
470
|
+
if (source.preloadedDocxSession) {
|
|
471
|
+
source.preloadedDocxSession.initialSessionState = {
|
|
472
|
+
...source.preloadedDocxSession.initialSessionState,
|
|
473
|
+
canonicalDocument: resolvedDocument,
|
|
474
|
+
};
|
|
475
|
+
source.preloadedDocxSession.initialSnapshot = {
|
|
476
|
+
...source.preloadedDocxSession.initialSnapshot,
|
|
477
|
+
canonicalDocument: resolvedDocument,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
(
|
|
481
|
+
runtimeRef.current as InternalWordReviewEditorRuntime | null
|
|
482
|
+
)?.hydrateChartPreviews?.(resolvedDocument);
|
|
483
|
+
};
|
|
451
484
|
const preloaded = await loadDocxEditorSessionAsync({
|
|
452
485
|
documentId,
|
|
453
486
|
sourceLabel: source.sourceLabel,
|
|
454
487
|
bytes: source.initialDocx,
|
|
455
488
|
editorBuild: "dev",
|
|
456
489
|
scheduler,
|
|
490
|
+
onChartPreviewsReady: handleChartPreviewsReady,
|
|
457
491
|
...(probeResult ? { laycacheEnvelope: probeResult.envelope } : {
|
|
458
492
|
// C2c: on the cold path (no cached envelope), emit the first
|
|
459
493
|
// viewport surface as soon as body-normalize completes so the
|
|
@@ -475,6 +509,16 @@ export function useEditorRuntimeBoundary(
|
|
|
475
509
|
return;
|
|
476
510
|
}
|
|
477
511
|
source.preloadedDocxSession = preloaded;
|
|
512
|
+
if (pendingChartPreviewDocRef.current) {
|
|
513
|
+
source.preloadedDocxSession.initialSessionState = {
|
|
514
|
+
...source.preloadedDocxSession.initialSessionState,
|
|
515
|
+
canonicalDocument: pendingChartPreviewDocRef.current,
|
|
516
|
+
};
|
|
517
|
+
source.preloadedDocxSession.initialSnapshot = {
|
|
518
|
+
...source.preloadedDocxSession.initialSnapshot,
|
|
519
|
+
canonicalDocument: pendingChartPreviewDocRef.current,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
478
522
|
if (probeResult) {
|
|
479
523
|
source.preloadedLaycacheGraph = probeResult.envelope.graph;
|
|
480
524
|
}
|
|
@@ -499,6 +543,11 @@ export function useEditorRuntimeBoundary(
|
|
|
499
543
|
);
|
|
500
544
|
recordPerfSample("runtime.create");
|
|
501
545
|
runtimeRef.current = nextRuntime;
|
|
546
|
+
if (pendingChartPreviewDocRef.current) {
|
|
547
|
+
(
|
|
548
|
+
nextRuntime as InternalWordReviewEditorRuntime
|
|
549
|
+
).hydrateChartPreviews?.(pendingChartPreviewDocRef.current);
|
|
550
|
+
}
|
|
502
551
|
pendingReadySourceRef.current = source.source;
|
|
503
552
|
// C2c: clear the transient progressive surface — the full runtime
|
|
504
553
|
// snapshot supersedes it. No need for startTransition here since
|
|
@@ -530,6 +579,8 @@ export function useEditorRuntimeBoundary(
|
|
|
530
579
|
|
|
531
580
|
return () => {
|
|
532
581
|
cancelled = true;
|
|
582
|
+
pendingChartPreviewDocRef.current = null;
|
|
583
|
+
activeLoadTokenRef.current = null;
|
|
533
584
|
};
|
|
534
585
|
}, [
|
|
535
586
|
documentId,
|
|
@@ -753,6 +804,11 @@ function createRuntime(
|
|
|
753
804
|
defaultAuthorId: args.currentUserId,
|
|
754
805
|
onCommandApplied: args.commandAppliedBridge?.onCommandApplied,
|
|
755
806
|
});
|
|
807
|
+
const internalBaseRuntime = baseRuntime as DocumentRuntime & {
|
|
808
|
+
hydrateCanonicalDocumentInternally?: (
|
|
809
|
+
document: EditorSessionState["canonicalDocument"],
|
|
810
|
+
) => boolean;
|
|
811
|
+
};
|
|
756
812
|
|
|
757
813
|
// Schema 1.2: drive load-path hydration from the parsed envelope.
|
|
758
814
|
if (docxSession?.initialEditorStatePayload) {
|
|
@@ -784,7 +840,7 @@ function createRuntime(
|
|
|
784
840
|
});
|
|
785
841
|
}
|
|
786
842
|
|
|
787
|
-
const runtime:
|
|
843
|
+
const runtime: InternalWordReviewEditorRuntime = Object.assign(baseRuntime, {
|
|
788
844
|
drainBootstrapEvents: () => bootstrapEvents.splice(0, bootstrapEvents.length),
|
|
789
845
|
emitBlockedCommand: (
|
|
790
846
|
command: string,
|
|
@@ -802,6 +858,25 @@ function createRuntime(
|
|
|
802
858
|
},
|
|
803
859
|
});
|
|
804
860
|
},
|
|
861
|
+
hydrateChartPreviews: (
|
|
862
|
+
resolvedDocument: EditorSessionState["canonicalDocument"],
|
|
863
|
+
): boolean => {
|
|
864
|
+
if (docxSession) {
|
|
865
|
+
docxSession.initialSessionState = {
|
|
866
|
+
...docxSession.initialSessionState,
|
|
867
|
+
canonicalDocument: resolvedDocument,
|
|
868
|
+
};
|
|
869
|
+
docxSession.initialSnapshot = {
|
|
870
|
+
...docxSession.initialSnapshot,
|
|
871
|
+
canonicalDocument: resolvedDocument,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
return (
|
|
875
|
+
internalBaseRuntime.hydrateCanonicalDocumentInternally?.(
|
|
876
|
+
resolvedDocument,
|
|
877
|
+
) ?? false
|
|
878
|
+
);
|
|
879
|
+
},
|
|
805
880
|
});
|
|
806
881
|
|
|
807
882
|
return runtime;
|
|
@@ -947,8 +1022,21 @@ function createLoadingRuntimeBridge(input: {
|
|
|
947
1022
|
},
|
|
948
1023
|
getCanonicalDocument: () => input.sessionState.canonicalDocument,
|
|
949
1024
|
getSourcePackage: () => input.sessionState.sourcePackage,
|
|
1025
|
+
getFontTable: () => input.sessionState.canonicalDocument.fontTable,
|
|
1026
|
+
getFontEntry: (name: string) =>
|
|
1027
|
+
input.sessionState.canonicalDocument.fontTable?.fonts[name],
|
|
950
1028
|
replaceText: () => undefined,
|
|
951
1029
|
insertFragment: () => undefined,
|
|
1030
|
+
copy: () => undefined,
|
|
1031
|
+
cut: () => undefined,
|
|
1032
|
+
getClipboardBuffer: () => null,
|
|
1033
|
+
getClipboardWireFormats: () => null,
|
|
1034
|
+
selectObject: () => undefined,
|
|
1035
|
+
deselectObject: () => undefined,
|
|
1036
|
+
getGrabbedObject: () => null,
|
|
1037
|
+
startAction: () => undefined,
|
|
1038
|
+
endAction: () => undefined,
|
|
1039
|
+
isInAction: () => false,
|
|
952
1040
|
applyActiveStoryTextCommand: () => ({
|
|
953
1041
|
kind: "rejected",
|
|
954
1042
|
newRevisionToken: "",
|
|
@@ -1016,6 +1104,11 @@ function createLoadingRuntimeBridge(input: {
|
|
|
1016
1104
|
changeKinds: ["content"],
|
|
1017
1105
|
}),
|
|
1018
1106
|
getFieldSnapshot: () => emptyFieldSnapshot,
|
|
1107
|
+
getFieldResolver: () => ({
|
|
1108
|
+
resolve: () => undefined,
|
|
1109
|
+
cacheKey: "loading",
|
|
1110
|
+
}),
|
|
1111
|
+
getFootnoteResolver: () => undefined,
|
|
1019
1112
|
updateFields: () => emptyFieldResult,
|
|
1020
1113
|
updateTableOfContents: () => emptyTocResult,
|
|
1021
1114
|
getSessionState: () => {
|
|
@@ -1061,6 +1154,9 @@ function createLoadingRuntimeBridge(input: {
|
|
|
1061
1154
|
definitions: [],
|
|
1062
1155
|
entries: [],
|
|
1063
1156
|
}),
|
|
1157
|
+
queryScopes: () => [],
|
|
1158
|
+
findScopesAt: () => [],
|
|
1159
|
+
findScopesIntersecting: () => [],
|
|
1064
1160
|
setHostAnnotationOverlay: () => undefined,
|
|
1065
1161
|
clearHostAnnotationOverlay: () => undefined,
|
|
1066
1162
|
getHostAnnotationSnapshot: () => ({
|
|
@@ -154,7 +154,7 @@ export function EditorShellView(props: EditorShellViewProps) {
|
|
|
154
154
|
aria-describedby={`${accessibilityInstructionsId} ${accessibilityStatusId}${
|
|
155
155
|
diagnosticsModeMessage ? ` ${accessibilityAlertId}` : ""
|
|
156
156
|
}`}
|
|
157
|
-
className="relative h-full"
|
|
157
|
+
className="wre-editor relative h-full"
|
|
158
158
|
onKeyDownCapture={onShellKeyDownCapture}
|
|
159
159
|
>
|
|
160
160
|
<p id={accessibilityInstructionsId} style={visuallyHiddenStyles}>
|
|
@@ -19,6 +19,14 @@ export interface ShellShortcutContext {
|
|
|
19
19
|
|
|
20
20
|
export interface SurfaceShortcutContext {
|
|
21
21
|
inTable: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* I3 widening tail — when the caret is in the last cell of the last row of
|
|
24
|
+
* a table, Tab (without shift) triggers implicit row-insert-below instead
|
|
25
|
+
* of cell navigation. Matches Word's Tab-at-table-tail behavior. Flag is
|
|
26
|
+
* computed by the PM bridge at dispatch time (bridge knows cell position);
|
|
27
|
+
* defaults to false for callers that don't supply it.
|
|
28
|
+
*/
|
|
29
|
+
isAtTableTail?: boolean;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export type ShellShortcutResolution =
|
|
@@ -46,6 +54,7 @@ export type SurfaceShortcutResolution =
|
|
|
46
54
|
| { kind: "insert-tab" }
|
|
47
55
|
| { kind: "outdent-tab" }
|
|
48
56
|
| { kind: "navigate-table-cell"; direction: 1 | -1 }
|
|
57
|
+
| { kind: "table-insert-row-below" }
|
|
49
58
|
| { kind: "history"; history: "undo" | "redo" };
|
|
50
59
|
|
|
51
60
|
export function resolveShellShortcut(
|
|
@@ -190,9 +199,14 @@ export function resolveSurfaceShortcut(
|
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
if (key === "tab" && !hasAnyModifiers(input)) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
if (context.inTable) {
|
|
203
|
+
// I3 widening tail — Tab at the last cell inserts a new row below
|
|
204
|
+
// (Word behavior). Shift+Tab continues to navigate regardless.
|
|
205
|
+
return context.isAtTableTail
|
|
206
|
+
? { kind: "table-insert-row-below" }
|
|
207
|
+
: { kind: "navigate-table-cell", direction: 1 };
|
|
208
|
+
}
|
|
209
|
+
return { kind: "insert-tab" };
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
return { kind: "none" };
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import React from "react";
|
|
25
25
|
import type { ChartModel } from "../../io/ooxml/chart/types.ts";
|
|
26
26
|
import type { ResolvedTheme } from "../../model/canonical-document.ts";
|
|
27
|
-
import { layoutPlotArea, type PlotAreaLayout } from "./layout/plot-area.ts";
|
|
27
|
+
import { layoutPlotArea, CHROME_PAINTED_DEFAULT, type PlotAreaLayout } from "./layout/plot-area.ts";
|
|
28
28
|
import { AreaChart } from "./render/area.tsx";
|
|
29
29
|
import { BarColumnChart } from "./render/bar-column.tsx";
|
|
30
30
|
import { BubbleChart } from "./render/bubble.tsx";
|
|
@@ -63,7 +63,15 @@ function ChartSurfaceImpl({
|
|
|
63
63
|
resolveMediaUrl,
|
|
64
64
|
previewMediaId,
|
|
65
65
|
}: ChartSurfaceProps): React.ReactElement {
|
|
66
|
-
|
|
66
|
+
// Graceful degradation: pass CHROME_PAINTED_DEFAULT so title / legend /
|
|
67
|
+
// axis bands are NOT reserved by the layout pass. No component paints
|
|
68
|
+
// them today (documented gap in docs/wiki/images-and-media.md §"Known
|
|
69
|
+
// gaps"), so reserving space would leave empty margins around the plot.
|
|
70
|
+
// When Stage-C chrome wire-up lands, flip the matching flag here.
|
|
71
|
+
const resolvedLayout =
|
|
72
|
+
layout ?? layoutPlotArea({ w: width, h: height }, model, theme, {
|
|
73
|
+
paintedChrome: CHROME_PAINTED_DEFAULT,
|
|
74
|
+
});
|
|
67
75
|
const defs = new DefsRegistry();
|
|
68
76
|
|
|
69
77
|
const body = dispatchBody({
|
|
@@ -187,20 +195,38 @@ function renderDefs(defs: DefsRegistry): React.ReactElement[] {
|
|
|
187
195
|
|
|
188
196
|
/**
|
|
189
197
|
* Convert a gradient angle (degrees, OOXML clockwise-from-horizontal)
|
|
190
|
-
* to SVG `x1/y1/x2/y2` percentage values
|
|
191
|
-
* 90° = top-to-bottom.
|
|
198
|
+
* to SVG `x1/y1/x2/y2` percentage values on the object bounding box.
|
|
199
|
+
* 0° = left-to-right, 90° = top-to-bottom. Returned percentages are
|
|
200
|
+
* continuous in the angle rather than snapped to the four corners —
|
|
201
|
+
* non-cardinal gradients (45°, 60°, 120°, …) render with the correct
|
|
202
|
+
* direction instead of collapsing to the nearest corner-pair.
|
|
192
203
|
*/
|
|
193
204
|
function angleToCoords(angleDeg: number): {
|
|
194
205
|
x1: string; y1: string; x2: string; y2: string;
|
|
195
206
|
} {
|
|
196
|
-
const
|
|
207
|
+
const normalized = ((angleDeg % 360) + 360) % 360;
|
|
208
|
+
const rad = (normalized * Math.PI) / 180;
|
|
197
209
|
const dx = Math.cos(rad);
|
|
198
210
|
const dy = Math.sin(rad);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
|
|
212
|
+
// Center + half-length projection onto the [0,1]² bounding box. Scale
|
|
213
|
+
// so the dominant component reaches 0.5 (half the box side), keeping
|
|
214
|
+
// the gradient within the object bbox and edge-to-edge along the axis.
|
|
215
|
+
const cx = 0.5;
|
|
216
|
+
const cy = 0.5;
|
|
217
|
+
const absDx = Math.abs(dx);
|
|
218
|
+
const absDy = Math.abs(dy);
|
|
219
|
+
const scale = absDx === 0 && absDy === 0 ? 0 : 0.5 / Math.max(absDx, absDy);
|
|
220
|
+
const hx = dx * scale;
|
|
221
|
+
const hy = dy * scale;
|
|
222
|
+
|
|
223
|
+
const fmtPct = (n: number): string => `${(n * 100).toFixed(2)}%`;
|
|
224
|
+
return {
|
|
225
|
+
x1: fmtPct(cx - hx),
|
|
226
|
+
y1: fmtPct(cy - hy),
|
|
227
|
+
x2: fmtPct(cx + hx),
|
|
228
|
+
y2: fmtPct(cy + hy),
|
|
229
|
+
};
|
|
204
230
|
}
|
|
205
231
|
|
|
206
232
|
// ---------------------------------------------------------------------------
|