@beyondwork/docx-react-component 1.0.105 → 1.0.108

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.
Files changed (193) hide show
  1. package/package.json +19 -5
  2. package/src/api/geometry-overlay-rects.ts +5 -0
  3. package/src/api/package-version.ts +1 -1
  4. package/src/api/page-anchor-id.ts +5 -0
  5. package/src/api/public-types.ts +16 -9
  6. package/src/api/table-node-specs.ts +6 -0
  7. package/src/api/v3/_create.ts +10 -2
  8. package/src/api/v3/_page-anchor-id.ts +52 -0
  9. package/src/api/v3/_runtime-handle.ts +92 -1
  10. package/src/api/v3/ai/_audit-reference.ts +28 -0
  11. package/src/api/v3/ai/_audit-time.ts +5 -0
  12. package/src/api/v3/ai/_pe2-evidence.ts +310 -6
  13. package/src/api/v3/ai/attach.ts +29 -4
  14. package/src/api/v3/ai/bundle.ts +6 -2
  15. package/src/api/v3/ai/inspect.ts +6 -2
  16. package/src/api/v3/ai/replacement.ts +112 -18
  17. package/src/api/v3/ai/resolve.ts +2 -2
  18. package/src/api/v3/ai/review.ts +177 -3
  19. package/src/api/v3/index.ts +8 -0
  20. package/src/api/v3/runtime/collab.ts +462 -0
  21. package/src/api/v3/runtime/document.ts +503 -20
  22. package/src/api/v3/runtime/geometry.ts +97 -0
  23. package/src/api/v3/runtime/layout.ts +744 -0
  24. package/src/api/v3/runtime/perf-probe.ts +14 -0
  25. package/src/api/v3/runtime/viewport.ts +9 -8
  26. package/src/api/v3/ui/_types.ts +202 -55
  27. package/src/api/v3/ui/chrome-preset-model.ts +5 -5
  28. package/src/api/v3/ui/debug.ts +115 -2
  29. package/src/api/v3/ui/index.ts +17 -0
  30. package/src/api/v3/ui/overlays.ts +0 -8
  31. package/src/api/v3/ui/surface.ts +56 -0
  32. package/src/api/v3/ui/viewport.ts +119 -9
  33. package/src/core/commands/image-commands.ts +1 -0
  34. package/src/core/commands/index.ts +6 -0
  35. package/src/core/schema/text-schema.ts +43 -5
  36. package/src/core/selection/mapping.ts +8 -1
  37. package/src/core/selection/review-anchors.ts +5 -1
  38. package/src/core/state/text-transaction.ts +8 -2
  39. package/src/io/export/serialize-revisions.ts +149 -1
  40. package/src/io/normalize/normalize-text.ts +6 -0
  41. package/src/io/ooxml/parse-bookmark-references.ts +55 -0
  42. package/src/io/ooxml/parse-fields.ts +24 -2
  43. package/src/io/ooxml/parse-headers-footers.ts +38 -5
  44. package/src/io/ooxml/parse-main-document.ts +153 -9
  45. package/src/io/ooxml/parse-numbering.ts +20 -0
  46. package/src/io/ooxml/parse-revisions.ts +19 -8
  47. package/src/io/opc/package-reader.ts +98 -8
  48. package/src/model/anchor.ts +4 -3
  49. package/src/model/canonical-document.ts +220 -2
  50. package/src/model/canonical-hash.ts +221 -0
  51. package/src/model/canonical-layout-inputs.ts +245 -6
  52. package/src/model/layout/index.ts +1 -0
  53. package/src/model/layout/page-graph-types.ts +147 -1
  54. package/src/model/review/revision-types.ts +14 -3
  55. package/src/preservation/store.ts +20 -4
  56. package/src/review/README.md +1 -1
  57. package/src/review/store/revision-actions.ts +14 -2
  58. package/src/runtime/collab/event-types.ts +67 -1
  59. package/src/runtime/collab/runtime-collab-sync.ts +177 -5
  60. package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
  61. package/src/runtime/document-heading-outline.ts +147 -0
  62. package/src/runtime/document-navigation.ts +8 -243
  63. package/src/runtime/document-runtime.ts +279 -115
  64. package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
  65. package/src/runtime/formatting/layout-inputs.ts +38 -5
  66. package/src/runtime/formatting/numbering/geometry.ts +28 -2
  67. package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
  68. package/src/runtime/geometry/caret-geometry.ts +5 -6
  69. package/src/runtime/geometry/geometry-facet.ts +60 -10
  70. package/src/runtime/geometry/geometry-index.ts +661 -16
  71. package/src/runtime/geometry/geometry-types.ts +59 -0
  72. package/src/runtime/geometry/hit-test.ts +11 -1
  73. package/src/runtime/geometry/overlay-rects.ts +5 -3
  74. package/src/runtime/geometry/project-anchors.ts +1 -1
  75. package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
  76. package/src/runtime/layout/index.ts +6 -0
  77. package/src/runtime/layout/layout-engine-instance.ts +6 -1
  78. package/src/runtime/layout/layout-engine-version.ts +188 -16
  79. package/src/runtime/layout/layout-facet-types.ts +6 -0
  80. package/src/runtime/layout/page-graph.ts +23 -4
  81. package/src/runtime/layout/paginated-layout-engine.ts +149 -15
  82. package/src/runtime/layout/project-block-fragments.ts +351 -14
  83. package/src/runtime/layout/public-facet.ts +162 -24
  84. package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
  85. package/src/runtime/layout/table-row-split.ts +92 -35
  86. package/src/runtime/prerender/cache-envelope.ts +2 -2
  87. package/src/runtime/prerender/cache-key.ts +5 -4
  88. package/src/runtime/prerender/customxml-cache.ts +0 -1
  89. package/src/runtime/render/render-kernel.ts +1 -1
  90. package/src/runtime/revision-runtime.ts +112 -10
  91. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  92. package/src/runtime/scopes/action-validation.ts +22 -2
  93. package/src/runtime/scopes/capabilities.ts +316 -0
  94. package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
  95. package/src/runtime/scopes/compiler-service.ts +108 -4
  96. package/src/runtime/scopes/content-control-evidence.ts +79 -0
  97. package/src/runtime/scopes/create-issue.ts +5 -5
  98. package/src/runtime/scopes/evidence.ts +91 -0
  99. package/src/runtime/scopes/formatting/apply.ts +2 -0
  100. package/src/runtime/scopes/geometry-evidence.ts +130 -0
  101. package/src/runtime/scopes/index.ts +54 -0
  102. package/src/runtime/scopes/issue-lifecycle.ts +224 -0
  103. package/src/runtime/scopes/layout-evidence.ts +374 -0
  104. package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
  105. package/src/runtime/scopes/preservation-boundary.ts +7 -1
  106. package/src/runtime/scopes/replacement/apply.ts +97 -34
  107. package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
  108. package/src/runtime/scopes/semantic-scope-types.ts +242 -3
  109. package/src/runtime/scopes/visualization.ts +28 -0
  110. package/src/runtime/surface-projection.ts +44 -5
  111. package/src/runtime/telemetry/perf-probe.ts +216 -0
  112. package/src/runtime/virtualized-rendering.ts +36 -1
  113. package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
  114. package/src/runtime/workflow/coordinator.ts +39 -11
  115. package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
  116. package/src/runtime/workflow/index.ts +4 -0
  117. package/src/runtime/workflow/overlay-lane-types.ts +58 -0
  118. package/src/runtime/workflow/overlay-lanes.ts +386 -0
  119. package/src/runtime/workflow/overlay-store.ts +2 -2
  120. package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
  121. package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
  122. package/src/session/_sync-legacy.ts +17 -27
  123. package/src/session/import/loader.ts +6 -4
  124. package/src/session/import/source-package-evidence.ts +186 -2
  125. package/src/session/index.ts +5 -6
  126. package/src/session/session.ts +30 -56
  127. package/src/session/types.ts +8 -13
  128. package/src/shell/session-bootstrap.ts +155 -81
  129. package/src/ui/WordReviewEditor.tsx +520 -12
  130. package/src/ui/editor-shell-view.tsx +14 -4
  131. package/src/ui/editor-surface-controller.tsx +5 -3
  132. package/src/ui/headless/selection-tool-resolver.ts +1 -2
  133. package/src/ui/presence-overlay-lane.ts +130 -0
  134. package/src/ui/ui-controller-factory.ts +17 -0
  135. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
  136. package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
  137. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
  138. package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
  139. package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
  140. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
  141. package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
  142. package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
  143. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
  144. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
  145. package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
  146. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
  147. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
  148. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
  149. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  150. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
  151. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
  152. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
  153. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
  154. package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
  155. package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
  156. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
  157. package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
  158. package/src/ui-tailwind/debug/README.md +4 -1
  159. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
  160. package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
  161. package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
  162. package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
  163. package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
  164. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
  165. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
  166. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
  167. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
  168. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
  169. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
  170. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
  171. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
  172. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
  173. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
  174. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
  175. package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
  176. package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
  177. package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
  178. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
  179. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
  180. package/src/ui-tailwind/review-workspace/types.ts +26 -12
  181. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
  182. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
  183. package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
  184. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
  185. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
  186. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
  187. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
  188. package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
  189. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
  190. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
  191. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
  192. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
  193. package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
