@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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 05 — Fragment projection helpers.
|
|
3
|
+
*
|
|
4
|
+
* Owns:
|
|
5
|
+
* - `collectLineBoxesForRegion` — synthesize line boxes for header/footer/
|
|
6
|
+
* column/footnote-area regions (body regions carry line boxes on the page
|
|
7
|
+
* node already). Moved from `src/runtime/layout/public-facet.ts` in
|
|
8
|
+
* refactor/05 Slice 2b.
|
|
9
|
+
* - `resolveRegionEntry` — map a `PublicPageRegion["kind"]` + optional
|
|
10
|
+
* column index onto the corresponding `RuntimePageRegion` on a node.
|
|
11
|
+
* Moved from the same origin.
|
|
12
|
+
* - `getBlockRectsFromFrame` — walk a `RenderFrame` and collect every
|
|
13
|
+
* block fragment rect for a given `blockId`. New in Slice 2b; the
|
|
14
|
+
* layout facet does not currently expose block-rect lists. Required by
|
|
15
|
+
* `v3 runtime.geometry.getBlockRects` whose adapter promotes from
|
|
16
|
+
* `mock` to `live-with-adapter` in Slice 5.
|
|
17
|
+
*
|
|
18
|
+
* Contracts:
|
|
19
|
+
* - G5 — rects here are frame-local pixels (`space: "frame"`) because
|
|
20
|
+
* the underlying `RenderFrameRect` is kernel-zoomed. Consumers that
|
|
21
|
+
* need twip coordinates re-derive from layout, not from here.
|
|
22
|
+
* - G6 — callers must route through the `GeometryFacet`, not reach in.
|
|
23
|
+
*
|
|
24
|
+
* Slices 4–5 add caret / envelope projection in sibling files.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
RuntimePageGraph,
|
|
29
|
+
RuntimePageNode,
|
|
30
|
+
RuntimePageRegion,
|
|
31
|
+
} from "../layout/page-graph.ts";
|
|
32
|
+
import type { PublicPageRegion } from "../layout/public-facet.ts";
|
|
33
|
+
import type { RenderFrame } from "../render/index.ts";
|
|
34
|
+
import type {
|
|
35
|
+
BlockGeometry,
|
|
36
|
+
GeometryRect,
|
|
37
|
+
PageGeometry,
|
|
38
|
+
} from "./geometry-types.ts";
|
|
39
|
+
|
|
40
|
+
// `lineBoxes` on `RuntimePageNode` is already strongly typed; alias it
|
|
41
|
+
// locally so we don't re-import the raw `RuntimeLineBox` type just to
|
|
42
|
+
// declare the helper's return shape.
|
|
43
|
+
export type RuntimeLineBoxAlias = RuntimePageNode["lineBoxes"][number];
|
|
44
|
+
|
|
45
|
+
export const EMPTY_LINE_BOXES: readonly RuntimeLineBoxAlias[] = Object.freeze(
|
|
46
|
+
[],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Collect raw line boxes for `region` on `node`. Body returns the node's
|
|
51
|
+
* precomputed `lineBoxes`; non-body regions synthesize line boxes from the
|
|
52
|
+
* region's fragment list (one entry per fragment, stacked vertically from a
|
|
53
|
+
* zero cursor). Multi-column layouts resolve against the requested
|
|
54
|
+
* `columnIndex`; footnote-area uses the first allocated footnote region on
|
|
55
|
+
* the page.
|
|
56
|
+
*/
|
|
57
|
+
export function collectLineBoxesForRegion(
|
|
58
|
+
node: RuntimePageNode,
|
|
59
|
+
region: PublicPageRegion["kind"],
|
|
60
|
+
graph: RuntimePageGraph,
|
|
61
|
+
columnIndex: number | undefined,
|
|
62
|
+
): readonly RuntimeLineBoxAlias[] {
|
|
63
|
+
if (region === "body") {
|
|
64
|
+
return node.lineBoxes;
|
|
65
|
+
}
|
|
66
|
+
const regionEntry = resolveRegionEntry(node, region, columnIndex);
|
|
67
|
+
if (!regionEntry || regionEntry.fragmentIds.length === 0) {
|
|
68
|
+
return EMPTY_LINE_BOXES;
|
|
69
|
+
}
|
|
70
|
+
const fragmentsById = new Map(
|
|
71
|
+
graph.fragments.map((f) => [f.fragmentId, f] as const),
|
|
72
|
+
);
|
|
73
|
+
const result: RuntimeLineBoxAlias[] = [];
|
|
74
|
+
let cursorTwips = 0;
|
|
75
|
+
let lineIndex = 0;
|
|
76
|
+
for (const fragmentId of regionEntry.fragmentIds) {
|
|
77
|
+
const fragment = fragmentsById.get(fragmentId);
|
|
78
|
+
if (!fragment) continue;
|
|
79
|
+
const heightTwips = Math.max(1, fragment.heightTwips);
|
|
80
|
+
result.push({
|
|
81
|
+
fragmentId,
|
|
82
|
+
lineIndex: lineIndex++,
|
|
83
|
+
baselineTwips: cursorTwips + heightTwips,
|
|
84
|
+
heightTwips,
|
|
85
|
+
widthTwips: regionEntry.widthTwips,
|
|
86
|
+
});
|
|
87
|
+
cursorTwips += heightTwips;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveRegionEntry(
|
|
93
|
+
node: RuntimePageNode,
|
|
94
|
+
region: PublicPageRegion["kind"],
|
|
95
|
+
columnIndex: number | undefined,
|
|
96
|
+
): RuntimePageRegion | undefined {
|
|
97
|
+
switch (region) {
|
|
98
|
+
case "header":
|
|
99
|
+
return node.regions.header;
|
|
100
|
+
case "footer":
|
|
101
|
+
return node.regions.footer;
|
|
102
|
+
case "column": {
|
|
103
|
+
const columns = node.regions.columns ?? [];
|
|
104
|
+
if (columns.length === 0) return undefined;
|
|
105
|
+
const idx = columnIndex ?? 0;
|
|
106
|
+
return columns[idx];
|
|
107
|
+
}
|
|
108
|
+
case "footnote-area": {
|
|
109
|
+
// Footnote area sits at the bottom of the body region per OOXML.
|
|
110
|
+
// Returns the first footnote region when allocated (page layouts
|
|
111
|
+
// allocate one block of footnotes per page today; multi-block
|
|
112
|
+
// layouts come later).
|
|
113
|
+
const footnoteRegionList = node.regions.footnotes;
|
|
114
|
+
if (footnoteRegionList && footnoteRegionList.length > 0) {
|
|
115
|
+
return footnoteRegionList[0];
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Collect every fragment rect for `blockId` from a `RenderFrame`. A block
|
|
126
|
+
* that spans multiple pages has one rect per page. Returns rects in
|
|
127
|
+
* page order, tagged `space: "frame"`. Empty when the frame is null, has
|
|
128
|
+
* no pages, or no fragment on any page carries the target blockId.
|
|
129
|
+
*
|
|
130
|
+
* New in Slice 2b. `v3 runtime.geometry.getBlockRects` promotes from
|
|
131
|
+
* `mock` to `live-with-adapter` against this implementation in Slice 5.
|
|
132
|
+
*/
|
|
133
|
+
export function getBlockRectsFromFrame(
|
|
134
|
+
frame: RenderFrame | null,
|
|
135
|
+
blockId: string,
|
|
136
|
+
): readonly GeometryRect[] {
|
|
137
|
+
if (!frame) return [];
|
|
138
|
+
const rects: GeometryRect[] = [];
|
|
139
|
+
for (const page of frame.pages) {
|
|
140
|
+
const regions = [
|
|
141
|
+
page.regions.body,
|
|
142
|
+
page.regions.header,
|
|
143
|
+
page.regions.footer,
|
|
144
|
+
...(page.regions.columns ?? []),
|
|
145
|
+
...(page.regions.footnotes ?? []),
|
|
146
|
+
];
|
|
147
|
+
for (const region of regions) {
|
|
148
|
+
if (!region) continue;
|
|
149
|
+
for (const block of region.blocks) {
|
|
150
|
+
if (block.fragment.blockId !== blockId) continue;
|
|
151
|
+
rects.push({
|
|
152
|
+
leftPx: block.frame.leftPx,
|
|
153
|
+
topPx: block.frame.topPx,
|
|
154
|
+
widthPx: block.frame.widthPx,
|
|
155
|
+
heightPx: block.frame.heightPx,
|
|
156
|
+
space: "frame",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return rects;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Project a page from a `RenderFrame` into a `PageGeometry`. Accepts
|
|
166
|
+
* either a numeric `pageIndex` or a string `pageId` (the same overload
|
|
167
|
+
* the public facet's `getPage` exposes). Returns `null` when the frame
|
|
168
|
+
* is null or the target page isn't in the current frame.
|
|
169
|
+
*
|
|
170
|
+
* New in Slice 3a — chrome overlays read page 0's `frame` via the
|
|
171
|
+
* geometry facet instead of calling `getBoundingClientRect` on the
|
|
172
|
+
* overlay root, closing the G2 violation.
|
|
173
|
+
*/
|
|
174
|
+
export function getPageFromFrame(
|
|
175
|
+
frame: RenderFrame | null,
|
|
176
|
+
target: number | string,
|
|
177
|
+
): PageGeometry | null {
|
|
178
|
+
if (!frame) return null;
|
|
179
|
+
const page =
|
|
180
|
+
typeof target === "number"
|
|
181
|
+
? frame.pages[target]
|
|
182
|
+
: frame.pages.find((p) => p.page.pageId === target);
|
|
183
|
+
if (!page) return null;
|
|
184
|
+
return {
|
|
185
|
+
pageId: page.page.pageId,
|
|
186
|
+
pageIndex: page.page.pageIndex,
|
|
187
|
+
frame: {
|
|
188
|
+
leftPx: page.frame.leftPx,
|
|
189
|
+
topPx: page.frame.topPx,
|
|
190
|
+
widthPx: page.frame.widthPx,
|
|
191
|
+
heightPx: page.frame.heightPx,
|
|
192
|
+
space: "frame",
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Project a block from a `RenderFrame` into a `BlockGeometry` — wraps
|
|
199
|
+
* `getBlockRectsFromFrame` with the block id so consumers that want a
|
|
200
|
+
* named handle don't have to thread the id alongside the rect list.
|
|
201
|
+
* Returns `null` when the frame is null or no fragment on any page
|
|
202
|
+
* carries the target blockId.
|
|
203
|
+
*
|
|
204
|
+
* New in Slice 3a — scroll-anchor reads block rects via this helper
|
|
205
|
+
* (through the geometry facet) instead of walking the `offsetTop` /
|
|
206
|
+
* `offsetParent` chain, closing the G2 violation in
|
|
207
|
+
* `src/ui-tailwind/editor-surface/scroll-anchor.ts`.
|
|
208
|
+
*/
|
|
209
|
+
export function getBlockGeometryFromFrame(
|
|
210
|
+
frame: RenderFrame | null,
|
|
211
|
+
blockId: string,
|
|
212
|
+
): BlockGeometry | null {
|
|
213
|
+
const rects = getBlockRectsFromFrame(frame, blockId);
|
|
214
|
+
if (rects.length === 0) return null;
|
|
215
|
+
return { blockId, rects };
|
|
216
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 05 — geometry debug projector.
|
|
3
|
+
*
|
|
4
|
+
* Produces a `GeometryDebugEntry` for the debug-inspector snapshot when
|
|
5
|
+
* the `layout` telemetry channel is on. Architecture reference:
|
|
6
|
+
* `docs/architecture/05-geometry-projection.md` §"Public facet".
|
|
7
|
+
*
|
|
8
|
+
* Mirrors `src/runtime/formatting/projector.ts` in spirit — strict about
|
|
9
|
+
* cost, gated by the bus channel toggle. Consumers must call
|
|
10
|
+
* `buildGeometryDebugEntry` only when `bus.isEnabled("layout")`.
|
|
11
|
+
*
|
|
12
|
+
* Slice-6 scope: minimum-viable summary — page count, frame revision,
|
|
13
|
+
* sampled block rects, caret resolution shape. Slice 6 does NOT wire
|
|
14
|
+
* the `geometry.projected` emit site on the layout-engine-instance
|
|
15
|
+
* recompute boundary — that is an edit-path perf concern (Performance
|
|
16
|
+
* Invariant 4) and lands alongside a perf-bench pair in a follow-up.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { GeometryFacet } from "./geometry-facet.ts";
|
|
20
|
+
|
|
21
|
+
export interface GeometryDebugCatalog {
|
|
22
|
+
/** Layout revision the snapshot was taken against (0 when unknown). */
|
|
23
|
+
readonly layoutRevision: number;
|
|
24
|
+
/** Number of pages in the kernel's current frame. */
|
|
25
|
+
readonly pageCount: number;
|
|
26
|
+
/** Total block rects across all pages sampled (bounded by `sampleLimit`). */
|
|
27
|
+
readonly sampledBlockRectCount: number;
|
|
28
|
+
/** Per-block pages the first sampled block spans (0 when no sample). */
|
|
29
|
+
readonly firstBlockPageSpan: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface GeometryDebugSample {
|
|
33
|
+
readonly blockId: string;
|
|
34
|
+
readonly pageCount: number;
|
|
35
|
+
readonly space: "twips" | "frame" | "overlay";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GeometryDebugEntry {
|
|
39
|
+
readonly schemaVersion: 1;
|
|
40
|
+
readonly catalog: GeometryDebugCatalog;
|
|
41
|
+
/** Up to `sampleLimit` sampled blocks for the inspector UI. */
|
|
42
|
+
readonly samples: readonly GeometryDebugSample[];
|
|
43
|
+
/** Viewport snapshot at projection time (scrollTop/scrollLeft/dpr/zoom). */
|
|
44
|
+
readonly viewport: {
|
|
45
|
+
readonly scrollLeftPx: number;
|
|
46
|
+
readonly scrollTopPx: number;
|
|
47
|
+
readonly dpr: number;
|
|
48
|
+
readonly pxPerTwip: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BuildGeometryDebugEntryInputs {
|
|
53
|
+
readonly facet: GeometryFacet;
|
|
54
|
+
/**
|
|
55
|
+
* Block ids to sample. When omitted, an empty sample list is emitted —
|
|
56
|
+
* consumers are expected to supply a bounded list (e.g. the visible
|
|
57
|
+
* page range's blocks) to keep the projector's cost proportional.
|
|
58
|
+
*/
|
|
59
|
+
readonly sampleBlockIds?: readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* Maximum number of sampled rects materialized. Default 16 — enough
|
|
62
|
+
* for an inspector UI, small enough to keep projection O(1) even on
|
|
63
|
+
* large documents.
|
|
64
|
+
*/
|
|
65
|
+
readonly sampleLimit?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Layout revision the caller is inspecting. The facet does not expose
|
|
68
|
+
* the revision directly; pass it through from the layout engine when
|
|
69
|
+
* available. Defaults to 0.
|
|
70
|
+
*/
|
|
71
|
+
readonly layoutRevision?: number;
|
|
72
|
+
/** Frame page count — passed in so the projector stays facet-only. */
|
|
73
|
+
readonly pageCount?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DEFAULT_SAMPLE_LIMIT = 16;
|
|
77
|
+
|
|
78
|
+
export function buildGeometryDebugEntry(
|
|
79
|
+
inputs: BuildGeometryDebugEntryInputs,
|
|
80
|
+
): GeometryDebugEntry {
|
|
81
|
+
const {
|
|
82
|
+
facet,
|
|
83
|
+
sampleBlockIds,
|
|
84
|
+
sampleLimit = DEFAULT_SAMPLE_LIMIT,
|
|
85
|
+
layoutRevision = 0,
|
|
86
|
+
pageCount = 0,
|
|
87
|
+
} = inputs;
|
|
88
|
+
|
|
89
|
+
const samples: GeometryDebugSample[] = [];
|
|
90
|
+
let sampledRectTotal = 0;
|
|
91
|
+
let firstBlockPageSpan = 0;
|
|
92
|
+
|
|
93
|
+
if (sampleBlockIds && sampleBlockIds.length > 0) {
|
|
94
|
+
const limit = Math.max(0, Math.min(sampleBlockIds.length, sampleLimit));
|
|
95
|
+
for (let i = 0; i < limit; i += 1) {
|
|
96
|
+
const blockId = sampleBlockIds[i]!;
|
|
97
|
+
const geometry = facet.getBlock(blockId);
|
|
98
|
+
if (!geometry) continue;
|
|
99
|
+
const rectCount = geometry.rects.length;
|
|
100
|
+
sampledRectTotal += rectCount;
|
|
101
|
+
if (samples.length === 0) firstBlockPageSpan = rectCount;
|
|
102
|
+
const firstSpace = geometry.rects[0]?.space ?? "frame";
|
|
103
|
+
samples.push({
|
|
104
|
+
blockId,
|
|
105
|
+
pageCount: rectCount,
|
|
106
|
+
space: firstSpace,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const viewport = facet.getViewport();
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
catalog: {
|
|
116
|
+
layoutRevision,
|
|
117
|
+
pageCount,
|
|
118
|
+
sampledBlockRectCount: sampledRectTotal,
|
|
119
|
+
firstBlockPageSpan,
|
|
120
|
+
},
|
|
121
|
+
samples,
|
|
122
|
+
viewport: {
|
|
123
|
+
scrollLeftPx: viewport.scrollLeftPx,
|
|
124
|
+
scrollTopPx: viewport.scrollTopPx,
|
|
125
|
+
dpr: viewport.dpr,
|
|
126
|
+
pxPerTwip: viewport.pxPerTwip,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 05 — Replacement envelope projection.
|
|
3
|
+
*
|
|
4
|
+
* A **replacement envelope** answers "what rects does this scope
|
|
5
|
+
* currently occupy?" so agent-facing replacement tooling can position
|
|
6
|
+
* chrome (overlays, confirmation cards, diff highlights) and know
|
|
7
|
+
* whether the scope is still resolvable. Architecture 05 G7:
|
|
8
|
+
*
|
|
9
|
+
* > If scope S still resolves, `getReplacementEnvelope(S)` produces a
|
|
10
|
+
* > rect bundle describing "the area occupied by this scope right
|
|
11
|
+
* > now". This is the geometry input to layer 08 scope bundles.
|
|
12
|
+
*
|
|
13
|
+
* Slice-5 shape: a pure helper that accepts a resolved scope shape
|
|
14
|
+
* (`{kind: "range", from, to}` or `{kind: "detached", lastKnownRange}`)
|
|
15
|
+
* plus the render frame, and produces an `EnvelopeBundle`. The facet's
|
|
16
|
+
* `getReplacementEnvelope(scopeId)` combines `resolveScope(document,
|
|
17
|
+
* scopeId)` with this helper.
|
|
18
|
+
*
|
|
19
|
+
* Slice-5 substrate notes:
|
|
20
|
+
*
|
|
21
|
+
* - `scopeRects` routes through `resolveSelectionRects` — i.e. the
|
|
22
|
+
* same Slice-4 path that returns the kernel's union rect for
|
|
23
|
+
* non-collapsed ranges. When per-line anchors ship in a later
|
|
24
|
+
* slice, `scopeRects` upgrades to one-rect-per-line automatically.
|
|
25
|
+
* - `linesCrossed` reflects the number of rects the helper produced,
|
|
26
|
+
* which matches the plan's semantics ("how many lines the scope
|
|
27
|
+
* crosses") once per-line enumeration is real. For Slice 5 it's
|
|
28
|
+
* `1` whenever the scope resolved.
|
|
29
|
+
* - **Wrap-aware envelopes** (scope adjacent to a floating image) are
|
|
30
|
+
* deferred — the current render frame does not expose wrap region
|
|
31
|
+
* metadata. The plan's `replacement-envelope-wrap-aware.test.ts`
|
|
32
|
+
* is reserved for that slice.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type {
|
|
36
|
+
GeometryRect,
|
|
37
|
+
EnvelopeBundle,
|
|
38
|
+
GeometrySpace,
|
|
39
|
+
} from "./geometry-types.ts";
|
|
40
|
+
import type { RenderFrame } from "../render/index.ts";
|
|
41
|
+
import { resolveSelectionRects } from "./caret-geometry.ts";
|
|
42
|
+
import type { EditorStoryTarget } from "../../api/public-types";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scope shape the helper accepts. Mirrors a subset of
|
|
46
|
+
* `EditorAnchorProjection` from the runtime API so the helper stays
|
|
47
|
+
* dependency-free and consumers can pass either the runtime's
|
|
48
|
+
* resolveScope output directly or a hand-built range.
|
|
49
|
+
*/
|
|
50
|
+
export type ReplacementScope =
|
|
51
|
+
| { kind: "range"; from: number; to: number }
|
|
52
|
+
| {
|
|
53
|
+
kind: "detached";
|
|
54
|
+
lastKnownRange: { from: number; to: number };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the replacement envelope for a scope against the current
|
|
59
|
+
* render frame. Returns `null` when `frame` is null or when the range
|
|
60
|
+
* is empty and the scope is not detached (detached scopes without a
|
|
61
|
+
* lastKnownRange still return null — nothing to anchor on).
|
|
62
|
+
*
|
|
63
|
+
* The envelope's `space` carries through from the rect projection; the
|
|
64
|
+
* attachPoint uses the same space.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveReplacementEnvelope(
|
|
67
|
+
frame: RenderFrame | null,
|
|
68
|
+
scope: ReplacementScope,
|
|
69
|
+
story?: EditorStoryTarget,
|
|
70
|
+
): EnvelopeBundle | null {
|
|
71
|
+
if (!frame) return null;
|
|
72
|
+
|
|
73
|
+
if (scope.kind === "range") {
|
|
74
|
+
const { from, to } = scope;
|
|
75
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return null;
|
|
76
|
+
const rects = resolveSelectionRects(frame, { from, to, story });
|
|
77
|
+
if (rects.length === 0) return null;
|
|
78
|
+
return buildBundle(rects, "exact");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Detached: project the last-known range and tag confidence as such.
|
|
82
|
+
const { from, to } = scope.lastKnownRange;
|
|
83
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return null;
|
|
84
|
+
const rects = resolveSelectionRects(frame, { from, to, story });
|
|
85
|
+
if (rects.length === 0) return null;
|
|
86
|
+
return buildBundle(rects, "detached");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildBundle(
|
|
90
|
+
rects: readonly GeometryRect[],
|
|
91
|
+
confidence: EnvelopeBundle["confidence"],
|
|
92
|
+
): EnvelopeBundle {
|
|
93
|
+
const first = rects[0]!;
|
|
94
|
+
const space: GeometrySpace = first.space;
|
|
95
|
+
// Slice 7b (2026-04-22): derive envelope precision from the
|
|
96
|
+
// underlying rects — when `resolveSelectionRects` returned a single
|
|
97
|
+
// union rect (wrapped range) it carries `precision: "within-
|
|
98
|
+
// tolerance"`; when it returned a caret rect (collapsed range) the
|
|
99
|
+
// rect is untagged (`"exact"` by default). The envelope's own
|
|
100
|
+
// precision is the coarsest of its rects, biased toward
|
|
101
|
+
// `"heuristic"` when the scope is detached because the last-known
|
|
102
|
+
// range is a point-in-time snapshot that may no longer match the
|
|
103
|
+
// current doc.
|
|
104
|
+
const rectPrecision = coarsestRectPrecision(rects);
|
|
105
|
+
const envelopePrecision: EnvelopeBundle["precision"] =
|
|
106
|
+
confidence === "detached" ? "heuristic" : rectPrecision;
|
|
107
|
+
return {
|
|
108
|
+
scopeRects: rects,
|
|
109
|
+
attachPoint: {
|
|
110
|
+
xPx: first.leftPx,
|
|
111
|
+
yPx: first.topPx,
|
|
112
|
+
space,
|
|
113
|
+
},
|
|
114
|
+
confidence,
|
|
115
|
+
linesCrossed: rects.length,
|
|
116
|
+
precision: envelopePrecision,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function coarsestRectPrecision(
|
|
121
|
+
rects: readonly GeometryRect[],
|
|
122
|
+
): "exact" | "within-tolerance" | "heuristic" {
|
|
123
|
+
let worst: "exact" | "within-tolerance" | "heuristic" = "exact";
|
|
124
|
+
for (const rect of rects) {
|
|
125
|
+
const p = rect.precision ?? "exact";
|
|
126
|
+
if (p === "heuristic") return "heuristic";
|
|
127
|
+
if (p === "within-tolerance" && worst === "exact") worst = "within-tolerance";
|
|
128
|
+
}
|
|
129
|
+
return worst;
|
|
130
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 05 — Viewport model.
|
|
3
|
+
*
|
|
4
|
+
* This module is the **only** place permitted to read DOM viewport state
|
|
5
|
+
* (`scrollTop` / `scrollLeft`) and DPR. Contract G2 in
|
|
6
|
+
* `docs/architecture/05-geometry-projection.md` forbids every other file
|
|
7
|
+
* under `src/runtime/geometry/**` from reading DOM layout; the CI guard at
|
|
8
|
+
* `scripts/ci-check-geometry-no-dom-authority.mjs` enforces this by
|
|
9
|
+
* rejecting `getBoundingClientRect`, `offsetTop/Left/Width/Height`,
|
|
10
|
+
* `clientHeight`, and `offsetParent` outside this file. Slice 3 additionally
|
|
11
|
+
* permits this module to read viewport geometry from a host-supplied
|
|
12
|
+
* editor-root element.
|
|
13
|
+
*
|
|
14
|
+
* Slice 1: stub `createViewport()` returning a frozen no-DPR value.
|
|
15
|
+
* Slice 3 (this slice): adds `createViewportFromRoot(root, { pxPerTwip })`
|
|
16
|
+
* which attaches passive scroll + resize listeners to the root, reads
|
|
17
|
+
* `scrollLeft` / `scrollTop` only — per G2's viewport exception — and
|
|
18
|
+
* emits a new `Viewport` to subscribers via rAF coalescing (performance
|
|
19
|
+
* invariant 1). The stub `createViewport()` remains for tests and for
|
|
20
|
+
* wiring sites that have not yet flipped to a real root.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Viewport, ViewportListener } from "./geometry-types.ts";
|
|
24
|
+
|
|
25
|
+
export interface ViewportHandle {
|
|
26
|
+
getViewport(): Viewport;
|
|
27
|
+
subscribe(listener: ViewportListener): () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Tear down any DOM listeners + cancel pending rAF frames. Always safe
|
|
30
|
+
* to call; no-op on the stub handle from `createViewport()`.
|
|
31
|
+
*/
|
|
32
|
+
dispose(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_VIEWPORT: Viewport = {
|
|
36
|
+
scrollLeftPx: 0,
|
|
37
|
+
scrollTopPx: 0,
|
|
38
|
+
dpr: 1,
|
|
39
|
+
pxPerTwip: 0,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Slice 1 stub — preserved for tests and for Slice-2a-wired call sites
|
|
44
|
+
* that do not yet have a live editor root. Returns a frozen viewport and
|
|
45
|
+
* a no-op subscribe/dispose.
|
|
46
|
+
*/
|
|
47
|
+
export function createViewport(): ViewportHandle {
|
|
48
|
+
return {
|
|
49
|
+
getViewport() {
|
|
50
|
+
return DEFAULT_VIEWPORT;
|
|
51
|
+
},
|
|
52
|
+
subscribe() {
|
|
53
|
+
return () => {};
|
|
54
|
+
},
|
|
55
|
+
dispose() {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CreateViewportFromRootInput {
|
|
60
|
+
/**
|
|
61
|
+
* Editor root element. `scrollLeft` / `scrollTop` are read from here on
|
|
62
|
+
* scroll and resize; no other DOM fields are read (per G2). When the
|
|
63
|
+
* argument is `null` or the element is detached, the handle falls back
|
|
64
|
+
* to the stub viewport and `subscribe` never fires — matches the
|
|
65
|
+
* degraded Slice-1 path.
|
|
66
|
+
*/
|
|
67
|
+
root: HTMLElement | null;
|
|
68
|
+
/**
|
|
69
|
+
* Current `pxPerTwip` from the render kernel's zoom. The handle stores
|
|
70
|
+
* it so consumers reading `Viewport.pxPerTwip` see the zoom the kernel
|
|
71
|
+
* used for the latest frame. Update via `setPxPerTwip(next)` when the
|
|
72
|
+
* kernel emits a `zoom_changed` event.
|
|
73
|
+
*/
|
|
74
|
+
pxPerTwip: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface RootViewportHandle extends ViewportHandle {
|
|
78
|
+
/**
|
|
79
|
+
* Push a new `pxPerTwip` value. Notifies subscribers immediately so
|
|
80
|
+
* chrome overlays re-project on zoom changes without waiting for a
|
|
81
|
+
* scroll/resize event.
|
|
82
|
+
*/
|
|
83
|
+
setPxPerTwip(value: number): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Slice 3 — DOM-backed viewport. Listens for passive scroll + ResizeObserver
|
|
88
|
+
* resize events on `root`; rAF-coalesces reads into a single listener
|
|
89
|
+
* fan-out per animation frame (performance invariant 1). Reads only
|
|
90
|
+
* `scrollLeft` / `scrollTop`; no layout-flushing geometry reads.
|
|
91
|
+
*/
|
|
92
|
+
export function createViewportFromRoot(
|
|
93
|
+
input: CreateViewportFromRootInput,
|
|
94
|
+
): RootViewportHandle {
|
|
95
|
+
const { root } = input;
|
|
96
|
+
if (!root) {
|
|
97
|
+
// Degraded path — behaves like the Slice-1 stub but exposes the
|
|
98
|
+
// setPxPerTwip entry point so call sites don't have to branch on
|
|
99
|
+
// whether a root was available.
|
|
100
|
+
let pxPerTwip = input.pxPerTwip;
|
|
101
|
+
const listeners = new Set<ViewportListener>();
|
|
102
|
+
const readOnce = (): Viewport => ({
|
|
103
|
+
...DEFAULT_VIEWPORT,
|
|
104
|
+
pxPerTwip,
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
getViewport: readOnce,
|
|
108
|
+
subscribe(listener) {
|
|
109
|
+
listeners.add(listener);
|
|
110
|
+
return () => listeners.delete(listener);
|
|
111
|
+
},
|
|
112
|
+
dispose() {
|
|
113
|
+
listeners.clear();
|
|
114
|
+
},
|
|
115
|
+
setPxPerTwip(value) {
|
|
116
|
+
if (value === pxPerTwip) return;
|
|
117
|
+
pxPerTwip = value;
|
|
118
|
+
const snapshot = readOnce();
|
|
119
|
+
for (const listener of listeners) listener(snapshot);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const win = root.ownerDocument?.defaultView ?? null;
|
|
125
|
+
let pxPerTwip = input.pxPerTwip;
|
|
126
|
+
const listeners = new Set<ViewportListener>();
|
|
127
|
+
let rafHandle: number | null = null;
|
|
128
|
+
let disposed = false;
|
|
129
|
+
|
|
130
|
+
const read = (): Viewport => ({
|
|
131
|
+
// G2 exception: scroll position is a viewport input, not a document-
|
|
132
|
+
// structure input. This is the one place in `src/runtime/geometry/`
|
|
133
|
+
// that reads these fields.
|
|
134
|
+
scrollLeftPx: root.scrollLeft,
|
|
135
|
+
scrollTopPx: root.scrollTop,
|
|
136
|
+
dpr:
|
|
137
|
+
typeof win?.devicePixelRatio === "number" ? win.devicePixelRatio : 1,
|
|
138
|
+
pxPerTwip,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
let current = read();
|
|
142
|
+
|
|
143
|
+
const fanOut = () => {
|
|
144
|
+
if (disposed) return;
|
|
145
|
+
const next = read();
|
|
146
|
+
if (
|
|
147
|
+
next.scrollLeftPx === current.scrollLeftPx &&
|
|
148
|
+
next.scrollTopPx === current.scrollTopPx &&
|
|
149
|
+
next.dpr === current.dpr &&
|
|
150
|
+
next.pxPerTwip === current.pxPerTwip
|
|
151
|
+
) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
current = next;
|
|
155
|
+
for (const listener of listeners) listener(next);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const schedule = () => {
|
|
159
|
+
if (disposed) return;
|
|
160
|
+
if (rafHandle !== null) return;
|
|
161
|
+
const raf = win?.requestAnimationFrame;
|
|
162
|
+
if (!raf) {
|
|
163
|
+
// Test / SSR / older jsdom — fire synchronously.
|
|
164
|
+
fanOut();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
rafHandle = raf.call(win, () => {
|
|
168
|
+
rafHandle = null;
|
|
169
|
+
fanOut();
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const onScroll = () => schedule();
|
|
174
|
+
|
|
175
|
+
root.addEventListener("scroll", onScroll, { passive: true });
|
|
176
|
+
|
|
177
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
178
|
+
const ResizeObserverCtor = (win as { ResizeObserver?: typeof ResizeObserver } | null)
|
|
179
|
+
?.ResizeObserver;
|
|
180
|
+
if (ResizeObserverCtor) {
|
|
181
|
+
resizeObserver = new ResizeObserverCtor(() => schedule());
|
|
182
|
+
resizeObserver.observe(root);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
getViewport() {
|
|
187
|
+
return current;
|
|
188
|
+
},
|
|
189
|
+
subscribe(listener) {
|
|
190
|
+
listeners.add(listener);
|
|
191
|
+
return () => {
|
|
192
|
+
listeners.delete(listener);
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
dispose() {
|
|
196
|
+
if (disposed) return;
|
|
197
|
+
disposed = true;
|
|
198
|
+
root.removeEventListener("scroll", onScroll);
|
|
199
|
+
if (resizeObserver) {
|
|
200
|
+
resizeObserver.disconnect();
|
|
201
|
+
resizeObserver = null;
|
|
202
|
+
}
|
|
203
|
+
if (rafHandle !== null && win?.cancelAnimationFrame) {
|
|
204
|
+
win.cancelAnimationFrame(rafHandle);
|
|
205
|
+
rafHandle = null;
|
|
206
|
+
}
|
|
207
|
+
listeners.clear();
|
|
208
|
+
},
|
|
209
|
+
setPxPerTwip(value) {
|
|
210
|
+
if (value === pxPerTwip) return;
|
|
211
|
+
pxPerTwip = value;
|
|
212
|
+
// Zoom change is observable immediately — bypass rAF so consumers
|
|
213
|
+
// don't see a stale pxPerTwip on the next render frame.
|
|
214
|
+
current = { ...current, pxPerTwip };
|
|
215
|
+
for (const listener of listeners) listener(current);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|