@beyondwork/docx-react-component 1.0.39 → 1.0.41
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 +122 -0
- package/src/index.ts +9 -0
- package/src/runtime/document-runtime.ts +7 -0
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/public-facet.ts +41 -0
- package/src/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +17 -0
- package/src/ui/editor-shell-view.tsx +18 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +89 -20
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +55 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +26 -1
- package/src/ui-tailwind/index.ts +5 -0
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- package/src/ui-tailwind/tw-review-workspace.tsx +75 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.41",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"packageManager": "pnpm@10.30.3",
|
|
7
7
|
"type": "module",
|
package/src/api/public-types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PersistedEditorSnapshot as RuntimePersistedEditorSnapshot } from "../core/state/editor-state.ts";
|
|
2
2
|
import type { CanonicalParagraphFormatting, CanonicalRunFormatting } from "../model/canonical-document.ts";
|
|
3
3
|
import type { WordReviewEditorLayoutFacet } from "../runtime/layout/public-facet.ts";
|
|
4
|
+
import type { RenderFrameRect } from "../runtime/render/index.ts";
|
|
5
|
+
import type { ScopeRailPosture } from "../runtime/workflow-rail-segments.ts";
|
|
4
6
|
|
|
5
7
|
export type { CanonicalParagraphFormatting, CanonicalRunFormatting };
|
|
6
8
|
|
|
@@ -1675,6 +1677,102 @@ export interface WorkflowMetadataSnapshot {
|
|
|
1675
1677
|
entries: WorkflowMetadataEntry[];
|
|
1676
1678
|
}
|
|
1677
1679
|
|
|
1680
|
+
// ---------------------------------------------------------------------------
|
|
1681
|
+
// R2 — issue metadata (scope-card-overlay P1)
|
|
1682
|
+
// ---------------------------------------------------------------------------
|
|
1683
|
+
|
|
1684
|
+
/**
|
|
1685
|
+
* Canonical metadata id for playbook-raised issues. Hosts register a
|
|
1686
|
+
* `WorkflowMetadataDefinition` with this id and push one
|
|
1687
|
+
* `WorkflowMetadataEntry` per issue; the scope card reads the entries
|
|
1688
|
+
* whose `workItemId` or `scopeId` matches a rendered scope.
|
|
1689
|
+
*
|
|
1690
|
+
* Field names line up 1:1 with the CCEP Playbook Engine rule shape so
|
|
1691
|
+
* hosts can push rule output without remapping.
|
|
1692
|
+
*/
|
|
1693
|
+
export const ISSUE_METADATA_ID = "workflow.metadata.issue" as const;
|
|
1694
|
+
|
|
1695
|
+
export type IssueSeverity = "low" | "medium" | "high" | "blocker";
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Playbook rule modes. `guidance` is an advisory preference; `fallback`
|
|
1699
|
+
* is a tiered alternative; `mandatory` cannot be silently removed;
|
|
1700
|
+
* `escalate` requires named-owner sign-off; `block` refuses the edit
|
|
1701
|
+
* outright.
|
|
1702
|
+
*/
|
|
1703
|
+
export type IssueMode =
|
|
1704
|
+
| "guidance"
|
|
1705
|
+
| "fallback"
|
|
1706
|
+
| "mandatory"
|
|
1707
|
+
| "escalate"
|
|
1708
|
+
| "block";
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Named escalation / ownership targets for playbook issues. Hosts can
|
|
1712
|
+
* extend the literal via a widened `string` alias if a tenant needs a
|
|
1713
|
+
* custom owner, but the canonical set matches CCEP's playbook rule
|
|
1714
|
+
* shape.
|
|
1715
|
+
*/
|
|
1716
|
+
export type IssueOwner =
|
|
1717
|
+
| "procurement"
|
|
1718
|
+
| "legal"
|
|
1719
|
+
| "risk"
|
|
1720
|
+
| "finance"
|
|
1721
|
+
| "sustainability";
|
|
1722
|
+
|
|
1723
|
+
export interface IssueMetadataValue {
|
|
1724
|
+
issueId: string;
|
|
1725
|
+
ruleId?: string;
|
|
1726
|
+
topic: string;
|
|
1727
|
+
severity: IssueSeverity;
|
|
1728
|
+
mode: IssueMode;
|
|
1729
|
+
owner?: IssueOwner;
|
|
1730
|
+
escalateTo?: IssueOwner;
|
|
1731
|
+
checklistState: "open" | "acknowledged" | "resolved" | "waived";
|
|
1732
|
+
escalationState?: "none" | "requested" | "approved" | "rejected";
|
|
1733
|
+
suggestionIds?: string[];
|
|
1734
|
+
rationale?: string;
|
|
1735
|
+
title: string;
|
|
1736
|
+
summary?: string;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* Per-scope action the scope card's issue row exposes. Dispatched as a
|
|
1741
|
+
* `scope-issue-action-requested` event — host decides what the action
|
|
1742
|
+
* means and updates the `IssueMetadataValue.checklistState` /
|
|
1743
|
+
* `escalationState` accordingly. The card never mutates runtime state.
|
|
1744
|
+
*/
|
|
1745
|
+
export type ScopeIssueAction =
|
|
1746
|
+
| "resolve"
|
|
1747
|
+
| "waive"
|
|
1748
|
+
| "escalate"
|
|
1749
|
+
| "acknowledge";
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Scope card projection consumed by the chrome overlay's card layer.
|
|
1753
|
+
* Joins a `WorkflowScope` with its attached issue metadata (R2),
|
|
1754
|
+
* suggestion groups (R3 — P2 populates), review-action count (K1 —
|
|
1755
|
+
* P2 populates), and agent-pending flag (K2 — P2 populates).
|
|
1756
|
+
*
|
|
1757
|
+
* `primaryAnchorRect` is the position the card should hover next to —
|
|
1758
|
+
* resolved via `RenderAnchorIndex.bySelection(fromOffset, toOffset)`
|
|
1759
|
+
* on the active page, or `null` when the scope is off-screen.
|
|
1760
|
+
*/
|
|
1761
|
+
export interface ScopeCardModel {
|
|
1762
|
+
scopeId: string;
|
|
1763
|
+
workItemId?: string;
|
|
1764
|
+
posture: ScopeRailPosture;
|
|
1765
|
+
label: string;
|
|
1766
|
+
primaryAnchorRect: RenderFrameRect | null;
|
|
1767
|
+
issue?: IssueMetadataValue;
|
|
1768
|
+
/** R3 suggestion groups attached to the scope. P2 populates; P1 = []. */
|
|
1769
|
+
suggestionGroupIds: readonly string[];
|
|
1770
|
+
/** K1 review-action count for the scope's issue. P2 populates; P1 = 0. */
|
|
1771
|
+
reviewActionCount: number;
|
|
1772
|
+
/** K2 agent-pending flag (overlapping WorkflowCandidateRange source:"ai"). P2 populates; P1 = false. */
|
|
1773
|
+
agentPending: boolean;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1678
1776
|
export interface WorkflowBlockedCommandReason {
|
|
1679
1777
|
code:
|
|
1680
1778
|
| "outside_workflow_scope"
|
|
@@ -2192,6 +2290,30 @@ export type WordReviewEditorEvent =
|
|
|
2192
2290
|
documentId: string;
|
|
2193
2291
|
command: string;
|
|
2194
2292
|
reasons: WorkflowBlockedCommandReason[];
|
|
2293
|
+
}
|
|
2294
|
+
| {
|
|
2295
|
+
/**
|
|
2296
|
+
* Scope card mode selector fired a mode change. Host relays to the
|
|
2297
|
+
* existing `setWorkflowOverlay` path (or the CCEP workflow
|
|
2298
|
+
* endpoint) — the card never mutates runtime state directly.
|
|
2299
|
+
*/
|
|
2300
|
+
type: "scope-mode-change-requested";
|
|
2301
|
+
documentId: string;
|
|
2302
|
+
scopeId: string;
|
|
2303
|
+
mode: WorkflowScopeMode;
|
|
2304
|
+
}
|
|
2305
|
+
| {
|
|
2306
|
+
/**
|
|
2307
|
+
* Scope card issue row fired an action (resolve / waive /
|
|
2308
|
+
* escalate / acknowledge). Host updates the attached
|
|
2309
|
+
* `IssueMetadataValue.checklistState` / `escalationState` and
|
|
2310
|
+
* re-pushes via `setWorkflowMetadataEntries`.
|
|
2311
|
+
*/
|
|
2312
|
+
type: "scope-issue-action-requested";
|
|
2313
|
+
documentId: string;
|
|
2314
|
+
scopeId: string;
|
|
2315
|
+
issueId: string;
|
|
2316
|
+
action: ScopeIssueAction;
|
|
2195
2317
|
};
|
|
2196
2318
|
|
|
2197
2319
|
export interface LoadResult {
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export {
|
|
|
7
7
|
validateEditorSessionState,
|
|
8
8
|
EDITOR_SESSION_STATE_VERSION,
|
|
9
9
|
} from "./api/session-state.ts";
|
|
10
|
+
// R2 — issue metadata id for scope-card-overlay P1.
|
|
11
|
+
export { ISSUE_METADATA_ID } from "./api/public-types.ts";
|
|
10
12
|
export type {
|
|
11
13
|
LoadRequest,
|
|
12
14
|
LoadSourcePolicy,
|
|
@@ -104,6 +106,13 @@ export type {
|
|
|
104
106
|
WorkflowMetadataDefinition,
|
|
105
107
|
WorkflowMetadataEntry,
|
|
106
108
|
WorkflowMetadataSnapshot,
|
|
109
|
+
// R2 — issue metadata (scope-card-overlay P1)
|
|
110
|
+
IssueSeverity,
|
|
111
|
+
IssueMode,
|
|
112
|
+
IssueOwner,
|
|
113
|
+
IssueMetadataValue,
|
|
114
|
+
ScopeIssueAction,
|
|
115
|
+
ScopeCardModel,
|
|
107
116
|
WorkflowBlockedCommandReason,
|
|
108
117
|
WorkflowScopeSnapshot,
|
|
109
118
|
InteractionGuardSnapshot,
|
|
@@ -496,6 +496,13 @@ export function createDocumentRuntime(
|
|
|
496
496
|
activeStory,
|
|
497
497
|
};
|
|
498
498
|
},
|
|
499
|
+
// R2 / scope-card-overlay P1 — surface metadata markup so
|
|
500
|
+
// `facet.getAllScopeCardModels()` can attach `IssueMetadataValue`
|
|
501
|
+
// to its scope without the chrome overlay having to re-fetch a
|
|
502
|
+
// separate snapshot. Reads through the same cached snapshot the
|
|
503
|
+
// runtime already builds for comment/revision/search consumers.
|
|
504
|
+
getWorkflowMarkupMetadata: () =>
|
|
505
|
+
getCachedWorkflowMarkupSnapshot().metadata,
|
|
499
506
|
});
|
|
500
507
|
renderKernelRef = createRenderKernel({
|
|
501
508
|
facet: layoutFacet,
|
|
@@ -45,12 +45,35 @@ export interface DocxFontLoader {
|
|
|
45
45
|
refresh(input: FontLoaderInput): void;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
interface MinimalFontFace {
|
|
49
|
+
load(): Promise<MinimalFontFace>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface MinimalFontFaceDescriptors {
|
|
53
|
+
weight?: string;
|
|
54
|
+
style?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface MinimalFontFaceConstructor {
|
|
58
|
+
new (
|
|
59
|
+
family: string,
|
|
60
|
+
source: ArrayBuffer | ArrayBufferView | string,
|
|
61
|
+
descriptors?: MinimalFontFaceDescriptors,
|
|
62
|
+
): MinimalFontFace;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface MinimalFontFaceSet {
|
|
66
|
+
add(face: MinimalFontFace): void;
|
|
67
|
+
check(font: string): boolean;
|
|
68
|
+
ready: Promise<MinimalFontFaceSet>;
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
72
|
+
const globalDocument = (globalThis as unknown as { document?: { fonts?: unknown } }).document;
|
|
49
73
|
const supported =
|
|
50
|
-
|
|
74
|
+
globalDocument !== undefined &&
|
|
51
75
|
typeof (globalThis as { FontFace?: unknown }).FontFace !== "undefined" &&
|
|
52
|
-
|
|
53
|
-
Boolean((document as Document & { fonts?: FontFaceSet }).fonts);
|
|
76
|
+
Boolean(globalDocument.fonts);
|
|
54
77
|
|
|
55
78
|
let current: FontLoaderInput = initial;
|
|
56
79
|
let readyPromise: Promise<void>;
|
|
@@ -58,7 +81,7 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
58
81
|
|
|
59
82
|
function run(input: FontLoaderInput): Promise<void> {
|
|
60
83
|
if (!supported) return Promise.resolve();
|
|
61
|
-
const fontSet =
|
|
84
|
+
const fontSet = globalDocument?.fonts as MinimalFontFaceSet | undefined;
|
|
62
85
|
if (!fontSet) return Promise.resolve();
|
|
63
86
|
|
|
64
87
|
const pending: Array<Promise<unknown>> = [];
|
|
@@ -70,10 +93,8 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
70
93
|
|
|
71
94
|
for (const [descriptor, data] of variantsOf(variants)) {
|
|
72
95
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
new (family: string, source: BufferSource, descriptors?: FontFaceDescriptors): FontFace;
|
|
76
|
-
};
|
|
96
|
+
const FontFaceCtor = (globalThis as { FontFace?: MinimalFontFaceConstructor }).FontFace;
|
|
97
|
+
if (!FontFaceCtor) continue;
|
|
77
98
|
const face = new FontFaceCtor(family, data, descriptor);
|
|
78
99
|
pending.push(
|
|
79
100
|
face.load().then((loaded) => {
|
|
@@ -88,8 +109,6 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
|
|
91
|
-
// Mark declared families as registered if the browser already resolves
|
|
92
|
-
// them (e.g. system fonts like Calibri, Arial).
|
|
93
112
|
for (const family of input.families) {
|
|
94
113
|
try {
|
|
95
114
|
const probe = `12px "${family.replace(/"/g, "'")}", serif`;
|
|
@@ -127,7 +146,7 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
127
146
|
|
|
128
147
|
function* variantsOf(
|
|
129
148
|
variants: EmbeddedFontBytes,
|
|
130
|
-
): IterableIterator<[
|
|
149
|
+
): IterableIterator<[MinimalFontFaceDescriptors, ArrayBuffer]> {
|
|
131
150
|
if (variants.regular) {
|
|
132
151
|
yield [{ weight: "400", style: "normal" }, variants.regular];
|
|
133
152
|
}
|
|
@@ -46,6 +46,7 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
46
46
|
getAnchorRects: () => [],
|
|
47
47
|
getScopeRailSegments: () => [],
|
|
48
48
|
getAllScopeRailSegments: () => [],
|
|
49
|
+
getAllScopeCardModels: () => [],
|
|
49
50
|
getResolvedFormatting: () => null,
|
|
50
51
|
getResolvedRunFormatting: () => null,
|
|
51
52
|
getMeasurement: () => null,
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
type ActiveMarginPreset,
|
|
58
58
|
} from "./margin-preset-catalog.ts";
|
|
59
59
|
import {
|
|
60
|
+
attachScopeCardModel,
|
|
60
61
|
collectScopeRailSegments,
|
|
61
62
|
type ScopeRailSegment,
|
|
62
63
|
} from "../workflow-rail-segments.ts";
|
|
@@ -392,6 +393,20 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
392
393
|
/** Return every scope rail segment across the document. */
|
|
393
394
|
getAllScopeRailSegments(): readonly import("../workflow-rail-segments.ts").ScopeRailSegment[];
|
|
394
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Scope-card projection consumed by the chrome overlay's card layer
|
|
398
|
+
* (scope-card-overlay P1). Joins every unique scope segment with
|
|
399
|
+
* its attached issue metadata (R2 `ISSUE_METADATA_ID`), its
|
|
400
|
+
* work-item id, and its `primaryAnchorRect` via the render kernel's
|
|
401
|
+
* anchor index. P2 fields (`suggestionGroupIds`,
|
|
402
|
+
* `reviewActionCount`, `agentPending`) are defaulted to empty /
|
|
403
|
+
* zero / false in P1 and populated by later phases.
|
|
404
|
+
*
|
|
405
|
+
* Returns an empty list when no workflow rail input has been wired
|
|
406
|
+
* or when no scopes are currently on the active story.
|
|
407
|
+
*/
|
|
408
|
+
getAllScopeCardModels(): readonly import("../../api/public-types.ts").ScopeCardModel[];
|
|
409
|
+
|
|
395
410
|
// Measurement exposure -------------------------------------------------
|
|
396
411
|
getResolvedFormatting(blockId: string): PublicResolvedParagraphFormatting | null;
|
|
397
412
|
getResolvedRunFormatting(runId: string): PublicResolvedRunFormatting | null;
|
|
@@ -471,6 +486,17 @@ export interface CreateLayoutFacetInput {
|
|
|
471
486
|
>
|
|
472
487
|
| null
|
|
473
488
|
| undefined;
|
|
489
|
+
/**
|
|
490
|
+
* Optional workflow-metadata accessor for scope-card issue resolution
|
|
491
|
+
* (R2 / scope-card-overlay P1). Returns the
|
|
492
|
+
* `WorkflowMarkupSnapshot.metadata` array so `getAllScopeCardModels`
|
|
493
|
+
* can attach issue values to their scopes. Omit when the host does
|
|
494
|
+
* not push metadata entries.
|
|
495
|
+
*/
|
|
496
|
+
getWorkflowMarkupMetadata?: () =>
|
|
497
|
+
| readonly import("../../api/public-types.ts").WorkflowMetadataMarkup[]
|
|
498
|
+
| null
|
|
499
|
+
| undefined;
|
|
474
500
|
}
|
|
475
501
|
|
|
476
502
|
export function createLayoutFacet(
|
|
@@ -711,6 +737,21 @@ export function createLayoutFacet(
|
|
|
711
737
|
);
|
|
712
738
|
},
|
|
713
739
|
|
|
740
|
+
getAllScopeCardModels() {
|
|
741
|
+
const railInput = input.getWorkflowRailInput?.();
|
|
742
|
+
const segments = collectScopeRailSegmentsForQuery(railInput, currentGraph());
|
|
743
|
+
if (segments.length === 0) return [];
|
|
744
|
+
const kernel = input.renderKernel?.();
|
|
745
|
+
const anchorIndex = kernel?.getRenderFrame?.()?.anchorIndex ?? null;
|
|
746
|
+
const metadata = input.getWorkflowMarkupMetadata?.();
|
|
747
|
+
return attachScopeCardModel({
|
|
748
|
+
segments,
|
|
749
|
+
scopes: railInput?.scopes ?? [],
|
|
750
|
+
metadata: metadata ?? undefined,
|
|
751
|
+
anchorIndex,
|
|
752
|
+
});
|
|
753
|
+
},
|
|
754
|
+
|
|
714
755
|
getFragmentsForPage(pageIndex) {
|
|
715
756
|
const graph = currentGraph();
|
|
716
757
|
const node = graph.pages[pageIndex];
|
|
@@ -15,14 +15,21 @@
|
|
|
15
15
|
import type {
|
|
16
16
|
EditorAnchorProjection,
|
|
17
17
|
EditorStoryTarget,
|
|
18
|
+
IssueMetadataValue,
|
|
19
|
+
ScopeCardModel,
|
|
18
20
|
WorkflowBlockedCommandReason,
|
|
19
21
|
WorkflowCandidateRange,
|
|
20
22
|
WorkflowLockedZone,
|
|
23
|
+
WorkflowMetadataMarkup,
|
|
21
24
|
WorkflowScope,
|
|
22
25
|
} from "../api/public-types";
|
|
26
|
+
import { ISSUE_METADATA_ID } from "../api/public-types";
|
|
23
27
|
import { MAIN_STORY_TARGET, storyTargetsEqual } from "../core/selection/mapping.ts";
|
|
24
28
|
import type { RuntimePageGraph } from "./layout/page-graph.ts";
|
|
25
|
-
import type {
|
|
29
|
+
import type {
|
|
30
|
+
RenderAnchorIndex,
|
|
31
|
+
RenderFrameRect,
|
|
32
|
+
} from "./render/render-frame-types.ts";
|
|
26
33
|
|
|
27
34
|
// ---------------------------------------------------------------------------
|
|
28
35
|
// Public shape
|
|
@@ -278,3 +285,144 @@ function resolveScopePosture(scope: WorkflowScope): ScopeRailPosture {
|
|
|
278
285
|
return "view";
|
|
279
286
|
}
|
|
280
287
|
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Scope card projection (P1b — consumed by ref.layout.getAllScopeCardModels)
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
export interface AttachScopeCardModelInput {
|
|
294
|
+
/**
|
|
295
|
+
* Segments produced by `collectScopeRailSegments`. The card model
|
|
296
|
+
* keys off segments so per-scope card state stays aligned with the
|
|
297
|
+
* rail stripe rendering.
|
|
298
|
+
*/
|
|
299
|
+
segments: readonly ScopeRailSegment[];
|
|
300
|
+
/**
|
|
301
|
+
* Original workflow scopes — used to resolve `workItemId` for each
|
|
302
|
+
* segment (the segment shape predates workItemId passthrough).
|
|
303
|
+
* Optional; when absent, cards drop `workItemId` and issue matching
|
|
304
|
+
* falls back to scope-id only.
|
|
305
|
+
*/
|
|
306
|
+
scopes?: readonly WorkflowScope[];
|
|
307
|
+
/**
|
|
308
|
+
* Workflow metadata markup from `WorkflowMarkupSnapshot.metadata`.
|
|
309
|
+
* Entries with `metadataId === ISSUE_METADATA_ID` that match a
|
|
310
|
+
* segment's `workItemId` or `scopeId` attach as the card's issue.
|
|
311
|
+
*/
|
|
312
|
+
metadata?: readonly WorkflowMetadataMarkup[];
|
|
313
|
+
/**
|
|
314
|
+
* Optional anchor index used to resolve each card's
|
|
315
|
+
* `primaryAnchorRect`. When omitted, cards are produced without
|
|
316
|
+
* rects — chrome consumers fall back to on-render positioning.
|
|
317
|
+
*/
|
|
318
|
+
anchorIndex?: RenderAnchorIndex | null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Project rail segments into per-scope `ScopeCardModel` values.
|
|
323
|
+
*
|
|
324
|
+
* For each unique `scopeId` in the segment list:
|
|
325
|
+
* - use the first segment (document order) as the card's anchor
|
|
326
|
+
* - look up an `ISSUE_METADATA_ID` metadata entry whose
|
|
327
|
+
* `workItemId` (preferred) or `scopeId` matches the segment
|
|
328
|
+
* - coerce that entry's `value` into `IssueMetadataValue` when the
|
|
329
|
+
* required shape is present; drop the issue silently otherwise
|
|
330
|
+
* so malformed host input never breaks chrome rendering
|
|
331
|
+
*
|
|
332
|
+
* P2 fields (`suggestionGroupIds`, `reviewActionCount`,
|
|
333
|
+
* `agentPending`) are populated as empty defaults and wired in a
|
|
334
|
+
* later phase.
|
|
335
|
+
*/
|
|
336
|
+
export function attachScopeCardModel(
|
|
337
|
+
input: AttachScopeCardModelInput,
|
|
338
|
+
): ScopeCardModel[] {
|
|
339
|
+
if (input.segments.length === 0) return [];
|
|
340
|
+
|
|
341
|
+
// Take the first segment per scopeId (document order preserved by
|
|
342
|
+
// the segment collector).
|
|
343
|
+
const firstByScope = new Map<string, ScopeRailSegment>();
|
|
344
|
+
for (const segment of input.segments) {
|
|
345
|
+
if (!firstByScope.has(segment.scopeId)) {
|
|
346
|
+
firstByScope.set(segment.scopeId, segment);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const workItemByScope = new Map<string, string>();
|
|
351
|
+
for (const scope of input.scopes ?? []) {
|
|
352
|
+
if (scope.workItemId) {
|
|
353
|
+
workItemByScope.set(scope.scopeId, scope.workItemId);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const models: ScopeCardModel[] = [];
|
|
358
|
+
for (const segment of firstByScope.values()) {
|
|
359
|
+
const workItemId = workItemByScope.get(segment.scopeId);
|
|
360
|
+
const issue = resolveIssueForScope(
|
|
361
|
+
segment.scopeId,
|
|
362
|
+
workItemId,
|
|
363
|
+
input.metadata,
|
|
364
|
+
);
|
|
365
|
+
const primaryAnchorRect = input.anchorIndex
|
|
366
|
+
? input.anchorIndex.bySelection(segment.fromOffset, segment.toOffset)
|
|
367
|
+
: null;
|
|
368
|
+
|
|
369
|
+
models.push({
|
|
370
|
+
scopeId: segment.scopeId,
|
|
371
|
+
...(workItemId ? { workItemId } : {}),
|
|
372
|
+
label: segment.label ?? "",
|
|
373
|
+
posture: segment.posture,
|
|
374
|
+
primaryAnchorRect,
|
|
375
|
+
...(issue ? { issue } : {}),
|
|
376
|
+
suggestionGroupIds: [],
|
|
377
|
+
reviewActionCount: 0,
|
|
378
|
+
agentPending: false,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return models;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function resolveIssueForScope(
|
|
386
|
+
scopeId: string,
|
|
387
|
+
workItemId: string | undefined,
|
|
388
|
+
metadata: readonly WorkflowMetadataMarkup[] | undefined,
|
|
389
|
+
): IssueMetadataValue | undefined {
|
|
390
|
+
if (!metadata || metadata.length === 0) return undefined;
|
|
391
|
+
for (const entry of metadata) {
|
|
392
|
+
if (entry.metadataId !== ISSUE_METADATA_ID) continue;
|
|
393
|
+
const matchesWorkItem =
|
|
394
|
+
workItemId !== undefined &&
|
|
395
|
+
entry.workItemId !== undefined &&
|
|
396
|
+
entry.workItemId === workItemId;
|
|
397
|
+
const matchesScope =
|
|
398
|
+
entry.scopeId !== undefined && entry.scopeId === scopeId;
|
|
399
|
+
if (!matchesWorkItem && !matchesScope) continue;
|
|
400
|
+
const coerced = coerceIssueValue(entry.value);
|
|
401
|
+
if (coerced) return coerced;
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Validate that a host-supplied metadata value looks like an
|
|
408
|
+
* `IssueMetadataValue`. Guards against malformed input — the card
|
|
409
|
+
* reads strongly-typed fields, so a missing severity or title must
|
|
410
|
+
* drop the issue rather than render `undefined` in the UI.
|
|
411
|
+
*/
|
|
412
|
+
function coerceIssueValue(
|
|
413
|
+
value: unknown,
|
|
414
|
+
): IssueMetadataValue | undefined {
|
|
415
|
+
if (!value || typeof value !== "object") return undefined;
|
|
416
|
+
const candidate = value as Partial<IssueMetadataValue>;
|
|
417
|
+
if (
|
|
418
|
+
typeof candidate.issueId !== "string" ||
|
|
419
|
+
typeof candidate.topic !== "string" ||
|
|
420
|
+
typeof candidate.severity !== "string" ||
|
|
421
|
+
typeof candidate.mode !== "string" ||
|
|
422
|
+
typeof candidate.checklistState !== "string" ||
|
|
423
|
+
typeof candidate.title !== "string"
|
|
424
|
+
) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
return candidate as IssueMetadataValue;
|
|
428
|
+
}
|
|
@@ -2433,6 +2433,23 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2433
2433
|
document={documentElement}
|
|
2434
2434
|
onReviewSidebarTrackedChanges={onReviewSidebarTrackedChanges}
|
|
2435
2435
|
onReviewSidebarComments={onReviewSidebarComments}
|
|
2436
|
+
onScopeModeChangeRequested={(payload) => {
|
|
2437
|
+
onEventRef.current?.({
|
|
2438
|
+
type: "scope-mode-change-requested",
|
|
2439
|
+
documentId,
|
|
2440
|
+
scopeId: payload.scopeId,
|
|
2441
|
+
mode: payload.mode,
|
|
2442
|
+
});
|
|
2443
|
+
}}
|
|
2444
|
+
onScopeIssueActionRequested={(payload) => {
|
|
2445
|
+
onEventRef.current?.({
|
|
2446
|
+
type: "scope-issue-action-requested",
|
|
2447
|
+
documentId,
|
|
2448
|
+
scopeId: payload.scopeId,
|
|
2449
|
+
issueId: payload.issueId,
|
|
2450
|
+
action: payload.action,
|
|
2451
|
+
});
|
|
2452
|
+
}}
|
|
2436
2453
|
/>
|
|
2437
2454
|
);
|
|
2438
2455
|
},
|
|
@@ -87,6 +87,24 @@ export interface EditorShellViewProps {
|
|
|
87
87
|
onReviewSidebarTrackedChanges?: () => void;
|
|
88
88
|
/** Review-role sidebar panel: open sidebar to comments panel. */
|
|
89
89
|
onReviewSidebarComments?: () => void;
|
|
90
|
+
/**
|
|
91
|
+
* Scope card mode selector fired a mode change (forwarded from the
|
|
92
|
+
* workspace). The editor turns this into a
|
|
93
|
+
* `scope-mode-change-requested` event.
|
|
94
|
+
*/
|
|
95
|
+
onScopeModeChangeRequested?: (payload: {
|
|
96
|
+
scopeId: string;
|
|
97
|
+
mode: import("../api/public-types.ts").WorkflowScopeMode;
|
|
98
|
+
}) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Scope card issue action fired (forwarded from the workspace).
|
|
101
|
+
* The editor turns this into a `scope-issue-action-requested` event.
|
|
102
|
+
*/
|
|
103
|
+
onScopeIssueActionRequested?: (payload: {
|
|
104
|
+
scopeId: string;
|
|
105
|
+
issueId: string;
|
|
106
|
+
action: import("../api/public-types.ts").ScopeIssueAction;
|
|
107
|
+
}) => void;
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
export function EditorShellView(props: EditorShellViewProps) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface TwModeDockAction {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
icon: ReactNode;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
isActive?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TwModeDockProps {
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
actions?: readonly TwModeDockAction[];
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const focusRingClass =
|
|
19
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-canvas";
|
|
20
|
+
|
|
21
|
+
export function TwModeDock(props: TwModeDockProps) {
|
|
22
|
+
const actions = (props.actions ?? []).slice(0, 3);
|
|
23
|
+
|
|
24
|
+
const className = [
|
|
25
|
+
"pointer-events-auto fixed bottom-4 left-1/2 z-40 -translate-x-1/2",
|
|
26
|
+
"flex h-9 items-center gap-2 rounded-[var(--radius-pill)] border border-border bg-canvas px-2 py-1",
|
|
27
|
+
"shadow-[var(--shadow-float)] backdrop-blur-sm",
|
|
28
|
+
"transition-opacity duration-[var(--motion-fast)]",
|
|
29
|
+
props.className,
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(" ");
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
role="toolbar"
|
|
37
|
+
aria-label="Mode dock"
|
|
38
|
+
className={className}
|
|
39
|
+
data-testid="tw-mode-dock"
|
|
40
|
+
>
|
|
41
|
+
<div className="flex items-center gap-1.5 pl-1 pr-1.5">
|
|
42
|
+
{props.icon ? (
|
|
43
|
+
<span aria-hidden="true" className="flex h-3.5 w-3.5 items-center text-tertiary">
|
|
44
|
+
{props.icon}
|
|
45
|
+
</span>
|
|
46
|
+
) : null}
|
|
47
|
+
<span
|
|
48
|
+
className="text-[10px] font-semibold uppercase tracking-[0.14em] text-secondary"
|
|
49
|
+
data-testid="tw-mode-dock__label"
|
|
50
|
+
>
|
|
51
|
+
{props.label}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
{actions.length > 0 ? (
|
|
55
|
+
<div className="flex items-center gap-0.5 border-l border-border/70 pl-1.5">
|
|
56
|
+
{actions.map((action) => (
|
|
57
|
+
<button
|
|
58
|
+
key={action.id}
|
|
59
|
+
type="button"
|
|
60
|
+
onClick={action.onClick}
|
|
61
|
+
aria-label={action.label}
|
|
62
|
+
aria-pressed={action.isActive ?? false}
|
|
63
|
+
title={action.label}
|
|
64
|
+
className={[
|
|
65
|
+
"inline-flex h-7 w-7 items-center justify-center rounded-[var(--radius-control)] transition-colors",
|
|
66
|
+
action.isActive
|
|
67
|
+
? "bg-accent-soft text-accent"
|
|
68
|
+
: "text-tertiary hover:bg-surface-hover hover:text-primary",
|
|
69
|
+
focusRingClass,
|
|
70
|
+
].join(" ")}
|
|
71
|
+
data-testid={`tw-mode-dock__action-${action.id}`}
|
|
72
|
+
>
|
|
73
|
+
<span aria-hidden="true">{action.icon}</span>
|
|
74
|
+
</button>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
) : null}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|