@@ -0,0 +1,328 @@
1
+ import * as React from "react";
2
+ import { ChevronDown, ChevronUp, Rows3, Search, Target, X } from "lucide-react";
3
+
4
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
5
+
6
+ export type NavigationCommandMode = "find" | "replace" | "go-to";
7
+ export type NavigationSearchScope = "main" | "story" | "scope";
8
+
9
+ export interface NavigationSearchScopeOption {
10
+ id: NavigationSearchScope;
11
+ label: string;
12
+ disabled?: boolean;
13
+ disabledReason?: string;
14
+ }
15
+
16
+ export interface NavigationGoToItem {
17
+ id: string;
18
+ label: string;
19
+ detail?: string;
20
+ }
21
+
22
+ export interface NavigationGoToGroup {
23
+ id: string;
24
+ label: string;
25
+ emptyLabel: string;
26
+ items: readonly NavigationGoToItem[];
27
+ }
28
+
29
+ export interface TwNavigationCommandBarProps {
30
+ mode: NavigationCommandMode;
31
+ query: string;
32
+ replacement: string;
33
+ activeIndex: number;
34
+ resultCount: number;
35
+ searchScope: NavigationSearchScope;
36
+ searchScopes: readonly NavigationSearchScopeOption[];
37
+ goToGroups: readonly NavigationGoToGroup[];
38
+ activeGoToGroupId: string;
39
+ onModeChange: (mode: NavigationCommandMode) => void;
40
+ onQueryChange: (query: string) => void;
41
+ onReplacementChange: (replacement: string) => void;
42
+ onSearchScopeChange: (scope: NavigationSearchScope) => void;
43
+ onPrevious: () => void;
44
+ onNext: () => void;
45
+ onReplaceCurrent: () => void;
46
+ onGoToGroupChange: (groupId: string) => void;
47
+ onGoToTarget: (groupId: string, itemId: string) => void;
48
+ onClose: () => void;
49
+ }
50
+
51
+ const MODE_ITEMS: ReadonlyArray<{
52
+ id: NavigationCommandMode;
53
+ label: string;
54
+ shortcut: string;
55
+ }> = [
56
+ { id: "find", label: "Find", shortcut: "Ctrl+F" },
57
+ { id: "replace", label: "Replace", shortcut: "Ctrl+H" },
58
+ { id: "go-to", label: "Go to", shortcut: "Ctrl+G" },
59
+ ];
60
+
61
+ export function TwNavigationCommandBar(
62
+ props: TwNavigationCommandBarProps,
63
+ ): React.JSX.Element {
64
+ const inputRef = React.useRef<HTMLInputElement | null>(null);
65
+ const hasResults = props.resultCount > 0;
66
+ const hasQuery = props.query.trim().length > 0;
67
+ const resultState = hasQuery ? (hasResults ? "matches" : "empty") : "idle";
68
+ const statusLabel = hasQuery
69
+ ? hasResults
70
+ ? `${props.activeIndex + 1}/${props.resultCount}`
71
+ : "No results"
72
+ : "Find";
73
+ const navDisabledReason = hasQuery
74
+ ? "No matches to navigate."
75
+ : "Enter a query to navigate matches.";
76
+ const activeGroup =
77
+ props.goToGroups.find((group) => group.id === props.activeGoToGroupId) ??
78
+ props.goToGroups[0] ??
79
+ null;
80
+
81
+ React.useEffect(() => {
82
+ if (props.mode === "go-to") return;
83
+ inputRef.current?.focus();
84
+ inputRef.current?.select();
85
+ }, [props.mode]);
86
+
87
+ const handleRootKeyDown = React.useCallback(
88
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
89
+ if (event.key !== "Escape") return;
90
+ event.preventDefault();
91
+ props.onClose();
92
+ },
93
+ [props.onClose],
94
+ );
95
+
96
+ return (
97
+ <div
98
+ className="pointer-events-auto flex w-[min(560px,calc(100vw-2rem))] flex-col gap-2 rounded-lg border border-[color:color-mix(in_srgb,var(--color-accent-primary)_26%,var(--color-border-subtle))] bg-[color:color-mix(in_srgb,var(--color-bg-canvas)_96%,white)] p-2 shadow-[var(--shadow-float)]"
99
+ data-testid="navigation-command-bar"
100
+ data-mode={props.mode}
101
+ data-result-state={resultState}
102
+ role="search"
103
+ aria-label="Document navigation"
104
+ onKeyDown={handleRootKeyDown}
105
+ >
106
+ <div className="flex items-center gap-2">
107
+ <div className="inline-flex rounded-md bg-surface p-0.5" role="tablist" aria-label="Navigation mode">
108
+ {MODE_ITEMS.map((item) => {
109
+ const selected = item.id === props.mode;
110
+ return (
111
+ <button
112
+ key={item.id}
113
+ type="button"
114
+ role="tab"
115
+ aria-selected={selected}
116
+ title={`${item.label} (${item.shortcut})`}
117
+ data-testid={`navigation-command-bar-mode-${item.id}`}
118
+ onMouseDown={preserveEditorSelectionMouseDown}
119
+ onClick={() => props.onModeChange(item.id)}
120
+ className={[
121
+ "inline-flex h-7 items-center gap-1 rounded px-2 text-[11px] font-semibold transition-colors focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
122
+ selected
123
+ ? "bg-canvas text-primary shadow-sm ring-1 ring-border"
124
+ : "text-secondary hover:bg-hover hover:text-primary",
125
+ ].join(" ")}
126
+ >
127
+ {item.label}
128
+ </button>
129
+ );
130
+ })}
131
+ </div>
132
+ <button
133
+ type="button"
134
+ aria-label="Close navigation"
135
+ className="ml-auto inline-flex h-7 w-7 items-center justify-center rounded-md text-tertiary transition-colors hover:bg-hover hover:text-primary focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]"
136
+ onMouseDown={preserveEditorSelectionMouseDown}
137
+ onClick={props.onClose}
138
+ >
139
+ <X className="h-4 w-4" aria-hidden="true" />
140
+ </button>
141
+ </div>
142
+
143
+ {props.mode === "go-to" ? (
144
+ <div className="grid gap-2" data-testid="navigation-command-bar-go-to">
145
+ <div className="flex flex-wrap gap-1">
146
+ {props.goToGroups.map((group) => {
147
+ const selected = group.id === activeGroup?.id;
148
+ return (
149
+ <button
150
+ key={group.id}
151
+ type="button"
152
+ aria-pressed={selected}
153
+ onMouseDown={preserveEditorSelectionMouseDown}
154
+ onClick={() => props.onGoToGroupChange(group.id)}
155
+ className={[
156
+ "inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
157
+ selected
158
+ ? "border-[var(--color-border-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]"
159
+ : "border-border bg-canvas text-secondary hover:bg-hover hover:text-primary",
160
+ ].join(" ")}
161
+ >
162
+ <Target className="h-3 w-3" aria-hidden="true" />
163
+ {group.label}
164
+ <span className="text-[10px] text-tertiary">{group.items.length}</span>
165
+ </button>
166
+ );
167
+ })}
168
+ </div>
169
+ <div className="max-h-64 overflow-auto rounded-md border border-border bg-canvas p-1">
170
+ {activeGroup && activeGroup.items.length > 0 ? (
171
+ activeGroup.items.map((item) => (
172
+ <button
173
+ key={item.id}
174
+ type="button"
175
+ className="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left text-[12px] transition-colors hover:bg-surface focus-visible:bg-surface focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]"
176
+ onMouseDown={preserveEditorSelectionMouseDown}
177
+ onClick={() => props.onGoToTarget(activeGroup.id, item.id)}
178
+ >
179
+ <Rows3 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-secondary" aria-hidden="true" />
180
+ <span className="min-w-0">
181
+ <span className="block truncate font-medium text-primary">{item.label}</span>
182
+ {item.detail ? (
183
+ <span className="block truncate text-[11px] text-secondary">{item.detail}</span>
184
+ ) : null}
185
+ </span>
186
+ </button>
187
+ ))
188
+ ) : (
189
+ <div className="px-2 py-3 text-[12px] text-secondary">
190
+ {activeGroup?.emptyLabel ?? "No navigation targets available."}
191
+ </div>
192
+ )}
193
+ </div>
194
+ </div>
195
+ ) : (
196
+ <div className="grid gap-2" data-testid="navigation-command-bar-search">
197
+ <div className="flex items-center gap-2">
198
+ <Search className="h-4 w-4 shrink-0 text-accent" aria-hidden="true" />
199
+ <input
200
+ ref={inputRef}
201
+ aria-label={props.mode === "replace" ? "Find text to replace" : "Find text"}
202
+ aria-describedby="navigation-command-bar-status"
203
+ className="min-w-0 flex-1 bg-transparent px-1 text-[13px] font-medium text-primary outline-none placeholder:text-tertiary"
204
+ placeholder={props.mode === "replace" ? "Find text to replace" : "Find in document"}
205
+ type="search"
206
+ value={props.query}
207
+ onChange={(event) => props.onQueryChange(event.currentTarget.value)}
208
+ onKeyDown={(event) => {
209
+ if (event.key === "Escape") {
210
+ event.preventDefault();
211
+ event.stopPropagation();
212
+ props.onClose();
213
+ return;
214
+ }
215
+ if (event.key === "Enter") {
216
+ event.preventDefault();
217
+ if (event.shiftKey) {
218
+ props.onPrevious();
219
+ } else {
220
+ props.onNext();
221
+ }
222
+ }
223
+ }}
224
+ />
225
+ <span
226
+ id="navigation-command-bar-status"
227
+ data-testid="navigation-command-bar-status"
228
+ className={`min-w-[72px] rounded-full px-2 py-1 text-center text-[11px] font-semibold tabular-nums ${
229
+ resultState === "empty"
230
+ ? "bg-[var(--color-semantic-warning-soft)] text-[var(--color-semantic-warning)]"
231
+ : "bg-[color:color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)] text-accent"
232
+ }`}
233
+ aria-live="polite"
234
+ >
235
+ {statusLabel}
236
+ </span>
237
+ <IconButton
238
+ label="Previous match"
239
+ shortcut="Shift+Enter"
240
+ disabled={!hasResults}
241
+ title={hasResults ? "Previous match (Shift+Enter)" : navDisabledReason}
242
+ onClick={props.onPrevious}
243
+ >
244
+ <ChevronUp className="h-4 w-4" aria-hidden="true" />
245
+ </IconButton>
246
+ <IconButton
247
+ label="Next match"
248
+ shortcut="Enter"
249
+ disabled={!hasResults}
250
+ title={hasResults ? "Next match (Enter)" : navDisabledReason}
251
+ onClick={props.onNext}
252
+ >
253
+ <ChevronDown className="h-4 w-4" aria-hidden="true" />
254
+ </IconButton>
255
+ </div>
256
+ <div className="flex flex-wrap items-center gap-1">
257
+ {props.searchScopes.map((scope) => (
258
+ <button
259
+ key={scope.id}
260
+ type="button"
261
+ aria-pressed={props.searchScope === scope.id}
262
+ disabled={scope.disabled}
263
+ title={scope.disabled ? scope.disabledReason : undefined}
264
+ onMouseDown={preserveEditorSelectionMouseDown}
265
+ onClick={() => props.onSearchScopeChange(scope.id)}
266
+ className={[
267
+ "inline-flex h-6 items-center rounded-md border px-2 text-[10px] font-semibold transition-colors focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] disabled:cursor-not-allowed disabled:opacity-40",
268
+ props.searchScope === scope.id
269
+ ? "border-[var(--color-border-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]"
270
+ : "border-border bg-canvas text-secondary hover:bg-hover hover:text-primary",
271
+ ].join(" ")}
272
+ >
273
+ {scope.label}
274
+ </button>
275
+ ))}
276
+ </div>
277
+ {props.mode === "replace" ? (
278
+ <div className="flex items-center gap-2 border-t border-border/70 pt-2">
279
+ <input
280
+ aria-label="Replacement text"
281
+ className="min-w-0 flex-1 rounded-md border border-border bg-canvas px-2 py-1.5 text-[13px] text-primary outline-none placeholder:text-tertiary focus-visible:shadow-[var(--shadow-focus)]"
282
+ placeholder="Replace with"
283
+ value={props.replacement}
284
+ onChange={(event) => props.onReplacementChange(event.currentTarget.value)}
285
+ />
286
+ <button
287
+ type="button"
288
+ disabled={!hasResults}
289
+ title={hasResults ? "Replace current match" : navDisabledReason}
290
+ onMouseDown={preserveEditorSelectionMouseDown}
291
+ onClick={props.onReplaceCurrent}
292
+ className="inline-flex h-8 items-center rounded-md bg-[var(--color-accent-primary)] px-3 text-[12px] font-semibold text-[var(--color-text-inverse)] transition-colors hover:bg-[var(--color-accent-primary-hover)] focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] disabled:cursor-not-allowed disabled:opacity-40"
293
+ >
294
+ Replace
295
+ </button>
296
+ </div>
297
+ ) : null}
298
+ </div>
299
+ )}
300
+ </div>
301
+ );
302
+ }
303
+
304
+ function IconButton(props: {
305
+ label: string;
306
+ shortcut: string;
307
+ disabled?: boolean;
308
+ title?: string;
309
+ children: React.ReactNode;
310
+ onClick: () => void;
311
+ }): React.JSX.Element {
312
+ return (
313
+ <button
314
+ type="button"
315
+ aria-label={props.label}
316
+ aria-keyshortcuts={props.shortcut}
317
+ disabled={props.disabled}
318
+ title={props.title}
319
+ className="inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-hover focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] disabled:cursor-not-allowed disabled:opacity-35"
320
+ onMouseDown={preserveEditorSelectionMouseDown}
321
+ onClick={props.onClick}
322
+ >
323
+ {props.children}
324
+ </button>
325
+ );
326
+ }
327
+
328
+ export default TwNavigationCommandBar;
@@ -3,6 +3,8 @@ import type { ActiveObjectContext } from "../../ui/headless/selection-tool-types
3
3
 
