@beyondwork/docx-react-component 1.0.66 → 1.0.69
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 +75 -931
- package/package.json +26 -27
- package/src/api/anchor-conversion.ts +43 -0
- package/src/api/editor-state-types.ts +2 -1
- package/src/api/public-types.ts +504 -101
- package/src/api/session-state.ts +4 -0
- package/src/api/v3/README.md +91 -0
- package/src/api/v3/_create.ts +146 -0
- package/src/api/v3/_layer-metadata.ts +362 -0
- package/src/api/v3/_mocks.ts +84 -0
- package/src/api/v3/_runtime-handle.ts +162 -0
- package/src/api/v3/_ux-response.ts +73 -0
- package/src/api/v3/ai/_metadata-audit.ts +225 -0
- package/src/api/v3/ai/attach.ts +235 -0
- package/src/api/v3/ai/bundle.ts +132 -0
- package/src/api/v3/ai/explain.ts +144 -0
- package/src/api/v3/ai/export.ts +54 -0
- package/src/api/v3/ai/inspect.ts +118 -0
- package/src/api/v3/ai/policy.ts +77 -0
- package/src/api/v3/ai/replacement.ts +341 -0
- package/src/api/v3/ai/resolve.ts +133 -0
- package/src/api/v3/index.ts +79 -0
- package/src/api/v3/runtime/chart.ts +310 -0
- package/src/api/v3/runtime/clipboard.ts +81 -0
- package/src/api/v3/runtime/collab.ts +331 -0
- package/src/api/v3/runtime/content.ts +236 -0
- package/src/api/v3/runtime/document.ts +282 -0
- package/src/api/v3/runtime/formatting.ts +186 -0
- package/src/api/v3/runtime/geometry.ts +349 -0
- package/src/api/v3/runtime/layout.ts +108 -0
- package/src/api/v3/runtime/review.ts +129 -0
- package/src/api/v3/runtime/search.ts +74 -0
- package/src/api/v3/runtime/table.ts +63 -0
- package/src/api/v3/runtime/workflow.ts +434 -0
- package/src/api/v3/ui/_context.ts +86 -0
- package/src/api/v3/ui/_create.ts +65 -0
- package/src/api/v3/ui/_types.ts +520 -0
- package/src/api/v3/ui/chrome-composition.ts +342 -0
- package/src/{ui-tailwind/chrome → api/v3/ui}/chrome-preset-model.ts +11 -1
- package/src/api/v3/ui/chrome.ts +476 -0
- package/src/api/v3/ui/debug.ts +124 -0
- package/src/api/v3/ui/index.ts +64 -0
- package/src/api/v3/ui/overlays-visibility.ts +170 -0
- package/src/api/v3/ui/overlays.ts +427 -0
- package/src/api/v3/ui/scope.ts +71 -0
- package/src/api/v3/ui/session.ts +100 -0
- package/src/api/v3/ui/surface.ts +170 -0
- package/src/api/v3/ui/viewport.ts +303 -0
- package/src/core/commands/index.ts +28 -6
- package/src/core/commands/list-commands.ts +3 -2
- package/src/core/commands/section-layout-commands.ts +9 -8
- package/src/core/schema/text-schema.ts +16 -0
- package/src/core/selection/mapping.ts +33 -72
- package/src/core/state/editor-state.ts +96 -189
- package/src/index.ts +23 -4
- package/src/io/chart-preview-resolver.ts +1 -1
- package/src/io/docx-session.ts +36 -4795
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +1 -1
- package/src/io/export/serialize-headers-footers.ts +6 -1
- package/src/io/export/serialize-main-document.ts +45 -0
- package/src/io/export/serialize-run-formatting.ts +17 -2
- package/src/io/export/twip.ts +1 -1
- package/src/io/normalize/normalize-text.ts +27 -20
- package/src/io/ooxml/chart/parse-series.ts +1 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +1 -1
- package/src/io/ooxml/classify-embedding.ts +83 -33
- package/src/io/ooxml/parse-fill.ts +1 -1
- package/src/io/ooxml/parse-main-document.ts +71 -1
- package/src/io/ooxml/parse-object.ts +14 -10
- package/src/io/ooxml/parse-run-formatting.ts +47 -1
- package/src/io/ooxml/property-grab-bag.ts +2 -2
- package/src/io/ooxml/units.ts +11 -0
- package/src/io/ooxml/workflow-payload.ts +282 -7
- package/src/model/anchor.ts +85 -0
- package/src/model/canonical-document.ts +351 -15
- package/src/model/chart-types.ts +1 -1
- package/src/model/layout/index.ts +83 -0
- package/src/model/layout/page-graph-types.ts +181 -0
- package/src/model/layout/page-layout-snapshot.ts +105 -0
- package/src/model/layout/resolved-layout-types.ts +47 -0
- package/src/model/layout/runtime-page-graph-types.ts +102 -0
- package/src/model/paragraph-scope-ids.ts +72 -0
- package/src/model/review/comment-types.ts +112 -0
- package/src/model/review/index.ts +2 -0
- package/src/model/review/revision-types.ts +215 -0
- package/src/model/snapshot.ts +32 -0
- package/src/review/store/comment-store.ts +21 -47
- package/src/review/store/revision-types.ts +40 -198
- package/src/runtime/collab/base-doc-fingerprint.ts +6 -1
- package/src/runtime/collab/runtime-collab-sync.ts +13 -3
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/debug/build-debug-inspector-snapshot.ts +686 -0
- package/src/runtime/debug/event-ring-buffer.ts +64 -0
- package/src/runtime/debug/probability-sampler.ts +18 -0
- package/src/runtime/debug/runtime-debug-facet.ts +67 -0
- package/src/runtime/debug/stage-tokens.ts +31 -0
- package/src/runtime/debug/telemetry-bus.ts +271 -0
- package/src/runtime/debug/types.ts +275 -0
- package/src/runtime/debug/wrap-ref-for-telemetry.ts +118 -0
- package/src/runtime/document-layout.ts +8 -6
- package/src/runtime/document-runtime.ts +843 -1141
- package/src/runtime/document-search.ts +1 -1
- package/src/runtime/edit-ops/index.ts +1 -1
- package/src/runtime/external-send-runtime.ts +1 -1
- package/src/runtime/formatting/document-lookup.ts +235 -0
- package/src/runtime/formatting/field/registry.ts +41 -0
- package/src/runtime/{field-resolver.ts → formatting/field/resolver.ts} +27 -2
- package/src/runtime/formatting/font-resolution.ts +83 -0
- package/src/runtime/formatting/formatting-context.ts +903 -0
- package/src/runtime/formatting/formatting-types.ts +157 -0
- package/src/runtime/{hyperlink-color-resolver.ts → formatting/hyperlink-color.ts} +2 -2
- package/src/runtime/formatting/index.ts +125 -0
- package/src/runtime/{resolved-numbering-geometry.ts → formatting/numbering/geometry.ts} +1 -1
- package/src/runtime/{numbering-prefix.ts → formatting/numbering/prefix.ts} +170 -3
- package/src/runtime/formatting/paragraph-style-resolver.ts +92 -0
- package/src/runtime/formatting/projector.ts +75 -0
- package/src/runtime/formatting/resolve-effective.ts +407 -0
- package/src/runtime/formatting/revision-display.ts +105 -0
- package/src/runtime/{paragraph-style-resolver.ts → formatting/style-cascade.ts} +84 -141
- package/src/runtime/{table-style-resolver.ts → formatting/table-style-resolver.ts} +1 -1
- package/src/runtime/formatting/telemetry-bridge.ts +106 -0
- package/src/runtime/{theme-color-resolver.ts → formatting/theme-color.ts} +2 -30
- package/src/runtime/geometry/caret-geometry.ts +164 -0
- package/src/runtime/geometry/geometry-facet.ts +364 -0
- package/src/runtime/geometry/geometry-types.ts +256 -0
- package/src/runtime/geometry/hit-test.ts +125 -0
- package/src/runtime/geometry/index.ts +71 -0
- package/src/runtime/geometry/inert-geometry-facet.ts +43 -0
- package/src/runtime/geometry/invalidation.ts +35 -0
- package/src/runtime/geometry/object-handles.ts +77 -0
- package/src/runtime/geometry/overlay-rects.ts +85 -0
- package/src/runtime/geometry/project-anchors.ts +100 -0
- package/src/runtime/geometry/project-fragments.ts +216 -0
- package/src/runtime/geometry/projector.ts +129 -0
- package/src/runtime/geometry/replacement-envelope.ts +130 -0
- package/src/runtime/geometry/viewport.ts +218 -0
- package/src/runtime/layout/compat-input-ledger.ts +211 -0
- package/src/runtime/layout/index.ts +6 -1
- package/src/runtime/layout/inert-layout-facet.ts +12 -7
- package/src/runtime/layout/layout-engine-instance.ts +189 -11
- package/src/runtime/layout/layout-engine-version.ts +450 -1
- package/src/runtime/layout/layout-facet-types.ts +60 -0
- package/src/runtime/layout/layout-measurement-provider.ts +13 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +14 -2
- package/src/runtime/layout/measurement-backend-empirical.ts +23 -4
- package/src/runtime/layout/page-graph.ts +62 -209
- package/src/runtime/layout/page-story-resolver.ts +7 -12
- package/src/runtime/layout/paginated-layout-engine.ts +186 -11
- package/src/runtime/layout/project-block-fragments.ts +11 -0
- package/src/runtime/layout/projector.ts +90 -0
- package/src/runtime/layout/public-facet.ts +187 -442
- package/src/runtime/layout/resolved-formatting-state.ts +158 -26
- package/src/runtime/layout/table-render-plan.ts +1 -1
- package/src/runtime/prerender/cache-envelope.ts +6 -1
- package/src/runtime/prerender/prerender-document.ts +18 -23
- package/src/runtime/render/decoration-resolver.ts +1 -1
- package/src/runtime/render/render-frame-types.ts +20 -0
- package/src/runtime/render/render-kernel.ts +94 -25
- package/src/runtime/scopes/_formatting-seam.ts +262 -0
- package/src/runtime/scopes/_scope-dependencies.ts +49 -0
- package/src/runtime/scopes/action-validation.ts +356 -0
- package/src/runtime/scopes/attach-explanation.ts +102 -0
- package/src/runtime/scopes/audit-bundle.ts +71 -0
- package/src/runtime/scopes/compile-scope-bundle.ts +163 -0
- package/src/runtime/scopes/compile-scope.ts +262 -0
- package/src/runtime/scopes/compiler-service.ts +431 -0
- package/src/runtime/scopes/create-issue.ts +107 -0
- package/src/runtime/scopes/enumerate-scopes.ts +543 -0
- package/src/runtime/scopes/evidence.ts +233 -0
- package/src/runtime/scopes/index.ts +150 -0
- package/src/runtime/scopes/position-map.ts +214 -0
- package/src/runtime/scopes/preservation-boundary.ts +91 -0
- package/src/runtime/scopes/projector.ts +49 -0
- package/src/runtime/scopes/replaceability.ts +87 -0
- package/src/runtime/scopes/replacement/apply.ts +228 -0
- package/src/runtime/scopes/replacement/compile.ts +59 -0
- package/src/runtime/scopes/replacement/propose.ts +42 -0
- package/src/runtime/scopes/resolve-reference.ts +347 -0
- package/src/runtime/scopes/review-bundle.ts +141 -0
- package/src/runtime/scopes/scope-kinds/_paragraph-text.ts +57 -0
- package/src/runtime/scopes/scope-kinds/_table-text.ts +42 -0
- package/src/runtime/scopes/scope-kinds/comment-thread.ts +59 -0
- package/src/runtime/scopes/scope-kinds/field.ts +65 -0
- package/src/runtime/scopes/scope-kinds/heading.ts +84 -0
- package/src/runtime/scopes/scope-kinds/list-item.ts +77 -0
- package/src/runtime/scopes/scope-kinds/paragraph.ts +182 -0
- package/src/runtime/scopes/scope-kinds/revision.ts +62 -0
- package/src/runtime/scopes/scope-kinds/table-cell.ts +57 -0
- package/src/runtime/scopes/scope-kinds/table-row.ts +61 -0
- package/src/runtime/scopes/scope-kinds/table.ts +55 -0
- package/src/runtime/scopes/scope-range.ts +208 -0
- package/src/runtime/scopes/semantic-scope-types.ts +454 -0
- package/src/runtime/scopes/workflow-overlap.ts +92 -0
- package/src/runtime/selection/index.ts +1 -1
- package/src/runtime/structure-ops/fragment-insert.ts +1 -1
- package/src/runtime/structure-ops/index.ts +1 -1
- package/src/runtime/surface-projection.ts +232 -262
- package/src/runtime/units.ts +4 -2
- package/src/runtime/workflow/coordinator.ts +1348 -0
- package/src/runtime/workflow/derived-scope-resolver.ts +125 -0
- package/src/runtime/workflow/index.ts +25 -0
- package/src/runtime/workflow/markup-mode-policy.ts +98 -0
- package/src/runtime/{workflow-markup.ts → workflow/markup.ts} +6 -6
- package/src/runtime/workflow/metadata-persistence.ts +306 -0
- package/src/runtime/workflow/metadata-writer.ts +123 -0
- package/src/runtime/workflow/overlay-store.ts +690 -0
- package/src/runtime/workflow/projector.ts +127 -0
- package/src/runtime/{query-scopes.ts → workflow/query-scopes.ts} +3 -3
- package/src/runtime/{workflow-rail-segments.ts → workflow/rail/compose.ts} +60 -165
- package/src/runtime/workflow/rail/types.ts +198 -0
- package/src/runtime/workflow/scope-rail-composer.ts +39 -0
- package/src/runtime/{scope-resolver.ts → workflow/scope-resolver.ts} +3 -3
- package/src/runtime/workflow/scope-writer.ts +188 -0
- package/src/runtime/{tamper-gate.ts → workflow/tamper-gate.ts} +1 -1
- package/src/runtime/workflow/visibility-policy.ts +129 -0
- package/src/session/_sync-legacy.ts +66 -0
- package/src/session/export/embedded-reconstitute.ts +104 -0
- package/src/session/export/export-diagnostics.ts +85 -0
- package/src/session/export/export-validation.ts +110 -0
- package/src/session/export/index.ts +34 -0
- package/src/session/export/preservation-reattach.ts +30 -0
- package/src/session/export/serialize-dispatch.ts +165 -0
- package/src/session/export/stateful-export-pipeline.ts +432 -0
- package/src/session/export/stateful-export.ts +684 -0
- package/src/session/import/canonical-assembly.ts +227 -0
- package/src/session/import/diagnostics-session.ts +54 -0
- package/src/session/import/embedded-discovery.ts +225 -0
- package/src/session/import/embedded-offload.ts +337 -0
- package/src/session/import/import-diagnostics.ts +69 -0
- package/src/session/import/loader-types.ts +313 -0
- package/src/session/import/loader.ts +1834 -0
- package/src/session/import/normalize.ts +195 -0
- package/src/session/import/package-parts.ts +217 -0
- package/src/session/import/package-read.ts +195 -0
- package/src/session/import/parse-orchestration.ts +105 -0
- package/src/session/import/part-constants.ts +70 -0
- package/src/session/import/part-discovery.ts +94 -0
- package/src/session/import/preservation-index.ts +46 -0
- package/src/{runtime/read-only-diagnostics-runtime.ts → session/import/read-only-diagnostics.ts} +24 -3
- package/src/session/import/review-import.ts +508 -0
- package/src/session/import/styles-consolidation.ts +281 -0
- package/src/session/import/workflow-scope-import.ts +256 -0
- package/src/session/index.ts +37 -0
- package/src/session/session-state.ts +69 -0
- package/src/session/session.ts +532 -0
- package/src/session/shared/protection.ts +228 -0
- package/src/session/shared/session-utils.ts +82 -0
- package/src/session/types.ts +499 -0
- package/src/shell/chart-snapshots.ts +96 -0
- package/src/shell/media-previews.ts +85 -0
- package/src/shell/overlay-anchor-bridge.ts +53 -0
- package/src/shell/paste-adapter.ts +23 -0
- package/src/shell/ref-commands.ts +1697 -0
- package/src/shell/ref-utilities.ts +48 -0
- package/src/shell/search.ts +51 -0
- package/src/{ui/editor-runtime-boundary.ts → shell/session-bootstrap.ts} +243 -67
- package/src/shell/ui-subscriber-channels.ts +81 -0
- package/src/shell/use-collab-sync.ts +116 -0
- package/src/ui/WordReviewEditor.tsx +496 -2051
- package/src/ui/editor-shell-view.tsx +30 -1
- package/src/ui/editor-surface-controller.tsx +49 -1
- package/src/ui/headless/revision-decoration-model.ts +83 -0
- package/src/{ui-tailwind/chrome → ui/headless}/role-action-sets.ts +1 -1
- package/src/ui/headless/scoped-chrome-policy.ts +2 -2
- package/src/ui/headless/selection-tool-context.ts +1 -1
- package/src/ui/headless/selection-tool-resolver.ts +1 -1
- package/src/ui/runtime-shortcut-dispatch.ts +46 -1
- package/src/ui/ui-controller-factory.ts +221 -0
- package/src/ui-tailwind/chart/ChartSurface.tsx +2 -2
- package/src/ui-tailwind/chart/layout/legend-layout.ts +1 -1
- package/src/ui-tailwind/chart/layout/plot-area.ts +2 -2
- package/src/ui-tailwind/chart/layout/title-layout.ts +1 -1
- package/src/ui-tailwind/chart/render/area.tsx +3 -3
- package/src/ui-tailwind/chart/render/bar-column.tsx +3 -3
- package/src/ui-tailwind/chart/render/bubble.tsx +3 -3
- package/src/ui-tailwind/chart/render/combo.tsx +2 -2
- package/src/ui-tailwind/chart/render/data-labels.tsx +2 -2
- package/src/ui-tailwind/chart/render/font-metrics.ts +2 -2
- package/src/ui-tailwind/chart/render/line.tsx +3 -3
- package/src/ui-tailwind/chart/render/pie.tsx +6 -6
- package/src/ui-tailwind/chart/render/scatter.tsx +3 -3
- package/src/ui-tailwind/chart/render/svg-primitives.ts +3 -3
- package/src/ui-tailwind/chart/render/unsupported.tsx +2 -2
- package/src/ui-tailwind/chrome/build-context-menu-entries.ts +88 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +1 -1
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +1 -1
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +1 -1
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +1 -1
- package/src/ui-tailwind/chrome/editor-action-registry.ts +553 -0
- package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +182 -0
- package/src/ui-tailwind/chrome/local-surface-arbiter.ts +534 -0
- package/src/ui-tailwind/chrome/resolve-target-kind.ts +226 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +38 -4
- package/src/ui-tailwind/chrome/tw-context-band.tsx +125 -0
- package/src/ui-tailwind/chrome/tw-context-menu-portal.tsx +248 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +42 -1
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +8 -7
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +38 -4
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +104 -6
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +66 -7
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +54 -8
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +33 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +78 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +16 -8
- package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +276 -0
- package/src/ui-tailwind/chrome/use-context-menu-controller.ts +201 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +22 -4
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +11 -5
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +197 -3
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +35 -6
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +24 -16
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +1 -1
- package/src/ui-tailwind/debug/README.md +57 -0
- package/src/ui-tailwind/debug/index.ts +3 -0
- package/src/ui-tailwind/debug/tw-debug-overlay.tsx +186 -0
- package/src/ui-tailwind/debug/tw-debug-presentation.tsx +80 -0
- package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +83 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +2 -2
- package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +135 -10
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +40 -13
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +3 -3
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +1 -1
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +2 -2
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +91 -9
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +1 -1
- package/src/ui-tailwind/editor-surface/surface-layer.ts +1 -1
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +1 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +23 -6
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -22
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +1 -1
- package/src/ui-tailwind/index.ts +0 -5
- package/src/ui-tailwind/overlay-anchor-bridge-context.tsx +33 -0
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +66 -29
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +25 -2
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +15 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +92 -4
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +1 -1
- package/src/ui-tailwind/review-workspace/page-chrome.ts +210 -0
- package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +101 -0
- package/src/ui-tailwind/review-workspace/paragraph-layout.ts +115 -0
- package/src/ui-tailwind/review-workspace/selection-toolbar-placement.ts +97 -0
- package/src/ui-tailwind/review-workspace/tw-review-workspace-navigator.tsx +130 -0
- package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +240 -0
- package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +59 -0
- package/src/ui-tailwind/review-workspace/types.ts +408 -0
- package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +104 -0
- package/src/ui-tailwind/review-workspace/use-derived-view-state.ts +151 -0
- package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +70 -0
- package/src/ui-tailwind/review-workspace/use-grabbed-segment-offsets.ts +40 -0
- package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +55 -0
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +130 -0
- package/src/ui-tailwind/review-workspace/use-pm-surface-capture.ts +60 -0
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +63 -0
- package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +170 -0
- package/src/ui-tailwind/review-workspace/use-scroll-root-capture.ts +28 -0
- package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +113 -0
- package/src/ui-tailwind/review-workspace/use-shell-selection-anchor-bridge.ts +120 -0
- package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +55 -0
- package/src/ui-tailwind/review-workspace/use-viewport-dimensions.ts +43 -0
- package/src/ui-tailwind/review-workspace/use-workspace-arbiter.ts +25 -0
- package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +86 -0
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +150 -0
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +61 -98
- package/src/ui-tailwind/tw-review-workspace.tsx +521 -1802
- package/src/ui-tailwind/ui-api-context.tsx +43 -0
- package/src/ui-tailwind/ui-shell-channels-context.tsx +49 -0
- package/src/validation/compatibility-engine.ts +6 -6
- package/src/runtime/styles-cascade.ts +0 -33
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +0 -85
- /package/src/runtime/{page-number-format.ts → formatting/field/page-number-format.ts} +0 -0
- /package/src/runtime/{ai-action-policy.ts → workflow/ai-action-policy.ts} +0 -0
- /package/src/runtime/{scope-tag-registry.ts → workflow/scope-tag-registry.ts} +0 -0
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
type CSSProperties,
|
|
3
|
-
type FocusEventHandler,
|
|
4
|
-
type ReactNode,
|
|
5
|
-
type Ref,
|
|
6
3
|
useCallback,
|
|
7
4
|
useEffect,
|
|
8
5
|
useMemo,
|
|
@@ -11,41 +8,8 @@ import React, {
|
|
|
11
8
|
} from "react";
|
|
12
9
|
|
|
13
10
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
14
|
-
import {
|
|
11
|
+
import { ChevronRight } from "lucide-react";
|
|
15
12
|
|
|
16
|
-
import type {
|
|
17
|
-
ActiveListContext,
|
|
18
|
-
CommentSidebarThreadSnapshot,
|
|
19
|
-
DocumentNavigationSnapshot,
|
|
20
|
-
EditorAnchorProjection,
|
|
21
|
-
EditorStoryTarget,
|
|
22
|
-
EditorViewStateSnapshot,
|
|
23
|
-
FormattingStateSnapshot,
|
|
24
|
-
FormattingAlignment,
|
|
25
|
-
HeaderFooterLinkPatch,
|
|
26
|
-
InteractionGuardSnapshot,
|
|
27
|
-
InsertImageOptions,
|
|
28
|
-
RuntimeContextAnalyticsSnapshot,
|
|
29
|
-
RuntimeRenderSnapshot,
|
|
30
|
-
ReviewQueueSnapshot,
|
|
31
|
-
SectionPageNumberingPatch,
|
|
32
|
-
SectionBreakType,
|
|
33
|
-
StyleCatalogSnapshot,
|
|
34
|
-
SurfaceBlockSnapshot,
|
|
35
|
-
TrackedChangeEntrySnapshot,
|
|
36
|
-
WordReviewEditorChromeOptions,
|
|
37
|
-
WordReviewEditorChromePreset,
|
|
38
|
-
WordReviewEditorChromeVisibility,
|
|
39
|
-
WorkflowScopeSnapshot,
|
|
40
|
-
WorkspaceMode,
|
|
41
|
-
ZoomLevel,
|
|
42
|
-
} from "../api/public-types";
|
|
43
|
-
import { createCanvasBackend } from "../runtime/layout/index.ts";
|
|
44
|
-
import { DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP } from "../runtime/page-layout-estimation.ts";
|
|
45
|
-
import {
|
|
46
|
-
incrementInvalidationCounter,
|
|
47
|
-
recordPerfSample,
|
|
48
|
-
} from "./editor-surface/perf-probe.ts";
|
|
49
13
|
import {
|
|
50
14
|
useVisibleBlockRange,
|
|
51
15
|
useVisiblePageIndexRange,
|
|
@@ -57,384 +21,110 @@ import {
|
|
|
57
21
|
} from "./editor-surface/scroll-anchor.ts";
|
|
58
22
|
import { TwPageBlockView } from "./editor-surface/tw-page-block-view.tsx";
|
|
59
23
|
import { computeLineMarkersIfEnabled } from "./page-chrome-model.ts";
|
|
60
|
-
import
|
|
61
|
-
import type {
|
|
62
|
-
ActiveSelectionToolModel,
|
|
63
|
-
SelectionToolAnchor,
|
|
64
|
-
} from "../ui/headless/selection-tool-types";
|
|
65
|
-
import type { MarkupDisplay } from "../ui/headless/comment-decoration-model";
|
|
66
|
-
import {
|
|
67
|
-
resolveScopedChromePolicy,
|
|
68
|
-
shouldRenderSelectionToolKind,
|
|
69
|
-
} from "../ui/headless/scoped-chrome-policy";
|
|
24
|
+
import { shouldRenderSelectionToolKind } from "../ui/headless/scoped-chrome-policy";
|
|
70
25
|
import type { EditorCommandBag } from "../ui/editor-command-bag.ts";
|
|
71
26
|
import { preserveEditorSelectionMouseDown } from "../ui/headless/preserve-editor-selection";
|
|
72
27
|
import { TwAlertBanner } from "./chrome/tw-alert-banner";
|
|
73
|
-
import { TwModeDock } from "./chrome/tw-mode-dock";
|
|
74
28
|
import { TwLayoutPanel } from "./chrome/tw-layout-panel";
|
|
75
29
|
import { TwPageRuler } from "./chrome/tw-page-ruler";
|
|
76
|
-
import {
|
|
77
|
-
getInitialReviewRailOpen,
|
|
78
|
-
isNarrowChromeViewport,
|
|
79
|
-
resolveResponsiveChromeState,
|
|
80
|
-
} from "./chrome/responsive-chrome";
|
|
81
30
|
import { ChromePresetToolbar } from "./chrome/chrome-preset-toolbar";
|
|
82
31
|
import {
|
|
83
32
|
resolveChromePreset,
|
|
84
33
|
resolveChromePresetOptions,
|
|
85
34
|
resolveChromeVisibilityForPreset,
|
|
86
|
-
} from "
|
|
35
|
+
} from "../api/v3/ui/chrome-preset-model";
|
|
87
36
|
import { TwSelectionToolHost } from "./chrome/tw-selection-tool-host";
|
|
88
|
-
import {
|
|
89
|
-
import {
|
|
90
|
-
|
|
37
|
+
import { type ReviewRailTab } from "./review/tw-review-rail";
|
|
38
|
+
import {
|
|
39
|
+
TwReviewWorkspaceRail,
|
|
40
|
+
type TwReviewWorkspaceRailMode,
|
|
41
|
+
} from "./review-workspace/tw-review-workspace-rail.tsx";
|
|
91
42
|
import { TwStatusBar } from "./status/tw-status-bar";
|
|
92
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
TwShellHeader,
|
|
45
|
+
type ShellHeaderMode,
|
|
46
|
+
type ShellHeaderModeOption,
|
|
47
|
+
} from "./toolbar/tw-shell-header";
|
|
48
|
+
import { TwContextBand } from "./chrome/tw-context-band";
|
|
49
|
+
import { TwRoleActionRegion } from "./toolbar/tw-role-action-region";
|
|
50
|
+
import { LocalSurfaceArbiterContext } from "./chrome/local-surface-arbiter";
|
|
51
|
+
import { TwWorkspaceChromeHost } from "./chrome/tw-workspace-chrome-host";
|
|
93
52
|
import { TwChromeOverlay, TwPageStackOverlayLayer } from "./chrome-overlay";
|
|
94
53
|
import { TwFloatingImageLayer } from "./page-stack/tw-floating-image-layer.tsx";
|
|
95
|
-
import
|
|
96
|
-
import {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} from "./
|
|
100
|
-
|
|
101
|
-
|
|
54
|
+
import { shouldHidePageBorderForSelection } from "./review-workspace/paragraph-layout.ts";
|
|
55
|
+
import { useSelectionToolbarPlacement } from "./review-workspace/use-selection-toolbar-placement.ts";
|
|
56
|
+
import { useShellSelectionAnchorBridge } from "./review-workspace/use-shell-selection-anchor-bridge.ts";
|
|
57
|
+
import { useChromePolicy } from "./review-workspace/use-chrome-policy.ts";
|
|
58
|
+
import { useStatusBarPageFacts } from "./review-workspace/use-status-bar-page-facts.ts";
|
|
59
|
+
import { useGrabbedSegmentOffsets } from "./review-workspace/use-grabbed-segment-offsets.ts";
|
|
60
|
+
import { useDerivedViewState } from "./review-workspace/use-derived-view-state.ts";
|
|
61
|
+
import { useReviewRailState } from "./review-workspace/use-review-rail-state.ts";
|
|
62
|
+
import { useWorkspaceSideEffects } from "./review-workspace/use-workspace-side-effects.ts";
|
|
63
|
+
import { useViewportDimensions } from "./review-workspace/use-viewport-dimensions.ts";
|
|
64
|
+
import { useScopeCardState } from "./review-workspace/use-scope-card-state.ts";
|
|
65
|
+
import { usePageMarkers } from "./review-workspace/use-page-markers.ts";
|
|
66
|
+
import { useDiagnosticsSignal } from "./review-workspace/use-diagnostics-signal.ts";
|
|
67
|
+
import { useWorkspaceComposition } from "./review-workspace/use-workspace-composition.ts";
|
|
68
|
+
import { useWorkspaceArbiter } from "./review-workspace/use-workspace-arbiter.ts";
|
|
69
|
+
import { useLayoutFacetRenderSignal } from "./review-workspace/use-layout-facet-render-signal.ts";
|
|
70
|
+
import { useScrollRootCapture } from "./review-workspace/use-scroll-root-capture.ts";
|
|
71
|
+
import { usePmSurfaceCapture } from "./review-workspace/use-pm-surface-capture.ts";
|
|
72
|
+
import { TwReviewWorkspaceNavigator } from "./review-workspace/tw-review-workspace-navigator.tsx";
|
|
73
|
+
import { TwReviewWorkspacePageToolbar } from "./review-workspace/tw-review-workspace-page-toolbar.tsx";
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
FRAME_PX_PER_TWIP_AT_96DPI,
|
|
77
|
+
MIN_BAND_HEIGHT_PX,
|
|
78
|
+
buildPageShellMetrics,
|
|
79
|
+
resolveZoomMultiplier,
|
|
80
|
+
type PageShellMetrics,
|
|
81
|
+
} from "./review-workspace/page-shell-metrics.ts";
|
|
82
|
+
|
|
83
|
+
import type { TwReviewWorkspaceProps } from "./review-workspace/types.ts";
|
|
84
|
+
import { useUiApi } from "./ui-api-context.tsx";
|
|
85
|
+
import { useUiShellChannels } from "./ui-shell-channels-context.tsx";
|
|
86
|
+
export type {
|
|
87
|
+
ReviewWorkspaceChromeVisibility,
|
|
88
|
+
TwReviewWorkspaceProps,
|
|
89
|
+
} from "./review-workspace/types.ts";
|
|
90
|
+
|
|
91
|
+
import type { EditorRole } from "../api/public-types.ts";
|
|
92
|
+
|
|
93
|
+
// Default shell-header modes for the workspace's default composition.
|
|
94
|
+
// Designsystem §6.1 prescribes a 4-mode switcher (edit / review / workflow
|
|
95
|
+
// / more). "more" is kept in the layout for parity but marked disabled
|
|
96
|
+
// until its handler is defined by refactor/11 Slice 7 + refactor/10
|
|
97
|
+
// Slice 5 (Phase Q debug UX). Hosts that want a fully-wired 4-mode set
|
|
98
|
+
// supply their own `shellHeader` prop.
|
|
99
|
+
const DEFAULT_WORKSPACE_SHELL_MODES: readonly ShellHeaderModeOption[] = [
|
|
100
|
+
{ id: "edit", label: "Edit" },
|
|
101
|
+
{ id: "review", label: "Review" },
|
|
102
|
+
{ id: "workflow", label: "Workflow" },
|
|
103
|
+
{ id: "more", label: "More", disabled: true },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
function editorRoleToShellMode(role: EditorRole): ShellHeaderMode {
|
|
107
|
+
switch (role) {
|
|
108
|
+
case "editor":
|
|
109
|
+
return "edit";
|
|
110
|
+
case "review":
|
|
111
|
+
return "review";
|
|
112
|
+
case "workflow":
|
|
113
|
+
return "workflow";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* supplied, the ChromeOverlay plane (scope rail, workflow dock, etc.)
|
|
115
|
-
* renders over the document column.
|
|
116
|
-
*/
|
|
117
|
-
layoutFacet?: import("../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
|
|
118
|
-
/**
|
|
119
|
-
* Optional shell header mounted above the formatting toolbar. Pass a
|
|
120
|
-
* pre-assembled `<TwShellHeader />` with brand / mode switcher /
|
|
121
|
-
* primaryAction, or any other ReactNode. Hosts that do not supply this
|
|
122
|
-
* get the legacy layout.
|
|
123
|
-
*/
|
|
124
|
-
shellHeader?: ReactNode;
|
|
125
|
-
/**
|
|
126
|
-
* Optional host-provided Workflow-tab override for the review rail.
|
|
127
|
-
* When unset the rail renders the built-in `TwWorkflowTab` sourced from
|
|
128
|
-
* `layoutFacet.getAllScopeRailSegments()`.
|
|
129
|
-
*/
|
|
130
|
-
reviewRailWorkflowTab?: ReactNode;
|
|
131
|
-
reviewRailWorkflowCount?: number;
|
|
132
|
-
reviewRailWorkflowScopesTitle?: string;
|
|
133
|
-
reviewRailIntelligenceEyebrow?: string;
|
|
134
|
-
/** Opt in to the editorial DOCUMENT INTELLIGENCE header + underline tab chip. */
|
|
135
|
-
reviewRailIntelligenceHeader?: boolean;
|
|
136
|
-
/** Optional SEARCH / HELP utility footer at the bottom of the rail. */
|
|
137
|
-
reviewRailFooter?: {
|
|
138
|
-
onSearch?: () => void;
|
|
139
|
-
helpHref?: string;
|
|
140
|
-
searchLabel?: string;
|
|
141
|
-
helpLabel?: string;
|
|
142
|
-
};
|
|
143
|
-
/**
|
|
144
|
-
* @deprecated — Lane 6b §6b.U4 (designsystem §6.26).
|
|
145
|
-
*
|
|
146
|
-
* The floating bottom-center mode dock is retained for back-compat but is
|
|
147
|
-
* off by default. Its functionality is covered by TwShellHeader mode tabs
|
|
148
|
-
* + TwRoleActionRegion. To render it, hosts must supply BOTH `modeDock`
|
|
149
|
-
* data and `experimental.showModeDock = true`.
|
|
150
|
-
*/
|
|
151
|
-
modeDock?: {
|
|
152
|
-
label: string;
|
|
153
|
-
icon?: ReactNode;
|
|
154
|
-
actions?: readonly import("./chrome/tw-mode-dock").TwModeDockAction[];
|
|
155
|
-
};
|
|
156
|
-
/**
|
|
157
|
-
* Experimental feature flags. Anything here is subject to change or
|
|
158
|
-
* removal without a major-version bump. Do not depend on these in
|
|
159
|
-
* production consumers.
|
|
160
|
-
*/
|
|
161
|
-
experimental?: {
|
|
162
|
-
/**
|
|
163
|
-
* Re-enable the deprecated TwModeDock floating bottom dock. Defaults to
|
|
164
|
-
* `false` — the shell header + role action region supersede this surface
|
|
165
|
-
* per designsystem §6.26.
|
|
166
|
-
*/
|
|
167
|
-
showModeDock?: boolean;
|
|
168
|
-
};
|
|
169
|
-
document: ReactNode;
|
|
170
|
-
workspaceMode: WorkspaceMode;
|
|
171
|
-
zoomLevel?: ZoomLevel;
|
|
172
|
-
formattingState?: FormattingStateSnapshot;
|
|
173
|
-
activeListContext?: ActiveListContext | null;
|
|
174
|
-
styleCatalog?: StyleCatalogSnapshot;
|
|
175
|
-
activeRailTab: ReviewRailTab;
|
|
176
|
-
activeCommentId?: string;
|
|
177
|
-
activeRevisionId?: string;
|
|
178
|
-
showTrackedChanges: boolean;
|
|
179
|
-
workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
|
|
180
|
-
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
181
|
-
chromePreset?: WordReviewEditorChromePreset;
|
|
182
|
-
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|
|
183
|
-
/** P9g — live collab session for the `"collab"` chrome preset's top nav. */
|
|
184
|
-
collabSession?: import("../runtime/collab-session.ts").CollabSession;
|
|
185
|
-
collabTransportStatus?: import("../api/awareness-identity-types.ts").TransportStatus;
|
|
186
|
-
collabActorId?: string;
|
|
187
|
-
collabSendBaseline?: {
|
|
188
|
-
originDocumentId: string;
|
|
189
|
-
originPayloadId: string;
|
|
190
|
-
originContentHash: string;
|
|
191
|
-
payloadXml: string;
|
|
192
|
-
};
|
|
193
|
-
reviewQueue?: ReviewQueueSnapshot;
|
|
194
|
-
documentContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
195
|
-
selectionContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
196
|
-
currentScopeContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
197
|
-
commands: EditorCommandBag;
|
|
198
|
-
onActivateFloatingImage?: (payload: {
|
|
199
|
-
mediaId: string;
|
|
200
|
-
from: number;
|
|
201
|
-
to: number;
|
|
202
|
-
storyTarget: EditorStoryTarget;
|
|
203
|
-
}) => void;
|
|
204
|
-
/** N6 — release the grabbed image/shape. Wired to `runtime.deselectObject()` by the host. */
|
|
205
|
-
onDeselectObject?: () => void;
|
|
206
|
-
activeSelectionTool?: ActiveSelectionToolModel | null;
|
|
207
|
-
selectionToolAnchor?: SelectionToolAnchor | null;
|
|
208
|
-
documentNavigation?: DocumentNavigationSnapshot;
|
|
209
|
-
/**
|
|
210
|
-
* R2.3: chrome-pin change handler. When supplied, selection tools
|
|
211
|
-
* expose their detach affordance and persist pin state through to
|
|
212
|
-
* runtime ViewState (via the host's `setChromePin` action). When
|
|
213
|
-
* omitted, the detach handle is suppressed — the tool behaves as
|
|
214
|
-
* a non-pinnable anchored panel (pre-R2 behavior for most kinds).
|
|
215
|
-
*/
|
|
216
|
-
onChromePinChange?: (
|
|
217
|
-
surface: import("../api/public-types").ChromePinSurface,
|
|
218
|
-
pin: import("../api/public-types").PinState | null,
|
|
219
|
-
) => void;
|
|
220
|
-
onWorkspaceModeChange?: (value: WorkspaceMode) => void;
|
|
221
|
-
onZoomChange?: (level: ZoomLevel) => void;
|
|
222
|
-
onActiveRailTabChange?: (value: ReviewRailTab) => void;
|
|
223
|
-
onShowTrackedChangesChange?: (show: boolean) => void;
|
|
224
|
-
onUndo?: () => void;
|
|
225
|
-
onRedo?: () => void;
|
|
226
|
-
onSetParagraphStyle?: (styleId: string) => void;
|
|
227
|
-
onToggleBold?: () => void;
|
|
228
|
-
onToggleItalic?: () => void;
|
|
229
|
-
onToggleUnderline?: () => void;
|
|
230
|
-
onSetSelectionTextColor?: (color: string) => void;
|
|
231
|
-
onSetSelectionHighlightColor?: (color: string | null) => void;
|
|
232
|
-
onToggleStrikethrough?: () => void;
|
|
233
|
-
onToggleSuperscript?: () => void;
|
|
234
|
-
onToggleSubscript?: () => void;
|
|
235
|
-
onSetFontFamily?: (fontFamily: string) => void;
|
|
236
|
-
onSetFontSize?: (fontSize: number) => void;
|
|
237
|
-
onSetTextColor?: (color: string) => void;
|
|
238
|
-
onSetHighlightColor?: (color: string | null) => void;
|
|
239
|
-
onSetAlignment?: (alignment: FormattingAlignment) => void;
|
|
240
|
-
onToggleBulletedList?: () => void;
|
|
241
|
-
onToggleNumberedList?: () => void;
|
|
242
|
-
onOutdent?: () => void;
|
|
243
|
-
onIndent?: () => void;
|
|
244
|
-
onAddComment?: () => void;
|
|
245
|
-
onInsertPageBreak?: () => void;
|
|
246
|
-
onInsertTable?: () => void;
|
|
247
|
-
onInsertSectionBreak?: (type: SectionBreakType) => void;
|
|
248
|
-
onInsertImage?: (options: InsertImageOptions) => void;
|
|
249
|
-
onSetTableStyle?: (styleId: string) => void;
|
|
250
|
-
onAddRowBefore?: () => void;
|
|
251
|
-
onAddRowAfter?: () => void;
|
|
252
|
-
onAddColumnBefore?: () => void;
|
|
253
|
-
onAddColumnAfter?: () => void;
|
|
254
|
-
onDeleteRow?: () => void;
|
|
255
|
-
onDeleteColumn?: () => void;
|
|
256
|
-
onDeleteTable?: () => void;
|
|
257
|
-
onMergeCells?: () => void;
|
|
258
|
-
onSplitCell?: () => void;
|
|
259
|
-
onSetCellBackground?: (color: string) => void;
|
|
260
|
-
onSetImageLayout?: (
|
|
261
|
-
mediaId: string,
|
|
262
|
-
dimensions: { widthEmu: number; heightEmu: number },
|
|
263
|
-
) => void;
|
|
264
|
-
onSetImageFrame?: (
|
|
265
|
-
mediaId: string,
|
|
266
|
-
offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
|
|
267
|
-
) => void;
|
|
268
|
-
onDeleteSectionBreak?: (sectionIndex: number) => void;
|
|
269
|
-
onUpdateSectionLayout?: (
|
|
270
|
-
sectionIndex: number,
|
|
271
|
-
patch: {
|
|
272
|
-
pageSize?: { width?: number; height?: number; orientation?: "portrait" | "landscape" };
|
|
273
|
-
pageMargins?: {
|
|
274
|
-
top?: number;
|
|
275
|
-
right?: number;
|
|
276
|
-
bottom?: number;
|
|
277
|
-
left?: number;
|
|
278
|
-
header?: number;
|
|
279
|
-
footer?: number;
|
|
280
|
-
gutter?: number;
|
|
281
|
-
};
|
|
282
|
-
columns?: {
|
|
283
|
-
count?: number;
|
|
284
|
-
space?: number;
|
|
285
|
-
equalWidth?: boolean;
|
|
286
|
-
columns?: Array<{ width: number; space?: number }>;
|
|
287
|
-
separator?: boolean;
|
|
288
|
-
};
|
|
289
|
-
titlePage?: boolean;
|
|
290
|
-
sectionType?: SectionBreakType;
|
|
291
|
-
},
|
|
292
|
-
) => void;
|
|
293
|
-
onSetSectionPageNumbering?: (
|
|
294
|
-
sectionIndex: number,
|
|
295
|
-
patch: SectionPageNumberingPatch | null,
|
|
296
|
-
) => void;
|
|
297
|
-
onSetHeaderFooterLink?: (
|
|
298
|
-
sectionIndex: number,
|
|
299
|
-
patch: HeaderFooterLinkPatch,
|
|
300
|
-
) => void;
|
|
301
|
-
onAddCommentFromSelection?: () => void;
|
|
302
|
-
onExport?: () => void;
|
|
303
|
-
onDismissSelectionToolbar?: () => void;
|
|
304
|
-
onAcceptSuggestion?: () => void;
|
|
305
|
-
onRejectSuggestion?: () => void;
|
|
306
|
-
onEditSuggestion?: () => void;
|
|
307
|
-
onAddCommentFromSuggestion?: () => void;
|
|
308
|
-
onSelectionToolbarFocusCapture?: FocusEventHandler<HTMLDivElement>;
|
|
309
|
-
onSelectionToolbarBlurCapture?: FocusEventHandler<HTMLDivElement>;
|
|
310
|
-
selectionToolbarRef?: Ref<HTMLDivElement>;
|
|
311
|
-
onOpenComment?: (thread: CommentSidebarThreadSnapshot) => void;
|
|
312
|
-
onResolveComment?: (commentId: string) => void;
|
|
313
|
-
onReopenComment?: (commentId: string) => void;
|
|
314
|
-
onAddReply?: (commentId: string, body: string) => void;
|
|
315
|
-
onEditBody?: (commentId: string, body: string) => void;
|
|
316
|
-
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
317
|
-
onAcceptRevision?: (revisionId: string) => void;
|
|
318
|
-
onRejectRevision?: (revisionId: string) => void;
|
|
319
|
-
onAcceptAllChanges?: () => void;
|
|
320
|
-
onRejectAllChanges?: () => void;
|
|
321
|
-
onCloseStory?: () => void;
|
|
322
|
-
/**
|
|
323
|
-
* @deprecated P8.11 — the workspace no longer renders a workspace-level
|
|
324
|
-
* header band with an "Edit header" button; per-page header bands route
|
|
325
|
-
* clicks via `onOpenStory` / `runtime.openStory` directly. The prop
|
|
326
|
-
* remains optional for one release so existing hosts continue to
|
|
327
|
-
* compile; supplying it emits a `console.warn` on mount.
|
|
328
|
-
*/
|
|
329
|
-
onOpenHeaderStory?: () => void;
|
|
330
|
-
/**
|
|
331
|
-
* @deprecated P8.11 — see `onOpenHeaderStory`. Footer variant of the
|
|
332
|
-
* same deprecation.
|
|
333
|
-
*/
|
|
334
|
-
onOpenFooterStory?: () => void;
|
|
335
|
-
/**
|
|
336
|
-
* Open a header/footer story for a specific page. Called when the user
|
|
337
|
-
* double-clicks a per-page header/footer band in the page-stack chrome.
|
|
338
|
-
* Must resolve the correct variant for that page's section and call
|
|
339
|
-
* `runtime.openStory()`.
|
|
340
|
-
*/
|
|
341
|
-
onOpenHeaderStoryForPage?: (pageIndex: number) => void;
|
|
342
|
-
onOpenFooterStoryForPage?: (pageIndex: number) => void;
|
|
343
|
-
/**
|
|
344
|
-
* P8.11 — fired when a per-page chrome band (header / footer) is
|
|
345
|
-
* clicked to promote it into the active editing surface. Wire to
|
|
346
|
-
* `runtime.openStory(target)`; the chrome layer's portal mechanism
|
|
347
|
-
* then reparents the PM surface into the matching band's active slot.
|
|
348
|
-
*/
|
|
349
|
-
onOpenStory?: (target: EditorStoryTarget) => void;
|
|
350
|
-
onSetParagraphIndentation?: (indentation: {
|
|
351
|
-
left?: number;
|
|
352
|
-
right?: number;
|
|
353
|
-
firstLine?: number;
|
|
354
|
-
hanging?: number;
|
|
355
|
-
}) => void;
|
|
356
|
-
onSetParagraphTabStops?: (tabStops: Array<{ pos: number; val?: string; leader?: string }>) => void;
|
|
357
|
-
onRestartNumbering?: () => void;
|
|
358
|
-
onContinueNumbering?: () => void;
|
|
359
|
-
// P6: new table ops
|
|
360
|
-
onToggleRowHeader?: () => void;
|
|
361
|
-
onToggleRowCantSplit?: () => void;
|
|
362
|
-
onDistributeColumnsEvenly?: () => void;
|
|
363
|
-
onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
|
|
364
|
-
onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
|
|
365
|
-
/** P6: active table context for chrome overlay grips. */
|
|
366
|
-
tableContext?: import("../api/public-types").TableStructureContextSnapshot | null;
|
|
367
|
-
/** P6: column resize committed from overlay grip → set-column-width op. */
|
|
368
|
-
onSetColumnWidth?: (columnIndex: number, twips: number) => void;
|
|
369
|
-
/** P6: row resize committed from overlay grip → set-row-height op. */
|
|
370
|
-
onSetRowHeight?: (rowIndex: number, twips: number, rule: "auto" | "atLeast" | "exact") => void;
|
|
371
|
-
onListIndent?: () => void;
|
|
372
|
-
onListOutdent?: () => void;
|
|
373
|
-
onUpdateFields?: () => void;
|
|
374
|
-
onUpdateTableOfContents?: () => void;
|
|
375
|
-
onGoToPreviousReviewItem?: () => void;
|
|
376
|
-
onGoToNextReviewItem?: () => void;
|
|
377
|
-
onMarkSectionForReview?: () => void;
|
|
378
|
-
/** Optional: open sidebar to tracked-changes panel. When provided, the review role shows a sidebar-TC icon. */
|
|
379
|
-
onReviewSidebarTrackedChanges?: () => void;
|
|
380
|
-
/** Optional: open sidebar to comments panel. When provided, the review role shows a sidebar-comments icon. */
|
|
381
|
-
onReviewSidebarComments?: () => void;
|
|
382
|
-
onNavigateHeading?: (headingId: string) => void;
|
|
383
|
-
chromeVisibility?: Partial<ReviewWorkspaceChromeVisibility>;
|
|
384
|
-
/**
|
|
385
|
-
* Called when the shell-header mode tab changes or any chrome surface fires
|
|
386
|
-
* a role switch. Wire to `runtime.setEditorRole(role)` so the workspace
|
|
387
|
-
* re-renders with the new per-role action set.
|
|
388
|
-
*/
|
|
389
|
-
onEditorRoleChange?: (role: import("../api/public-types.ts").EditorRole) => void;
|
|
390
|
-
/**
|
|
391
|
-
* Scope card mode selector fired a mode change. Wire to the host's
|
|
392
|
-
* existing overlay-apply path (or an equivalent CCEP workflow
|
|
393
|
-
* endpoint). The card never mutates runtime state directly.
|
|
394
|
-
*/
|
|
395
|
-
onScopeModeChangeRequested?: (payload: {
|
|
396
|
-
scopeId: string;
|
|
397
|
-
mode: import("../api/public-types.ts").WorkflowScopeMode;
|
|
398
|
-
}) => void;
|
|
399
|
-
/**
|
|
400
|
-
* Scope card issue row fired an action (resolve/waive/escalate/
|
|
401
|
-
* acknowledge). Host updates the attached `IssueMetadataValue`
|
|
402
|
-
* state and re-pushes via `setWorkflowMetadataEntries`.
|
|
403
|
-
*/
|
|
404
|
-
onScopeIssueActionRequested?: (payload: {
|
|
405
|
-
scopeId: string;
|
|
406
|
-
issueId: string;
|
|
407
|
-
action: import("../api/public-types.ts").ScopeIssueAction;
|
|
408
|
-
}) => void;
|
|
409
|
-
/**
|
|
410
|
-
* R3 — scope card suggestion-group accept button fired. WordReview-
|
|
411
|
-
* Editor relays to `ref.acceptSuggestionGroup(groupId)` which fans
|
|
412
|
-
* out to individual `acceptChange` calls across the group members.
|
|
413
|
-
*/
|
|
414
|
-
onScopeAcceptSuggestionGroup?: (payload: {
|
|
415
|
-
scopeId: string;
|
|
416
|
-
groupId: string;
|
|
417
|
-
}) => void;
|
|
418
|
-
/** R3 — scope card suggestion-group reject. */
|
|
419
|
-
onScopeRejectSuggestionGroup?: (payload: {
|
|
420
|
-
scopeId: string;
|
|
421
|
-
groupId: string;
|
|
422
|
-
}) => void;
|
|
423
|
-
/**
|
|
424
|
-
* K2 — scope card "Ask review agent" fired. WordReviewEditor emits
|
|
425
|
-
* `agent-on-selection-requested` via WordReviewEditorEvent.
|
|
426
|
-
*/
|
|
427
|
-
onScopeAskAgent?: (payload: {
|
|
428
|
-
scopeId: string;
|
|
429
|
-
anchor?: EditorAnchorProjection;
|
|
430
|
-
}) => void;
|
|
431
|
-
/**
|
|
432
|
-
* P3 — optional scope-tag editor slot rendered inside the scope
|
|
433
|
-
* card when `editorRole === "workflow"`. Hosts pass a chip picker,
|
|
434
|
-
* free-text input, or whatever authoring surface they want. Unset
|
|
435
|
-
* in editor/review roles.
|
|
436
|
-
*/
|
|
437
|
-
scopeCardScopeTagEditor?: ReactNode;
|
|
117
|
+
function shellModeToEditorRole(mode: ShellHeaderMode): EditorRole | null {
|
|
118
|
+
switch (mode) {
|
|
119
|
+
case "edit":
|
|
120
|
+
return "editor";
|
|
121
|
+
case "review":
|
|
122
|
+
return "review";
|
|
123
|
+
case "workflow":
|
|
124
|
+
return "workflow";
|
|
125
|
+
case "more":
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
438
128
|
}
|
|
439
129
|
|
|
440
130
|
export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
@@ -449,12 +139,8 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
449
139
|
// measure per-page rects and to reparent PM's DOM node across band
|
|
450
140
|
// portals when `activeStory` changes. See comment near the body slot
|
|
451
141
|
// in the render tree below.
|
|
452
|
-
const bodySlotRef
|
|
453
|
-
const scrollRootRef
|
|
454
|
-
const [pmSurfaceElement, setPmSurfaceElement] =
|
|
455
|
-
useState<HTMLElement | null>(null);
|
|
456
|
-
const [pageStackScrollRoot, setPageStackScrollRoot] =
|
|
457
|
-
useState<HTMLElement | null>(null);
|
|
142
|
+
const { bodySlotRef, pmSurfaceElement } = usePmSurfaceCapture();
|
|
143
|
+
const { scrollRootRef, pageStackScrollRoot } = useScrollRootCapture();
|
|
458
144
|
const caps = props.capabilities;
|
|
459
145
|
const isPageWorkspace = props.workspaceMode === "page";
|
|
460
146
|
const markupDisplay = props.markupDisplay;
|
|
@@ -462,93 +148,27 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
462
148
|
const [layoutToolsOpen, setLayoutToolsOpen] = useState(false);
|
|
463
149
|
|
|
464
150
|
// Scope card state — tracks which scope's card is currently open so
|
|
465
|
-
// the ChromeOverlay's card layer renders the right one.
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (!layoutFacet || typeof layoutFacet.getAllScopeCardModels !== "function") {
|
|
487
|
-
return undefined;
|
|
488
|
-
}
|
|
489
|
-
const onKey = (event: KeyboardEvent) => {
|
|
490
|
-
if (!shouldHandleScopeNavKey(event)) return;
|
|
491
|
-
const models = layoutFacet.getAllScopeCardModels();
|
|
492
|
-
if (models.length === 0) return;
|
|
493
|
-
const ids = models.map((model) => model.scopeId);
|
|
494
|
-
const key = event.key.toLowerCase();
|
|
495
|
-
if (key === "enter") {
|
|
496
|
-
if (!activeScopeId) {
|
|
497
|
-
setActiveScopeId(ids[0] ?? null);
|
|
498
|
-
event.preventDefault();
|
|
499
|
-
}
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
const direction: 1 | -1 = key === "j" ? 1 : -1;
|
|
503
|
-
const next = cycleScopeIndex(activeScopeId, ids, direction);
|
|
504
|
-
setActiveScopeId(next);
|
|
505
|
-
event.preventDefault();
|
|
506
|
-
};
|
|
507
|
-
window.addEventListener("keydown", onKey);
|
|
508
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
509
|
-
}, [props.layoutFacet, activeScopeId]);
|
|
510
|
-
const onScopeModeChangeRequested = props.onScopeModeChangeRequested;
|
|
511
|
-
const handleScopeCardModeChange = useCallback(
|
|
512
|
-
(scopeId: string, mode: import("../api/public-types.ts").WorkflowScopeMode) => {
|
|
513
|
-
onScopeModeChangeRequested?.({ scopeId, mode });
|
|
514
|
-
},
|
|
515
|
-
[onScopeModeChangeRequested],
|
|
516
|
-
);
|
|
517
|
-
const onScopeIssueActionRequested = props.onScopeIssueActionRequested;
|
|
518
|
-
const handleScopeCardIssueAction = useCallback(
|
|
519
|
-
(
|
|
520
|
-
scopeId: string,
|
|
521
|
-
issueId: string,
|
|
522
|
-
action: import("../api/public-types.ts").ScopeIssueAction,
|
|
523
|
-
) => {
|
|
524
|
-
onScopeIssueActionRequested?.({ scopeId, issueId, action });
|
|
525
|
-
},
|
|
526
|
-
[onScopeIssueActionRequested],
|
|
527
|
-
);
|
|
528
|
-
const onScopeAcceptSuggestionGroup = props.onScopeAcceptSuggestionGroup;
|
|
529
|
-
const handleScopeCardAcceptSuggestionGroup = useCallback(
|
|
530
|
-
(scopeId: string, groupId: string) => {
|
|
531
|
-
onScopeAcceptSuggestionGroup?.({ scopeId, groupId });
|
|
532
|
-
},
|
|
533
|
-
[onScopeAcceptSuggestionGroup],
|
|
534
|
-
);
|
|
535
|
-
const onScopeRejectSuggestionGroup = props.onScopeRejectSuggestionGroup;
|
|
536
|
-
const handleScopeCardRejectSuggestionGroup = useCallback(
|
|
537
|
-
(scopeId: string, groupId: string) => {
|
|
538
|
-
onScopeRejectSuggestionGroup?.({ scopeId, groupId });
|
|
539
|
-
},
|
|
540
|
-
[onScopeRejectSuggestionGroup],
|
|
541
|
-
);
|
|
542
|
-
const onScopeAskAgent = props.onScopeAskAgent;
|
|
543
|
-
const handleScopeCardAskAgent = useCallback(
|
|
544
|
-
(scopeId: string) => {
|
|
545
|
-
const cardModel = props.layoutFacet
|
|
546
|
-
?.getAllScopeCardModels?.()
|
|
547
|
-
?.find((m) => m.scopeId === scopeId);
|
|
548
|
-
onScopeAskAgent?.({ scopeId, anchor: cardModel?.anchor });
|
|
549
|
-
},
|
|
550
|
-
[onScopeAskAgent, props.layoutFacet],
|
|
551
|
-
);
|
|
151
|
+
// the ChromeOverlay's card layer renders the right one. Open/close
|
|
152
|
+
// rules + J/K keyboard navigation + all 5 scope-card callback
|
|
153
|
+
// handlers live in the hook.
|
|
154
|
+
const {
|
|
155
|
+
activeScopeId,
|
|
156
|
+
handleScopeStripeClick,
|
|
157
|
+
handleScopeCardClose,
|
|
158
|
+
handleScopeCardModeChange,
|
|
159
|
+
handleScopeCardIssueAction,
|
|
160
|
+
handleScopeCardAcceptSuggestionGroup,
|
|
161
|
+
handleScopeCardRejectSuggestionGroup,
|
|
162
|
+
handleScopeCardAskAgent,
|
|
163
|
+
} = useScopeCardState({
|
|
164
|
+
layoutFacet: props.layoutFacet,
|
|
165
|
+
workflowFacet: props.workflowFacet,
|
|
166
|
+
onScopeModeChangeRequested: props.onScopeModeChangeRequested,
|
|
167
|
+
onScopeIssueActionRequested: props.onScopeIssueActionRequested,
|
|
168
|
+
onScopeAcceptSuggestionGroup: props.onScopeAcceptSuggestionGroup,
|
|
169
|
+
onScopeRejectSuggestionGroup: props.onScopeRejectSuggestionGroup,
|
|
170
|
+
onScopeAskAgent: props.onScopeAskAgent,
|
|
171
|
+
});
|
|
552
172
|
const zoomLevel = props.zoomLevel ?? 100;
|
|
553
173
|
// Numeric zooms resolve immediately; "pageWidth" / "onePage" need the
|
|
554
174
|
// page-frame dimensions to fit against — they're resolved below once
|
|
@@ -570,18 +190,36 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
570
190
|
chromeVisibility: props.chromeVisibility,
|
|
571
191
|
});
|
|
572
192
|
const reviewRailAvailable = chromeVisibility.reviewRail && (caps?.reviewRailVisible ?? true);
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
reviewRailAvailable,
|
|
579
|
-
}),
|
|
580
|
-
);
|
|
193
|
+
const { viewportWidth, viewportHeight } = useViewportDimensions();
|
|
194
|
+
const { reviewRailOpen, setReviewRailOpen } = useReviewRailState({
|
|
195
|
+
reviewRailAvailable,
|
|
196
|
+
viewportWidth,
|
|
197
|
+
});
|
|
581
198
|
// Incremented on zoom_changed / render_frame_ready so the placement
|
|
582
199
|
// useMemo below re-executes when the render kernel emits new rects.
|
|
583
|
-
const
|
|
584
|
-
|
|
200
|
+
const renderFrameRevision = useLayoutFacetRenderSignal(props.layoutFacet);
|
|
201
|
+
|
|
202
|
+
// refactor/10 chrome-contract Slice 2 (2026-04-23) — emit into the
|
|
203
|
+
// shell UI-API subscriber channels when the geometry/layout facet
|
|
204
|
+
// bumps its render-frame revision. Consumers of
|
|
205
|
+
// `ui.viewport.subscribe` / `ui.overlays.subscribe` get one event per
|
|
206
|
+
// layout invalidation. Coalescing is upstream (the layout facet
|
|
207
|
+
// itself bumps at most once per frame).
|
|
208
|
+
//
|
|
209
|
+
// Payload choices:
|
|
210
|
+
// - viewport: current `ViewportState` snapshot. Consumer can diff
|
|
211
|
+
// or re-read as they like.
|
|
212
|
+
// - overlays: `{kind: "page", value: 0}` as a universal "geometry
|
|
213
|
+
// changed, re-read any attached anchors" marker. Per-query
|
|
214
|
+
// invalidation fan-out is a Phase Q follow-up — the geometry
|
|
215
|
+
// facet does not yet expose per-kind invalidation events.
|
|
216
|
+
const uiApiForEmit = useUiApi();
|
|
217
|
+
const shellChannels = useUiShellChannels();
|
|
218
|
+
React.useEffect(() => {
|
|
219
|
+
if (!uiApiForEmit || !shellChannels) return;
|
|
220
|
+
shellChannels.viewport.emit(uiApiForEmit.viewport.get());
|
|
221
|
+
shellChannels.overlays.emit({ kind: "page", value: 0 });
|
|
222
|
+
}, [renderFrameRevision, uiApiForEmit, shellChannels]);
|
|
585
223
|
const headings = props.documentNavigation?.headings ?? [];
|
|
586
224
|
const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
|
|
587
225
|
const footerVariant = snapshot.pageLayout?.footerVariants[0]?.variant ?? "default";
|
|
@@ -590,98 +228,47 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
590
228
|
? viewState.selection.activeRange.at
|
|
591
229
|
: viewState.selection.head;
|
|
592
230
|
const shouldResolveActiveParagraphLayout =
|
|
593
|
-
isPageWorkspace &&
|
|
594
|
-
chromeVisibility.pageChrome &&
|
|
595
|
-
layoutToolsOpen;
|
|
596
|
-
const activeParagraphLayout = useMemo(
|
|
597
|
-
() =>
|
|
598
|
-
shouldResolveActiveParagraphLayout
|
|
599
|
-
? resolveActiveParagraphLayout(snapshot.surface, selectionPosition)
|
|
600
|
-
: null,
|
|
601
|
-
[selectionPosition, shouldResolveActiveParagraphLayout, snapshot.surface],
|
|
602
|
-
);
|
|
603
|
-
const pageChromeModel = useMemo(
|
|
604
|
-
() =>
|
|
605
|
-
buildPageChromeModel(
|
|
606
|
-
snapshot.surface,
|
|
607
|
-
snapshot.pageLayout,
|
|
608
|
-
props.documentNavigation,
|
|
609
|
-
snapshot.activeStory,
|
|
610
|
-
),
|
|
611
|
-
[props.documentNavigation, snapshot.activeStory, snapshot.pageLayout, snapshot.surface],
|
|
612
|
-
);
|
|
231
|
+
isPageWorkspace && chromeVisibility.pageChrome && layoutToolsOpen;
|
|
613
232
|
const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
|
|
614
233
|
const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
// real paper dimensions. Numeric zooms pass through. Falls back to
|
|
630
|
-
// `numericZoomScale` (1.0 for symbolic zooms when paper dims are
|
|
631
|
-
// unavailable, e.g., during initial load).
|
|
632
|
-
const zoomScale = useMemo(() => {
|
|
633
|
-
if (typeof zoomLevel === "number") return numericZoomScale;
|
|
634
|
-
return resolveZoomMultiplier(
|
|
635
|
-
zoomLevel,
|
|
636
|
-
pageShellMetrics.frameWidthPx ?? 0,
|
|
637
|
-
pageShellMetrics.frameHeightPx ?? 0,
|
|
638
|
-
viewportWidth,
|
|
639
|
-
viewportHeight,
|
|
640
|
-
);
|
|
641
|
-
}, [
|
|
234
|
+
const {
|
|
235
|
+
activeParagraphLayout,
|
|
236
|
+
pageChromeModel,
|
|
237
|
+
gatedSelectionTool,
|
|
238
|
+
pageShellMetrics,
|
|
239
|
+
zoomScale,
|
|
240
|
+
pageZoomBucket,
|
|
241
|
+
} = useDerivedViewState({
|
|
242
|
+
snapshot,
|
|
243
|
+
documentNavigation: props.documentNavigation,
|
|
244
|
+
activeSelectionTool: props.activeSelectionTool,
|
|
245
|
+
chromeVisibility,
|
|
246
|
+
selectionPosition,
|
|
247
|
+
shouldResolveActiveParagraphLayout,
|
|
642
248
|
zoomLevel,
|
|
643
249
|
numericZoomScale,
|
|
644
|
-
pageShellMetrics.frameWidthPx,
|
|
645
|
-
pageShellMetrics.frameHeightPx,
|
|
646
250
|
viewportWidth,
|
|
647
251
|
viewportHeight,
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (anchorRect && selectionToolbarRootRef.current) {
|
|
662
|
-
const containerRect = selectionToolbarRootRef.current.getBoundingClientRect();
|
|
663
|
-
const result = resolveSelectionToolPlacement({
|
|
664
|
-
anchor: anchorRect,
|
|
665
|
-
container: { widthPx: containerRect.width, heightPx: containerRect.height },
|
|
666
|
-
});
|
|
667
|
-
if (result) return result;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
// Fall back to DOM rects for hosts that do not supply a layout facet.
|
|
671
|
-
return resolveSelectionToolbarPlacement(
|
|
672
|
-
props.selectionToolAnchor,
|
|
673
|
-
selectionToolbarRootRef.current,
|
|
674
|
-
zoomScale,
|
|
675
|
-
);
|
|
676
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
677
|
-
}, [
|
|
678
|
-
props.layoutFacet,
|
|
679
|
-
props.selectionToolAnchor,
|
|
252
|
+
isPageWorkspace,
|
|
253
|
+
});
|
|
254
|
+
// DS-C1 — publish the current selection anchor so
|
|
255
|
+
// `ui.overlays.getAnchor({ kind: "selection" })` resolves to a real
|
|
256
|
+
// rect on the mounted path (designsystem.md §8.8.1 "Selection toolbar"
|
|
257
|
+
// row). No-op when no `OverlayAnchorBridgeProvider` is mounted.
|
|
258
|
+
useShellSelectionAnchorBridge({
|
|
259
|
+
geometryFacet: props.geometryFacet,
|
|
260
|
+
selection: viewState.selection,
|
|
261
|
+
gatedSelectionTool,
|
|
262
|
+
renderFrameRevision,
|
|
263
|
+
});
|
|
264
|
+
const selectionToolbarPlacement = useSelectionToolbarPlacement({
|
|
680
265
|
gatedSelectionTool,
|
|
681
|
-
viewState.selection,
|
|
266
|
+
selection: viewState.selection,
|
|
267
|
+
selectionToolAnchor: props.selectionToolAnchor,
|
|
682
268
|
zoomScale,
|
|
683
269
|
renderFrameRevision,
|
|
684
|
-
|
|
270
|
+
selectionToolbarRootRef,
|
|
271
|
+
});
|
|
685
272
|
const activePage = props.documentNavigation?.pages[props.documentNavigation.activePageIndex] ?? null;
|
|
686
273
|
// P5b — status-bar facts derived from the layout facet so the
|
|
687
274
|
// Page-N-of-M display + measurement-fidelity badge ("E" / "C" / "C+F")
|
|
@@ -689,48 +276,14 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
689
276
|
// above bumps `renderFrameRevision` on the same kinds; including it
|
|
690
277
|
// in the dependency list re-runs this memo without a separate
|
|
691
278
|
// subscription.
|
|
692
|
-
|
|
693
|
-
// selection overlay can query the anchor index without a full surface walk.
|
|
694
|
-
const grabbedSegmentOffsets = useMemo(() => {
|
|
695
|
-
const objectId = snapshot.grabbedObjectId ?? null;
|
|
696
|
-
if (!objectId || !snapshot.surface) return null;
|
|
697
|
-
for (const block of snapshot.surface.blocks) {
|
|
698
|
-
if (!("segments" in block)) continue;
|
|
699
|
-
for (const seg of (block as { segments?: unknown[] }).segments ?? []) {
|
|
700
|
-
const s = seg as { kind?: string; mediaId?: string; from?: number; to?: number };
|
|
701
|
-
if ((s.kind === "image" || s.kind === "shape") && s.mediaId === objectId && s.from != null) {
|
|
702
|
-
return { from: s.from, to: s.to ?? s.from + 1 };
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return null;
|
|
707
|
-
}, [snapshot.grabbedObjectId, snapshot.surface]);
|
|
279
|
+
const grabbedSegmentOffsets = useGrabbedSegmentOffsets(snapshot);
|
|
708
280
|
|
|
709
|
-
const statusBarPageFacts =
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
measurementFidelity: undefined as
|
|
716
|
-
| import("../api/public-types.ts").PublicMeasurementFidelity
|
|
717
|
-
| undefined,
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
const head = selectionPosition;
|
|
721
|
-
const pageRef = facet.getPageForOffset(head, snapshot.activeStory);
|
|
722
|
-
const displayPageNumber =
|
|
723
|
-
pageRef !== null && typeof pageRef.pageIndex === "number"
|
|
724
|
-
? facet.getDisplayPageNumber(pageRef.pageIndex) ?? pageRef.pageIndex + 1
|
|
725
|
-
: null;
|
|
726
|
-
const pageCount = facet.getPageCount();
|
|
727
|
-
return {
|
|
728
|
-
displayPageNumber,
|
|
729
|
-
pageCount,
|
|
730
|
-
measurementFidelity: facet.getMeasurementFidelity(),
|
|
731
|
-
};
|
|
732
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
733
|
-
}, [props.layoutFacet, selectionPosition, snapshot.activeStory, renderFrameRevision]);
|
|
281
|
+
const statusBarPageFacts = useStatusBarPageFacts({
|
|
282
|
+
layoutFacet: props.layoutFacet,
|
|
283
|
+
selectionPosition,
|
|
284
|
+
activeStory: snapshot.activeStory,
|
|
285
|
+
renderFrameRevision,
|
|
286
|
+
});
|
|
734
287
|
// P8.11 — `headerBandLabel` / `footerBandLabel` retired along with the
|
|
735
288
|
// workspace-level bands. Per-page bands in `TwPageStackChromeLayer`
|
|
736
289
|
// render the actual header / footer story blocks via
|
|
@@ -743,377 +296,250 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
743
296
|
snapshot.readOnly ||
|
|
744
297
|
snapshot.activeStory.kind !== "main" ||
|
|
745
298
|
effectiveSelectionMode !== "edit";
|
|
746
|
-
const responsiveChrome = useMemo(
|
|
747
|
-
() =>
|
|
748
|
-
resolveResponsiveChromeState({
|
|
749
|
-
viewportWidth,
|
|
750
|
-
reviewRailAvailable,
|
|
751
|
-
reviewRailOpen,
|
|
752
|
-
}),
|
|
753
|
-
[reviewRailAvailable, reviewRailOpen, viewportWidth],
|
|
754
|
-
);
|
|
755
299
|
const hasSidebarPanelAccess = Boolean(
|
|
756
300
|
props.onReviewSidebarTrackedChanges || props.onReviewSidebarComments,
|
|
757
301
|
);
|
|
758
|
-
const scopedChromePolicy =
|
|
759
|
-
(
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
capabilities: caps,
|
|
764
|
-
interactionGuardSnapshot: props.interactionGuardSnapshot,
|
|
765
|
-
workflowScopeSnapshot: props.workflowScopeSnapshot,
|
|
766
|
-
activeListContext: props.activeListContext,
|
|
767
|
-
role: viewState.editorRole,
|
|
768
|
-
hasSidebarPanelAccess,
|
|
769
|
-
}),
|
|
770
|
-
[
|
|
771
|
-
caps,
|
|
302
|
+
const { responsiveChrome, scopedChromePolicy, toolbarInteractionPolicy } =
|
|
303
|
+
useChromePolicy({
|
|
304
|
+
viewportWidth,
|
|
305
|
+
reviewRailAvailable,
|
|
306
|
+
reviewRailOpen,
|
|
772
307
|
chromePreset,
|
|
308
|
+
caps,
|
|
309
|
+
interactionGuardSnapshot: props.interactionGuardSnapshot,
|
|
310
|
+
workflowScopeSnapshot: props.workflowScopeSnapshot,
|
|
311
|
+
activeListContext: props.activeListContext,
|
|
312
|
+
role: viewState.editorRole,
|
|
773
313
|
hasSidebarPanelAccess,
|
|
774
|
-
|
|
775
|
-
props.interactionGuardSnapshot,
|
|
776
|
-
props.workflowScopeSnapshot,
|
|
777
|
-
responsiveChrome.isNarrow,
|
|
778
|
-
viewState.editorRole,
|
|
779
|
-
],
|
|
780
|
-
);
|
|
781
|
-
const toolbarInteractionPolicy: ToolbarInteractionPolicy | undefined = caps
|
|
782
|
-
? {
|
|
783
|
-
mode: effectiveSelectionMode,
|
|
784
|
-
canFormatText: caps.canEdit && effectiveSelectionMode === "edit",
|
|
785
|
-
canInsertStructural: caps.canEdit && effectiveSelectionMode === "edit",
|
|
786
|
-
canAddComment:
|
|
787
|
-
caps.canAddComment &&
|
|
788
|
-
effectiveSelectionMode !== "view" &&
|
|
789
|
-
effectiveSelectionMode !== "blocked",
|
|
790
|
-
}
|
|
791
|
-
: undefined;
|
|
792
|
-
|
|
793
|
-
useEffect(() => {
|
|
794
|
-
recordPerfSample("workspace.chrome");
|
|
795
|
-
incrementInvalidationCounter("workspace.chrome.recomputes");
|
|
796
|
-
}, [activeParagraphLayout, pageChromeModel, pageShellMetrics]);
|
|
797
|
-
|
|
798
|
-
useEffect(() => {
|
|
799
|
-
if (isPageWorkspace && snapshot.activeStory.kind !== "main") {
|
|
800
|
-
setLayoutToolsOpen(true);
|
|
801
|
-
}
|
|
802
|
-
}, [isPageWorkspace, snapshot.activeStory.kind]);
|
|
803
|
-
|
|
804
|
-
// P8.11 — capture the scroll-root DOM element on mount so the chrome
|
|
805
|
-
// overlay's `TwPageStackChromeLayer` can measure per-page rects and
|
|
806
|
-
// observe DOM mutations. `scrollRootRef` is attached to the existing
|
|
807
|
-
// `[data-wre-scroll-root]` container; rely on a mount effect rather
|
|
808
|
-
// than a ref callback so render-time state stays cheap.
|
|
809
|
-
useEffect(() => {
|
|
810
|
-
if (scrollRootRef.current !== pageStackScrollRoot) {
|
|
811
|
-
setPageStackScrollRoot(scrollRootRef.current);
|
|
812
|
-
}
|
|
813
|
-
// A `useEffect` re-runs after every render; the comparison guard
|
|
814
|
-
// keeps `setPageStackScrollRoot` from firing every commit. The
|
|
815
|
-
// scroll-root identity only changes when the component re-mounts.
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
// P8.11 — capture the PM surface DOM element. The ProseMirror surface
|
|
819
|
-
// mounts inside `bodySlotRef` on its own schedule (the PM constructor
|
|
820
|
-
// runs inside the `TwProseMirrorSurface` child component). A
|
|
821
|
-
// `MutationObserver` scoped to the body slot's `childList` picks up
|
|
822
|
-
// the PM root on first commit; once captured, the chrome layer owns
|
|
823
|
-
// reparent state (including portal-slot promotion), so we skip
|
|
824
|
-
// further updates unless PM is actually disconnected from the
|
|
825
|
-
// document (e.g. session/document swap tearing PM down).
|
|
826
|
-
useEffect(() => {
|
|
827
|
-
const slot = bodySlotRef.current;
|
|
828
|
-
if (!slot) return undefined;
|
|
829
|
-
|
|
830
|
-
// If we already hold a live reference, the chrome layer may have
|
|
831
|
-
// portaled PM into a per-page band — PM has left `bodySlotRef` but
|
|
832
|
-
// is still connected to the document. We keep the reference until
|
|
833
|
-
// the node is fully disconnected.
|
|
834
|
-
if (pmSurfaceElement && pmSurfaceElement.isConnected) {
|
|
835
|
-
return undefined;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
const readPm = (): HTMLElement | null =>
|
|
839
|
-
slot.querySelector<HTMLElement>(".ProseMirror");
|
|
840
|
-
|
|
841
|
-
const current = readPm();
|
|
842
|
-
if (current !== pmSurfaceElement) {
|
|
843
|
-
setPmSurfaceElement(current);
|
|
844
|
-
}
|
|
845
|
-
const runtime = slot.ownerDocument?.defaultView as
|
|
846
|
-
| (Window & { MutationObserver?: typeof MutationObserver })
|
|
847
|
-
| null;
|
|
848
|
-
if (!runtime?.MutationObserver) return undefined;
|
|
849
|
-
const observer = new runtime.MutationObserver(() => {
|
|
850
|
-
const next = readPm();
|
|
851
|
-
if (next !== null && next !== pmSurfaceElement) {
|
|
852
|
-
setPmSurfaceElement(next);
|
|
853
|
-
}
|
|
314
|
+
effectiveSelectionMode,
|
|
854
315
|
});
|
|
855
|
-
// `childList: true, subtree: false` — we only care when children of
|
|
856
|
-
// the body slot change (e.g. PM is added for the first time).
|
|
857
|
-
// Subtree mutations (PM's own edits) are not our concern and would
|
|
858
|
-
// fire on every keystroke.
|
|
859
|
-
observer.observe(slot, { childList: true, subtree: false });
|
|
860
|
-
return () => observer.disconnect();
|
|
861
|
-
}, [pmSurfaceElement]);
|
|
862
|
-
|
|
863
|
-
// P8.11 — deprecation shim for the legacy `onOpenHeaderStory` /
|
|
864
|
-
// `onOpenFooterStory` props. Per-page chrome bands route clicks via
|
|
865
|
-
// `onOpenStory` + `runtime.openStory` directly; the workspace-level
|
|
866
|
-
// bands that consumed these callbacks are gone. Kept optional for one
|
|
867
|
-
// release so existing hosts compile; a mount-time `console.warn` nudges
|
|
868
|
-
// them toward `onOpenStory`.
|
|
869
|
-
useEffect(() => {
|
|
870
|
-
if (props.onOpenHeaderStory) {
|
|
871
|
-
// eslint-disable-next-line no-console
|
|
872
|
-
console.warn(
|
|
873
|
-
"[docx-react-component] `onOpenHeaderStory` is deprecated. Per-page header bands route clicks via runtime.openStory directly. (P8)",
|
|
874
|
-
);
|
|
875
|
-
}
|
|
876
|
-
if (props.onOpenFooterStory) {
|
|
877
|
-
// eslint-disable-next-line no-console
|
|
878
|
-
console.warn(
|
|
879
|
-
"[docx-react-component] `onOpenFooterStory` is deprecated. Per-page footer bands route clicks via runtime.openStory directly. (P8)",
|
|
880
|
-
);
|
|
881
|
-
}
|
|
882
|
-
// Mount-once: we only want to nudge hosts at startup, not per render.
|
|
883
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
884
|
-
}, []);
|
|
885
316
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
updateViewport();
|
|
897
|
-
window.addEventListener("resize", updateViewport);
|
|
898
|
-
return () => {
|
|
899
|
-
window.removeEventListener("resize", updateViewport);
|
|
900
|
-
};
|
|
901
|
-
}, []);
|
|
317
|
+
// L7 Phase 2 Task 2.2.4a — viewport-scroll wiring. Page marker
|
|
318
|
+
// collection, selection-backed visible block/page ranges, and the
|
|
319
|
+
// facet push all live in the hook.
|
|
320
|
+
const { visibleBlockRange, visiblePageIndexRange } = usePageMarkers({
|
|
321
|
+
pageStackScrollRoot,
|
|
322
|
+
snapshot,
|
|
323
|
+
layoutFacet: props.layoutFacet,
|
|
324
|
+
});
|
|
902
325
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const scheduleBump = () => {
|
|
918
|
-
if (pendingBump || cancelled) return;
|
|
919
|
-
pendingBump = true;
|
|
920
|
-
queueMicrotask(() => {
|
|
921
|
-
pendingBump = false;
|
|
922
|
-
if (cancelled) return;
|
|
923
|
-
setRenderFrameRevision((n) => n + 1);
|
|
924
|
-
});
|
|
925
|
-
};
|
|
926
|
-
const unsub = props.layoutFacet.subscribe((event) => {
|
|
927
|
-
switch (event.kind) {
|
|
928
|
-
case "zoom_changed":
|
|
929
|
-
case "render_frame_ready":
|
|
930
|
-
case "layout_recomputed":
|
|
931
|
-
case "incremental_relayout":
|
|
932
|
-
case "page_count_changed":
|
|
933
|
-
case "page_field_dirtied":
|
|
934
|
-
case "measurement_backend_ready":
|
|
935
|
-
case "layout_committed":
|
|
936
|
-
scheduleBump();
|
|
937
|
-
break;
|
|
938
|
-
default:
|
|
939
|
-
break;
|
|
940
|
-
}
|
|
326
|
+
const { dismissSelectionToolbar, runWithSelectionToolbarDismiss } =
|
|
327
|
+
useWorkspaceSideEffects({
|
|
328
|
+
layoutFacet: props.layoutFacet,
|
|
329
|
+
activeParagraphLayout,
|
|
330
|
+
pageChromeModel,
|
|
331
|
+
pageShellMetrics,
|
|
332
|
+
isPageWorkspace,
|
|
333
|
+
activeStoryKind: snapshot.activeStory.kind,
|
|
334
|
+
setLayoutToolsOpen,
|
|
335
|
+
showDrawerReviewRail: responsiveChrome.showDrawerReviewRail,
|
|
336
|
+
setReviewRailOpen,
|
|
337
|
+
onOpenHeaderStory: props.onOpenHeaderStory,
|
|
338
|
+
onOpenFooterStory: props.onOpenFooterStory,
|
|
339
|
+
onDismissSelectionToolbar: props.onDismissSelectionToolbar,
|
|
941
340
|
});
|
|
942
|
-
return () => {
|
|
943
|
-
cancelled = true;
|
|
944
|
-
unsub();
|
|
945
|
-
};
|
|
946
|
-
}, [props.layoutFacet]);
|
|
947
341
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
342
|
+
// Audit §2.4 — the shell header is ALWAYS present in default composition.
|
|
343
|
+
// When the host does not supply a pre-assembled shell node, fall back to
|
|
344
|
+
// a default TwShellHeader wired to the workspace's editor-role state so
|
|
345
|
+
// the mode tabs actually change the active role instead of being
|
|
346
|
+
// decorative. The "more" tab is included for layout parity with §6.1
|
|
347
|
+
// but disabled until its handler is defined (Phase Q debug UX / Slice 7).
|
|
348
|
+
// Host-supplied shells continue to win (back-compat).
|
|
349
|
+
const defaultShellModes: readonly ShellHeaderModeOption[] =
|
|
350
|
+
DEFAULT_WORKSPACE_SHELL_MODES;
|
|
351
|
+
const defaultShellActiveMode: ShellHeaderMode = editorRoleToShellMode(
|
|
352
|
+
viewState.editorRole,
|
|
353
|
+
);
|
|
354
|
+
const renderedShell =
|
|
355
|
+
props.shellHeader !== undefined ? (
|
|
356
|
+
props.shellHeader
|
|
357
|
+
) : (
|
|
358
|
+
<TwShellHeader
|
|
359
|
+
modes={defaultShellModes}
|
|
360
|
+
activeMode={defaultShellActiveMode}
|
|
361
|
+
onModeChange={(mode) => {
|
|
362
|
+
const nextRole = shellModeToEditorRole(mode);
|
|
363
|
+
if (nextRole !== null && nextRole !== viewState.editorRole) {
|
|
364
|
+
props.onEditorRoleChange?.(nextRole);
|
|
365
|
+
}
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
960
368
|
);
|
|
961
|
-
}, [reviewRailAvailable, viewportWidth]);
|
|
962
|
-
|
|
963
|
-
// L7 Phase 2 Task 2.2.4a — viewport-scroll wiring.
|
|
964
|
-
// Collect DOM elements with `[data-page-frame]` from the PM surface so the
|
|
965
|
-
// IntersectionObserver inside `useVisibleBlockRange` can determine which
|
|
966
|
-
// pages are currently visible. The MutationObserver refreshes the set when
|
|
967
|
-
// the PM surface re-renders (e.g. a document load changes the page count).
|
|
968
|
-
const [pageMarkers, setPageMarkers] = useState<readonly HTMLElement[]>([]);
|
|
969
369
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
// The boundary widget's `data-page-first-block-index` for page 1 tells
|
|
992
|
-
// us where page 0 ends.
|
|
993
|
-
const page1Marker = found.find(
|
|
994
|
-
(el) => el.getAttribute("data-page-frame") === "1",
|
|
995
|
-
);
|
|
996
|
-
const page1First = page1Marker
|
|
997
|
-
? Number(page1Marker.getAttribute("data-page-first-block-index") ?? "")
|
|
998
|
-
: NaN;
|
|
999
|
-
const page0Last = Number.isFinite(page1First) && page1First > 0
|
|
1000
|
-
? page1First - 1
|
|
1001
|
-
: -1; // page 0 empty or unknown; synthetic marker contributes nothing.
|
|
1002
|
-
const ownerDoc = found[0]!.ownerDocument;
|
|
1003
|
-
const synth = ownerDoc.createElement("span");
|
|
1004
|
-
synth.setAttribute("data-page-frame", "0");
|
|
1005
|
-
synth.setAttribute("data-page-first-block-index", "0");
|
|
1006
|
-
synth.setAttribute("data-page-last-block-index", String(Math.max(0, page0Last)));
|
|
1007
|
-
setPageMarkers([synth, ...found]);
|
|
1008
|
-
} else {
|
|
1009
|
-
setPageMarkers(found);
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1012
|
-
refresh();
|
|
1013
|
-
// Observe PM surface mutations for page-count changes (new doc load, etc).
|
|
1014
|
-
const view = root.ownerDocument?.defaultView;
|
|
1015
|
-
if (!view?.MutationObserver) return undefined;
|
|
1016
|
-
const mo = new view.MutationObserver(refresh);
|
|
1017
|
-
mo.observe(root, { childList: true, subtree: true });
|
|
1018
|
-
return () => mo.disconnect();
|
|
1019
|
-
// Re-run when the scroll root changes OR when a new snapshot lands
|
|
1020
|
-
// (which may have a different page count and new widgets in the PM DOM).
|
|
1021
|
-
// NOTE: snapshot.surface is intentionally excluded — its reference changes on
|
|
1022
|
-
// every requestViewportRefresh(), which would add two extra render passes per
|
|
1023
|
-
// scroll event. The page-0 fallback uses -1 when page1First is unknown,
|
|
1024
|
-
// which is correct (no page-0 blocks → synthetic marker contributes nothing).
|
|
1025
|
-
}, [pageStackScrollRoot, snapshot.revisionToken]);
|
|
1026
|
-
|
|
1027
|
-
// Derive the surface block index for the current selection head so the
|
|
1028
|
-
// hook can extend the visible range to always include the selection.
|
|
1029
|
-
const selectionBlockIndex = useMemo(() => {
|
|
1030
|
-
const sel = snapshot.selection;
|
|
1031
|
-
const blocks = snapshot.surface?.blocks;
|
|
1032
|
-
if (!sel || !blocks) return null;
|
|
1033
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1034
|
-
const block = blocks[i]!; // from/to are required on all SurfaceBlockSnapshot variants
|
|
1035
|
-
const blockFrom = block.from;
|
|
1036
|
-
const blockTo = block.to;
|
|
1037
|
-
if (sel.head >= blockFrom && sel.head <= blockTo) return i;
|
|
1038
|
-
}
|
|
1039
|
-
return null;
|
|
1040
|
-
}, [snapshot.selection, snapshot.surface]);
|
|
1041
|
-
|
|
1042
|
-
const visibleBlockRange = useVisibleBlockRange({
|
|
1043
|
-
pageMarkers,
|
|
1044
|
-
overscanPages: 2,
|
|
1045
|
-
selectionBlockIndex,
|
|
1046
|
-
totalBlockCount: snapshot.surface?.blocks.length ?? 0,
|
|
370
|
+
// Audit §2.5 — context band mounts as a composition-level sibling of
|
|
371
|
+
// the toolbar so the workspace row becomes
|
|
372
|
+
// [toolbar left cluster] · [context band] · [toolbar right cluster]
|
|
373
|
+
// and the band can render mode-owned content (Phase B.3 additive mount;
|
|
374
|
+
// Phase D migrates role-action contents into the band and retires the
|
|
375
|
+
// legacy inline role region inside TwToolbar).
|
|
376
|
+
//
|
|
377
|
+
// `resolveChromeComposition` is pure but each call allocates new Sets +
|
|
378
|
+
// arrays. `TwReviewWorkspace` re-renders on every PM transaction (the
|
|
379
|
+
// inverted-truth architecture calls `view.updateState()` wholesale) so
|
|
380
|
+
// we `useMemo` to keep downstream React.memo consumers stable across
|
|
381
|
+
// commits — perf invariant #4. `readOnly` is intentionally omitted:
|
|
382
|
+
// TwReviewWorkspaceProps doesn't expose it and the composition seam is
|
|
383
|
+
// independent of read-only posture today; Phase E will thread it
|
|
384
|
+
// through the interactionGuardSnapshot wiring once the rail owns the
|
|
385
|
+
// full posture composition.
|
|
386
|
+
const diagnosticsSignal = useDiagnosticsSignal({
|
|
387
|
+
snapshot,
|
|
388
|
+
caps,
|
|
389
|
+
preserveOnlyCount,
|
|
390
|
+
blockedReasonsCount: blockedReasons.length,
|
|
1047
391
|
});
|
|
392
|
+
const healthIssueCount = diagnosticsSignal.count;
|
|
1048
393
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
pageMarkers,
|
|
1057
|
-
overscanPages: 2,
|
|
394
|
+
const composition = useWorkspaceComposition({
|
|
395
|
+
chromePreset,
|
|
396
|
+
chromeOptions: props.chromeOptions,
|
|
397
|
+
reviewMode: props.reviewMode,
|
|
398
|
+
role: viewState.editorRole,
|
|
399
|
+
markupDisplay: props.markupDisplay,
|
|
400
|
+
diagnosticsSignal,
|
|
1058
401
|
});
|
|
402
|
+
const showHealthRailTab = composition.rail.visibleTabs.has("health");
|
|
1059
403
|
|
|
1060
|
-
|
|
1061
|
-
// runtime's viewport-culling machinery). Depend on `[start, end]` values
|
|
1062
|
-
// (not the range object) so identity-preserving updates are a no-op.
|
|
1063
|
-
useEffect(() => {
|
|
1064
|
-
if (!props.layoutFacet) return;
|
|
1065
|
-
props.layoutFacet.setVisibleBlockRange(visibleBlockRange);
|
|
1066
|
-
props.layoutFacet.requestViewportRefresh();
|
|
1067
|
-
}, [props.layoutFacet, visibleBlockRange.start, visibleBlockRange.end]);
|
|
1068
|
-
|
|
1069
|
-
const dismissSelectionToolbar = useCallback(() => {
|
|
1070
|
-
props.onDismissSelectionToolbar?.();
|
|
1071
|
-
}, [props.onDismissSelectionToolbar]);
|
|
1072
|
-
|
|
1073
|
-
const runWithSelectionToolbarDismiss = useCallback(
|
|
1074
|
-
(action?: () => void) => () => {
|
|
1075
|
-
dismissSelectionToolbar();
|
|
1076
|
-
action?.();
|
|
1077
|
-
},
|
|
1078
|
-
[dismissSelectionToolbar],
|
|
1079
|
-
);
|
|
1080
|
-
|
|
1081
|
-
useEffect(() => {
|
|
1082
|
-
if (!responsiveChrome.showDrawerReviewRail || typeof window === "undefined") {
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1087
|
-
if (event.key !== "Escape") {
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
setReviewRailOpen(false);
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
1095
|
-
return () => {
|
|
1096
|
-
window.removeEventListener("keydown", handleKeyDown);
|
|
1097
|
-
};
|
|
1098
|
-
}, [responsiveChrome.showDrawerReviewRail]);
|
|
1099
|
-
|
|
1100
|
-
useEffect(() => {
|
|
1101
|
-
if (!props.layoutFacet) return;
|
|
1102
|
-
const facet = props.layoutFacet;
|
|
1103
|
-
const fontSet = document.fonts;
|
|
1104
|
-
if (!fontSet?.ready) {
|
|
1105
|
-
facet.swapMeasurementProvider(createCanvasBackend());
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
void fontSet.ready.then(() => {
|
|
1109
|
-
facet.swapMeasurementProvider(createCanvasBackend());
|
|
1110
|
-
});
|
|
1111
|
-
}, [props.layoutFacet]);
|
|
404
|
+
const arbiter = useWorkspaceArbiter();
|
|
1112
405
|
|
|
1113
406
|
return (
|
|
407
|
+
<LocalSurfaceArbiterContext.Provider value={arbiter}>
|
|
1114
408
|
<Tooltip.Provider delayDuration={400}>
|
|
1115
409
|
<div className="flex h-full flex-col bg-canvas text-primary">
|
|
1116
|
-
{
|
|
410
|
+
{renderedShell}
|
|
411
|
+
{/*
|
|
412
|
+
* The context band is a workspace-layer surface (DESIGN-EDITOR.md
|
|
413
|
+
* §4.2) and therefore gated on the toolbar-layer visibility flag
|
|
414
|
+
* — the `selection` chrome preset suppresses both toolbar and
|
|
415
|
+
* band so minimal embeds stay chrome-free. If a future preset
|
|
416
|
+
* wants the band without the formatting toolbar, introduce a
|
|
417
|
+
* dedicated `chromeVisibility.contextBand` flag rather than
|
|
418
|
+
* decoupling this branch.
|
|
419
|
+
*/}
|
|
420
|
+
{/*
|
|
421
|
+
* Chrome Closure Pass · Task 1 (designsystem.md §6.3) — the
|
|
422
|
+
* role-action region used to render inline in the toolbar's
|
|
423
|
+
* center subregion. It now lives inside `TwContextBand` so the
|
|
424
|
+
* workspace row reads
|
|
425
|
+
* [shell header] · [TwContextBand carrying role actions] · [TwToolbar (formatting only)]
|
|
426
|
+
* matching §8.8.1's "Workspace context row / mode-owned band"
|
|
427
|
+
* row. Callbacks below mirror what was previously threaded
|
|
428
|
+
* through `ChromePresetToolbar` to `TwRoleActionRegion`; the
|
|
429
|
+
* toolbar still receives `role` for `isChromeItemOwnedByRoleRegion`
|
|
430
|
+
* deferral but no longer renders the region itself.
|
|
431
|
+
*/}
|
|
432
|
+
{chromeVisibility.toolbar ? (
|
|
433
|
+
<TwContextBand mode={composition.mode}>
|
|
434
|
+
{viewState.editorRole ? (
|
|
435
|
+
<TwRoleActionRegion
|
|
436
|
+
role={viewState.editorRole}
|
|
437
|
+
policy={scopedChromePolicy}
|
|
438
|
+
compactMode={responsiveChrome.isNarrow}
|
|
439
|
+
reviewQueue={props.reviewQueue}
|
|
440
|
+
markupDisplay={markupDisplay}
|
|
441
|
+
canAddComment={
|
|
442
|
+
toolbarInteractionPolicy?.canAddComment ??
|
|
443
|
+
(caps ? caps.canAddComment : false)
|
|
444
|
+
}
|
|
445
|
+
showTrackedChanges={props.showTrackedChanges}
|
|
446
|
+
capabilities={caps}
|
|
447
|
+
onAddComment={
|
|
448
|
+
props.onAddComment
|
|
449
|
+
? runWithSelectionToolbarDismiss(props.onAddComment)
|
|
450
|
+
: undefined
|
|
451
|
+
}
|
|
452
|
+
onShowTrackedChangesChange={(show) => {
|
|
453
|
+
dismissSelectionToolbar();
|
|
454
|
+
props.onShowTrackedChangesChange(show);
|
|
455
|
+
}}
|
|
456
|
+
onReviewSidebarTrackedChanges={
|
|
457
|
+
props.onReviewSidebarTrackedChanges
|
|
458
|
+
? runWithSelectionToolbarDismiss(props.onReviewSidebarTrackedChanges)
|
|
459
|
+
: undefined
|
|
460
|
+
}
|
|
461
|
+
onReviewSidebarComments={
|
|
462
|
+
props.onReviewSidebarComments
|
|
463
|
+
? runWithSelectionToolbarDismiss(props.onReviewSidebarComments)
|
|
464
|
+
: undefined
|
|
465
|
+
}
|
|
466
|
+
onMarkScopePosture={
|
|
467
|
+
props.onMarkSectionForReview
|
|
468
|
+
? runWithSelectionToolbarDismiss(props.onMarkSectionForReview)
|
|
469
|
+
: undefined
|
|
470
|
+
}
|
|
471
|
+
onReviewPrev={
|
|
472
|
+
props.onGoToPreviousReviewItem
|
|
473
|
+
? runWithSelectionToolbarDismiss(props.onGoToPreviousReviewItem)
|
|
474
|
+
: undefined
|
|
475
|
+
}
|
|
476
|
+
onReviewNext={
|
|
477
|
+
props.onGoToNextReviewItem
|
|
478
|
+
? runWithSelectionToolbarDismiss(props.onGoToNextReviewItem)
|
|
479
|
+
: undefined
|
|
480
|
+
}
|
|
481
|
+
onReviewAccept={(() => {
|
|
482
|
+
const active = props.reviewQueue?.items[props.reviewQueue.activeIndex];
|
|
483
|
+
if (active?.kind !== "change" || !props.onAcceptRevision) {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
const revisionId = active.itemId;
|
|
487
|
+
return () => {
|
|
488
|
+
dismissSelectionToolbar();
|
|
489
|
+
props.onAcceptRevision?.(revisionId);
|
|
490
|
+
};
|
|
491
|
+
})()}
|
|
492
|
+
onReviewReject={(() => {
|
|
493
|
+
const active = props.reviewQueue?.items[props.reviewQueue.activeIndex];
|
|
494
|
+
if (active?.kind !== "change" || !props.onRejectRevision) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
const revisionId = active.itemId;
|
|
498
|
+
return () => {
|
|
499
|
+
dismissSelectionToolbar();
|
|
500
|
+
props.onRejectRevision?.(revisionId);
|
|
501
|
+
};
|
|
502
|
+
})()}
|
|
503
|
+
onReviewAcceptAll={
|
|
504
|
+
props.onAcceptAllChanges
|
|
505
|
+
? runWithSelectionToolbarDismiss(props.onAcceptAllChanges)
|
|
506
|
+
: undefined
|
|
507
|
+
}
|
|
508
|
+
onReviewRejectAll={
|
|
509
|
+
props.onRejectAllChanges
|
|
510
|
+
? runWithSelectionToolbarDismiss(props.onRejectAllChanges)
|
|
511
|
+
: undefined
|
|
512
|
+
}
|
|
513
|
+
/>
|
|
514
|
+
) : null}
|
|
515
|
+
</TwContextBand>
|
|
516
|
+
) : null}
|
|
517
|
+
{/*
|
|
518
|
+
* Phase C.γ.3 — mount the workspace chrome host when the
|
|
519
|
+
* integrator has wired an editor-action bag. Omitting the
|
|
520
|
+
* bag preserves pre-C behavior. Renders portals + a global
|
|
521
|
+
* palette listener; adds no DOM on the edit path.
|
|
522
|
+
*/}
|
|
523
|
+
{props.editorActionHost ? (
|
|
524
|
+
<TwWorkspaceChromeHost
|
|
525
|
+
mode={composition.mode}
|
|
526
|
+
editorActionHost={props.editorActionHost}
|
|
527
|
+
{...(props.chromeControllerRef
|
|
528
|
+
? { controllerRef: props.chromeControllerRef }
|
|
529
|
+
: {})}
|
|
530
|
+
{...(props.commandPaletteDisabled !== undefined
|
|
531
|
+
? { paletteDisabled: props.commandPaletteDisabled }
|
|
532
|
+
: {})}
|
|
533
|
+
// Chrome Closure Pass · Task 3 — thread the live table
|
|
534
|
+
// context into the chrome host so the right-click menu
|
|
535
|
+
// augments target-kinds with the matching tier.
|
|
536
|
+
tableContext={props.tableContext ?? null}
|
|
537
|
+
onOpenRailTab={(tab) => {
|
|
538
|
+
setReviewRailOpen(true);
|
|
539
|
+
props.onActiveRailTabChange?.(tab);
|
|
540
|
+
}}
|
|
541
|
+
/>
|
|
542
|
+
) : null}
|
|
1117
543
|
{chromeVisibility.toolbar ? (
|
|
1118
544
|
<div className="px-3 pt-3">
|
|
1119
545
|
<ChromePresetToolbar
|
|
@@ -1140,6 +566,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1140
566
|
interactionPolicy={toolbarInteractionPolicy}
|
|
1141
567
|
scopedChromePolicy={scopedChromePolicy}
|
|
1142
568
|
compactMode={responsiveChrome.isNarrow}
|
|
569
|
+
onOpenHealthRail={() => {
|
|
570
|
+
setReviewRailOpen(true);
|
|
571
|
+
props.onActiveRailTabChange?.("health");
|
|
572
|
+
}}
|
|
1143
573
|
workspaceMode={props.workspaceMode}
|
|
1144
574
|
zoomLevel={props.zoomLevel}
|
|
1145
575
|
formattingState={props.formattingState}
|
|
@@ -1305,99 +735,23 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1305
735
|
snapshot={snapshot}
|
|
1306
736
|
preserveOnlyCount={preserveOnlyCount}
|
|
1307
737
|
workflowBlockedReasons={blockedReasons}
|
|
738
|
+
onShowDetail={() => {
|
|
739
|
+
setReviewRailOpen(true);
|
|
740
|
+
props.onActiveRailTabChange?.("health");
|
|
741
|
+
}}
|
|
1308
742
|
/> : null}
|
|
1309
743
|
|
|
1310
744
|
<div className="relative flex flex-1 min-h-0">
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
|
1322
|
-
<span className="text-xs font-medium text-secondary uppercase tracking-wider">Navigator</span>
|
|
1323
|
-
<Tooltip.Root>
|
|
1324
|
-
<Tooltip.Trigger asChild>
|
|
1325
|
-
<button
|
|
1326
|
-
type="button"
|
|
1327
|
-
aria-label="Collapse navigator"
|
|
1328
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1329
|
-
onClick={() => {
|
|
1330
|
-
dismissSelectionToolbar();
|
|
1331
|
-
setNavOpen(false);
|
|
1332
|
-
}}
|
|
1333
|
-
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary hover:bg-surface-hover transition-colors"
|
|
1334
|
-
>
|
|
1335
|
-
<ChevronLeft className="h-3.5 w-3.5" />
|
|
1336
|
-
</button>
|
|
1337
|
-
</Tooltip.Trigger>
|
|
1338
|
-
<Tooltip.Portal>
|
|
1339
|
-
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
1340
|
-
Collapse navigator
|
|
1341
|
-
</Tooltip.Content>
|
|
1342
|
-
</Tooltip.Portal>
|
|
1343
|
-
</Tooltip.Root>
|
|
1344
|
-
</div>
|
|
1345
|
-
<nav className="flex-1 overflow-y-auto px-2 py-2" aria-label="Document headings">
|
|
1346
|
-
{headings.length > 0 ? (
|
|
1347
|
-
<ul className="space-y-0.5">
|
|
1348
|
-
{headings.map((entry) => (
|
|
1349
|
-
<li key={entry.headingId}>
|
|
1350
|
-
<button
|
|
1351
|
-
type="button"
|
|
1352
|
-
className="block w-full truncate rounded-md px-2 py-1 text-left text-xs text-primary hover:bg-surface-hover"
|
|
1353
|
-
style={{ paddingLeft: `${8 + (entry.level - 1) * 12}px` }}
|
|
1354
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1355
|
-
onClick={() => {
|
|
1356
|
-
dismissSelectionToolbar();
|
|
1357
|
-
props.onNavigateHeading?.(entry.headingId);
|
|
1358
|
-
setNavOpen(false);
|
|
1359
|
-
}}
|
|
1360
|
-
>
|
|
1361
|
-
{entry.text}
|
|
1362
|
-
</button>
|
|
1363
|
-
</li>
|
|
1364
|
-
))}
|
|
1365
|
-
</ul>
|
|
1366
|
-
) : (
|
|
1367
|
-
<p className="px-2 py-4 text-xs text-tertiary">No headings found.</p>
|
|
1368
|
-
)}
|
|
1369
|
-
</nav>
|
|
1370
|
-
</div>
|
|
1371
|
-
) : null}
|
|
1372
|
-
</aside>
|
|
1373
|
-
) : null}
|
|
1374
|
-
|
|
1375
|
-
{/* Navigator expand toggle — page mode only when collapsed */}
|
|
1376
|
-
{isPageWorkspace && chromeVisibility.pageChrome && !navOpen ? (
|
|
1377
|
-
<div className="shrink-0 flex items-start pt-2 pl-1">
|
|
1378
|
-
<Tooltip.Root>
|
|
1379
|
-
<Tooltip.Trigger asChild>
|
|
1380
|
-
<button
|
|
1381
|
-
type="button"
|
|
1382
|
-
aria-label="Open document navigator"
|
|
1383
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1384
|
-
onClick={() => {
|
|
1385
|
-
dismissSelectionToolbar();
|
|
1386
|
-
setNavOpen(true);
|
|
1387
|
-
}}
|
|
1388
|
-
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary hover:bg-surface-hover transition-colors"
|
|
1389
|
-
>
|
|
1390
|
-
<List className="h-3.5 w-3.5" />
|
|
1391
|
-
</button>
|
|
1392
|
-
</Tooltip.Trigger>
|
|
1393
|
-
<Tooltip.Portal>
|
|
1394
|
-
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
1395
|
-
Open document navigator
|
|
1396
|
-
</Tooltip.Content>
|
|
1397
|
-
</Tooltip.Portal>
|
|
1398
|
-
</Tooltip.Root>
|
|
1399
|
-
</div>
|
|
1400
|
-
) : null}
|
|
745
|
+
<TwReviewWorkspaceNavigator
|
|
746
|
+
enabled={isPageWorkspace && chromeVisibility.pageChrome}
|
|
747
|
+
navOpen={navOpen}
|
|
748
|
+
setNavOpen={setNavOpen}
|
|
749
|
+
headings={headings}
|
|
750
|
+
{...(props.onNavigateHeading
|
|
751
|
+
? { onNavigateHeading: props.onNavigateHeading }
|
|
752
|
+
: {})}
|
|
753
|
+
dismissSelectionToolbar={dismissSelectionToolbar}
|
|
754
|
+
/>
|
|
1401
755
|
|
|
1402
756
|
{/* Document column */}
|
|
1403
757
|
<div className="flex flex-1 flex-col min-w-0">
|
|
@@ -1414,157 +768,38 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1414
768
|
: "wre-canvas-surface relative my-8 overflow-hidden"
|
|
1415
769
|
}`}
|
|
1416
770
|
data-zoom-bucket={pageZoomBucket}
|
|
1417
|
-
data-zoom-scale={isPageWorkspace ? zoomScale : undefined}
|
|
1418
771
|
data-workspace-canvas={isPageWorkspace ? "true" : undefined}
|
|
1419
772
|
data-workspace-mode={isPageWorkspace ? "page" : "canvas"}
|
|
1420
773
|
>
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
type="button"
|
|
1451
|
-
aria-label="Link header to previous"
|
|
1452
|
-
disabled={!props.onSetHeaderFooterLink || !allowLocalChromeMutations}
|
|
1453
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1454
|
-
onClick={() => {
|
|
1455
|
-
dismissSelectionToolbar();
|
|
1456
|
-
props.onSetHeaderFooterLink?.(snapshot.pageLayout!.sectionIndex, {
|
|
1457
|
-
kind: "header",
|
|
1458
|
-
variant: headerVariant,
|
|
1459
|
-
linkToPrevious: true,
|
|
1460
|
-
});
|
|
1461
|
-
}}
|
|
1462
|
-
className="inline-flex items-center gap-1 rounded-md border border-border bg-canvas px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40"
|
|
1463
|
-
>
|
|
1464
|
-
Link header
|
|
1465
|
-
</button>
|
|
1466
|
-
<button
|
|
1467
|
-
type="button"
|
|
1468
|
-
aria-label="Link footer to previous"
|
|
1469
|
-
disabled={!props.onSetHeaderFooterLink || !allowLocalChromeMutations}
|
|
1470
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1471
|
-
onClick={() => {
|
|
1472
|
-
dismissSelectionToolbar();
|
|
1473
|
-
props.onSetHeaderFooterLink?.(snapshot.pageLayout!.sectionIndex, {
|
|
1474
|
-
kind: "footer",
|
|
1475
|
-
variant: footerVariant,
|
|
1476
|
-
linkToPrevious: true,
|
|
1477
|
-
});
|
|
1478
|
-
}}
|
|
1479
|
-
className="inline-flex items-center gap-1 rounded-md border border-border bg-canvas px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40"
|
|
1480
|
-
>
|
|
1481
|
-
Link footer
|
|
1482
|
-
</button>
|
|
1483
|
-
</>
|
|
1484
|
-
) : null}
|
|
1485
|
-
<button
|
|
1486
|
-
type="button"
|
|
1487
|
-
aria-label="Toggle layout tools"
|
|
1488
|
-
aria-expanded={layoutToolsOpen}
|
|
1489
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1490
|
-
onClick={() => {
|
|
1491
|
-
dismissSelectionToolbar();
|
|
1492
|
-
setLayoutToolsOpen((open) => !open);
|
|
1493
|
-
}}
|
|
1494
|
-
className="inline-flex items-center gap-1 rounded-md border border-border bg-canvas px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
1495
|
-
>
|
|
1496
|
-
<ChevronRight className={`h-3.5 w-3.5 transition-transform ${layoutToolsOpen ? "rotate-90" : ""}`} />
|
|
1497
|
-
Layout tools
|
|
1498
|
-
</button>
|
|
1499
|
-
</div>
|
|
1500
|
-
</div>
|
|
1501
|
-
</div>
|
|
1502
|
-
) : null}
|
|
1503
|
-
{isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout && layoutToolsOpen ? (
|
|
1504
|
-
<div className="px-5 pt-3">
|
|
1505
|
-
<TwPageRuler
|
|
1506
|
-
pageLayout={snapshot.pageLayout}
|
|
1507
|
-
viewState={viewState}
|
|
1508
|
-
paragraphLayout={activeParagraphLayout}
|
|
1509
|
-
readOnly={pageChromeReadOnly}
|
|
1510
|
-
onReturnToBody={props.onCloseStory
|
|
1511
|
-
? runWithSelectionToolbarDismiss(props.onCloseStory)
|
|
1512
|
-
: () => undefined}
|
|
1513
|
-
onOpenHeader={props.onOpenHeaderStory
|
|
1514
|
-
? runWithSelectionToolbarDismiss(props.onOpenHeaderStory)
|
|
1515
|
-
: undefined}
|
|
1516
|
-
onOpenFooter={props.onOpenFooterStory
|
|
1517
|
-
? runWithSelectionToolbarDismiss(props.onOpenFooterStory)
|
|
1518
|
-
: undefined}
|
|
1519
|
-
onSetIndentation={props.onSetParagraphIndentation
|
|
1520
|
-
? (indentation) => {
|
|
1521
|
-
dismissSelectionToolbar();
|
|
1522
|
-
props.onSetParagraphIndentation?.(indentation);
|
|
1523
|
-
}
|
|
1524
|
-
: undefined}
|
|
1525
|
-
onSetTabStops={props.onSetParagraphTabStops
|
|
1526
|
-
? (tabStops) => {
|
|
1527
|
-
dismissSelectionToolbar();
|
|
1528
|
-
props.onSetParagraphTabStops?.(tabStops);
|
|
1529
|
-
}
|
|
1530
|
-
: undefined}
|
|
1531
|
-
onRestartNumbering={props.onRestartNumbering
|
|
1532
|
-
? runWithSelectionToolbarDismiss(props.onRestartNumbering)
|
|
1533
|
-
: undefined}
|
|
1534
|
-
onContinueNumbering={props.onContinueNumbering
|
|
1535
|
-
? runWithSelectionToolbarDismiss(props.onContinueNumbering)
|
|
1536
|
-
: undefined}
|
|
1537
|
-
/>
|
|
1538
|
-
<TwLayoutPanel
|
|
1539
|
-
pageLayout={snapshot.pageLayout}
|
|
1540
|
-
readOnly={pageChromeReadOnly}
|
|
1541
|
-
onInsertSectionBreak={props.onInsertSectionBreak
|
|
1542
|
-
? (type) => {
|
|
1543
|
-
dismissSelectionToolbar();
|
|
1544
|
-
props.onInsertSectionBreak?.(type);
|
|
1545
|
-
}
|
|
1546
|
-
: undefined}
|
|
1547
|
-
onDeleteSectionBreak={props.onDeleteSectionBreak
|
|
1548
|
-
? (sectionIndex) => {
|
|
1549
|
-
dismissSelectionToolbar();
|
|
1550
|
-
props.onDeleteSectionBreak?.(sectionIndex);
|
|
1551
|
-
}
|
|
1552
|
-
: undefined}
|
|
1553
|
-
onUpdateSectionLayout={props.onUpdateSectionLayout
|
|
1554
|
-
? (sectionIndex, patch) => {
|
|
1555
|
-
dismissSelectionToolbar();
|
|
1556
|
-
props.onUpdateSectionLayout?.(sectionIndex, patch);
|
|
1557
|
-
}
|
|
1558
|
-
: undefined}
|
|
1559
|
-
onSetSectionPageNumbering={props.onSetSectionPageNumbering
|
|
1560
|
-
? (sectionIndex, patch) => {
|
|
1561
|
-
dismissSelectionToolbar();
|
|
1562
|
-
props.onSetSectionPageNumbering?.(sectionIndex, patch);
|
|
1563
|
-
}
|
|
1564
|
-
: undefined}
|
|
1565
|
-
/>
|
|
1566
|
-
</div>
|
|
1567
|
-
) : null}
|
|
774
|
+
<TwReviewWorkspacePageToolbar
|
|
775
|
+
enabled={isPageWorkspace && chromeVisibility.pageChrome && Boolean(snapshot.pageLayout)}
|
|
776
|
+
pageLayout={snapshot.pageLayout}
|
|
777
|
+
activeStory={snapshot.activeStory}
|
|
778
|
+
activePage={activePage}
|
|
779
|
+
pageCount={props.documentNavigation?.pageCount ?? 1}
|
|
780
|
+
headerVariant={headerVariant}
|
|
781
|
+
footerVariant={footerVariant}
|
|
782
|
+
allowLocalChromeMutations={allowLocalChromeMutations}
|
|
783
|
+
pageChromeReadOnly={pageChromeReadOnly}
|
|
784
|
+
layoutToolsOpen={layoutToolsOpen}
|
|
785
|
+
setLayoutToolsOpen={setLayoutToolsOpen}
|
|
786
|
+
viewState={viewState}
|
|
787
|
+
activeParagraphLayout={activeParagraphLayout}
|
|
788
|
+
dismissSelectionToolbar={dismissSelectionToolbar}
|
|
789
|
+
runWithSelectionToolbarDismiss={runWithSelectionToolbarDismiss}
|
|
790
|
+
{...(props.onCloseStory ? { onCloseStory: props.onCloseStory } : {})}
|
|
791
|
+
{...(props.onOpenHeaderStory ? { onOpenHeaderStory: props.onOpenHeaderStory } : {})}
|
|
792
|
+
{...(props.onOpenFooterStory ? { onOpenFooterStory: props.onOpenFooterStory } : {})}
|
|
793
|
+
{...(props.onSetHeaderFooterLink ? { onSetHeaderFooterLink: props.onSetHeaderFooterLink } : {})}
|
|
794
|
+
{...(props.onSetParagraphIndentation ? { onSetParagraphIndentation: props.onSetParagraphIndentation } : {})}
|
|
795
|
+
{...(props.onSetParagraphTabStops ? { onSetParagraphTabStops: props.onSetParagraphTabStops } : {})}
|
|
796
|
+
{...(props.onRestartNumbering ? { onRestartNumbering: props.onRestartNumbering } : {})}
|
|
797
|
+
{...(props.onContinueNumbering ? { onContinueNumbering: props.onContinueNumbering } : {})}
|
|
798
|
+
{...(props.onInsertSectionBreak ? { onInsertSectionBreak: props.onInsertSectionBreak } : {})}
|
|
799
|
+
{...(props.onDeleteSectionBreak ? { onDeleteSectionBreak: props.onDeleteSectionBreak } : {})}
|
|
800
|
+
{...(props.onUpdateSectionLayout ? { onUpdateSectionLayout: props.onUpdateSectionLayout } : {})}
|
|
801
|
+
{...(props.onSetSectionPageNumbering ? { onSetSectionPageNumbering: props.onSetSectionPageNumbering } : {})}
|
|
802
|
+
/>
|
|
1568
803
|
{chromeVisibility.selectionOverlay &&
|
|
1569
804
|
gatedSelectionTool &&
|
|
1570
805
|
shouldRenderSelectionToolKind(scopedChromePolicy, gatedSelectionTool.kind) ? (
|
|
@@ -1624,6 +859,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1624
859
|
}
|
|
1625
860
|
data-line-numbering={pageChromeModel.lineNumberingEnabled ? "enabled" : "disabled"}
|
|
1626
861
|
data-paper-frame={isPageWorkspace ? "true" : undefined}
|
|
862
|
+
data-zoom-scale={zoomScale}
|
|
1627
863
|
data-debug-page-layout={
|
|
1628
864
|
isPageWorkspace && snapshot.pageLayout
|
|
1629
865
|
? `${snapshot.pageLayout.pageWidth}:${snapshot.pageLayout.pageHeight}:${snapshot.pageLayout.orientation}:${snapshot.pageLayout.sectionIndex}`
|
|
@@ -1646,7 +882,15 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1646
882
|
: {}),
|
|
1647
883
|
...(zoomScale !== 1 ? { zoom: zoomScale } : {}),
|
|
1648
884
|
}
|
|
1649
|
-
:
|
|
885
|
+
: zoomScale !== 1
|
|
886
|
+
? // U2 fix — canvas mode's zoom control was a no-op
|
|
887
|
+
// because this wrapper only applied `zoom:` in page
|
|
888
|
+
// mode. Toolbar advertises "Zoom controls —
|
|
889
|
+
// available in all workspace modes", so canvas must
|
|
890
|
+
// honor it too. Same CSS `zoom` property as page
|
|
891
|
+
// mode for consistent overlay/selection math.
|
|
892
|
+
{ zoom: zoomScale }
|
|
893
|
+
: undefined
|
|
1650
894
|
}
|
|
1651
895
|
>
|
|
1652
896
|
{/* N1 (L8 Phase D): per-page paper card backgrounds at z-0,
|
|
@@ -1656,6 +900,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1656
900
|
{isPageWorkspace && chromeVisibility.pageChrome && props.layoutFacet ? (
|
|
1657
901
|
<TwPageStackOverlayLayer
|
|
1658
902
|
facet={props.layoutFacet}
|
|
903
|
+
geometryFacet={props.geometryFacet}
|
|
1659
904
|
scrollRoot={pageStackScrollRoot}
|
|
1660
905
|
renderFrameRevision={renderFrameRevision}
|
|
1661
906
|
visiblePageIndexRange={visiblePageIndexRange}
|
|
@@ -1665,6 +910,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1665
910
|
{isPageWorkspace && chromeVisibility.pageChrome && props.layoutFacet ? (
|
|
1666
911
|
<TwFloatingImageLayer
|
|
1667
912
|
facet={props.layoutFacet}
|
|
913
|
+
geometryFacet={props.geometryFacet}
|
|
1668
914
|
scrollRoot={pageStackScrollRoot}
|
|
1669
915
|
renderFrameRevision={renderFrameRevision}
|
|
1670
916
|
visiblePageIndexRange={visiblePageIndexRange}
|
|
@@ -1730,6 +976,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1730
976
|
{isPageWorkspace && chromeVisibility.pageChrome && props.layoutFacet ? (
|
|
1731
977
|
<TwFloatingImageLayer
|
|
1732
978
|
facet={props.layoutFacet}
|
|
979
|
+
geometryFacet={props.geometryFacet}
|
|
1733
980
|
scrollRoot={pageStackScrollRoot}
|
|
1734
981
|
renderFrameRevision={renderFrameRevision}
|
|
1735
982
|
visiblePageIndexRange={visiblePageIndexRange}
|
|
@@ -1739,9 +986,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1739
986
|
onActivateFloatingImage={props.onActivateFloatingImage}
|
|
1740
987
|
/>
|
|
1741
988
|
) : null}
|
|
1742
|
-
{props.layoutFacet ? (
|
|
989
|
+
{props.layoutFacet && props.geometryFacet ? (
|
|
1743
990
|
<TwChromeOverlay
|
|
1744
991
|
facet={props.layoutFacet}
|
|
992
|
+
geometryFacet={props.geometryFacet}
|
|
993
|
+
workflowFacet={props.workflowFacet ?? null}
|
|
1745
994
|
grabbedObjectId={snapshot.grabbedObjectId ?? null}
|
|
1746
995
|
grabbedObjectFromOffset={grabbedSegmentOffsets?.from ?? null}
|
|
1747
996
|
grabbedObjectToOffset={grabbedSegmentOffsets?.to ?? null}
|
|
@@ -1757,17 +1006,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1757
1006
|
onScopeCardModeChange={handleScopeCardModeChange}
|
|
1758
1007
|
onScopeCardIssueAction={handleScopeCardIssueAction}
|
|
1759
1008
|
onScopeCardAcceptSuggestionGroup={
|
|
1760
|
-
onScopeAcceptSuggestionGroup
|
|
1009
|
+
props.onScopeAcceptSuggestionGroup
|
|
1761
1010
|
? handleScopeCardAcceptSuggestionGroup
|
|
1762
1011
|
: undefined
|
|
1763
1012
|
}
|
|
1764
1013
|
onScopeCardRejectSuggestionGroup={
|
|
1765
|
-
onScopeRejectSuggestionGroup
|
|
1014
|
+
props.onScopeRejectSuggestionGroup
|
|
1766
1015
|
? handleScopeCardRejectSuggestionGroup
|
|
1767
1016
|
: undefined
|
|
1768
1017
|
}
|
|
1769
1018
|
onScopeCardAskAgent={
|
|
1770
|
-
onScopeAskAgent
|
|
1019
|
+
props.onScopeAskAgent
|
|
1771
1020
|
? handleScopeCardAskAgent
|
|
1772
1021
|
: undefined
|
|
1773
1022
|
}
|
|
@@ -1815,590 +1064,60 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1815
1064
|
</div>
|
|
1816
1065
|
|
|
1817
1066
|
{/* Review rail — docked on desktop, drawer-backed on narrow layouts */}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
markupDisplay={markupDisplay}
|
|
1826
|
-
contextAnalytics={
|
|
1827
|
-
chromeVisibility.contextAnalytics
|
|
1828
|
-
? props.currentScopeContextAnalytics
|
|
1829
|
-
: null
|
|
1067
|
+
<TwReviewWorkspaceRail
|
|
1068
|
+
mode={
|
|
1069
|
+
(responsiveChrome.showDockedReviewRail
|
|
1070
|
+
? "docked"
|
|
1071
|
+
: responsiveChrome.showDrawerReviewRail
|
|
1072
|
+
? "drawer"
|
|
1073
|
+
: "hidden") satisfies TwReviewWorkspaceRailMode
|
|
1830
1074
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
markupDisplay={markupDisplay}
|
|
1874
|
-
contextAnalytics={
|
|
1875
|
-
chromeVisibility.contextAnalytics
|
|
1876
|
-
? props.currentScopeContextAnalytics
|
|
1877
|
-
: null
|
|
1878
|
-
}
|
|
1879
|
-
activeCommentId={props.activeCommentId}
|
|
1880
|
-
activeRevisionId={props.activeRevisionId}
|
|
1881
|
-
onActiveTabChange={props.onActiveRailTabChange}
|
|
1882
|
-
onOpenComment={props.onOpenComment}
|
|
1883
|
-
onResolveComment={props.onResolveComment}
|
|
1884
|
-
onReopenComment={props.onReopenComment}
|
|
1885
|
-
onAddReply={props.onAddReply}
|
|
1886
|
-
onEditBody={props.onEditBody}
|
|
1887
|
-
onOpenRevision={props.onOpenRevision}
|
|
1888
|
-
onAcceptRevision={props.onAcceptRevision}
|
|
1889
|
-
onRejectRevision={props.onRejectRevision}
|
|
1890
|
-
onAcceptAllChanges={props.onAcceptAllChanges}
|
|
1891
|
-
onRejectAllChanges={props.onRejectAllChanges}
|
|
1892
|
-
scopeRailSegments={props.layoutFacet?.getAllScopeRailSegments?.() ?? []}
|
|
1893
|
-
workflowTab={props.reviewRailWorkflowTab}
|
|
1894
|
-
workflowCount={props.reviewRailWorkflowCount}
|
|
1895
|
-
workflowScopesTitle={props.reviewRailWorkflowScopesTitle}
|
|
1896
|
-
intelligenceEyebrow={props.reviewRailIntelligenceEyebrow}
|
|
1897
|
-
intelligenceHeader={props.reviewRailIntelligenceHeader}
|
|
1898
|
-
railFooter={props.reviewRailFooter}
|
|
1899
|
-
/>
|
|
1900
|
-
</div>
|
|
1901
|
-
</div>
|
|
1902
|
-
) : null}
|
|
1903
|
-
</div>
|
|
1904
|
-
{props.modeDock && props.experimental?.showModeDock === true ? (
|
|
1905
|
-
<TwModeDock
|
|
1906
|
-
label={props.modeDock.label}
|
|
1907
|
-
icon={props.modeDock.icon}
|
|
1908
|
-
actions={props.modeDock.actions}
|
|
1075
|
+
onDrawerClose={() => setReviewRailOpen(false)}
|
|
1076
|
+
rail={{
|
|
1077
|
+
activeTab: props.activeRailTab,
|
|
1078
|
+
currentUserId: props.currentUserId,
|
|
1079
|
+
comments: snapshot.comments,
|
|
1080
|
+
trackedChanges: snapshot.trackedChanges,
|
|
1081
|
+
compatibility: snapshot.compatibility,
|
|
1082
|
+
warnings: snapshot.warnings,
|
|
1083
|
+
markupDisplay,
|
|
1084
|
+
contextAnalytics: chromeVisibility.contextAnalytics
|
|
1085
|
+
? props.currentScopeContextAnalytics
|
|
1086
|
+
: null,
|
|
1087
|
+
activeCommentId: props.activeCommentId,
|
|
1088
|
+
activeRevisionId: props.activeRevisionId,
|
|
1089
|
+
onActiveTabChange: props.onActiveRailTabChange,
|
|
1090
|
+
onOpenComment: props.onOpenComment,
|
|
1091
|
+
onResolveComment: props.onResolveComment,
|
|
1092
|
+
onReopenComment: props.onReopenComment,
|
|
1093
|
+
onAddReply: props.onAddReply,
|
|
1094
|
+
onEditBody: props.onEditBody,
|
|
1095
|
+
onOpenRevision: props.onOpenRevision,
|
|
1096
|
+
onAcceptRevision: props.onAcceptRevision,
|
|
1097
|
+
onRejectRevision: props.onRejectRevision,
|
|
1098
|
+
onAcceptAllChanges: props.onAcceptAllChanges,
|
|
1099
|
+
onRejectAllChanges: props.onRejectAllChanges,
|
|
1100
|
+
// Slice 4C rail-seam inversion: segments now come from the
|
|
1101
|
+
// Layer-06 workflow facet. Layout facet no longer exposes
|
|
1102
|
+
// `getAllScopeRailSegments` (methods removed in v40 / Slice 4C).
|
|
1103
|
+
scopeRailSegments: props.workflowFacet?.getAllRailSegments() ?? [],
|
|
1104
|
+
workflowTab: props.reviewRailWorkflowTab,
|
|
1105
|
+
workflowCount: props.reviewRailWorkflowCount,
|
|
1106
|
+
workflowScopesTitle: props.reviewRailWorkflowScopesTitle,
|
|
1107
|
+
intelligenceEyebrow: props.reviewRailIntelligenceEyebrow,
|
|
1108
|
+
intelligenceHeader: props.reviewRailIntelligenceHeader,
|
|
1109
|
+
railFooter: props.reviewRailFooter,
|
|
1110
|
+
showHealthTab: showHealthRailTab,
|
|
1111
|
+
healthIssueCount,
|
|
1112
|
+
...(diagnosticsSignal.severity !== "none"
|
|
1113
|
+
? { healthSeverity: diagnosticsSignal.severity }
|
|
1114
|
+
: {}),
|
|
1115
|
+
workflowBlockedReasons: blockedReasons,
|
|
1116
|
+
}}
|
|
1909
1117
|
/>
|
|
1910
|
-
|
|
1118
|
+
</div>
|
|
1911
1119
|
</div>
|
|
1912
1120
|
</Tooltip.Provider>
|
|
1121
|
+
</LocalSurfaceArbiterContext.Provider>
|
|
1913
1122
|
);
|
|
1914
1123
|
}
|
|
1915
|
-
|
|
1916
|
-
function readViewportWidth(): number | undefined {
|
|
1917
|
-
return typeof window === "undefined" ? undefined : window.innerWidth;
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
function readViewportHeight(): number | undefined {
|
|
1921
|
-
return typeof window === "undefined" ? undefined : window.innerHeight;
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
function shouldHidePageBorderForSelection(
|
|
1925
|
-
selection: EditorViewStateSnapshot["selection"],
|
|
1926
|
-
): boolean {
|
|
1927
|
-
if (selection.isCollapsed) {
|
|
1928
|
-
return false;
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
return selection.activeRange.kind === "range";
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
function resolveActiveParagraphLayout(
|
|
1935
|
-
surface: RuntimeRenderSnapshot["surface"],
|
|
1936
|
-
position: number,
|
|
1937
|
-
): {
|
|
1938
|
-
leftIndent: number;
|
|
1939
|
-
rightIndent: number;
|
|
1940
|
-
firstLineOffset: number;
|
|
1941
|
-
tabStops: Array<{ pos: number; val?: string; leader?: string }>;
|
|
1942
|
-
indentationReadOnly?: boolean;
|
|
1943
|
-
tabStopsReadOnly?: boolean;
|
|
1944
|
-
} | null {
|
|
1945
|
-
const paragraph = surface ? findActiveParagraph(surface.blocks, position) : null;
|
|
1946
|
-
if (!paragraph) {
|
|
1947
|
-
return null;
|
|
1948
|
-
}
|
|
1949
|
-
const resolvedIndentation = paragraph.resolvedNumbering?.geometry.indentation;
|
|
1950
|
-
const resolvedTabStops = paragraph.resolvedNumbering?.geometry.tabStops;
|
|
1951
|
-
const indentation = resolvedIndentation ?? paragraph.indentation;
|
|
1952
|
-
const tabStops = resolvedTabStops ?? paragraph.tabStops;
|
|
1953
|
-
|
|
1954
|
-
return {
|
|
1955
|
-
leftIndent: indentation?.left ?? 0,
|
|
1956
|
-
rightIndent: indentation?.right ?? 0,
|
|
1957
|
-
firstLineOffset:
|
|
1958
|
-
indentation?.firstLine ??
|
|
1959
|
-
(indentation?.hanging ? -indentation.hanging : 0),
|
|
1960
|
-
tabStops: tabStops ? [...tabStops] : [],
|
|
1961
|
-
indentationReadOnly:
|
|
1962
|
-
Boolean(resolvedIndentation) &&
|
|
1963
|
-
!areIndentationsEqual(resolvedIndentation, paragraph.indentation),
|
|
1964
|
-
tabStopsReadOnly:
|
|
1965
|
-
Boolean(resolvedTabStops) &&
|
|
1966
|
-
!areTabStopsEqual(resolvedTabStops, paragraph.tabStops),
|
|
1967
|
-
};
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
function areIndentationsEqual(
|
|
1971
|
-
left:
|
|
1972
|
-
| Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["indentation"]
|
|
1973
|
-
| undefined,
|
|
1974
|
-
right:
|
|
1975
|
-
| Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["indentation"]
|
|
1976
|
-
| undefined,
|
|
1977
|
-
): boolean {
|
|
1978
|
-
return (
|
|
1979
|
-
left?.left === right?.left &&
|
|
1980
|
-
left?.right === right?.right &&
|
|
1981
|
-
left?.firstLine === right?.firstLine &&
|
|
1982
|
-
left?.hanging === right?.hanging
|
|
1983
|
-
);
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
function areTabStopsEqual(
|
|
1987
|
-
left: ReadonlyArray<{ pos: number; val?: string; leader?: string }> | undefined,
|
|
1988
|
-
right: ReadonlyArray<{ pos: number; val?: string; leader?: string }> | undefined,
|
|
1989
|
-
): boolean {
|
|
1990
|
-
if (!left?.length && !right?.length) {
|
|
1991
|
-
return true;
|
|
1992
|
-
}
|
|
1993
|
-
if (!left || !right || left.length !== right.length) {
|
|
1994
|
-
return false;
|
|
1995
|
-
}
|
|
1996
|
-
return left.every(
|
|
1997
|
-
(tabStop, index) =>
|
|
1998
|
-
tabStop.pos === right[index]?.pos &&
|
|
1999
|
-
tabStop.val === right[index]?.val &&
|
|
2000
|
-
tabStop.leader === right[index]?.leader,
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
function findActiveParagraph(
|
|
2005
|
-
blocks: readonly SurfaceBlockSnapshot[],
|
|
2006
|
-
position: number,
|
|
2007
|
-
): Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null {
|
|
2008
|
-
for (const block of blocks) {
|
|
2009
|
-
if (block.kind === "paragraph" && position >= block.from && position <= block.to) {
|
|
2010
|
-
return block;
|
|
2011
|
-
}
|
|
2012
|
-
if (block.kind === "table") {
|
|
2013
|
-
for (const row of block.rows) {
|
|
2014
|
-
for (const cell of row.cells) {
|
|
2015
|
-
const paragraph = findActiveParagraph(cell.content, position);
|
|
2016
|
-
if (paragraph) {
|
|
2017
|
-
return paragraph;
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
if (block.kind === "sdt_block") {
|
|
2023
|
-
const paragraph = findActiveParagraph(block.children, position);
|
|
2024
|
-
if (paragraph) {
|
|
2025
|
-
return paragraph;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
return null;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
interface PageChromeModel {
|
|
2033
|
-
lineNumberingEnabled: boolean;
|
|
2034
|
-
gutterWidthPx: number;
|
|
2035
|
-
lineMarkers: Array<{ id: string; label: string; topPx: number }>;
|
|
2036
|
-
showPageBorder: boolean;
|
|
2037
|
-
pageBorderDisplay: string;
|
|
2038
|
-
pageBorderStyle: CSSProperties | undefined;
|
|
2039
|
-
documentGridType: string;
|
|
2040
|
-
documentGridStyle: CSSProperties | undefined;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
const EMPTY_PAGE_CHROME_MODEL: PageChromeModel = {
|
|
2044
|
-
lineNumberingEnabled: false,
|
|
2045
|
-
gutterWidthPx: 0,
|
|
2046
|
-
lineMarkers: [],
|
|
2047
|
-
showPageBorder: false,
|
|
2048
|
-
pageBorderDisplay: "none",
|
|
2049
|
-
pageBorderStyle: undefined,
|
|
2050
|
-
documentGridType: "none",
|
|
2051
|
-
documentGridStyle: undefined,
|
|
2052
|
-
};
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
// P2.a — real-dimension page frame. Page frame width/height are
|
|
2056
|
-
// `pageLayout.pageWidth/pageHeight × FRAME_PX_PER_TWIP_AT_96DPI` so
|
|
2057
|
-
// every paper size renders at its Word-matching CSS px (Letter
|
|
2058
|
-
// 816×1056, A4 794×1123, Legal 816×1344, …). Constants are exported
|
|
2059
|
-
// so tests + harness panels can derive the same values.
|
|
2060
|
-
export const FRAME_PX_PER_TWIP_AT_96DPI = 96 / 1440;
|
|
2061
|
-
/** Floor on header/footer band heights so empty bands stay clickable. */
|
|
2062
|
-
export const MIN_BAND_HEIGHT_PX = 20;
|
|
2063
|
-
|
|
2064
|
-
export interface PageShellMetrics {
|
|
2065
|
-
/** P2.a — page frame CSS px width = `pageWidth × FRAME_PX_PER_TWIP_AT_96DPI`. */
|
|
2066
|
-
frameWidthPx?: number;
|
|
2067
|
-
/** P2.a — page frame CSS px height = `pageHeight × FRAME_PX_PER_TWIP_AT_96DPI`. */
|
|
2068
|
-
frameHeightPx?: number;
|
|
2069
|
-
contentInsetStyle: CSSProperties;
|
|
2070
|
-
pageFrameStyle: CSSProperties;
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
function buildPageChromeModel(
|
|
2074
|
-
surface: RuntimeRenderSnapshot["surface"] | undefined,
|
|
2075
|
-
pageLayout: RuntimeRenderSnapshot["pageLayout"] | undefined,
|
|
2076
|
-
navigation: DocumentNavigationSnapshot | undefined,
|
|
2077
|
-
activeStory: RuntimeRenderSnapshot["activeStory"],
|
|
2078
|
-
): PageChromeModel {
|
|
2079
|
-
if (!surface || !pageLayout || !navigation || activeStory.kind !== "main") {
|
|
2080
|
-
return EMPTY_PAGE_CHROME_MODEL;
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
const lineMarkers = computeLineMarkersIfEnabled({
|
|
2084
|
-
pageLayout,
|
|
2085
|
-
surfaceBlocks: surface.blocks,
|
|
2086
|
-
pages: navigation.pages,
|
|
2087
|
-
});
|
|
2088
|
-
const lineNumberingEnabled =
|
|
2089
|
-
Boolean(pageLayout.lineNumbering) && lineMarkers.length > 0;
|
|
2090
|
-
const distance = pageLayout.lineNumbering?.distance ?? 0;
|
|
2091
|
-
const gutterWidthPx = lineNumberingEnabled
|
|
2092
|
-
? Math.max(40, Math.min(88, 24 + Math.round(distance * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP)))
|
|
2093
|
-
: 0;
|
|
2094
|
-
const showPageBorder = shouldRenderPageBorder(pageLayout, navigation.pages, navigation.activePageIndex);
|
|
2095
|
-
|
|
2096
|
-
return {
|
|
2097
|
-
lineNumberingEnabled,
|
|
2098
|
-
gutterWidthPx,
|
|
2099
|
-
lineMarkers,
|
|
2100
|
-
showPageBorder,
|
|
2101
|
-
pageBorderDisplay: pageLayout.pageBorders?.display ?? "none",
|
|
2102
|
-
pageBorderStyle: showPageBorder ? buildPageBorderStyle(pageLayout) : undefined,
|
|
2103
|
-
documentGridType: pageLayout.documentGrid?.type ?? "none",
|
|
2104
|
-
documentGridStyle: buildDocumentGridStyle(pageLayout.documentGrid),
|
|
2105
|
-
};
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
export function buildPageShellMetrics(
|
|
2109
|
-
pageLayout: RuntimeRenderSnapshot["pageLayout"] | undefined,
|
|
2110
|
-
): PageShellMetrics {
|
|
2111
|
-
if (!pageLayout) {
|
|
2112
|
-
return {
|
|
2113
|
-
contentInsetStyle: {},
|
|
2114
|
-
pageFrameStyle: {},
|
|
2115
|
-
frameWidthPx: 0,
|
|
2116
|
-
frameHeightPx: 0,
|
|
2117
|
-
};
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
// P2.a — frame dimensions follow the section's real twip width/height
|
|
2121
|
-
// so every paper size in the catalog renders at its Word-matching CSS
|
|
2122
|
-
// px (Letter 816×1056, A4 794×1123, Legal 816×1344, …).
|
|
2123
|
-
const pxPerTwip = FRAME_PX_PER_TWIP_AT_96DPI;
|
|
2124
|
-
const frameWidthPx = Math.round(pageLayout.pageWidth * pxPerTwip);
|
|
2125
|
-
const frameHeightPx = Math.round(pageLayout.pageHeight * pxPerTwip);
|
|
2126
|
-
const horizontalInsetPx = Math.round(pageLayout.marginLeft * pxPerTwip);
|
|
2127
|
-
const horizontalInsetRightPx = Math.round(pageLayout.marginRight * pxPerTwip);
|
|
2128
|
-
|
|
2129
|
-
// P8.11 — `headerBandStyle` / `footerBandStyle` removed. The
|
|
2130
|
-
// workspace-level band divs that consumed them are gone; per-page
|
|
2131
|
-
// bands (rendered by `TwPageStackChromeLayer`) compute their own
|
|
2132
|
-
// heights from the runtime's `PageRegionsSnapshot`.
|
|
2133
|
-
|
|
2134
|
-
return {
|
|
2135
|
-
contentInsetStyle: {
|
|
2136
|
-
paddingLeft: `${horizontalInsetPx}px`,
|
|
2137
|
-
paddingRight: `${horizontalInsetRightPx}px`,
|
|
2138
|
-
},
|
|
2139
|
-
pageFrameStyle: {
|
|
2140
|
-
backgroundColor: "var(--color-page-bg)",
|
|
2141
|
-
borderRadius: "8px",
|
|
2142
|
-
boxShadow: "0 24px 48px -32px rgba(15, 23, 42, 0.38), 0 8px 20px -18px rgba(15, 23, 42, 0.22)",
|
|
2143
|
-
border: "1px solid rgba(148, 163, 184, 0.2)",
|
|
2144
|
-
},
|
|
2145
|
-
frameWidthPx,
|
|
2146
|
-
frameHeightPx,
|
|
2147
|
-
};
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
// P2.c — fit-to-width / fit-to-page resolves against the active section's
|
|
2151
|
-
// real paper size (not a global constant), so Letter and A4 produce the
|
|
2152
|
-
// expected 1.029:1 fit-width ratio at the same viewport. Clamped so
|
|
2153
|
-
// extreme viewports don't pin the editor at unreadable zooms.
|
|
2154
|
-
const FIT_WIDTH_CHROME_RESERVATION_PX = 96;
|
|
2155
|
-
const FIT_HEIGHT_CHROME_RESERVATION_PX = 180;
|
|
2156
|
-
const MIN_FIT_ZOOM = 0.5;
|
|
2157
|
-
const MAX_FIT_ZOOM = 2.0;
|
|
2158
|
-
|
|
2159
|
-
export function resolveZoomMultiplier(
|
|
2160
|
-
zoomLevel: number | "pageWidth" | "onePage",
|
|
2161
|
-
frameWidthPx: number,
|
|
2162
|
-
frameHeightPx: number,
|
|
2163
|
-
viewportWidth: number | undefined,
|
|
2164
|
-
viewportHeight: number | undefined,
|
|
2165
|
-
): number {
|
|
2166
|
-
if (typeof zoomLevel === "number") {
|
|
2167
|
-
return zoomLevel / 100;
|
|
2168
|
-
}
|
|
2169
|
-
if (!viewportWidth || frameWidthPx <= 0) return 1;
|
|
2170
|
-
const widthFit =
|
|
2171
|
-
(viewportWidth - FIT_WIDTH_CHROME_RESERVATION_PX) / frameWidthPx;
|
|
2172
|
-
if (zoomLevel === "pageWidth") {
|
|
2173
|
-
return Math.max(MIN_FIT_ZOOM, Math.min(MAX_FIT_ZOOM, widthFit));
|
|
2174
|
-
}
|
|
2175
|
-
if (!viewportHeight || frameHeightPx <= 0) {
|
|
2176
|
-
return Math.max(MIN_FIT_ZOOM, Math.min(MAX_FIT_ZOOM, widthFit));
|
|
2177
|
-
}
|
|
2178
|
-
const heightFit =
|
|
2179
|
-
(viewportHeight - FIT_HEIGHT_CHROME_RESERVATION_PX) / frameHeightPx;
|
|
2180
|
-
return Math.max(
|
|
2181
|
-
MIN_FIT_ZOOM,
|
|
2182
|
-
Math.min(MAX_FIT_ZOOM, Math.min(widthFit, heightFit)),
|
|
2183
|
-
);
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
function shouldRenderPageBorder(
|
|
2187
|
-
pageLayout: RuntimeRenderSnapshot["pageLayout"],
|
|
2188
|
-
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
|
|
2189
|
-
activePageIndex: number,
|
|
2190
|
-
): boolean {
|
|
2191
|
-
const display = pageLayout?.pageBorders?.display ?? "allPages";
|
|
2192
|
-
const activePage = pages[activePageIndex];
|
|
2193
|
-
if (!pageLayout?.pageBorders || !activePage) {
|
|
2194
|
-
return false;
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
switch (display) {
|
|
2198
|
-
case "firstPage":
|
|
2199
|
-
return activePage.pageInSection === 0;
|
|
2200
|
-
case "notFirstPage":
|
|
2201
|
-
return activePage.pageInSection > 0;
|
|
2202
|
-
default:
|
|
2203
|
-
return true;
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
function buildPageBorderStyle(
|
|
2208
|
-
pageLayout: NonNullable<RuntimeRenderSnapshot["pageLayout"]>,
|
|
2209
|
-
): CSSProperties | undefined {
|
|
2210
|
-
const pageBorders = pageLayout.pageBorders;
|
|
2211
|
-
if (!pageBorders) {
|
|
2212
|
-
return undefined;
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
const leftInset = createInsetValue(
|
|
2216
|
-
pageBorders.left?.space,
|
|
2217
|
-
pageBorders.offsetFrom === "text"
|
|
2218
|
-
? (pageLayout.marginLeft / Math.max(1, pageLayout.pageWidth)) * 100
|
|
2219
|
-
: 1.25,
|
|
2220
|
-
);
|
|
2221
|
-
const rightInset = createInsetValue(
|
|
2222
|
-
pageBorders.right?.space,
|
|
2223
|
-
pageBorders.offsetFrom === "text"
|
|
2224
|
-
? (pageLayout.marginRight / Math.max(1, pageLayout.pageWidth)) * 100
|
|
2225
|
-
: 1.25,
|
|
2226
|
-
);
|
|
2227
|
-
const topInset = createInsetValue(
|
|
2228
|
-
pageBorders.top?.space,
|
|
2229
|
-
pageBorders.offsetFrom === "text"
|
|
2230
|
-
? (pageLayout.marginTop / Math.max(1, pageLayout.pageHeight)) * 100
|
|
2231
|
-
: 1.5,
|
|
2232
|
-
);
|
|
2233
|
-
const bottomInset = createInsetValue(
|
|
2234
|
-
pageBorders.bottom?.space,
|
|
2235
|
-
pageBorders.offsetFrom === "text"
|
|
2236
|
-
? (pageLayout.marginBottom / Math.max(1, pageLayout.pageHeight)) * 100
|
|
2237
|
-
: 1.5,
|
|
2238
|
-
);
|
|
2239
|
-
|
|
2240
|
-
return {
|
|
2241
|
-
top: topInset,
|
|
2242
|
-
right: rightInset,
|
|
2243
|
-
bottom: bottomInset,
|
|
2244
|
-
left: leftInset,
|
|
2245
|
-
borderTop: toBorderCss(pageBorders.top),
|
|
2246
|
-
borderRight: toBorderCss(pageBorders.right),
|
|
2247
|
-
borderBottom: toBorderCss(pageBorders.bottom),
|
|
2248
|
-
borderLeft: toBorderCss(pageBorders.left),
|
|
2249
|
-
boxSizing: "border-box",
|
|
2250
|
-
mixBlendMode: pageBorders.zOrder === "back" ? "multiply" : undefined,
|
|
2251
|
-
};
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
function buildDocumentGridStyle(
|
|
2255
|
-
documentGrid: NonNullable<RuntimeRenderSnapshot["pageLayout"]>["documentGrid"] | undefined,
|
|
2256
|
-
): CSSProperties | undefined {
|
|
2257
|
-
if (!documentGrid || !documentGrid.type || documentGrid.type === "default") {
|
|
2258
|
-
return undefined;
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
const linePitchPx = Math.max(
|
|
2262
|
-
18,
|
|
2263
|
-
Math.round((documentGrid.linePitch ?? 360) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP),
|
|
2264
|
-
);
|
|
2265
|
-
const charSpacePx = Math.max(
|
|
2266
|
-
12,
|
|
2267
|
-
Math.round((documentGrid.charSpace ?? 204) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP),
|
|
2268
|
-
);
|
|
2269
|
-
const gridColor = "rgba(15, 23, 42, 0.06)";
|
|
2270
|
-
const backgrounds: string[] = [];
|
|
2271
|
-
|
|
2272
|
-
if (
|
|
2273
|
-
documentGrid.type === "lines" ||
|
|
2274
|
-
documentGrid.type === "linesAndChars" ||
|
|
2275
|
-
documentGrid.type === "snapToChars"
|
|
2276
|
-
) {
|
|
2277
|
-
backgrounds.push(
|
|
2278
|
-
`repeating-linear-gradient(to bottom, ${gridColor} 0, ${gridColor} 1px, transparent 1px, transparent ${linePitchPx}px)`,
|
|
2279
|
-
);
|
|
2280
|
-
}
|
|
2281
|
-
if (
|
|
2282
|
-
documentGrid.type === "linesAndChars" ||
|
|
2283
|
-
documentGrid.type === "snapToChars"
|
|
2284
|
-
) {
|
|
2285
|
-
backgrounds.push(
|
|
2286
|
-
`repeating-linear-gradient(to right, rgba(15, 23, 42, 0.04) 0, rgba(15, 23, 42, 0.04) 1px, transparent 1px, transparent ${charSpacePx}px)`,
|
|
2287
|
-
);
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
if (backgrounds.length === 0) {
|
|
2291
|
-
return undefined;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
return {
|
|
2295
|
-
backgroundImage: backgrounds.join(", "),
|
|
2296
|
-
backgroundOrigin: "content-box",
|
|
2297
|
-
};
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
function createInsetValue(spaceTwips: number | undefined, percent: number): string {
|
|
2301
|
-
const spacingPx = Math.max(0, Math.round((spaceTwips ?? 0) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP));
|
|
2302
|
-
return `calc(${percent.toFixed(2)}% + ${spacingPx}px)`;
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
function resolveSelectionToolbarPlacement(
|
|
2306
|
-
anchor: SelectionToolAnchor | null | undefined,
|
|
2307
|
-
root: HTMLDivElement | null,
|
|
2308
|
-
zoomScale: number,
|
|
2309
|
-
): { placement: "right" | "left" | "above" | "below"; style: CSSProperties } | null {
|
|
2310
|
-
if (!anchor || !root) {
|
|
2311
|
-
return null;
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
const rootRect = root.getBoundingClientRect();
|
|
2315
|
-
if (rootRect.width <= 0 || rootRect.height <= 0 || zoomScale <= 0) {
|
|
2316
|
-
return null;
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
const centerX = (anchor.left + anchor.right) / 2;
|
|
2320
|
-
const centerY = (anchor.top + anchor.bottom) / 2;
|
|
2321
|
-
const localLeftEdge = (anchor.left - rootRect.left) / zoomScale;
|
|
2322
|
-
const localRightEdge = (anchor.right - rootRect.left) / zoomScale;
|
|
2323
|
-
const localLeft = (centerX - rootRect.left) / zoomScale;
|
|
2324
|
-
const localCenterY = (centerY - rootRect.top) / zoomScale;
|
|
2325
|
-
const localTop = (anchor.top - rootRect.top) / zoomScale;
|
|
2326
|
-
const localBottom = (anchor.bottom - rootRect.top) / zoomScale;
|
|
2327
|
-
const edgePadding = 16 / zoomScale;
|
|
2328
|
-
const containerWidth = rootRect.width / zoomScale;
|
|
2329
|
-
const containerHeight = rootRect.height / zoomScale;
|
|
2330
|
-
const gapPx = 12 / zoomScale;
|
|
2331
|
-
const estimatedToolbarWidth = Math.min(260 / zoomScale, Math.max(168 / zoomScale, containerWidth * 0.32));
|
|
2332
|
-
const estimatedToolbarHeight = 44 / zoomScale;
|
|
2333
|
-
const clampedCenterLeft = Math.max(
|
|
2334
|
-
edgePadding,
|
|
2335
|
-
Math.min(localLeft, Math.max(edgePadding, containerWidth - edgePadding)),
|
|
2336
|
-
);
|
|
2337
|
-
const clampedCenterY = Math.max(
|
|
2338
|
-
edgePadding + estimatedToolbarHeight / 2,
|
|
2339
|
-
Math.min(localCenterY, Math.max(edgePadding + estimatedToolbarHeight / 2, containerHeight - edgePadding - estimatedToolbarHeight / 2)),
|
|
2340
|
-
);
|
|
2341
|
-
const rightClearance = containerWidth - localRightEdge - gapPx - edgePadding;
|
|
2342
|
-
const leftClearance = localLeftEdge - gapPx - edgePadding;
|
|
2343
|
-
|
|
2344
|
-
if (rightClearance >= estimatedToolbarWidth) {
|
|
2345
|
-
return {
|
|
2346
|
-
placement: "right",
|
|
2347
|
-
style: {
|
|
2348
|
-
left: `${localRightEdge}px`,
|
|
2349
|
-
top: `${clampedCenterY}px`,
|
|
2350
|
-
maxWidth: `${Math.max(220, containerWidth - edgePadding * 2)}px`,
|
|
2351
|
-
transform: `translate(${gapPx}px, -50%)`,
|
|
2352
|
-
},
|
|
2353
|
-
};
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
if (leftClearance >= estimatedToolbarWidth) {
|
|
2357
|
-
return {
|
|
2358
|
-
placement: "left",
|
|
2359
|
-
style: {
|
|
2360
|
-
left: `${localLeftEdge}px`,
|
|
2361
|
-
top: `${clampedCenterY}px`,
|
|
2362
|
-
maxWidth: `${Math.max(220, containerWidth - edgePadding * 2)}px`,
|
|
2363
|
-
transform: `translate(calc(-100% - ${gapPx}px), -50%)`,
|
|
2364
|
-
},
|
|
2365
|
-
};
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
const placement = localTop < estimatedToolbarHeight + gapPx + edgePadding ? "below" : "above";
|
|
2369
|
-
|
|
2370
|
-
return {
|
|
2371
|
-
placement,
|
|
2372
|
-
style: {
|
|
2373
|
-
left: `${clampedCenterLeft}px`,
|
|
2374
|
-
top: `${placement === "above" ? localTop : localBottom}px`,
|
|
2375
|
-
maxWidth: `${Math.max(220, containerWidth - edgePadding * 2)}px`,
|
|
2376
|
-
transform:
|
|
2377
|
-
placement === "above"
|
|
2378
|
-
? `translate(-50%, calc(-100% - ${gapPx}px))`
|
|
2379
|
-
: `translate(-50%, ${gapPx}px)`,
|
|
2380
|
-
},
|
|
2381
|
-
};
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
function toBorderCss(
|
|
2385
|
-
border:
|
|
2386
|
-
| NonNullable<NonNullable<RuntimeRenderSnapshot["pageLayout"]>["pageBorders"]>["top"]
|
|
2387
|
-
| undefined,
|
|
2388
|
-
): string | undefined {
|
|
2389
|
-
if (!border || border.value === "none" || border.value === "nil") {
|
|
2390
|
-
return undefined;
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
const width = border.size ? `${Math.max(1, Math.round(border.size / 8))}px` : "1px";
|
|
2394
|
-
const style =
|
|
2395
|
-
border.value === "double"
|
|
2396
|
-
? "double"
|
|
2397
|
-
: border.value === "dotted"
|
|
2398
|
-
? "dotted"
|
|
2399
|
-
: border.value === "dashed" || border.value === "dashSmallGap"
|
|
2400
|
-
? "dashed"
|
|
2401
|
-
: "solid";
|
|
2402
|
-
const color = border.color && border.color !== "auto" ? `#${border.color}` : "rgba(31, 31, 31, 0.28)";
|
|
2403
|
-
return `${width} ${style} ${color}`;
|
|
2404
|
-
}
|