@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.
- package/package.json +1 -1
- package/src/api/public-types.ts +319 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +1 -2
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/model/canonical-document.ts +34 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +114 -0
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +45 -0
- package/src/runtime/layout/inert-layout-facet.ts +14 -0
- package/src/runtime/layout/layout-engine-instance.ts +33 -23
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +142 -9
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +709 -16
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +84 -15
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/headless/chrome-registry.ts +280 -14
- package/src/ui/headless/scoped-chrome-policy.ts +20 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
- 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
|
|
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,
|
|
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 (
|
|
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 [
|
package/src/ui-tailwind/index.ts
CHANGED
|
@@ -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-
|
|
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(" ")}
|