4
4
  export interface TwObjectContextToolbarProps {
5
5
  activeObject: ActiveObjectContext;
6
+ /** Debug/operator-only preserve/export detail. Hidden in product chrome. */
7
+ showPreservationDiagnostics?: boolean;
6
8
  }
7
9
 
8
10
  function InfoIcon(props: { className?: string; "aria-hidden"?: boolean }) {
@@ -40,10 +42,12 @@ export function TwObjectContextToolbar(props: TwObjectContextToolbarProps) {
40
42
  <span className="rounded-full bg-surface px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-[0.1em] text-secondary">
41
43
  {props.activeObject.display}
42
44
  </span>
43
- <div className="inline-flex items-center gap-1.5">
44
- <InfoIcon className="h-3.5 w-3.5 text-[var(--color-semantic-info)]" aria-hidden={true} />
45
- <span className="text-[11px] text-[var(--color-text-secondary)]">Shape preserved for export — opens in Word.</span>
46
- </div>
45
+ {props.showPreservationDiagnostics ? (
46
+ <div className="inline-flex items-center gap-1.5">
47
+ <InfoIcon className="h-3.5 w-3.5 text-[var(--color-semantic-info)]" aria-hidden={true} />
48
+ <span className="text-[11px] text-[var(--color-text-secondary)]">Shape preserved for export — opens in Word.</span>
49
+ </div>
50
+ ) : null}
47
51
  </div>
48
52
  );
49
53
  }
