@beyondwork/docx-react-component 1.0.71 → 1.0.73
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 +964 -75
- package/package.json +1 -1
- package/src/api/public-types.ts +280 -1
- package/src/api/v3/_create.ts +16 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/evaluate.ts +113 -0
- package/src/api/v3/ai/outline.ts +140 -0
- package/src/api/v3/ai/policy.ts +31 -0
- package/src/api/v3/ai/replacement.ts +8 -0
- package/src/api/v3/ai/review.ts +342 -0
- package/src/api/v3/ai/stats.ts +62 -0
- package/src/api/v3/runtime/viewport.ts +181 -0
- package/src/api/v3/runtime/workflow.ts +114 -1
- package/src/api/v3/ui/_types.ts +35 -0
- package/src/api/v3/ui/chrome-preset-model.ts +6 -0
- package/src/api/v3/ui/index.ts +1 -0
- package/src/api/v3/ui/viewport.ts +112 -0
- package/src/compare/diff-engine.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/table-structure-commands.ts +1 -0
- package/src/core/state/editor-state.ts +49 -6
- package/src/io/export/serialize-footnotes.ts +6 -0
- package/src/io/export/serialize-headers-footers.ts +7 -0
- package/src/io/export/serialize-main-document.ts +20 -0
- package/src/io/export/serialize-paragraph-formatting.ts +34 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/normalize/normalize-text.ts +49 -2
- package/src/io/ooxml/parse-headers-footers.ts +31 -0
- package/src/io/ooxml/parse-main-document.ts +148 -7
- package/src/io/ooxml/parse-paragraph-formatting.ts +105 -0
- package/src/model/canonical-document.ts +401 -1
- package/src/runtime/formatting/formatting-context.ts +2 -1
- package/src/runtime/geometry/overlay-rects.ts +7 -10
- package/src/runtime/layout/layout-engine-version.ts +278 -1
- package/src/runtime/layout/paginated-layout-engine.ts +181 -8
- package/src/runtime/layout/resolved-formatting-state.ts +108 -13
- package/src/runtime/markdown-sanitizer.ts +21 -4
- package/src/runtime/render/render-kernel.ts +21 -1
- package/src/runtime/scopes/action-validation.ts +30 -4
- package/src/runtime/scopes/audit-bundle.ts +8 -0
- package/src/runtime/scopes/compiler-service.ts +1 -0
- package/src/runtime/scopes/enumerate-scopes.ts +61 -3
- package/src/runtime/scopes/replacement/apply.ts +50 -3
- package/src/runtime/scopes/scope-kinds/paragraph.ts +170 -7
- package/src/runtime/scopes/semantic-scope-types.ts +27 -0
- package/src/runtime/surface-projection.ts +77 -0
- package/src/runtime/workflow/coordinator.ts +3 -0
- package/src/runtime/workflow/scope-writer.ts +34 -0
- package/src/session/export/embedded-reconstitute.ts +37 -3
- package/src/session/import/embedded-offload.ts +26 -1
- package/src/session/import/loader-types.ts +18 -0
- package/src/session/import/loader.ts +2 -0
- package/src/shell/media-previews.ts +8 -6
- package/src/ui/WordReviewEditor.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +11 -0
- package/src/ui/headless/selection-helpers.ts +2 -2
- package/src/ui/runtime-shortcut-dispatch.ts +4 -4
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +22 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +11 -11
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +5 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +18 -1
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +22 -6
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +18 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +98 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +50 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +6 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +8 -1
- package/src/ui-tailwind/editor-surface/search-plugin.ts +2 -4
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +114 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +12 -4
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +29 -4
- package/src/ui-tailwind/index.ts +4 -2
- package/src/ui-tailwind/page-chrome-model.ts +5 -7
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +54 -34
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +10 -1
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +8 -1
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +11 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +139 -10
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/page-chrome.ts +4 -4
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +1 -1
- package/src/ui-tailwind/theme/editor-theme.css +15 -16
- package/src/ui-tailwind/tw-review-workspace.tsx +22 -14
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @endStateApi v3 — `runtime.viewport` family.
|
|
3
|
+
*
|
|
4
|
+
* Closes coord-07 §2.9 / coord-10 §γ — page-anchor + page-geometry
|
|
5
|
+
* primitives the visual-fidelity harness (and any future "go to page N"
|
|
6
|
+
* host) consumes through `ui.viewport.scrollToPage(n)`.
|
|
7
|
+
*
|
|
8
|
+
* Background (coord-07 §2.9 / coord-10 §γ filed 2026-04-23). The
|
|
9
|
+
* visual-fidelity harness approximated page navigation via PM
|
|
10
|
+
* `[data-page-frame='N']` boundary widgets + `scrollIntoView`. Three
|
|
11
|
+
* problems surfaced:
|
|
12
|
+
* 1. Widget naming drifted once already (`data-page-index` → `data-page-frame`).
|
|
13
|
+
* 2. Boundary widgets are zero-height strips between pages, not on
|
|
14
|
+
* page content — scrolling one to viewport-top parks the camera
|
|
15
|
+
* between pages.
|
|
16
|
+
* 3. Page 1 + final page have no boundary widget (only N-1 widgets for
|
|
17
|
+
* N pages).
|
|
18
|
+
*
|
|
19
|
+
* A runtime-owned primitive backed by the layout page graph eliminates
|
|
20
|
+
* both the chrome-drift risk and the gap-vs-content problem. Pure
|
|
21
|
+
* reads; `stateClass: "A-canonical"` + `persistsTo: "canonical"` — the
|
|
22
|
+
* layout facet is derived canonical state.
|
|
23
|
+
*
|
|
24
|
+
* `elementId` stays `null` until L11's per-page content wrapper (coord-11
|
|
25
|
+
* §19 / §P) lands with a stable `data-page-frame-start="page-<section>-<page>"`
|
|
26
|
+
* id. Until then callers rely on `scrollY` / `pageRect` for positioning.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
30
|
+
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
31
|
+
import { DEFAULT_PX_PER_TWIP } from "../../public-types.ts";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves a 1-based page number to a stable scroll target.
|
|
35
|
+
*
|
|
36
|
+
* - `elementId`: DOM id L11 populates on the per-page content wrapper.
|
|
37
|
+
* Returns `null` while the L11 wrapper contract (coord-11 §19 / §P)
|
|
38
|
+
* is still in flight — callers fall back to `scrollY`.
|
|
39
|
+
* - `scrollY`: scroll-container-relative Y offset of the page's top
|
|
40
|
+
* edge in CSS px. Computed as the sum of prior pages' heights via
|
|
41
|
+
* the layout facet's page graph.
|
|
42
|
+
* - `pageRect`: the page's content rectangle in the scroll container's
|
|
43
|
+
* coordinate space. `top === scrollY`; `left === 0`; `width/height`
|
|
44
|
+
* projected from the page's canonical `pageWidth` / `pageHeight`
|
|
45
|
+
* twips via `DEFAULT_PX_PER_TWIP`.
|
|
46
|
+
*/
|
|
47
|
+
export interface PageAnchor {
|
|
48
|
+
readonly elementId: string | null;
|
|
49
|
+
readonly scrollY: number;
|
|
50
|
+
readonly pageRect: {
|
|
51
|
+
readonly left: number;
|
|
52
|
+
readonly top: number;
|
|
53
|
+
readonly width: number;
|
|
54
|
+
readonly height: number;
|
|
55
|
+
} | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Per-page geometry for deterministic capture + truth-comparison.
|
|
60
|
+
* Stable across layout-recompute re-runs within the same
|
|
61
|
+
* `LAYOUT_ENGINE_VERSION`.
|
|
62
|
+
*/
|
|
63
|
+
export interface PageGeometry {
|
|
64
|
+
readonly widthPx: number;
|
|
65
|
+
readonly heightPx: number;
|
|
66
|
+
readonly marginsPx: {
|
|
67
|
+
readonly top: number;
|
|
68
|
+
readonly right: number;
|
|
69
|
+
readonly bottom: number;
|
|
70
|
+
readonly left: number;
|
|
71
|
+
};
|
|
72
|
+
/** CSS px-per-inch honoring the default kernel conversion (96 / 1440 × 1440 = 96). */
|
|
73
|
+
readonly dpi: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ================================================================== */
|
|
77
|
+
/* getPageAnchor */
|
|
78
|
+
/* ================================================================== */
|
|
79
|
+
|
|
80
|
+
export const getPageAnchorMetadata: ApiV3FnMetadata = {
|
|
81
|
+
name: "runtime.viewport.getPageAnchor",
|
|
82
|
+
status: "live-with-adapter",
|
|
83
|
+
sourceLayer: "layout-semantics",
|
|
84
|
+
liveEvidence: {
|
|
85
|
+
runnerTest: "test/api/v3/runtime/viewport-page-anchor.test.ts",
|
|
86
|
+
commit: "refactor-07-viewport-family-2026-04-24",
|
|
87
|
+
},
|
|
88
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
89
|
+
agentMetadata: {
|
|
90
|
+
readOrMutate: "read",
|
|
91
|
+
boundedScope: "document",
|
|
92
|
+
auditCategory: "viewport-read",
|
|
93
|
+
},
|
|
94
|
+
stateClass: "A-canonical",
|
|
95
|
+
persistsTo: "canonical",
|
|
96
|
+
rwdReference:
|
|
97
|
+
"§Runtime API § runtime.viewport.getPageAnchor. Reads the kernel-authoritative page frame from `handle.geometry.getPage(index).frame` — same source `ui.viewport.scrollToPage(n)` consumes (coord-10 §γ shipment `b8116b97`), so values stay consistent across the two surfaces. `elementId` stays null until L11's per-page content wrapper (coord-11 §19 / §P) lands; until then callers position off `scrollY` / `pageRect`. Promotes to `live` when the geometry facet surfaces a direct `getPageAnchor(pageNumber)` reader or when elementId is populated.",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/* ================================================================== */
|
|
101
|
+
/* getPageGeometry */
|
|
102
|
+
/* ================================================================== */
|
|
103
|
+
|
|
104
|
+
export const getPageGeometryMetadata: ApiV3FnMetadata = {
|
|
105
|
+
name: "runtime.viewport.getPageGeometry",
|
|
106
|
+
status: "live-with-adapter",
|
|
107
|
+
sourceLayer: "layout-semantics",
|
|
108
|
+
liveEvidence: {
|
|
109
|
+
runnerTest: "test/api/v3/runtime/viewport-page-anchor.test.ts",
|
|
110
|
+
commit: "refactor-07-viewport-family-2026-04-24",
|
|
111
|
+
},
|
|
112
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
113
|
+
agentMetadata: {
|
|
114
|
+
readOrMutate: "read",
|
|
115
|
+
boundedScope: "document",
|
|
116
|
+
auditCategory: "viewport-read",
|
|
117
|
+
},
|
|
118
|
+
stateClass: "A-canonical",
|
|
119
|
+
persistsTo: "canonical",
|
|
120
|
+
rwdReference:
|
|
121
|
+
"§Runtime API § runtime.viewport.getPageGeometry. Projects the selected page's `PageLayoutSnapshot` (pageWidth/Height + margins, all in twips) into CSS px via `DEFAULT_PX_PER_TWIP`. `dpi: 96` matches the kernel's default. Promotes to `live` when the layout facet projects PageLayoutSnapshot in px directly.",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/* ================================================================== */
|
|
125
|
+
/* family factory */
|
|
126
|
+
/* ================================================================== */
|
|
127
|
+
|
|
128
|
+
export function createViewportFamily(runtime: RuntimeApiHandle) {
|
|
129
|
+
const layout = runtime.layout;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
getPageAnchor(pageNumber: number): PageAnchor | null {
|
|
133
|
+
// @endStateApi — live-with-adapter. Reads the kernel-authoritative
|
|
134
|
+
// page frame from `handle.geometry.getPage(index)` — same source
|
|
135
|
+
// `ui.viewport.scrollToPage(n)` (coord-10 §γ shipment `b8116b97`)
|
|
136
|
+
// consumes. Geometry's `frame.topPx` includes page gaps + rounding
|
|
137
|
+
// the kernel applies, matching the DOM scroll-container's actual
|
|
138
|
+
// Y. Layout-only pageHeight summation would omit page gaps and
|
|
139
|
+
// diverge from both the DOM and the ui.viewport.scrollToPage return.
|
|
140
|
+
if (!layout) return null;
|
|
141
|
+
if (!Number.isInteger(pageNumber) || pageNumber < 1) return null;
|
|
142
|
+
if (pageNumber > layout.getPageCount()) return null;
|
|
143
|
+
const pageIndex = pageNumber - 1;
|
|
144
|
+
const geometry = runtime.geometry;
|
|
145
|
+
const page = geometry?.getPage(pageIndex);
|
|
146
|
+
if (!page) return null;
|
|
147
|
+
return {
|
|
148
|
+
elementId: null,
|
|
149
|
+
scrollY: page.frame.topPx,
|
|
150
|
+
pageRect: {
|
|
151
|
+
left: page.frame.leftPx,
|
|
152
|
+
top: page.frame.topPx,
|
|
153
|
+
width: page.frame.widthPx,
|
|
154
|
+
height: page.frame.heightPx,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
getPageGeometry(pageNumber: number): PageGeometry | null {
|
|
160
|
+
// @endStateApi — live-with-adapter.
|
|
161
|
+
if (!layout) return null;
|
|
162
|
+
if (!Number.isInteger(pageNumber) || pageNumber < 1) return null;
|
|
163
|
+
const pageCount = layout.getPageCount();
|
|
164
|
+
if (pageNumber > pageCount) return null;
|
|
165
|
+
const page = layout.getPage(pageNumber - 1);
|
|
166
|
+
if (!page) return null;
|
|
167
|
+
const lay = page.layout;
|
|
168
|
+
return {
|
|
169
|
+
widthPx: lay.pageWidth * DEFAULT_PX_PER_TWIP,
|
|
170
|
+
heightPx: lay.pageHeight * DEFAULT_PX_PER_TWIP,
|
|
171
|
+
marginsPx: {
|
|
172
|
+
top: lay.marginTop * DEFAULT_PX_PER_TWIP,
|
|
173
|
+
right: lay.marginRight * DEFAULT_PX_PER_TWIP,
|
|
174
|
+
bottom: lay.marginBottom * DEFAULT_PX_PER_TWIP,
|
|
175
|
+
left: lay.marginLeft * DEFAULT_PX_PER_TWIP,
|
|
176
|
+
},
|
|
177
|
+
dpi: 96,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* queryScopes (live) / getMarkup (live) / getGuard (live) /
|
|
5
5
|
* createScope (live-with-adapter) / attachMetadata (live-with-adapter) /
|
|
6
6
|
* getVisibilityPolicy · getVisibilityPolicies · setVisibilityPolicy ·
|
|
7
|
-
* clearVisibilityPolicy (live — W10 state-classes X1)
|
|
7
|
+
* clearVisibilityPolicy (live — W10 state-classes X1) /
|
|
8
|
+
* scopeTags (live — coord-10 L11-4 tag-catalog read).
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
@@ -17,6 +18,13 @@ import type {
|
|
|
17
18
|
import { emitUxResponse } from "../_ux-response.ts";
|
|
18
19
|
import { createScopeFromBlockId } from "../../../runtime/workflow/scope-writer.ts";
|
|
19
20
|
import { attachScopeMetadata } from "../../../runtime/workflow/metadata-writer.ts";
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_REGISTRY_ENTRIES,
|
|
23
|
+
DEFAULT_UNKNOWN_BEHAVIOR,
|
|
24
|
+
createScopeTagRegistry,
|
|
25
|
+
type ScopeTagBehavior,
|
|
26
|
+
type ScopeTagRegistry,
|
|
27
|
+
} from "../../../runtime/workflow/scope-tag-registry.ts";
|
|
20
28
|
|
|
21
29
|
export const queryScopesMetadata: ApiV3FnMetadata = {
|
|
22
30
|
name: "runtime.workflow.queryScopes",
|
|
@@ -79,6 +87,25 @@ export interface CreateScopeInput {
|
|
|
79
87
|
* leak in). Agents pick per scope family per coord-09 §1.14.
|
|
80
88
|
*/
|
|
81
89
|
readonly assoc?: { readonly start: -1 | 1; readonly end: -1 | 1 };
|
|
90
|
+
/**
|
|
91
|
+
* Coord-08 §9 / coord-09 §1.13 (A3) — caller-steerable identity
|
|
92
|
+
* strategy for the enumerated scope's `ScopeHandle.stableRef`.
|
|
93
|
+
* Passthrough to L06 scope-writer + L08 compiler. Honored for
|
|
94
|
+
* `"scope-id"` and `"semantic-path"` today; `"bookmark"` and
|
|
95
|
+
* `"runtime-handle"` fall back to the compiler default (bookmark
|
|
96
|
+
* lookup not wired in phase 1). See
|
|
97
|
+
* `src/runtime/scopes/enumerate-scopes.ts::stableRefHintForScopeId`.
|
|
98
|
+
*
|
|
99
|
+
* Ownership: L08 owns the selection policy; L06 owns the primitive
|
|
100
|
+
* (`scope-writer.ts::CreateScopeFromBlockIdInput.stableRefHint`);
|
|
101
|
+
* L07 is passthrough (this field); L02 owns the `ScopeStableRef`
|
|
102
|
+
* value shape.
|
|
103
|
+
*/
|
|
104
|
+
readonly stableRefHint?:
|
|
105
|
+
| "scope-id"
|
|
106
|
+
| "bookmark"
|
|
107
|
+
| "semantic-path"
|
|
108
|
+
| "runtime-handle";
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
export interface CreateScopeResult {
|
|
@@ -273,6 +300,68 @@ export const subscribeMarkupModePolicyMetadata: ApiV3FnMetadata = {
|
|
|
273
300
|
rwdReference: "§Runtime API § runtime.workflow.subscribeMarkupModePolicy",
|
|
274
301
|
};
|
|
275
302
|
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Coord-10 L11-4 — scope-tag catalog read, graduating the pragmatic
|
|
305
|
+
// `createScopeTagRegistry` re-export through `src/api/public-types.ts`
|
|
306
|
+
// (refactor/11 §4.17 commit `7a2d2fc0`, 2026-04-24) to a proper v3 family
|
|
307
|
+
// entry. Three methods: factory + catalog enumeration + per-tag peek. All
|
|
308
|
+
// A-canonical — the catalog is code-derived but describes canonical-shape
|
|
309
|
+
// invariants: every tag it catalogs is encoded into customXml (via scope-
|
|
310
|
+
// marker / bookmark / comment structures) and broadcast via crdt along
|
|
311
|
+
// with scope-marker commits.
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
export const createScopeTagRegistryMetadata: ApiV3FnMetadata = {
|
|
315
|
+
name: "runtime.workflow.createScopeTagRegistry",
|
|
316
|
+
status: "live",
|
|
317
|
+
sourceLayer: "workflow-review",
|
|
318
|
+
liveEvidence: {
|
|
319
|
+
runnerTest: "test/api/v3/runtime/workflow-scope-tags.test.ts",
|
|
320
|
+
commit: "coord-10-l11-4",
|
|
321
|
+
},
|
|
322
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
323
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "scope-tag-catalog" },
|
|
324
|
+
stateClass: "A-canonical",
|
|
325
|
+
persistsTo: "customXml",
|
|
326
|
+
broadcastsVia: "crdt",
|
|
327
|
+
rwdReference:
|
|
328
|
+
"§Runtime API § runtime.workflow.createScopeTagRegistry. Mints a fresh default-seeded ScopeTagRegistry. Hosts register custom annotation families on the returned registry via `.register(...)`. Retires the pragmatic `createScopeTagRegistry` re-export through public-types.ts (refactor/11 §4.17 2026-04-24) — the v3 family is the long-term home per coord-10 L11-4.",
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export const listScopeTagsMetadata: ApiV3FnMetadata = {
|
|
332
|
+
name: "runtime.workflow.listScopeTags",
|
|
333
|
+
status: "live",
|
|
334
|
+
sourceLayer: "workflow-review",
|
|
335
|
+
liveEvidence: {
|
|
336
|
+
runnerTest: "test/api/v3/runtime/workflow-scope-tags.test.ts",
|
|
337
|
+
commit: "coord-10-l11-4",
|
|
338
|
+
},
|
|
339
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
340
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "scope-tag-catalog" },
|
|
341
|
+
stateClass: "A-canonical",
|
|
342
|
+
persistsTo: "customXml",
|
|
343
|
+
broadcastsVia: "crdt",
|
|
344
|
+
rwdReference:
|
|
345
|
+
"§Runtime API § runtime.workflow.listScopeTags. Enumerates the shipped default catalog as `[tagType, behavior]` pairs without constructing a registry. Does not reflect host-registered custom tags — callers that want those iterate `createScopeTagRegistry().list()` on their own instance.",
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const getScopeTagMetadata: ApiV3FnMetadata = {
|
|
349
|
+
name: "runtime.workflow.getScopeTag",
|
|
350
|
+
status: "live",
|
|
351
|
+
sourceLayer: "workflow-review",
|
|
352
|
+
liveEvidence: {
|
|
353
|
+
runnerTest: "test/api/v3/runtime/workflow-scope-tags.test.ts",
|
|
354
|
+
commit: "coord-10-l11-4",
|
|
355
|
+
},
|
|
356
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
357
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "scope-tag-catalog" },
|
|
358
|
+
stateClass: "A-canonical",
|
|
359
|
+
persistsTo: "customXml",
|
|
360
|
+
broadcastsVia: "crdt",
|
|
361
|
+
rwdReference:
|
|
362
|
+
"§Runtime API § runtime.workflow.getScopeTag. Peeks a default tag's behavior by name. Returns DEFAULT_UNKNOWN_BEHAVIOR for unknown tag types (bail-on-cross) — matches `createScopeTagRegistry().get(unknownTagType)`.",
|
|
363
|
+
};
|
|
364
|
+
|
|
276
365
|
export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
277
366
|
return {
|
|
278
367
|
queryScopes(filter?: unknown) {
|
|
@@ -301,6 +390,9 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
301
390
|
mode: input.mode,
|
|
302
391
|
label: input.label,
|
|
303
392
|
...(input.assoc ? { assoc: input.assoc } : {}),
|
|
393
|
+
...(input.stableRefHint
|
|
394
|
+
? { stableRefHint: input.stableRefHint }
|
|
395
|
+
: {}),
|
|
304
396
|
});
|
|
305
397
|
emitUxResponse(runtime, {
|
|
306
398
|
apiFn: createScopeMetadata.name,
|
|
@@ -430,5 +522,26 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
430
522
|
}
|
|
431
523
|
return { status: "scope-not-found" };
|
|
432
524
|
},
|
|
525
|
+
|
|
526
|
+
createScopeTagRegistry(): ScopeTagRegistry {
|
|
527
|
+
// @endStateApi — live. Coord-10 L11-4. Mints a fresh default-
|
|
528
|
+
// seeded registry. Hosts register custom annotation families on
|
|
529
|
+
// the returned registry via `.register(...)`.
|
|
530
|
+
return createScopeTagRegistry();
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
listScopeTags(): readonly (readonly [string, ScopeTagBehavior])[] {
|
|
534
|
+
// @endStateApi — live. Coord-10 L11-4. Enumerates the shipped
|
|
535
|
+
// default catalog as `[tagType, behavior]` pairs. Does not
|
|
536
|
+
// reflect host-registered customs on any specific registry.
|
|
537
|
+
return Object.entries(DEFAULT_REGISTRY_ENTRIES);
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
getScopeTag(tagType: string): ScopeTagBehavior {
|
|
541
|
+
// @endStateApi — live. Coord-10 L11-4. Peeks a default tag's
|
|
542
|
+
// behavior by name. Returns `DEFAULT_UNKNOWN_BEHAVIOR` (bail-on-
|
|
543
|
+
// cross) for unknown tag types.
|
|
544
|
+
return DEFAULT_REGISTRY_ENTRIES[tagType] ?? DEFAULT_UNKNOWN_BEHAVIOR;
|
|
545
|
+
},
|
|
433
546
|
};
|
|
434
547
|
}
|
package/src/api/v3/ui/_types.ts
CHANGED
|
@@ -233,6 +233,26 @@ export type ScrollTarget =
|
|
|
233
233
|
| { kind: "revision"; value: string; behavior?: ScrollTargetBehavior }
|
|
234
234
|
| { kind: "page"; value: number; behavior?: ScrollTargetBehavior };
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Result of `ui.viewport.scrollToPage(n)` — the settled state after
|
|
238
|
+
* the scroll dispatch lands.
|
|
239
|
+
*
|
|
240
|
+
* - `actualPage` is the 1-based page number that was actually
|
|
241
|
+
* targeted. Differs from the requested `pageNumber` when the caller
|
|
242
|
+
* asked for a page beyond the document's resolved bounds —
|
|
243
|
+
* implementation clamps and reports the clamped value here.
|
|
244
|
+
* - `scrollY` is the document-relative Y offset (in CSS px) of the
|
|
245
|
+
* target page's top edge, sourced from
|
|
246
|
+
* `handle.geometry.getPage(index).frame.topPx`. Useful for consumers
|
|
247
|
+
* that want to verify their own scroll container landed at the
|
|
248
|
+
* expected coordinate (visual-fidelity harness: deterministic
|
|
249
|
+
* per-page capture).
|
|
250
|
+
*/
|
|
251
|
+
export interface ScrollToPageResult {
|
|
252
|
+
readonly actualPage: number;
|
|
253
|
+
readonly scrollY: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
236
256
|
export interface SelectionRangeInput {
|
|
237
257
|
readonly anchor: number;
|
|
238
258
|
readonly head: number;
|
|
@@ -369,6 +389,21 @@ export interface ApiV3UiViewport {
|
|
|
369
389
|
get(): ViewportState;
|
|
370
390
|
subscribe(listener: UiListener<ViewportState>): UiUnsubscribe;
|
|
371
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Scroll the mounted surface to a specific 1-based page number.
|
|
394
|
+
* Resolves via `handle.geometry.getPage`; dispatches through the
|
|
395
|
+
* bound controller's `dispatchScroll` hook. Returns the settled
|
|
396
|
+
* `{ actualPage, scrollY }` or `null` when geometry cannot resolve
|
|
397
|
+
* any page / no controller bound / no dispatchScroll hook.
|
|
398
|
+
* Clamps pageNumber to the document's valid range; `actualPage`
|
|
399
|
+
* reflects the clamp. coord-10 §γ first-class replacement for the
|
|
400
|
+
* visual-fidelity harness's DOM-scrape fallback.
|
|
401
|
+
*/
|
|
402
|
+
scrollToPage(
|
|
403
|
+
pageNumber: number,
|
|
404
|
+
opts?: { behavior?: ScrollTargetBehavior },
|
|
405
|
+
): Promise<ScrollToPageResult | null>;
|
|
406
|
+
|
|
372
407
|
/**
|
|
373
408
|
* X5 · Composed effective markup mode. Merges L06's class-A
|
|
374
409
|
* `WorkflowMarkupModePolicy` (via `handle.getMarkupModePolicy()`)
|
|
@@ -116,6 +116,7 @@ export function resolveChromeVisibilityForPreset(input: {
|
|
|
116
116
|
pageChrome: true,
|
|
117
117
|
statusBar: true,
|
|
118
118
|
reviewRail: false,
|
|
119
|
+
shellHeader: false,
|
|
119
120
|
},
|
|
120
121
|
simple: {
|
|
121
122
|
toolbar: true,
|
|
@@ -126,6 +127,7 @@ export function resolveChromeVisibilityForPreset(input: {
|
|
|
126
127
|
pageChrome: true,
|
|
127
128
|
statusBar: true,
|
|
128
129
|
reviewRail: false,
|
|
130
|
+
shellHeader: true,
|
|
129
131
|
},
|
|
130
132
|
advanced: {
|
|
131
133
|
toolbar: true,
|
|
@@ -136,6 +138,7 @@ export function resolveChromeVisibilityForPreset(input: {
|
|
|
136
138
|
pageChrome: true,
|
|
137
139
|
statusBar: true,
|
|
138
140
|
reviewRail: true,
|
|
141
|
+
shellHeader: true,
|
|
139
142
|
},
|
|
140
143
|
review: {
|
|
141
144
|
toolbar: true,
|
|
@@ -146,6 +149,7 @@ export function resolveChromeVisibilityForPreset(input: {
|
|
|
146
149
|
pageChrome: true,
|
|
147
150
|
statusBar: true,
|
|
148
151
|
reviewRail: options.showReviewRail,
|
|
152
|
+
shellHeader: true,
|
|
149
153
|
},
|
|
150
154
|
workflow: {
|
|
151
155
|
toolbar: true,
|
|
@@ -156,6 +160,7 @@ export function resolveChromeVisibilityForPreset(input: {
|
|
|
156
160
|
pageChrome: true,
|
|
157
161
|
statusBar: true,
|
|
158
162
|
reviewRail: options.showReviewRail,
|
|
163
|
+
shellHeader: true,
|
|
159
164
|
},
|
|
160
165
|
collab: {
|
|
161
166
|
toolbar: true,
|
|
@@ -166,6 +171,7 @@ export function resolveChromeVisibilityForPreset(input: {
|
|
|
166
171
|
pageChrome: true,
|
|
167
172
|
statusBar: true,
|
|
168
173
|
reviewRail: options.showReviewRail,
|
|
174
|
+
shellHeader: true,
|
|
169
175
|
},
|
|
170
176
|
};
|
|
171
177
|
|
package/src/api/v3/ui/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ import type {
|
|
|
27
27
|
UiListener,
|
|
28
28
|
UiUnsubscribe,
|
|
29
29
|
WorkflowMarkupMode,
|
|
30
|
+
ScrollTargetBehavior,
|
|
31
|
+
ScrollToPageResult,
|
|
30
32
|
} from "./_types.ts";
|
|
31
33
|
import type { UiApiContext } from "./_context.ts";
|
|
32
34
|
import { readComposedViewport } from "./_context.ts";
|
|
@@ -84,6 +86,35 @@ export const subscribeMetadata: ApiV3FnMetadata = {
|
|
|
84
86
|
rwdReference: "§UI API § ui.viewport.subscribe. Adapter delegates to UiController.subscribeViewport; throws when the active binding has no hook. Subscribe call emits one `ux.response.ui.viewport.subscribe` acknowledgement; per-tick ViewportState deliveries flow through the listener (rAF-coalesced, U7).",
|
|
85
87
|
};
|
|
86
88
|
|
|
89
|
+
// ----- scrollToPage (coord-10 §γ — visual-fidelity / Go-to-page UX) -----
|
|
90
|
+
|
|
91
|
+
export const scrollToPageMetadata: ApiV3FnMetadata = {
|
|
92
|
+
name: "ui.viewport.scrollToPage",
|
|
93
|
+
status: "live-with-adapter",
|
|
94
|
+
sourceLayer: "presentation",
|
|
95
|
+
liveEvidence: {
|
|
96
|
+
runnerTest: "test/api/v3/ui/scroll-to-page.test.ts",
|
|
97
|
+
commit: "refactor-10-slice-scroll-to-page",
|
|
98
|
+
},
|
|
99
|
+
uxIntent: {
|
|
100
|
+
uiVisible: true,
|
|
101
|
+
expectsUxResponse: "surface-refresh",
|
|
102
|
+
expectedDelta: "mounted surface scrolls to the target page's top edge",
|
|
103
|
+
},
|
|
104
|
+
agentMetadata: {
|
|
105
|
+
readOrMutate: "mutate",
|
|
106
|
+
boundedScope: "session",
|
|
107
|
+
auditCategory: "ui-viewport-scroll",
|
|
108
|
+
},
|
|
109
|
+
// Scroll position is a class-C local view state (per-session /
|
|
110
|
+
// per-mounted-instance); changing it doesn't mutate canonical document
|
|
111
|
+
// state and is not broadcast across collab peers.
|
|
112
|
+
stateClass: "C-local",
|
|
113
|
+
persistsTo: "none",
|
|
114
|
+
rwdReference:
|
|
115
|
+
"§UI API § ui.viewport.scrollToPage. Resolves pageNumber → scrollY via handle.geometry.getPage(pageIndex); dispatches through controller.dispatchScroll({ kind:'page', value, behavior }); returns the settled {actualPage, scrollY}. 1-based page numbers; clamps to [1, pageCount]. First-class API for visual-fidelity harness + 'Go to page N' UX — replaces DOM-scrape fallback (coord-10 §γ). Parity note: reads the same `handle.geometry.getPage(i).frame.topPx` source as `runtime.viewport.getPageAnchor` (L07 coord-07 §2.9, shipped 2026-04-24 in `src/api/v3/runtime/viewport.ts`), so `actualPage + scrollY` here and `{scrollY, pageRect}` on the runtime side stay consistent by construction. No direct delegation today because `scripts/ci-check-ui-api-layer-purity.mjs` restricts `src/api/v3/ui/**` from importing `src/api/v3/runtime/**`; both surfaces are thin wrappers over the shared geometry facet.",
|
|
116
|
+
};
|
|
117
|
+
|
|
87
118
|
// ----- X5 markup-mode metadata (state-classes cross-cutting Slice X5) -----
|
|
88
119
|
|
|
89
120
|
/**
|
|
@@ -249,6 +280,87 @@ export function createViewportFamily(ctx: UiApiContext) {
|
|
|
249
280
|
return unsubscribe;
|
|
250
281
|
},
|
|
251
282
|
|
|
283
|
+
// ----- scrollToPage (coord-10 §γ) -----
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Scroll the mounted surface to a specific 1-based page number.
|
|
287
|
+
*
|
|
288
|
+
* Resolution: `handle.geometry.getPage(pageNumber - 1)` supplies the
|
|
289
|
+
* page's `frame.topPx` (deterministic, renderer-frame coordinates).
|
|
290
|
+
* Dispatch: `controller.dispatchScroll({ kind: "page", value, behavior })`
|
|
291
|
+
* — same seam as `ui.surface.scrollTo`, so any consumer-specific
|
|
292
|
+
* scroll-container routing lives on the shell side.
|
|
293
|
+
*
|
|
294
|
+
* Clamping: `pageNumber < 1` resolves to page 1. A request beyond
|
|
295
|
+
* the document's page count returns the last valid page's scrollY;
|
|
296
|
+
* `actualPage` reflects the clamp so callers can detect it.
|
|
297
|
+
*
|
|
298
|
+
* Returns `null` when (a) no controller is bound, (b) the controller
|
|
299
|
+
* has no `dispatchScroll` hook, or (c) geometry cannot resolve any
|
|
300
|
+
* page (pre-paint / empty doc). Callers that need explicit failure
|
|
301
|
+
* handling check `result !== null` before trusting the scroll.
|
|
302
|
+
*/
|
|
303
|
+
async scrollToPage(
|
|
304
|
+
pageNumber: number,
|
|
305
|
+
opts?: { behavior?: ScrollTargetBehavior },
|
|
306
|
+
): Promise<ScrollToPageResult | null> {
|
|
307
|
+
const controller = ctx.binding?.controller;
|
|
308
|
+
if (!controller) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
"ui.viewport.scrollToPage: no controller bound — call ui.session.bind(controller) first",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (!controller.dispatchScroll) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`ui.viewport.scrollToPage: controller of kind "${controller.kind}" did not provide a dispatchScroll hook`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Clamp to [1, pageCount]. The geometry facet's `getPage(index)`
|
|
320
|
+
// takes a 0-based index; we accept 1-based input and convert.
|
|
321
|
+
// Walk forward from the clamped target to find the first page the
|
|
322
|
+
// facet actually resolves — this handles sparse / pre-paint
|
|
323
|
+
// states where higher indices may be null.
|
|
324
|
+
const getPage = ctx.handle.geometry?.getPage;
|
|
325
|
+
if (typeof getPage !== "function") return null;
|
|
326
|
+
|
|
327
|
+
const requestedClampedLow = Math.max(1, Math.floor(pageNumber));
|
|
328
|
+
// Try the requested page; if null, scan downward through lower
|
|
329
|
+
// indices to land on the largest resolvable page (the doc's last
|
|
330
|
+
// populated page). If nothing resolves, return null.
|
|
331
|
+
let resolved: { pageIndex: number; scrollY: number } | null = null;
|
|
332
|
+
for (let i = requestedClampedLow - 1; i >= 0; i--) {
|
|
333
|
+
const page = getPage.call(ctx.handle.geometry, i);
|
|
334
|
+
if (page) {
|
|
335
|
+
resolved = { pageIndex: i, scrollY: page.frame.topPx };
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!resolved) return null;
|
|
340
|
+
|
|
341
|
+
const actualPage = resolved.pageIndex + 1;
|
|
342
|
+
const behavior = opts?.behavior;
|
|
343
|
+
await controller.dispatchScroll({
|
|
344
|
+
kind: "page",
|
|
345
|
+
value: actualPage,
|
|
346
|
+
...(behavior ? { behavior } : {}),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
emitUxResponse(ctx.handle, {
|
|
350
|
+
apiFn: scrollToPageMetadata.name,
|
|
351
|
+
intent: scrollToPageMetadata.uxIntent.expectedDelta ?? "",
|
|
352
|
+
mockOrLive: "live-with-adapter",
|
|
353
|
+
uiVisible: true,
|
|
354
|
+
expectedDelta: scrollToPageMetadata.uxIntent.expectedDelta,
|
|
355
|
+
actualDelta: {
|
|
356
|
+
kind: "surface-refresh",
|
|
357
|
+
payload: { page: actualPage, scrollY: resolved.scrollY },
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return { actualPage, scrollY: resolved.scrollY };
|
|
362
|
+
},
|
|
363
|
+
|
|
252
364
|
// ----- X5 markup-mode (state-classes cross-cutting Slice X5) -----
|
|
253
365
|
|
|
254
366
|
getEffectiveMarkupMode(): WorkflowMarkupMode {
|
|
@@ -509,6 +509,7 @@ function getInlineLength(node: InlineNode): number {
|
|
|
509
509
|
case "tab":
|
|
510
510
|
case "hard_break":
|
|
511
511
|
case "column_break":
|
|
512
|
+
case "page_break":
|
|
512
513
|
case "symbol":
|
|
513
514
|
case "image":
|
|
514
515
|
case "opaque_inline":
|
|
@@ -555,6 +556,7 @@ function getInlineDisplayText(node: InlineNode): string {
|
|
|
555
556
|
return "\t";
|
|
556
557
|
case "hard_break":
|
|
557
558
|
case "column_break":
|
|
559
|
+
case "page_break":
|
|
558
560
|
return "\n";
|
|
559
561
|
case "symbol":
|
|
560
562
|
return node.char;
|
|
@@ -329,6 +329,7 @@ export function getTableStructureContext(
|
|
|
329
329
|
columnCount,
|
|
330
330
|
selectedCellCount,
|
|
331
331
|
isSimpleTable: simpleTable,
|
|
332
|
+
alignment: target.alignment ?? null,
|
|
332
333
|
currentCell: {
|
|
333
334
|
rowIndex: effectiveSelection.anchorCell.rowIndex,
|
|
334
335
|
columnIndex: effectiveSelection.anchorCell.columnIndex,
|
|
@@ -582,14 +582,57 @@ export function createPersistedEditorSnapshot(
|
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
function estimateParagraphCount(content: unknown): number {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
585
|
+
// Canonical shape: `{type:"doc", children: BlockNode[]}`. Older
|
|
586
|
+
// shapes (array / `.blocks`) handled for persistence-snapshot
|
|
587
|
+
// fallback. KI-P4 (2026-04-23): pre-fix the array + .blocks
|
|
588
|
+
// branches never matched the current envelope, so the fallback
|
|
589
|
+
// returned 1 on any non-empty document regardless of paragraph
|
|
590
|
+
// count. Fix counts ParagraphNode entries recursively, descending
|
|
591
|
+
// into table cells + SDT / customXml blocks so nested paragraphs
|
|
592
|
+
// contribute to the total.
|
|
593
|
+
let count = 0;
|
|
594
|
+
const walk = (node: unknown): void => {
|
|
595
|
+
if (!node || typeof node !== "object") return;
|
|
596
|
+
const typed = node as { type?: unknown };
|
|
597
|
+
if (typed.type === "paragraph") {
|
|
598
|
+
count += 1;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (typed.type === "table") {
|
|
602
|
+
const rows = (node as { rows?: unknown[] }).rows;
|
|
603
|
+
if (Array.isArray(rows)) {
|
|
604
|
+
for (const row of rows) {
|
|
605
|
+
const cells = (row as { cells?: unknown[] }).cells;
|
|
606
|
+
if (Array.isArray(cells)) {
|
|
607
|
+
for (const cell of cells) {
|
|
608
|
+
const children = (cell as { children?: unknown[] }).children;
|
|
609
|
+
if (Array.isArray(children)) children.forEach(walk);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const children = (node as { children?: unknown[] }).children;
|
|
617
|
+
if (Array.isArray(children)) children.forEach(walk);
|
|
618
|
+
};
|
|
588
619
|
|
|
589
|
-
if (content && typeof content === "object"
|
|
590
|
-
|
|
620
|
+
if (content && typeof content === "object") {
|
|
621
|
+
const children = (content as { children?: unknown[] }).children;
|
|
622
|
+
if (Array.isArray(children)) {
|
|
623
|
+
children.forEach(walk);
|
|
624
|
+
return count;
|
|
625
|
+
}
|
|
626
|
+
const blocks = (content as { blocks?: unknown[] }).blocks;
|
|
627
|
+
if (Array.isArray(blocks)) {
|
|
628
|
+
blocks.forEach(walk);
|
|
629
|
+
return count;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (Array.isArray(content)) {
|
|
633
|
+
content.forEach(walk);
|
|
634
|
+
return count;
|
|
591
635
|
}
|
|
592
|
-
|
|
593
636
|
return extractText(content).length > 0 ? 1 : 0;
|
|
594
637
|
}
|
|
595
638
|
|