@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,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-surface arbiter — single authority over the editor's floating
|
|
3
|
+
* surfaces.
|
|
4
|
+
*
|
|
5
|
+
* Problem (audit §2.7 + RC-3): the editor mounts many floating
|
|
6
|
+
* surfaces — scope cards, suggestion cards, blocked-command cards,
|
|
7
|
+
* workflow cards, comment previews, selection-format tools, right-click
|
|
8
|
+
* context menus. Before this arbiter landed the only coordinator was
|
|
9
|
+
* the 150ms dwell timer inside `tw-selection-tool-host.tsx` (which only
|
|
10
|
+
* kept the selection toolbar from thrashing). Overlapping requests
|
|
11
|
+
* from peer surfaces regressed into visual thrash — two floats visible
|
|
12
|
+
* at once, or a high-priority surface losing to a lower-priority peer
|
|
13
|
+
* that rendered first.
|
|
14
|
+
*
|
|
15
|
+
* Contract:
|
|
16
|
+
* 1. At most ONE non-pinned surface is active per frame.
|
|
17
|
+
* 2. Priority is fixed and declared in `LOCAL_SURFACE_PRIORITY`;
|
|
18
|
+
* a higher-priority request displaces the current active peer and
|
|
19
|
+
* fires the displaced handle's `onDismiss` callback.
|
|
20
|
+
* 3. Equal-priority requests respect insertion order — first request
|
|
21
|
+
* wins; the later one is queued. Dismissing the active surface
|
|
22
|
+
* promotes the highest queued peer.
|
|
23
|
+
* 4. A PINNED surface is tracked separately from the single active
|
|
24
|
+
* slot. Pinned scope cards may coexist with any other active
|
|
25
|
+
* surface (e.g. the rail detail) — they are orthogonal slots.
|
|
26
|
+
* Only one pinned surface at a time; a second pinned request
|
|
27
|
+
* displaces the prior pin.
|
|
28
|
+
* 5. Subscribers are notified via a single rAF-coalesced tick —
|
|
29
|
+
* 50 `request()` calls in one synchronous block fire listeners
|
|
30
|
+
* exactly once. This is perf invariant #4: no widening of the
|
|
31
|
+
* wholesale-snapshot path.
|
|
32
|
+
* 6. The arbiter itself NEVER subscribes to PM transactions. It
|
|
33
|
+
* consumes user-event-driven `request()` calls only.
|
|
34
|
+
*
|
|
35
|
+
* Surface landscape (audit §2.7):
|
|
36
|
+
*
|
|
37
|
+
* scope-card > suggestion-card > blocked-card > workflow-card >
|
|
38
|
+
* comment-preview > selection-format > context-menu
|
|
39
|
+
*
|
|
40
|
+
* Usage: callers create a request and gate rendering on the returned
|
|
41
|
+
* handle's `isActive()` flag. Dismiss the handle (explicitly or on
|
|
42
|
+
* unmount) to release the slot.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
createContext,
|
|
47
|
+
useContext,
|
|
48
|
+
useEffect,
|
|
49
|
+
useRef,
|
|
50
|
+
useSyncExternalStore,
|
|
51
|
+
} from "react";
|
|
52
|
+
|
|
53
|
+
export type LocalSurfaceKind =
|
|
54
|
+
| "scope-card"
|
|
55
|
+
| "suggestion-card"
|
|
56
|
+
| "blocked-card"
|
|
57
|
+
| "workflow-card"
|
|
58
|
+
| "comment-preview"
|
|
59
|
+
| "selection-format"
|
|
60
|
+
| "context-menu";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Priority ladder per audit §2.7. Higher number wins. Tweaking this
|
|
64
|
+
* table is a design contract change — update the audit and the docs
|
|
65
|
+
* before the code.
|
|
66
|
+
*/
|
|
67
|
+
export const LOCAL_SURFACE_PRIORITY: Readonly<Record<LocalSurfaceKind, number>> =
|
|
68
|
+
Object.freeze({
|
|
69
|
+
"scope-card": 7,
|
|
70
|
+
"suggestion-card": 6,
|
|
71
|
+
"blocked-card": 5,
|
|
72
|
+
"workflow-card": 4,
|
|
73
|
+
"comment-preview": 3,
|
|
74
|
+
"selection-format": 2,
|
|
75
|
+
"context-menu": 1,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export interface LocalSurfaceRequest {
|
|
79
|
+
readonly kind: LocalSurfaceKind;
|
|
80
|
+
/**
|
|
81
|
+
* Optional caller-provided id. Defaulted to a monotonic token when
|
|
82
|
+
* omitted. Useful for tests + audit tooling that wants to correlate
|
|
83
|
+
* a rendered surface with its request.
|
|
84
|
+
*/
|
|
85
|
+
readonly id?: string;
|
|
86
|
+
/**
|
|
87
|
+
* When true, the surface is tracked in the pinned slot instead of the
|
|
88
|
+
* single active slot — it coexists with any other active surface.
|
|
89
|
+
* Used for pinned scope cards.
|
|
90
|
+
*/
|
|
91
|
+
readonly pinned?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Called when the arbiter releases the slot via priority displacement,
|
|
94
|
+
* `dismissTopmost`, or `dismissAll`. NOT called when the caller
|
|
95
|
+
* voluntarily invokes `handle.dismiss()` — that path is self-managed
|
|
96
|
+
* by the caller.
|
|
97
|
+
*/
|
|
98
|
+
readonly onDismiss?: () => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ArbiterState {
|
|
102
|
+
readonly activeKind: LocalSurfaceKind | null;
|
|
103
|
+
readonly activeId: string | null;
|
|
104
|
+
readonly pinnedKind: LocalSurfaceKind | null;
|
|
105
|
+
readonly pinnedId: string | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface LocalSurfaceHandle {
|
|
109
|
+
readonly kind: LocalSurfaceKind;
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly pinned: boolean;
|
|
112
|
+
/** Whether this handle currently owns the active slot (or a pinned slot). */
|
|
113
|
+
isActive(): boolean;
|
|
114
|
+
/** Release this handle's slot without calling onDismiss. */
|
|
115
|
+
dismiss(): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface LocalSurfaceArbiter {
|
|
119
|
+
/** Submit a surface request. Returns a handle for gating + release. */
|
|
120
|
+
request(req: LocalSurfaceRequest): LocalSurfaceHandle;
|
|
121
|
+
/** Subscribe to arbiter state changes. Returns an unsubscribe fn. */
|
|
122
|
+
subscribe(listener: (state: ArbiterState) => void): () => void;
|
|
123
|
+
/** Synchronous state snapshot. */
|
|
124
|
+
getState(): ArbiterState;
|
|
125
|
+
/** Dismiss the non-pinned active slot; leaves pinned surface intact. */
|
|
126
|
+
dismissTopmost(): void;
|
|
127
|
+
/** Dismiss every tracked surface, pinned and active. */
|
|
128
|
+
dismissAll(): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface InternalEntry {
|
|
132
|
+
/**
|
|
133
|
+
* Populated immediately after the entry is constructed — see the
|
|
134
|
+
* `request()` initialisation sequence. Typed as `LocalSurfaceHandle
|
|
135
|
+
* | undefined` rather than a non-null cast so no read site silently
|
|
136
|
+
* dereferences a pre-assignment entry (M2 — type safety).
|
|
137
|
+
*/
|
|
138
|
+
handle: LocalSurfaceHandle | undefined;
|
|
139
|
+
readonly kind: LocalSurfaceKind;
|
|
140
|
+
readonly id: string;
|
|
141
|
+
readonly pinned: boolean;
|
|
142
|
+
readonly onDismiss?: () => void;
|
|
143
|
+
readonly sequence: number;
|
|
144
|
+
released: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* rAF-coalesced dispatcher. All state mutations enqueue a single
|
|
149
|
+
* dispatch in the next animation frame; synchronous callers see the
|
|
150
|
+
* updated `getState()` immediately, but subscribers receive one tick
|
|
151
|
+
* per frame regardless of how many mutations happened.
|
|
152
|
+
*
|
|
153
|
+
* We check `typeof` before using `requestAnimationFrame` because the
|
|
154
|
+
* arbiter runs in test environments (node:test) that do not expose rAF.
|
|
155
|
+
* In that case we fall back to a microtask; tests can await it with
|
|
156
|
+
* `setTimeout(0)` and see exactly one notification per synchronous
|
|
157
|
+
* burst. This mirrors the pattern in
|
|
158
|
+
* `tw-page-stack-overlay-layer.tsx:327-344`.
|
|
159
|
+
*/
|
|
160
|
+
function scheduleFrame(cb: () => void): void {
|
|
161
|
+
if (typeof globalThis.requestAnimationFrame === "function") {
|
|
162
|
+
globalThis.requestAnimationFrame(cb);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Fallback: microtask. Tests and non-browser runtimes take this
|
|
166
|
+
// path; production never does because DOM always exposes rAF.
|
|
167
|
+
queueMicrotask(cb);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let sequenceCounter = 0;
|
|
171
|
+
function nextSequence(): number {
|
|
172
|
+
sequenceCounter += 1;
|
|
173
|
+
return sequenceCounter;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function createLocalSurfaceArbiter(): LocalSurfaceArbiter {
|
|
177
|
+
const entries: InternalEntry[] = [];
|
|
178
|
+
const listeners = new Set<(state: ArbiterState) => void>();
|
|
179
|
+
let pendingFrame = false;
|
|
180
|
+
// Last state emitted to subscribers — used to suppress redundant
|
|
181
|
+
// ticks when a mutation doesn't change the computed `ArbiterState`
|
|
182
|
+
// (N1 — a lower-priority request that simply queues behind an
|
|
183
|
+
// existing active would otherwise fire one subscriber tick per
|
|
184
|
+
// request even though no consumer's `isActive()` changed).
|
|
185
|
+
let lastEmittedSnapshot: string | null = null;
|
|
186
|
+
|
|
187
|
+
// Consumer `onDismiss` callbacks typically do `setState` on the
|
|
188
|
+
// dismissed surface's host (see `useContextMenuController.dismiss`).
|
|
189
|
+
// Priority displacement fires during another component's render
|
|
190
|
+
// (the requester that owns the slot now) because `useLocalSurfaceRequest`
|
|
191
|
+
// issues the request synchronously in render body to get first-paint
|
|
192
|
+
// correct behavior. A synchronous `setState` across components during
|
|
193
|
+
// render triggers React's "Cannot update a component while rendering
|
|
194
|
+
// a different component" warning and can be dropped in concurrent
|
|
195
|
+
// retry passes. Defer every consumer-triggered `onDismiss` by one
|
|
196
|
+
// microtask — the arbiter state itself already updated synchronously
|
|
197
|
+
// (entry.released = true), so the next subscriber tick + next
|
|
198
|
+
// `isActive()` read already see the correct verdict; only the
|
|
199
|
+
// cross-component state sync needs the microtask boundary.
|
|
200
|
+
function scheduleOnDismiss(cb: () => void): void {
|
|
201
|
+
queueMicrotask(cb);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function recomputeState(): ArbiterState {
|
|
205
|
+
let pinned: InternalEntry | null = null;
|
|
206
|
+
let active: InternalEntry | null = null;
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
if (entry.released) continue;
|
|
209
|
+
if (entry.pinned) {
|
|
210
|
+
if (!pinned || entry.sequence > pinned.sequence) {
|
|
211
|
+
pinned = entry;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
if (entry.released || entry.pinned) continue;
|
|
217
|
+
if (!active) {
|
|
218
|
+
active = entry;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const activeP = LOCAL_SURFACE_PRIORITY[active.kind];
|
|
222
|
+
const candidateP = LOCAL_SURFACE_PRIORITY[entry.kind];
|
|
223
|
+
if (
|
|
224
|
+
candidateP > activeP ||
|
|
225
|
+
(candidateP === activeP && entry.sequence < active.sequence)
|
|
226
|
+
) {
|
|
227
|
+
active = entry;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
activeKind: active ? active.kind : null,
|
|
232
|
+
activeId: active ? active.id : null,
|
|
233
|
+
pinnedKind: pinned ? pinned.kind : null,
|
|
234
|
+
pinnedId: pinned ? pinned.id : null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function scheduleDispatch(): void {
|
|
239
|
+
if (pendingFrame) return;
|
|
240
|
+
pendingFrame = true;
|
|
241
|
+
scheduleFrame(() => {
|
|
242
|
+
pendingFrame = false;
|
|
243
|
+
const state = recomputeState();
|
|
244
|
+
// N1 — skip the subscriber tick when the snapshot is identical
|
|
245
|
+
// to the last one we emitted. Subscribers only need wake-ups on
|
|
246
|
+
// observable state transitions. Cheap stable serialization
|
|
247
|
+
// because the snapshot has exactly four primitive fields.
|
|
248
|
+
const serialized = `${state.activeKind ?? ""}:${state.activeId ?? ""}:${state.pinnedKind ?? ""}:${state.pinnedId ?? ""}`;
|
|
249
|
+
if (serialized === lastEmittedSnapshot) return;
|
|
250
|
+
lastEmittedSnapshot = serialized;
|
|
251
|
+
for (const listener of listeners) {
|
|
252
|
+
listener(state);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function currentHolders(): { active: InternalEntry | null; pinned: InternalEntry | null } {
|
|
258
|
+
const state = recomputeState();
|
|
259
|
+
const active =
|
|
260
|
+
entries.find((e) => !e.released && !e.pinned && e.id === state.activeId) ?? null;
|
|
261
|
+
const pinned =
|
|
262
|
+
entries.find((e) => !e.released && e.pinned && e.id === state.pinnedId) ?? null;
|
|
263
|
+
return { active, pinned };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function request(req: LocalSurfaceRequest): LocalSurfaceHandle {
|
|
267
|
+
const id = req.id ?? `local-surface-${nextSequence()}`;
|
|
268
|
+
const pinned = req.pinned ?? false;
|
|
269
|
+
|
|
270
|
+
// A new pinned request displaces any prior pinned entry — invariant
|
|
271
|
+
// "only one pinned surface at a time" per §2.7. Consumer onDismiss
|
|
272
|
+
// is deferred via `scheduleOnDismiss` to avoid cross-component
|
|
273
|
+
// setState during the requester's render (see C1 fix above).
|
|
274
|
+
if (pinned) {
|
|
275
|
+
for (const existing of entries) {
|
|
276
|
+
if (existing.released || !existing.pinned) continue;
|
|
277
|
+
existing.released = true;
|
|
278
|
+
const dismissCb = existing.onDismiss;
|
|
279
|
+
if (dismissCb) scheduleOnDismiss(dismissCb);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// For the non-pinned slot, priority rules apply. A higher-priority
|
|
284
|
+
// request displaces the current active; lower-priority requests
|
|
285
|
+
// simply queue and `isActive` returns false until they win.
|
|
286
|
+
if (!pinned) {
|
|
287
|
+
const { active } = currentHolders();
|
|
288
|
+
if (active) {
|
|
289
|
+
const activeP = LOCAL_SURFACE_PRIORITY[active.kind];
|
|
290
|
+
const candidateP = LOCAL_SURFACE_PRIORITY[req.kind];
|
|
291
|
+
if (candidateP > activeP) {
|
|
292
|
+
active.released = true;
|
|
293
|
+
const dismissCb = active.onDismiss;
|
|
294
|
+
if (dismissCb) scheduleOnDismiss(dismissCb);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const entry: InternalEntry = {
|
|
300
|
+
kind: req.kind,
|
|
301
|
+
id,
|
|
302
|
+
pinned,
|
|
303
|
+
sequence: nextSequence(),
|
|
304
|
+
released: false,
|
|
305
|
+
...(req.onDismiss ? { onDismiss: req.onDismiss } : {}),
|
|
306
|
+
// Assigned immediately below. Typed as optional so no read path
|
|
307
|
+
// silently dereferences a pre-assignment entry.
|
|
308
|
+
handle: undefined,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const handle: LocalSurfaceHandle = {
|
|
312
|
+
kind: req.kind,
|
|
313
|
+
id,
|
|
314
|
+
pinned,
|
|
315
|
+
isActive: () => {
|
|
316
|
+
if (entry.released) return false;
|
|
317
|
+
const state = recomputeState();
|
|
318
|
+
return entry.pinned ? state.pinnedId === id : state.activeId === id;
|
|
319
|
+
},
|
|
320
|
+
dismiss: () => {
|
|
321
|
+
if (entry.released) return;
|
|
322
|
+
entry.released = true;
|
|
323
|
+
scheduleDispatch();
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
entry.handle = handle;
|
|
327
|
+
|
|
328
|
+
entries.push(entry);
|
|
329
|
+
scheduleDispatch();
|
|
330
|
+
return handle;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function subscribe(listener: (state: ArbiterState) => void): () => void {
|
|
334
|
+
listeners.add(listener);
|
|
335
|
+
return () => {
|
|
336
|
+
listeners.delete(listener);
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function dismissTopmost(): void {
|
|
341
|
+
const { active } = currentHolders();
|
|
342
|
+
if (active) {
|
|
343
|
+
active.released = true;
|
|
344
|
+
const dismissCb = active.onDismiss;
|
|
345
|
+
if (dismissCb) scheduleOnDismiss(dismissCb);
|
|
346
|
+
scheduleDispatch();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function dismissAll(): void {
|
|
351
|
+
let changed = false;
|
|
352
|
+
for (const entry of entries) {
|
|
353
|
+
if (entry.released) continue;
|
|
354
|
+
entry.released = true;
|
|
355
|
+
const dismissCb = entry.onDismiss;
|
|
356
|
+
if (dismissCb) scheduleOnDismiss(dismissCb);
|
|
357
|
+
changed = true;
|
|
358
|
+
}
|
|
359
|
+
if (changed) scheduleDispatch();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Cache the most recent state snapshot so successive `getState()` calls
|
|
363
|
+
// return reference-equal results — required by `useSyncExternalStore`,
|
|
364
|
+
// which calls `getSnapshot` on every render and bails out on
|
|
365
|
+
// `Object.is` equality. Without this cache, `useLocalSurfaceArbiterState`
|
|
366
|
+
// (Chrome Closure Pass · Task 2) re-renders on every commit and the
|
|
367
|
+
// host hits "Maximum update depth exceeded".
|
|
368
|
+
let cachedSnapshot: ArbiterState | null = null;
|
|
369
|
+
let cachedSnapshotKey: string | null = null;
|
|
370
|
+
|
|
371
|
+
function getStateCached(): ArbiterState {
|
|
372
|
+
const fresh = recomputeState();
|
|
373
|
+
const key = `${fresh.activeKind ?? ""}:${fresh.activeId ?? ""}:${fresh.pinnedKind ?? ""}:${fresh.pinnedId ?? ""}`;
|
|
374
|
+
if (cachedSnapshot && cachedSnapshotKey === key) {
|
|
375
|
+
return cachedSnapshot;
|
|
376
|
+
}
|
|
377
|
+
cachedSnapshot = fresh;
|
|
378
|
+
cachedSnapshotKey = key;
|
|
379
|
+
return fresh;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
request,
|
|
384
|
+
subscribe,
|
|
385
|
+
getState: getStateCached,
|
|
386
|
+
dismissTopmost,
|
|
387
|
+
dismissAll,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* App-wide default arbiter — fallback for consumers that render outside
|
|
393
|
+
* the `LocalSurfaceArbiterContext.Provider` wrapped by `TwReviewWorkspace`.
|
|
394
|
+
*
|
|
395
|
+
* **Production contract:** the workspace always supplies a per-instance
|
|
396
|
+
* arbiter via context, so every in-tree chrome consumer gets workspace
|
|
397
|
+
* isolation for free. The default singleton exists for two narrow
|
|
398
|
+
* cases:
|
|
399
|
+
* 1. unit tests that render a consumer component directly without
|
|
400
|
+
* wrapping it in the provider;
|
|
401
|
+
* 2. host integrations that mount chrome components outside the
|
|
402
|
+
* `TwReviewWorkspace` subtree (rare, but supported).
|
|
403
|
+
*
|
|
404
|
+
* **Known limitation:** multiple `TwReviewWorkspace` instances on the
|
|
405
|
+
* same page each create their own arbiter (isolated); any two
|
|
406
|
+
* components that fall through to this default instead of a provider
|
|
407
|
+
* will share its slot table — symptoms are cross-editor priority
|
|
408
|
+
* displacement. Wrap every mount point in a provider to avoid.
|
|
409
|
+
*/
|
|
410
|
+
export const defaultLocalSurfaceArbiter: LocalSurfaceArbiter =
|
|
411
|
+
createLocalSurfaceArbiter();
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* React context for the per-editor arbiter. `TwReviewWorkspace`
|
|
415
|
+
* creates a stable instance (via `useRef`) and provides it here so
|
|
416
|
+
* every local surface inside the workspace coordinates through the
|
|
417
|
+
* same object. Consumers read via `useLocalSurfaceArbiter()` which
|
|
418
|
+
* falls back to the default singleton when no provider exists.
|
|
419
|
+
*/
|
|
420
|
+
export const LocalSurfaceArbiterContext =
|
|
421
|
+
createContext<LocalSurfaceArbiter | null>(null);
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Resolve the arbiter from context, falling back to the default
|
|
425
|
+
* singleton. This is the canonical entry point for floating-surface
|
|
426
|
+
* consumers.
|
|
427
|
+
*/
|
|
428
|
+
export function useLocalSurfaceArbiter(): LocalSurfaceArbiter {
|
|
429
|
+
return useContext(LocalSurfaceArbiterContext) ?? defaultLocalSurfaceArbiter;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Subscribe to arbiter state for read-only consumers (Chrome Closure
|
|
434
|
+
* Pass · Task 2). Returns the current `ArbiterState` and re-renders
|
|
435
|
+
* the consumer when it changes. Used by `TwContextMenuPortal` /
|
|
436
|
+
* `TwContextMenu` to project active + pinned `LocalSurfaceKind` into
|
|
437
|
+
* the menu's `ContextMenuContext` for dedupe-without-suppression
|
|
438
|
+
* (designsystem.md §6.24 — context menu must not vanish when local
|
|
439
|
+
* chrome is visible).
|
|
440
|
+
*/
|
|
441
|
+
export function useLocalSurfaceArbiterState(): ArbiterState {
|
|
442
|
+
const arbiter = useLocalSurfaceArbiter();
|
|
443
|
+
return useSyncExternalStore(
|
|
444
|
+
arbiter.subscribe,
|
|
445
|
+
arbiter.getState,
|
|
446
|
+
arbiter.getState,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Declarative hook: given a request (or `null` when no surface is
|
|
452
|
+
* needed) returns whether the consumer currently owns its slot. The
|
|
453
|
+
* hook requests synchronously on first render so initial paint already
|
|
454
|
+
* reflects the arbiter's verdict — callers that read the rendered DOM
|
|
455
|
+
* in the same frame (e.g. imperative layoutEffect positioners) see the
|
|
456
|
+
* final active state, not the lag-one-render state that a plain
|
|
457
|
+
* `useState` + `useEffect` pair would produce.
|
|
458
|
+
*
|
|
459
|
+
* Passing `null` releases any prior handle. The hook intentionally
|
|
460
|
+
* reads the arbiter from context, not from a parameter, so tests that
|
|
461
|
+
* want isolation wrap consumers in a `LocalSurfaceArbiterContext.Provider`
|
|
462
|
+
* with a fresh arbiter.
|
|
463
|
+
*/
|
|
464
|
+
export function useLocalSurfaceRequest(
|
|
465
|
+
request: LocalSurfaceRequest | null,
|
|
466
|
+
): boolean {
|
|
467
|
+
const arbiter = useLocalSurfaceArbiter();
|
|
468
|
+
|
|
469
|
+
// Request shape we last issued to the arbiter. When the caller passes
|
|
470
|
+
// a new (kind, id, pinned) triple the hook dismisses the prior handle
|
|
471
|
+
// and issues a fresh one — synchronously, inside render — so the first
|
|
472
|
+
// paint sees the real verdict. Side effects in render are usually
|
|
473
|
+
// forbidden, but this is idempotent: repeated calls with the same key
|
|
474
|
+
// skip the request, and strict-mode double-invocation dismisses the
|
|
475
|
+
// spare on the cleanup pass.
|
|
476
|
+
//
|
|
477
|
+
// I3 fix — the dedup key intentionally excludes `onDismiss` identity
|
|
478
|
+
// so a consumer that rebuilds the callback per render doesn't thrash
|
|
479
|
+
// the arbiter. To prevent stale-closure leaks, the hook keeps the
|
|
480
|
+
// consumer's latest `onDismiss` in a ref and forwards it via a stable
|
|
481
|
+
// wrapper that the arbiter holds — the arbiter's view of "onDismiss"
|
|
482
|
+
// always reads through the ref, so priority-displacement callbacks
|
|
483
|
+
// see the freshest closure.
|
|
484
|
+
const handleRef = useRef<LocalSurfaceHandle | null>(null);
|
|
485
|
+
const onDismissRef = useRef<(() => void) | undefined>(request?.onDismiss);
|
|
486
|
+
onDismissRef.current = request?.onDismiss;
|
|
487
|
+
const currentKey =
|
|
488
|
+
request === null
|
|
489
|
+
? null
|
|
490
|
+
: `${request.kind}:${request.id ?? ""}:${request.pinned ? "p" : "a"}`;
|
|
491
|
+
const lastKeyRef = useRef<string | null>(null);
|
|
492
|
+
|
|
493
|
+
if (currentKey !== lastKeyRef.current) {
|
|
494
|
+
handleRef.current?.dismiss();
|
|
495
|
+
if (request !== null) {
|
|
496
|
+
const { onDismiss: _consumerOnDismiss, ...rest } = request;
|
|
497
|
+
handleRef.current = arbiter.request({
|
|
498
|
+
...rest,
|
|
499
|
+
onDismiss: () => onDismissRef.current?.(),
|
|
500
|
+
});
|
|
501
|
+
} else {
|
|
502
|
+
handleRef.current = null;
|
|
503
|
+
}
|
|
504
|
+
lastKeyRef.current = currentKey;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Unmount cleanup — React runs this on component unmount, so the
|
|
508
|
+
// handle releases its arbiter slot. We re-issue in the render body
|
|
509
|
+
// above when `currentKey` changes, so this effect has an empty dep
|
|
510
|
+
// list intentionally.
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
return () => {
|
|
513
|
+
handleRef.current?.dismiss();
|
|
514
|
+
handleRef.current = null;
|
|
515
|
+
lastKeyRef.current = null;
|
|
516
|
+
};
|
|
517
|
+
}, []);
|
|
518
|
+
|
|
519
|
+
// Subscribe via `useSyncExternalStore` so React suspends on arbiter
|
|
520
|
+
// state and paints the correct value on the first frame.
|
|
521
|
+
//
|
|
522
|
+
// The server snapshot reads the same `handleRef.current?.isActive()`
|
|
523
|
+
// as the client — the request is issued synchronously in the render
|
|
524
|
+
// body above, so the handle already exists when SSR / static rendering
|
|
525
|
+
// (e.g. `renderToStaticMarkup`) asks for the initial state. Returning
|
|
526
|
+
// `false` unconditionally here would cause static renders to skip
|
|
527
|
+
// every arbiter-gated consumer (regressed rail-scope-sync in Phase G
|
|
528
|
+
// full-suite run).
|
|
529
|
+
return useSyncExternalStore(
|
|
530
|
+
(listener) => arbiter.subscribe(listener),
|
|
531
|
+
() => handleRef.current?.isActive() ?? false,
|
|
532
|
+
() => handleRef.current?.isActive() ?? false,
|
|
533
|
+
);
|
|
534
|
+
}
|