@@ -6,6 +6,13 @@ import React, {
6
6
  } from "react";
7
7
 
8
8
  import type { WordReviewEditorRef } from "../../api/public-types";
9
+ import {
10
+ LAYER_DEBUG_PANES,
11
+ getLayerDebugIdFromShortcut,
12
+ getLayerDebugPane,
13
+ getLayerDebugShortcutLabel,
14
+ type LayerDebugPaneId,
15
+ } from "./layer-debug-contracts";
9
16
 
10
17
  /**
11
18
  * Opaque runtime handle for the REPL. The REPL hands the value to
@@ -55,6 +62,7 @@ export function TwRuntimeReplDialog(props: TwRuntimeReplDialogProps): React.JSX.
55
62
  const [open, setOpen] = useState(false);
56
63
  const [input, setInput] = useState("");
57
64
  const [entries, setEntries] = useState<readonly ReplEntry[]>([]);
65
+ const [activeLayerId, setActiveLayerId] = useState<LayerDebugPaneId>("01");
58
66
  const [history, setHistory] = useState<readonly string[]>(() =>
59
67
  loadPersistedHistory(),
60
68
  );
@@ -73,10 +81,25 @@ export function TwRuntimeReplDialog(props: TwRuntimeReplDialogProps): React.JSX.
73
81
  });
74
82
  }, []);
75
83
 
84
+ const loadSnippet = useCallback((expression: string) => {
85
+ setInput(expression);
86
+ setHistoryIndex(null);
87
+ requestAnimationFrame(() => textareaRef.current?.focus());
88
+ }, []);
89
+
76
90
  useEffect(() => {
77
91
  if (disabled) return;
78
92
  if (typeof window === "undefined") return;
79
93
  const handler = (event: KeyboardEvent): void => {
94
+ if (openRef.current) {
95
+ const layerId = getLayerDebugIdFromShortcut(event);
96
+ if (layerId) {
97
+ event.preventDefault();
98
+ event.stopPropagation();
99
+ setActiveLayerId(layerId);
100
+ return;
101
+ }
102
+ }
80
103
  if (!isReplToggleShortcut(event)) return;
81
104
  event.preventDefault();
82
105
  event.stopPropagation();
@@ -208,6 +231,8 @@ export function TwRuntimeReplDialog(props: TwRuntimeReplDialogProps): React.JSX.
208
231
 
209
232
  if (!open) return null;
210
233
 
234
+ const activeLayer = getLayerDebugPane(activeLayerId);
235
+
211
236
  return (
212
237
  <div
213
238
  className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh]"
@@ -281,6 +306,105 @@ export function TwRuntimeReplDialog(props: TwRuntimeReplDialogProps): React.JSX.
281
306
  </div>
282
307
  </div>
283
308
 
309
+ <section
310
+ className="border-b border-[var(--color-border-subtle)] px-4 py-3"
311
+ data-testid="tw-runtime-repl__layer-view"
312
+ >
313
+ <div className="flex flex-wrap items-start justify-between gap-3">
314
+ <div>
315
+ <p className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[var(--color-text-tertiary)]">
316
+ Agent layer view
317
+ </p>
318
+ <p className="mt-1 text-sm font-medium text-[var(--color-text-primary)]">
319
+ {activeLayer.owner} - {activeLayer.label}
320
+ </p>
321
+ <p className="mt-1 text-xs leading-5 text-[var(--color-text-secondary)]">
322
+ {activeLayer.focus}
323
+ </p>
324
+ </div>
325
+ <label className="space-y-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]">
326
+ <span className="block">Layer</span>
327
+ <select
328
+ className="min-w-[13rem] rounded-[var(--radius-sm)] bg-[var(--color-bg-muted)] px-2 py-1.5 text-xs font-medium normal-case tracking-normal text-[var(--color-text-primary)] ring-1 ring-[var(--color-border-subtle)]"
329
+ data-testid="tw-runtime-repl__layer-select"
330
+ onChange={(event) =>
331
+ setActiveLayerId(event.currentTarget.value as LayerDebugPaneId)
332
+ }
333
+ value={activeLayerId}
334
+ >
335
+ {LAYER_DEBUG_PANES.map((pane) => (
336
+ <option key={pane.id} value={pane.id}>
337
+ L{pane.id} - {pane.label}
338
+ </option>
339
+ ))}
340
+ </select>
341
+ </label>
342
+ </div>
343
+
344
+ <div
345
+ className="mt-3 flex gap-1 overflow-x-auto pb-1"
346
+ data-testid="tw-runtime-repl__layer-nav"
347
+ >
348
+ {LAYER_DEBUG_PANES.map((pane) => {
349
+ const active = pane.id === activeLayerId;
350
+ return (
351
+ <button
352
+ className={[
353
+ "shrink-0 rounded-[var(--radius-sm)] px-2 py-1 text-[11px] font-medium",
354
+ "transition-colors duration-[var(--motion-fast)]",
355
+ active
356
+ ? "bg-[var(--color-bg-selected)] text-[var(--color-text-primary)] ring-1 ring-[var(--color-border-strong)]"
357
+ : "bg-[var(--color-bg-muted)] text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-primary)]",
358
+ ].join(" ")}
359
+ data-testid={`tw-runtime-repl__layer-option-${pane.id}`}
360
+ key={pane.id}
361
+ onClick={() => setActiveLayerId(pane.id)}
362
+ type="button"
363
+ >
364
+ L{pane.id}
365
+ <span className="ml-1 text-[10px] opacity-70">
366
+ {getLayerDebugShortcutLabel(pane.id)}
367
+ </span>
368
+ </button>
369
+ );
370
+ })}
371
+ </div>
372
+
373
+ <div
374
+ className="mt-3 grid gap-2"
375
+ data-testid="tw-runtime-repl__layer-snippets"
376
+ >
377
+ {activeLayer.snippets.map((snippet) => (
378
+ <button
379
+ className="rounded-[var(--radius-sm)] bg-[var(--color-bg-muted)] px-2 py-2 text-left font-mono text-[11px] leading-5 text-[var(--color-text-primary)] ring-1 ring-[var(--color-border-subtle)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--color-bg-hover)]"
380
+ data-testid={`tw-runtime-repl__layer-snippet-${slugify(snippet.label)}`}
381
+ key={snippet.label}
382
+ onClick={() => loadSnippet(snippet.expression)}
383
+ type="button"
384
+ >
385
+ <span className="block font-sans text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]">
386
+ {snippet.label}
387
+ </span>
388
+ {snippet.expression}
389
+ </button>
390
+ ))}
391
+ </div>
392
+
393
+ <details
394
+ className="mt-3 rounded-[var(--radius-sm)] bg-[var(--color-bg-muted)] px-2 py-2 text-xs text-[var(--color-text-secondary)] ring-1 ring-[var(--color-border-subtle)]"
395
+ data-testid="tw-runtime-repl__layer-routed"
396
+ >
397
+ <summary className="cursor-pointer text-[11px] font-medium text-[var(--color-text-primary)]">
398
+ Routed data gaps
399
+ </summary>
400
+ <ul className="mt-2 space-y-1 leading-5">
401
+ {activeLayer.routed.map((item) => (
402
+ <li key={item}>{item}</li>
403
+ ))}
404
+ </ul>
405
+ </details>
406
+ </section>
407
+
284
408
  <div
285
409
  ref={outputRef}
286
410
  className="max-h-[50vh] min-h-[160px] overflow-y-auto px-4 py-3 font-mono text-xs leading-relaxed text-[var(--color-text-primary)]"
@@ -365,7 +489,7 @@ export function TwRuntimeReplDialog(props: TwRuntimeReplDialogProps): React.JSX.
365
489
  ].join(" ")}
366
490
  />
367
491
  <div className="mt-1.5 flex items-center justify-between text-[10px] text-[var(--color-text-tertiary)]">
368
- <span>Enter to evaluate · Shift+Enter for newline · ↑/↓ history · Esc to close</span>
492
+ <span>Enter to evaluate · Shift+Enter newline · ↑/↓ history · Alt+1..9/0/- layers · Esc close</span>
369
493
  <span>{entries.length > 0 ? `${entries.length} entries` : ""}</span>
370
494
  </div>
371
495
  </div>
@@ -374,6 +498,10 @@ export function TwRuntimeReplDialog(props: TwRuntimeReplDialogProps): React.JSX.
374
498
  );
375
499
  }
376
500
 
501
+ function slugify(value: string): string {
502
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
503
+ }
504
+
377
505
  export function isReplToggleShortcut(event: KeyboardEvent): boolean {
378
506
  const isP =
379
507
  event.code === "KeyP" ||
@@ -27,6 +27,11 @@ export interface SelectionToolPlacement {
27
27
 
28
28
  export interface TwSelectionToolHostProps {
29
29
  tool: ActiveSelectionToolModel | null;
30
+ /**
31
+ * Stable identity for the current selection. When omitted, the host
32
+ * falls back to the tool preview key for direct component consumers.
33
+ */
34
+ selectionKey?: string | null;
30
35
  contextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
