@beyondwork/docx-react-component 1.0.37 → 1.0.38

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 (74) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +319 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +815 -55
  6. package/src/io/export/serialize-main-document.ts +2 -11
  7. package/src/io/export/serialize-numbering.ts +1 -2
  8. package/src/io/export/serialize-tables.ts +74 -0
  9. package/src/io/export/table-properties-xml.ts +139 -4
  10. package/src/io/normalize/normalize-text.ts +15 -0
  11. package/src/io/ooxml/parse-footnotes.ts +60 -0
  12. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  13. package/src/io/ooxml/parse-main-document.ts +137 -0
  14. package/src/io/ooxml/parse-tables.ts +249 -0
  15. package/src/model/canonical-document.ts +34 -0
  16. package/src/runtime/document-layout.ts +4 -2
  17. package/src/runtime/document-navigation.ts +1 -1
  18. package/src/runtime/document-runtime.ts +114 -0
  19. package/src/runtime/layout/default-page-format.ts +96 -0
  20. package/src/runtime/layout/index.ts +45 -0
  21. package/src/runtime/layout/inert-layout-facet.ts +14 -0
  22. package/src/runtime/layout/layout-engine-instance.ts +33 -23
  23. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  24. package/src/runtime/layout/page-format-catalog.ts +233 -0
  25. package/src/runtime/layout/page-graph.ts +19 -0
  26. package/src/runtime/layout/paginated-layout-engine.ts +142 -9
  27. package/src/runtime/layout/project-block-fragments.ts +91 -0
  28. package/src/runtime/layout/public-facet.ts +709 -16
  29. package/src/runtime/layout/table-render-plan.ts +229 -0
  30. package/src/runtime/render/block-fragment-projection.ts +35 -0
  31. package/src/runtime/render/decoration-resolver.ts +189 -0
  32. package/src/runtime/render/index.ts +57 -0
  33. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  34. package/src/runtime/render/render-frame-types.ts +317 -0
  35. package/src/runtime/render/render-kernel.ts +755 -0
  36. package/src/runtime/view-state.ts +67 -0
  37. package/src/runtime/workflow-markup.ts +1 -5
  38. package/src/runtime/workflow-rail-segments.ts +280 -0
  39. package/src/ui/WordReviewEditor.tsx +84 -15
  40. package/src/ui/editor-shell-view.tsx +6 -0
  41. package/src/ui/headless/chrome-registry.ts +280 -14
  42. package/src/ui/headless/scoped-chrome-policy.ts +20 -1
  43. package/src/ui/headless/selection-tool-types.ts +10 -0
  44. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  45. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  46. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  47. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  48. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  49. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  52. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  53. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  54. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  55. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  56. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  57. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  58. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  59. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  60. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
  61. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  62. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
  63. package/src/ui-tailwind/index.ts +33 -0
  64. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  65. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  66. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  68. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  69. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  70. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  71. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  72. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  73. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
  74. package/src/ui-tailwind/tw-review-workspace.tsx +136 -1
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ChromeOverlay — the single absolute-positioned overlay plane that hosts
3
+ * every over-document surface (scope rail, comment balloons, revision
4
+ * margin bars, object handles, workspace view-switcher dock).
5
+ *
6
+ * Per runtime-rendering-and-chrome-phase.md §6.2, every overlay child
7
+ * receives `ref.layout` and reads its position from the same render-frame
8
+ * anchor index — not DOM rects, not selection rects — so the chrome stays
9
+ * in place across scroll, zoom, and relayout.
10
+ *
11
+ * This component owns only the plane and the shared coordinate space.
12
+ * Each layer it composes is a pure consumer of the facet.
13
+ */
14
+
15
+ import * as React from "react";
16
+ import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
17
+ import type { ScopeRailSegment } from "../../runtime/layout";
18
+ import type { WordReviewEditorLayoutFacet } from "../../runtime/layout";
19
+ import { TwScopeRailLayer } from "./tw-scope-rail-layer";
20
+ import { TwWorkspaceViewSwitcher, type WorkspaceView } from "./tw-workspace-view-switcher";
21
+
22
+ export interface TwChromeOverlayProps {
23
+ /** Layout facet the overlay layers read from. */
24
+ facet: WordReviewEditorLayoutFacet;
25
+ /** Optional coordinate space override. Defaults to the overlay origin. */
26
+ space?: OverlayCoordinateSpace;
27
+ /** Active scope id (for emphasis + rail tab sync). */
28
+ activeScopeId?: string | null;
29
+ /** Click handler the rail layer forwards to consumers. */
30
+ onScopeSegmentClick?: (segment: ScopeRailSegment) => void;
31
+ /** Currently active workspace view preset (draft / layout / review / workflow). */
32
+ activeWorkspaceView?: WorkspaceView;
33
+ /** Handler that fires when the user picks a workspace view. */
34
+ onWorkspaceViewChange?: (view: WorkspaceView) => void;
35
+ /** Show the bottom workspace view-switcher dock. Default: true. */
36
+ showWorkspaceDock?: boolean;
37
+ /** Test id applied to the overlay root. */
38
+ "data-testid"?: string;
39
+ /** Optional extra children (e.g., future comment balloon layer). */
40
+ children?: React.ReactNode;
41
+ }
42
+
43
+ /**
44
+ * Placement contract:
45
+ * - The overlay is an absolutely positioned `div` that fills its parent.
46
+ * - The parent must be `position: relative` so the overlay anchors to
47
+ * the document column (not the viewport).
48
+ * - Pointer events are disabled on the root so the editor surface under
49
+ * the overlay continues to receive input; individual layers opt in to
50
+ * pointer events on their interactive elements (buttons, handles).
51
+ */
52
+ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
53
+ facet,
54
+ space,
55
+ activeScopeId,
56
+ onScopeSegmentClick,
57
+ activeWorkspaceView,
58
+ onWorkspaceViewChange,
59
+ showWorkspaceDock = true,
60
+ "data-testid": testId,
61
+ children,
62
+ }) => {
63
+ return (
64
+ <div
65
+ className="wre-chrome-overlay pointer-events-none absolute inset-0 z-30"
66
+ data-testid={testId ?? "chrome-overlay"}
67
+ role="presentation"
68
+ >
69
+ <TwScopeRailLayer
70
+ facet={facet}
71
+ space={space}
72
+ activeScopeId={activeScopeId}
73
+ onSegmentClick={onScopeSegmentClick}
74
+ />
75
+ {children}
76
+ {showWorkspaceDock ? (
77
+ <TwWorkspaceViewSwitcher
78
+ activeView={activeWorkspaceView ?? "review"}
79
+ onViewChange={onWorkspaceViewChange}
80
+ />
81
+ ) : null}
82
+ </div>
83
+ );
84
+ };
85
+
86
+ export default TwChromeOverlay;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Scope rail layer — renders workflow scopes as a continuous zone with a
3
+ * gutter icon + label column OUTSIDE the document flow plus a flat block-
4
+ * level tint BEHIND the scoped paragraphs.
5
+ *
6
+ * Per runtime-rendering-and-chrome-phase.md §5, the rail is a projection
7
+ * over canonical workflow scopes; it never lives inside the PM NodeView
8
+ * tree. Positions come from the render kernel's anchor index — not from
9
+ * DOM rect math — so the rail stays aligned across scroll, zoom, resize,
10
+ * and through predicted-text reconciliation.
11
+ */
12
+
13
+ import * as React from "react";
14
+ import {
15
+ inflateRect,
16
+ projectRectToOverlay,
17
+ unionRect,
18
+ type OverlayCoordinateSpace,
19
+ } from "./chrome-overlay-projector";
20
+ import type { RenderFrameRect } from "../../runtime/render";
21
+ import type { ScopeRailSegment, ScopeRailPosture } from "../../runtime/layout";
22
+ import type { WordReviewEditorLayoutFacet } from "../../runtime/layout";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Types
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export interface TwScopeRailLayerProps {
29
+ /** Layout facet that provides segments + anchor index. */
30
+ facet: WordReviewEditorLayoutFacet;
31
+ /** Overlay's coordinate space. Defaults to the overlay's own origin. */
32
+ space?: OverlayCoordinateSpace;
33
+ /** Horizontal padding (px) the rail gutter occupies to the left of body. */
34
+ railLaneWidthPx?: number;
35
+ /** Optional click handler for a segment label (open-scope drawer, etc). */
36
+ onSegmentClick?: (segment: ScopeRailSegment) => void;
37
+ /** Scope id that should render with the `active` emphasis. */
38
+ activeScopeId?: string | null;
39
+ /** Test id applied to the layer root. */
40
+ "data-testid"?: string;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Posture → visual grammar
45
+ // ---------------------------------------------------------------------------
46
+
47
+ interface PostureStyle {
48
+ labelText: string;
49
+ icon: string; // lucide-style key; CSS ::before handles glyph in production
50
+ railToken: string;
51
+ tintToken: string;
52
+ }
53
+
54
+ const POSTURE_STYLES: Record<ScopeRailPosture, PostureStyle> = {
55
+ edit: { labelText: "EDIT", icon: "pencil", railToken: "accent", tintToken: "accent" },
56
+ suggest: { labelText: "SUGGEST", icon: "sparkles", railToken: "warning", tintToken: "warning" },
57
+ comment: { labelText: "COMMENT", icon: "message", railToken: "insert", tintToken: "insert" },
58
+ view: { labelText: "IN SCOPE", icon: "eye", railToken: "secondary", tintToken: "secondary" },
59
+ candidate: { labelText: "PROPOSED", icon: "flag", railToken: "warning", tintToken: "warning" },
60
+ "preserve-only": { labelText: "BLOCKED", icon: "lock", railToken: "danger", tintToken: "danger" },
61
+ "blocked-import": { labelText: "BLOCKED", icon: "lock", railToken: "danger", tintToken: "danger" },
62
+ };
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Component
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const DEFAULT_RAIL_LANE_PX = 120;
69
+ const LABEL_WIDTH_PX = 92;
70
+
71
+ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
72
+ facet,
73
+ space,
74
+ railLaneWidthPx = DEFAULT_RAIL_LANE_PX,
75
+ onSegmentClick,
76
+ activeScopeId,
77
+ "data-testid": testId,
78
+ }) => {
79
+ // Read the render frame once per paint cycle. The facet.subscribe path
80
+ // already invalidates the caller's React state on layout changes, so we
81
+ // just read on render.
82
+ const frame = typeof facet.getRenderFrame === "function"
83
+ ? facet.getRenderFrame() ?? null
84
+ : null;
85
+ const segments = facet.getAllScopeRailSegments();
86
+
87
+ if (!frame || segments.length === 0) {
88
+ return null;
89
+ }
90
+
91
+ // Group segments by scopeId so multi-page scopes render one contiguous
92
+ // tint per page range. (Per-page render happens below because each
93
+ // scope may span pages.)
94
+ const items = segments.map((segment) => {
95
+ const rect = resolveSegmentRect(facet, frame, segment);
96
+ if (!rect) return null;
97
+ const style = POSTURE_STYLES[segment.posture];
98
+ return { segment, rect, style };
99
+ }).filter((item): item is NonNullable<typeof item> => item !== null);
100
+
101
+ const projectorSpace: OverlayCoordinateSpace = space ?? { originLeftPx: 0, originTopPx: 0 };
102
+
103
+ return (
104
+ <div
105
+ className="wre-scope-rail-layer pointer-events-none absolute inset-0 z-20"
106
+ data-testid={testId ?? "scope-rail-layer"}
107
+ aria-hidden="false"
108
+ role="group"
109
+ aria-label="Workflow scope rail"
110
+ >
111
+ {items.map(({ segment, rect, style }) => {
112
+ const isActive = activeScopeId === segment.scopeId || segment.isActiveWorkItem;
113
+ const tintRect = inflateRect(rect, { leftPx: 4, rightPx: 4, topPx: 2, bottomPx: 2 });
114
+ const labelRect: RenderFrameRect = {
115
+ leftPx: rect.leftPx - railLaneWidthPx,
116
+ topPx: rect.topPx,
117
+ widthPx: LABEL_WIDTH_PX,
118
+ heightPx: Math.max(20, Math.min(rect.heightPx, 48)),
119
+ };
120
+
121
+ return (
122
+ <React.Fragment key={`${segment.scopeId}:${segment.pageIndex}:${segment.fromOffset}`}>
123
+ {/* Flat tint behind the scoped block region */}
124
+ <div
125
+ className={`wre-scope-rail-tint wre-scope-rail-tint-${style.tintToken} absolute ${
126
+ isActive ? "wre-scope-rail-tint-active" : ""
127
+ }`}
128
+ data-scope-id={segment.scopeId}
129
+ data-posture={segment.posture}
130
+ style={projectRectToOverlay(tintRect, projectorSpace)}
131
+ />
132
+ {/* Gutter label + icon outside the page frame */}
133
+ <button
134
+ type="button"
135
+ className={`wre-scope-rail-label wre-scope-rail-label-${style.railToken} pointer-events-auto absolute flex flex-col items-center justify-center gap-1 rounded-md text-[10px] font-semibold uppercase tracking-[0.08em] ${
136
+ isActive ? "wre-scope-rail-label-active" : ""
137
+ }`}
138
+ data-scope-id={segment.scopeId}
139
+ data-posture={segment.posture}
140
+ data-icon={style.icon}
141
+ aria-label={`${style.labelText}${segment.label ? `: ${segment.label}` : ""}`}
142
+ onClick={onSegmentClick ? () => onSegmentClick(segment) : undefined}
143
+ style={projectRectToOverlay(labelRect, projectorSpace)}
144
+ >
145
+ <span aria-hidden="true" className={`wre-scope-rail-icon wre-scope-rail-icon-${style.icon}`} />
146
+ <span className="wre-scope-rail-label-text">{style.labelText}</span>
147
+ </button>
148
+ </React.Fragment>
149
+ );
150
+ })}
151
+ </div>
152
+ );
153
+ };
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Internals
157
+ // ---------------------------------------------------------------------------
158
+
159
+ function resolveSegmentRect(
160
+ facet: WordReviewEditorLayoutFacet,
161
+ _frame: { anchorIndex: { byRuntimeOffset: (offset: number) => RenderFrameRect | null } },
162
+ segment: ScopeRailSegment,
163
+ ): RenderFrameRect | null {
164
+ const fromRect = _frame.anchorIndex.byRuntimeOffset(segment.fromOffset);
165
+ const toRect = _frame.anchorIndex.byRuntimeOffset(Math.max(segment.fromOffset, segment.toOffset - 1));
166
+ const unioned = unionRect(fromRect, toRect);
167
+ if (unioned) return unioned;
168
+ // Fall back to the page rect so long scopes that can't resolve per-line
169
+ // still render a posture in the gutter.
170
+ const fallbackPage = facet.getPage(segment.pageIndex);
171
+ if (!fallbackPage) return null;
172
+ // Approximate: derive a rect from the page's body region by asking the
173
+ // render frame for the page rect. Without a render kernel kernel this
174
+ // function returns null and the segment is skipped.
175
+ return null;
176
+ }
177
+
178
+ export default TwScopeRailLayer;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * TwWorkspaceViewSwitcher — floating dock that sits at the bottom of the
3
+ * chrome overlay. Mirrors the "WORKFLOW VIEW" bottom bar in image copy.png.
4
+ *
5
+ * Per runtime-rendering-and-chrome-phase.md §6.3, the dock is a
6
+ * `DraggableFloat`-compatible surface that lives inside the `ChromeOverlay`
7
+ * plane. Today it ships the preset selector + the canonical workflow
8
+ * actions (review / comment / approve) so consumers can wire them without
9
+ * waiting for the full DraggableFloat primitive.
10
+ */
11
+
12
+ import * as React from "react";
13
+
14
+ export type WorkspaceView = "draft" | "layout" | "review" | "workflow";
15
+
16
+ export interface WorkspaceViewAction {
17
+ id: string;
18
+ label: string;
19
+ icon: string;
20
+ onClick?: () => void;
21
+ disabled?: boolean;
22
+ }
23
+
24
+ export interface TwWorkspaceViewSwitcherProps {
25
+ activeView: WorkspaceView;
26
+ onViewChange?: (view: WorkspaceView) => void;
27
+ actions?: readonly WorkspaceViewAction[];
28
+ "data-testid"?: string;
29
+ }
30
+
31
+ const DEFAULT_VIEW_ORDER: readonly WorkspaceView[] = [
32
+ "draft",
33
+ "layout",
34
+ "review",
35
+ "workflow",
36
+ ];
37
+
38
+ const VIEW_LABELS: Record<WorkspaceView, string> = {
39
+ draft: "DRAFT",
40
+ layout: "LAYOUT",
41
+ review: "REVIEW",
42
+ workflow: "WORKFLOW VIEW",
43
+ };
44
+
45
+ export const TwWorkspaceViewSwitcher: React.FC<TwWorkspaceViewSwitcherProps> = ({
46
+ activeView,
47
+ onViewChange,
48
+ actions,
49
+ "data-testid": testId,
50
+ }) => {
51
+ return (
52
+ <div
53
+ className="wre-workspace-dock pointer-events-auto absolute left-1/2 bottom-6 z-10 -translate-x-1/2 flex items-center gap-2 rounded-full border border-border/70 bg-canvas/95 px-4 py-2 shadow-lg backdrop-blur"
54
+ data-testid={testId ?? "workspace-view-switcher"}
55
+ role="toolbar"
56
+ aria-label="Workspace view"
57
+ >
58
+ {DEFAULT_VIEW_ORDER.map((view) => (
59
+ <button
60
+ key={view}
61
+ type="button"
62
+ className={`wre-workspace-dock-view-btn inline-flex items-center gap-1 rounded-full px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] ${
63
+ view === activeView
64
+ ? "wre-workspace-dock-view-btn-active bg-primary text-white"
65
+ : "text-secondary hover:text-primary"
66
+ }`}
67
+ aria-pressed={view === activeView}
68
+ onClick={onViewChange ? () => onViewChange(view) : undefined}
69
+ disabled={!onViewChange}
70
+ >
71
+ <span aria-hidden="true" className="wre-workspace-dock-icon h-3 w-3" />
72
+ {VIEW_LABELS[view]}
73
+ </button>
74
+ ))}
75
+ {actions && actions.length > 0 ? (
76
+ <div className="wre-workspace-dock-sep mx-1 h-5 w-px bg-border" />
77
+ ) : null}
78
+ {actions?.map((action) => (
79
+ <button
80
+ key={action.id}
81
+ type="button"
82
+ className="wre-workspace-dock-action inline-flex h-7 w-7 items-center justify-center rounded-full text-secondary hover:bg-surface hover:text-primary disabled:opacity-40"
83
+ aria-label={action.label}
84
+ disabled={action.disabled}
85
+ onClick={action.onClick}
86
+ data-action-id={action.id}
87
+ >
88
+ <span aria-hidden="true" className={`wre-workspace-dock-icon wre-workspace-dock-icon-${action.icon}`} />
89
+ </button>
90
+ ))}
91
+ </div>
92
+ );
93
+ };
94
+
95
+ export default TwWorkspaceViewSwitcher;
@@ -103,6 +103,10 @@ export function createFastTextEditLane(
103
103
  intent: PredictedIntent,
104
104
  buildTx: (tr: Transaction) => Transaction | null,
105
105
  ): void {
106
+ // IME composition holds the DOM range; predicting through it would
107
+ // fight the browser's composition DOM mutations. The bridge's
108
+ // compositionstart/end handlers toggle this flag on the session.
109
+ if (options.session.isComposing()) return;
106
110
  const view = options.getView();
107
111
  const positionMap = options.getPositionMap();
108
112
  if (!view || !positionMap) return;
@@ -43,6 +43,14 @@ export interface LocalEditSessionState {
43
43
  clearAllPending(): PendingOp[];
44
44
  hasPending(): boolean;
45
45
  isPredicted(opId: string): boolean;
46
+ /**
47
+ * IME composition state. Set to true while the browser is composing an
48
+ * IME input sequence (between `compositionstart` and `compositionend`);
49
+ * the predicted lane must bail from `run()` when composing so IME and
50
+ * prediction do not fight over the same DOM range.
51
+ */
52
+ isComposing(): boolean;
53
+ setComposing(composing: boolean): void;
46
54
  }
47
55
 
48
56
  export interface CreateLocalEditSessionStateOptions {
@@ -53,12 +61,15 @@ export function createLocalEditSessionState(
53
61
  options: CreateLocalEditSessionStateOptions,
54
62
  ): LocalEditSessionState {
55
63
  let baseRevisionToken = options.baseRevisionToken;
64
+ let composing = false;
56
65
  const pendingOps: PendingOp[] = [];
57
66
  const predictedIds = new Set<string>();
58
67
 
59
68
  return {
60
69
  getBaseRevisionToken: () => baseRevisionToken,
61
70
  getPendingOps: () => pendingOps.slice(),
71
+ isComposing: () => composing,
72
+ setComposing: (value) => { composing = value; },
62
73
  appendPending(op) {
63
74
  pendingOps.push(op);
64
75
  predictedIds.add(op.opId);
@@ -15,7 +15,13 @@ export type PerfProbeKind =
15
15
  | "workspace.chrome"
16
16
  | "selection.sync"
17
17
  | "layout.incremental"
18
- | "layout.full";
18
+ | "layout.full"
19
+ | "render.frame_build"
20
+ | "render.frame_diff"
21
+ | "render.decoration_resolve"
22
+ | "chrome.overlay_reposition"
23
+ | "chrome.hit_test"
24
+ | "rail.segment_project";
19
25
 
20
26
  /**
21
27
  * Counter names the FastTextEditLane emits via `incrementInvalidationCounter`.
@@ -26,6 +26,12 @@ export interface CommandBridgeCallbacks extends SelectionSyncCallbacks {
26
26
  onUndo: () => void;
27
27
  onRedo: () => void;
28
28
  onBlockedInput?: (command: "paste" | "drop", message: string) => void;
29
+ /**
30
+ * Optional. Fires on `compositionstart` (true) and `compositionend`
31
+ * (false). The surface forwards this to the predicted lane's session
32
+ * so the lane can bail from `run()` while IME is composing.
33
+ */
34
+ onCompositionChange?: (composing: boolean) => void;
29
35
  /**
30
36
  * Optional predicted-tx gate plugin. When provided, it replaces the
31
37
  * default unconditional filter so the FastTextEditLane can apply
@@ -91,15 +97,20 @@ export function createCommandBridgePlugins(
91
97
  props: {
92
98
  handleDOMEvents: {
93
99
  blur() {
94
- isComposing = false;
100
+ if (isComposing) {
101
+ isComposing = false;
102
+ callbacks.onCompositionChange?.(false);
103
+ }
95
104
  return false;
96
105
  },
97
106
  compositionstart() {
98
107
  isComposing = true;
108
+ callbacks.onCompositionChange?.(true);
99
109
  return false;
100
110
  },
101
111
  compositionend() {
102
112
  isComposing = false;
113
+ callbacks.onCompositionChange?.(false);
103
114
  return false;
104
115
  },
105
116
  },
@@ -223,6 +223,19 @@ function subtractInlineOverlaps(
223
223
  return segments.filter((segment) => segment.from < segment.to);
224
224
  }
225
225
 
226
+ /**
227
+ * Rail decorations are now rendered on the `ChromeOverlay` plane via the
228
+ * `TwScopeRailLayer` consumer of `facet.getAllScopeRailSegments()`, not
229
+ * through PM Decoration.node. This function keeps its signature so the
230
+ * call sites below continue to compile; it warms the range cache (which
231
+ * other PM decorations can still consume) but emits no node decoration.
232
+ *
233
+ * Per runtime-rendering-and-chrome-phase.md §5 the rail must live outside
234
+ * the PM NodeView tree so: (a) the user perceives it as chrome, not
235
+ * document content, (b) predicted transactions never flash rail visuals,
236
+ * and (c) the rail can extend into the page-margin gutter, which PM
237
+ * cannot paint through block decorations.
238
+ */
226
239
  function pushRailDecorations(
227
240
  decorations: Decoration[],
228
241
  doc: PMNode,
@@ -231,19 +244,11 @@ function pushRailDecorations(
231
244
  spec: RailDecorationSpec,
232
245
  rangeCache: Map<string, Array<{ from: number; to: number }>>,
233
246
  ): void {
247
+ void decorations;
248
+ void spec;
234
249
  const cacheKey = `${from}:${to}`;
235
- const ranges = rangeCache.get(cacheKey) ?? collectRailRanges(doc, from, to);
236
250
  if (!rangeCache.has(cacheKey)) {
237
- rangeCache.set(cacheKey, ranges);
238
- }
239
- for (const range of ranges) {
240
- decorations.push(
241
- Decoration.node(range.from, range.to, {
242
- class: spec.className,
243
- "data-workflow-rail": spec.railKind,
244
- ...spec.attrs,
245
- }),
246
- );
251
+ rangeCache.set(cacheKey, collectRailRanges(doc, from, to));
247
252
  }
248
253
  }
249
254
 
@@ -458,7 +463,12 @@ export function buildDecorations(
458
463
  activeScopeIds.has(scope.scopeId)
459
464
  );
460
465
 
461
- if (isSelectionZone && pmRange.allowInline && pmRange.from < pmRange.to) {
466
+ if (pmRange.allowInline && pmRange.from < pmRange.to) {
467
+ // Post-R3a: every workflow scope emits inline decorations with
468
+ // the scope-id attribution. The flat block-tint + gutter rail
469
+ // render on the ChromeOverlay — PM keeps only inline class hooks
470
+ // so selection tools, accessibility, and host scripts can still
471
+ // resolve the active scope at a text offset.
462
472
  const visibleScopeSegments = subtractInlineOverlaps(
463
473
  { from: pmRange.from, to: pmRange.to },
464
474
  lockedPmRanges.filter((range) => range.to > pmRange.from && range.from < pmRange.to),
@@ -340,6 +340,9 @@ export const TwProseMirrorSurface = forwardRef<
340
340
  onUndo: () => callbacksRef.current?.onUndo(),
341
341
  onRedo: () => callbacksRef.current?.onRedo(),
342
342
  onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
343
+ onCompositionChange: (composing) => {
344
+ sessionRef.current?.setComposing(composing);
345
+ },
343
346
  });
344
347
 
345
348
  return [
@@ -14,10 +14,31 @@ export { TwReviewRail, type TwReviewRailProps, type ReviewRailTab } from "./revi
14
14
  export { TwCommentSidebar } from "./review/tw-comment-sidebar";
15
15
  export { TwRevisionSidebar } from "./review/tw-revision-sidebar";
16
16
  export { TwHealthPanel } from "./review/tw-health-panel";
17
+ export { TwWorkflowTab, type TwWorkflowTabProps } from "./review/tw-workflow-tab";
18
+ export {
19
+ TwRailCard,
20
+ type TwRailCardProps,
21
+ type RailCardTone,
22
+ type RailCardAvatar,
23
+ type RailCardCounter,
24
+ type RailCardProgress,
25
+ } from "./review/tw-rail-card";
26
+ export {
27
+ TwReviewRailFooter,
28
+ type TwReviewRailFooterProps,
29
+ } from "./review/tw-review-rail-footer";
17
30
 
18
31
  // Toolbar
19
32
  export { TwToolbar, type TwToolbarProps } from "./toolbar/tw-toolbar";
20
33
  export { TwToolbarIconButton } from "./toolbar/tw-toolbar-icon-button";
34
+ export {
35
+ TwShellHeader,
36
+ type TwShellHeaderProps,
37
+ type ShellHeaderMode,
38
+ type ShellHeaderModeOption,
39
+ type ShellHeaderPrimaryAction,
40
+ type ShellHeaderIconAction,
41
+ } from "./toolbar/tw-shell-header";
21
42
  export type { WorkspaceMode, ZoomLevel } from "../api/public-types";
22
43
 
23
44
  // Status
@@ -27,6 +48,18 @@ export { TwStatusBar } from "./status/tw-status-bar";
27
48
  export { TwAlertBanner } from "./chrome/tw-alert-banner";
28
49
  export { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
29
50
 
51
+ // Chrome overlay plane (R3a — scope rail, workspace dock)
52
+ export {
53
+ TwChromeOverlay,
54
+ type TwChromeOverlayProps,
55
+ TwScopeRailLayer,
56
+ type TwScopeRailLayerProps,
57
+ TwWorkspaceViewSwitcher,
58
+ type TwWorkspaceViewSwitcherProps,
59
+ type WorkspaceView,
60
+ type WorkspaceViewAction,
61
+ } from "./chrome-overlay";
62
+
30
63
  // Session capabilities
31
64
  export {
32
65
  deriveCapabilities,
@@ -93,10 +93,10 @@ function CommentThreadCard(props: {
93
93
  role="button"
94
94
  tabIndex={0}
95
95
  className={[
96
- "cursor-pointer rounded-md bg-surface/90 px-3 py-2.5 transition-colors ring-1 ring-border/40",
96
+ "cursor-pointer rounded-lg bg-surface/90 px-3 py-2.5 transition-colors ring-1 ring-border/40",
97
97
  focusRingClass,
98
98
  isActive
99
- ? "bg-accent-soft/40 ring-accent/25"
99
+ ? "bg-accent-soft/40 ring-accent/25 shadow-[var(--shadow-soft)]"
100
100
  : "hover:bg-surface",
101
101
  thread.status === "detached" ? "opacity-70" : "",
102
102
  ].join(" ")}