31
36
  placement: SelectionToolPlacement | null;
32
37
  /**
@@ -76,6 +81,7 @@ export interface TwSelectionToolHostProps {
76
81
  onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
77
82
  onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
78
83
  onOpenTableMore?: (coords: { clientX: number; clientY: number }) => void;
84
+ showPreservationDiagnostics?: boolean;
79
85
  }
80
86
 
81
87
  /**
@@ -84,12 +90,17 @@ export interface TwSelectionToolHostProps {
84
90
  * previewText so that a new selection (which produces a new previewText)
85
91
  * resets the timer. Pure re-renders with the same selection do not reset.
86
92
  */
87
- function useDwellDensity(tool: ActiveSelectionToolModel | null): "micro" | "full" {
93
+ function useDwellDensity(
94
+ tool: ActiveSelectionToolModel | null,
95
+ selectionKeyOverride?: string | null,
96
+ ): "micro" | "full" {
88
97
  // Derive a stable selection key from the tool. For formatting-inline we
89
98
  // use the previewText (reflects selected content). For other kinds we use
90
- // a fixed sentinel so they never reset mid-render.
99
+ // a fixed sentinel so they never reset mid-render. Mounted callers pass
100
+ // the actual anchor/head identity so directional drag selections reset even
101
+ // when they cover the same preview text.
91
102
  const selectionKey = tool
92
- ? `${tool.kind}:${tool.previewText ?? "none"}`
103
+ ? `${tool.kind}:${selectionKeyOverride ?? tool.previewText ?? "none"}`
93
104
  : null;
94
105
  const [density, setDensity] = useState<"micro" | "full">("micro");
95
106
 
@@ -111,7 +122,7 @@ function useDwellDensity(tool: ActiveSelectionToolModel | null): "micro" | "full
111
122
  }
112
123
 
113
124
  export function TwSelectionToolHost(props: TwSelectionToolHostProps) {
114
- const density = useDwellDensity(props.tool);
125
+ const density = useDwellDensity(props.tool, props.selectionKey);
115
126
  const { onChromePinChange } = props;
116
127
 
117
128
  // Phase F.2 — gate the selection tool on the shared local-surface
@@ -162,6 +173,8 @@ export function TwSelectionToolHost(props: TwSelectionToolHostProps) {
162
173
 
163
174
  const overlayTestId = getOverlayTestId(props.tool.kind, Boolean(props.placement));
164
175
  const toolContent = renderTool(props, props.tool, density);
176
+ const shouldRenderContextAnalytics =
177
+ props.tool.kind !== "blocked-explainer" && Boolean(props.contextAnalytics);
165
178
  const content = toolContent ? (
166
179
  <div
167
180
  ref={props.rootRef}
@@ -169,7 +182,7 @@ export function TwSelectionToolHost(props: TwSelectionToolHostProps) {
169
182
  onBlurCapture={props.onBlurCapture}
170
183
  className="flex flex-col gap-1.5"
171
184
  >
172
- {props.contextAnalytics ? (
185
+ {shouldRenderContextAnalytics ? (
173
186
  <TwContextAnalyticsSummary
174
187
  snapshot={props.contextAnalytics}
175
188
  compact
@@ -301,6 +314,7 @@ function renderTool(
301
314
  onSetTableAlignment={props.onSetTableAlignment}
302
315
  onSetCellVerticalAlign={props.onSetCellVerticalAlign}
303
316
  onOpenTableMore={props.onOpenTableMore}
317
+ showPreservationDiagnostics={props.showPreservationDiagnostics}
304
318
  />
305
319
  );
306
320
  case "comment-thread":
@@ -36,6 +36,7 @@ export interface TwSelectionToolStructureProps {
36
36
  onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
37
37
  onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
38
38
  onOpenTableMore?: (coords: { clientX: number; clientY: number }) => void;
39
+ showPreservationDiagnostics?: boolean;
39
40
  }
40
41
 
41
42
  export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
@@ -53,7 +54,10 @@ export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
53
54
  // Shapes and text boxes are shipped as informational-only structure tools
54
55
  // until there is a real runtime-backed object mutation path.
55
56
  return props.model.activeObject ? (
56
- <TwObjectContextToolbar activeObject={props.model.activeObject} />
57
+ <TwObjectContextToolbar
58
+ activeObject={props.model.activeObject}
59
+ showPreservationDiagnostics={props.showPreservationDiagnostics}
60
+ />
57
61
  ) : null;
58
62
  case "table":
59
63
  return (