@beyondwork/docx-react-component 1.0.94 → 1.0.96
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/v3/ai/replacement.ts +3 -3
- package/src/api/v3/runtime/formatting.ts +3 -3
- package/src/core/commands/formatting-commands.ts +146 -3
- package/src/core/state/text-transaction.ts +6 -3
- package/src/runtime/document-runtime.ts +49 -1
- package/src/runtime/scopes/semantic-scope-types.ts +9 -0
- package/src/runtime/workflow/rail/compose.ts +5 -0
- package/src/ui/WordReviewEditor.tsx +9 -14
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +14 -4
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +1 -5
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +39 -16
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -7
- package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +34 -12
- package/src/ui-tailwind/theme/editor-theme.css +69 -78
- package/src/ui-tailwind/tw-review-workspace.tsx +15 -8
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.96",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
|
@@ -384,9 +384,9 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
|
|
|
384
384
|
},
|
|
385
385
|
|
|
386
386
|
applyScopeAction(input: ApplyScopeActionInput): ApplyResult {
|
|
387
|
-
// @endStateApi — live-with-adapter. Routes through the
|
|
388
|
-
// compiler
|
|
389
|
-
//
|
|
387
|
+
// @endStateApi — live-with-adapter. Routes scope actions through the
|
|
388
|
+
// shipped compiler/applyFormatting facade so AI-triggered formatting
|
|
389
|
+
// mutations share validation, audit, and undo semantics.
|
|
390
390
|
const proposalId =
|
|
391
391
|
input.proposalId ??
|
|
392
392
|
mockId(
|
|
@@ -216,9 +216,9 @@ export function createFormattingFamily(runtime: RuntimeApiHandle) {
|
|
|
216
216
|
},
|
|
217
217
|
|
|
218
218
|
applyToScope(input: FormattingApplyToScopeInput): FormattingApplyToScopeResult {
|
|
219
|
-
// @endStateApi — live-with-adapter. Routes through the scope
|
|
220
|
-
//
|
|
221
|
-
//
|
|
219
|
+
// @endStateApi — live-with-adapter. Routes through the scope-compiler
|
|
220
|
+
// facade so scope-targeted formatting shares Layer-08 validation,
|
|
221
|
+
// audit, and runtime-owned mutation semantics.
|
|
222
222
|
const result = compiler.applyFormatting({
|
|
223
223
|
targetScopeId: input.scopeId,
|
|
224
224
|
action: input.action,
|
|
@@ -591,7 +591,7 @@ export function applyFormattingOperationToDocument(
|
|
|
591
591
|
export type TextMarkClearTarget = TextMark["type"] | "visualHighlight";
|
|
592
592
|
|
|
593
593
|
export type TextMarkRangeOperation =
|
|
594
|
-
| { type: "clear-mark"; mark: TextMarkClearTarget }
|
|
594
|
+
| { type: "clear-mark"; mark: TextMarkClearTarget; expandToFullHighlight?: boolean }
|
|
595
595
|
| { type: "set-mark"; mark: TextMark };
|
|
596
596
|
|
|
597
597
|
/**
|
|
@@ -605,8 +605,14 @@ export function applyTextMarkOperationToDocumentRange(
|
|
|
605
605
|
range: { from: number; to: number },
|
|
606
606
|
operation: TextMarkRangeOperation,
|
|
607
607
|
): FormattingMutationResult {
|
|
608
|
-
const
|
|
609
|
-
const
|
|
608
|
+
const inputFrom = Math.min(range.from, range.to);
|
|
609
|
+
const inputTo = Math.max(range.from, range.to);
|
|
610
|
+
const expandedRange =
|
|
611
|
+
operation.type === "clear-mark" && operation.expandToFullHighlight === true
|
|
612
|
+
? expandRangeToCanonicalHighlightExtent(document, inputFrom, inputTo)
|
|
613
|
+
: { from: inputFrom, to: inputTo };
|
|
614
|
+
const selectionFrom = expandedRange.from;
|
|
615
|
+
const selectionTo = expandedRange.to;
|
|
610
616
|
const selection: RuntimeRenderSnapshot["selection"] = {
|
|
611
617
|
anchor: selectionFrom,
|
|
612
618
|
head: selectionTo,
|
|
@@ -671,6 +677,143 @@ export function applyTextMarkOperationToDocumentRange(
|
|
|
671
677
|
};
|
|
672
678
|
}
|
|
673
679
|
|
|
680
|
+
function expandRangeToCanonicalHighlightExtent(
|
|
681
|
+
document: CanonicalDocumentEnvelope,
|
|
682
|
+
inputFrom: number,
|
|
683
|
+
inputTo: number,
|
|
684
|
+
): { from: number; to: number } {
|
|
685
|
+
let from = inputFrom;
|
|
686
|
+
let to = inputTo;
|
|
687
|
+
const root = document.content as DocumentRootNode;
|
|
688
|
+
let cursor = 0;
|
|
689
|
+
|
|
690
|
+
for (let blockIndex = 0; blockIndex < root.children.length; blockIndex += 1) {
|
|
691
|
+
const block = root.children[blockIndex]!;
|
|
692
|
+
const blockFrom = cursor;
|
|
693
|
+
const blockLength =
|
|
694
|
+
block.type === "paragraph"
|
|
695
|
+
? block.children.reduce(
|
|
696
|
+
(total, child) => total + inlineNodeLength(child as InlineNode),
|
|
697
|
+
0,
|
|
698
|
+
)
|
|
699
|
+
: 1;
|
|
700
|
+
const blockTo = blockFrom + blockLength;
|
|
701
|
+
|
|
702
|
+
if (
|
|
703
|
+
block.type === "paragraph" &&
|
|
704
|
+
rangesOverlap(inputFrom, inputTo, blockFrom, blockTo)
|
|
705
|
+
) {
|
|
706
|
+
const spans = collectInlineHighlightSpans(block.children, blockFrom).spans;
|
|
707
|
+
const expanded = expandRangeWithinHighlightSpans(spans, inputFrom, inputTo);
|
|
708
|
+
if (expanded.from < from) from = expanded.from;
|
|
709
|
+
if (expanded.to > to) to = expanded.to;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
cursor = blockTo;
|
|
713
|
+
if (blockIndex < root.children.length - 1) {
|
|
714
|
+
cursor += 1;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return { from, to };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
interface HighlightSpan {
|
|
722
|
+
readonly from: number;
|
|
723
|
+
readonly to: number;
|
|
724
|
+
readonly highlighted: boolean;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function collectInlineHighlightSpans(
|
|
728
|
+
nodes: readonly InlineNode[],
|
|
729
|
+
start: number,
|
|
730
|
+
): { spans: HighlightSpan[]; nextPosition: number } {
|
|
731
|
+
const spans: HighlightSpan[] = [];
|
|
732
|
+
let position = start;
|
|
733
|
+
|
|
734
|
+
for (const node of nodes) {
|
|
735
|
+
if (node.type === "text") {
|
|
736
|
+
const length = Array.from(node.text).length;
|
|
737
|
+
if (length > 0) {
|
|
738
|
+
spans.push({
|
|
739
|
+
from: position,
|
|
740
|
+
to: position + length,
|
|
741
|
+
highlighted: marksHaveVisualHighlight(node.marks),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
position += length;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (node.type === "hyperlink") {
|
|
749
|
+
const nested = collectInlineHighlightSpans(node.children as InlineNode[], position);
|
|
750
|
+
spans.push(...nested.spans);
|
|
751
|
+
position = nested.nextPosition;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const length = inlineNodeLength(node);
|
|
756
|
+
if (length > 0) {
|
|
757
|
+
spans.push({
|
|
758
|
+
from: position,
|
|
759
|
+
to: position + length,
|
|
760
|
+
highlighted: false,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
position += length;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return { spans, nextPosition: position };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function expandRangeWithinHighlightSpans(
|
|
770
|
+
spans: readonly HighlightSpan[],
|
|
771
|
+
inputFrom: number,
|
|
772
|
+
inputTo: number,
|
|
773
|
+
): { from: number; to: number } {
|
|
774
|
+
let from = inputFrom;
|
|
775
|
+
let to = inputTo;
|
|
776
|
+
let touchedLeftIndex = -1;
|
|
777
|
+
let touchedRightIndex = -1;
|
|
778
|
+
|
|
779
|
+
for (let i = 0; i < spans.length; i += 1) {
|
|
780
|
+
const span = spans[i]!;
|
|
781
|
+
if (!span.highlighted) continue;
|
|
782
|
+
if (rangesOverlap(inputFrom, inputTo, span.from, span.to)) {
|
|
783
|
+
if (touchedLeftIndex === -1) touchedLeftIndex = i;
|
|
784
|
+
touchedRightIndex = i;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (touchedLeftIndex === -1) {
|
|
789
|
+
return { from, to };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
for (let i = touchedLeftIndex; i >= 0; i -= 1) {
|
|
793
|
+
const span = spans[i]!;
|
|
794
|
+
if (!span.highlighted) break;
|
|
795
|
+
if (span.to < from) break;
|
|
796
|
+
if (span.from < from) from = span.from;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
for (let i = touchedRightIndex; i < spans.length; i += 1) {
|
|
800
|
+
const span = spans[i]!;
|
|
801
|
+
if (!span.highlighted) break;
|
|
802
|
+
if (span.from > to) break;
|
|
803
|
+
if (span.to > to) to = span.to;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return { from, to };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function marksHaveVisualHighlight(marks: readonly TextMark[] | undefined): boolean {
|
|
810
|
+
return (
|
|
811
|
+
marks?.some((mark) =>
|
|
812
|
+
mark.type === "highlight" || mark.type === "backgroundColor"
|
|
813
|
+
) ?? false
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
674
817
|
function resolveTextMarkRangeUpdater(
|
|
675
818
|
operation: TextMarkRangeOperation,
|
|
676
819
|
): (marks?: TextMark[]) => TextMark[] | undefined {
|
|
@@ -130,9 +130,12 @@ function applyLinearTextTransaction(
|
|
|
130
130
|
|
|
131
131
|
// `normalizedRange.{from,to}` are logical positions (scope markers = 0 width,
|
|
132
132
|
// matching surface-projection). Translate to unit-array indices so scope
|
|
133
|
-
// marker units
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
// marker units sitting exactly on the replacement boundaries stay intact:
|
|
134
|
+
// start-boundary markers remain before the inserted payload, end-boundary
|
|
135
|
+
// markers remain after it. Markers strictly inside the range are still part
|
|
136
|
+
// of the replacement/delete slice.
|
|
137
|
+
const unitFrom = logicalPositionToUnitIndex(story.units, normalizedRange.from, "after");
|
|
138
|
+
const unitTo = logicalPositionToUnitIndex(story.units, normalizedRange.to, "before");
|
|
136
139
|
|
|
137
140
|
ensureEditableRange(story.units.slice(unitFrom, unitTo));
|
|
138
141
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type EditorWarning as InternalEditorWarning,
|
|
14
14
|
} from "../core/state/editor-state.ts";
|
|
15
15
|
import {
|
|
16
|
+
createPlainText,
|
|
16
17
|
logicalPositionToUnitIndex,
|
|
17
18
|
parseTextStory,
|
|
18
19
|
serializeTextStory,
|
|
@@ -3195,6 +3196,13 @@ export function createDocumentRuntime(
|
|
|
3195
3196
|
replaceText(text, target, formatting) {
|
|
3196
3197
|
try {
|
|
3197
3198
|
const timestamp = clock();
|
|
3199
|
+
const selection = target ? createSelectionFromPublicAnchor(target) : state.selection;
|
|
3200
|
+
if (
|
|
3201
|
+
shouldPreserveEquivalentReplacement(formatting) &&
|
|
3202
|
+
replacementTextMatchesCurrentRange(state.document, activeStory, selection, text)
|
|
3203
|
+
) {
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3198
3206
|
applyTextCommandInActiveStory(
|
|
3199
3207
|
{
|
|
3200
3208
|
type: "text.insert",
|
|
@@ -3203,7 +3211,7 @@ export function createDocumentRuntime(
|
|
|
3203
3211
|
origin: createOrigin("api", timestamp),
|
|
3204
3212
|
},
|
|
3205
3213
|
{
|
|
3206
|
-
selection
|
|
3214
|
+
selection,
|
|
3207
3215
|
blockedCommandName: "replaceText",
|
|
3208
3216
|
},
|
|
3209
3217
|
);
|
|
@@ -3531,6 +3539,9 @@ export function createDocumentRuntime(
|
|
|
3531
3539
|
? {
|
|
3532
3540
|
type: "clear-mark" as const,
|
|
3533
3541
|
mark: step.formattingAction.mark,
|
|
3542
|
+
...(step.formattingAction.expandToFullHighlight === true
|
|
3543
|
+
? { expandToFullHighlight: true as const }
|
|
3544
|
+
: {}),
|
|
3534
3545
|
}
|
|
3535
3546
|
: {
|
|
3536
3547
|
type: "set-mark" as const,
|
|
@@ -6445,6 +6456,43 @@ function createSelectionFromPublicAnchor(
|
|
|
6445
6456
|
}
|
|
6446
6457
|
}
|
|
6447
6458
|
|
|
6459
|
+
function shouldPreserveEquivalentReplacement(formatting: TextFormattingDirective | undefined): boolean {
|
|
6460
|
+
return !formatting || formatting.mode === "match-replaced-range";
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
function replacementTextMatchesCurrentRange(
|
|
6464
|
+
document: CanonicalDocumentEnvelope,
|
|
6465
|
+
activeStory: EditorStoryTarget,
|
|
6466
|
+
selection: import("../core/state/editor-state.ts").SelectionSnapshot,
|
|
6467
|
+
replacement: string,
|
|
6468
|
+
): boolean {
|
|
6469
|
+
const from = Math.max(0, Math.min(selection.anchor, selection.head));
|
|
6470
|
+
const to = Math.max(0, Math.max(selection.anchor, selection.head));
|
|
6471
|
+
if (from === to) {
|
|
6472
|
+
return replacement.length === 0;
|
|
6473
|
+
}
|
|
6474
|
+
|
|
6475
|
+
const content = activeStory.kind === "main"
|
|
6476
|
+
? document.content
|
|
6477
|
+
: {
|
|
6478
|
+
type: "doc" as const,
|
|
6479
|
+
children: [...getStoryBlocks(document, activeStory)],
|
|
6480
|
+
};
|
|
6481
|
+
const story = parseTextStory(content);
|
|
6482
|
+
if (from > story.size || to > story.size) {
|
|
6483
|
+
return false;
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
const unitFrom = logicalPositionToUnitIndex(story.units, from, "after");
|
|
6487
|
+
const unitTo = logicalPositionToUnitIndex(story.units, to, "before");
|
|
6488
|
+
const selectedText = createPlainText({
|
|
6489
|
+
firstParagraph: story.firstParagraph,
|
|
6490
|
+
units: story.units.slice(unitFrom, unitTo),
|
|
6491
|
+
size: to - from,
|
|
6492
|
+
});
|
|
6493
|
+
return selectedText === replacement;
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6448
6496
|
/**
|
|
6449
6497
|
* I2 Tier B Slice 4b — extract the selection range from a document as a
|
|
6450
6498
|
* `CanonicalDocumentFragment`. The fragment preserves text + marks +
|
|
@@ -332,6 +332,15 @@ export type ScopeFormattingAction =
|
|
|
332
332
|
* `"visualHighlight"` to remove both visual highlight layers.
|
|
333
333
|
*/
|
|
334
334
|
readonly mark: ScopeFormattingClearTarget;
|
|
335
|
+
/**
|
|
336
|
+
* When true, expand the target range to the full contiguous visual
|
|
337
|
+
* highlight span that touches it before clearing. Mirrors
|
|
338
|
+
* `clearHighlight({expandToFullHighlight:true})`; the clear remains
|
|
339
|
+
* source-layer exact, so `mark:"highlight"` still removes only
|
|
340
|
+
* `w:highlight`, while `mark:"visualHighlight"` removes highlight +
|
|
341
|
+
* shading across the expanded span.
|
|
342
|
+
*/
|
|
343
|
+
readonly expandToFullHighlight?: boolean;
|
|
335
344
|
}
|
|
336
345
|
| {
|
|
337
346
|
readonly kind: "set-mark";
|
|
@@ -94,6 +94,11 @@ export function collectScopeRailSegments(
|
|
|
94
94
|
const activeIds = new Set(input.activeWorkItemScopeIds ?? []);
|
|
95
95
|
|
|
96
96
|
for (const scope of input.scopes ?? []) {
|
|
97
|
+
// Invisible scopes are runtime/agent context only. They may still
|
|
98
|
+
// participate in guard decisions, but they must not surface as rail,
|
|
99
|
+
// card, or body-tint chrome.
|
|
100
|
+
if (scope.visibility === "invisible") continue;
|
|
101
|
+
|
|
97
102
|
const range = anchorToRuntimeRange(scope.anchor);
|
|
98
103
|
if (!range) continue;
|
|
99
104
|
const storyTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
|
|
@@ -3931,18 +3931,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3931
3931
|
}}
|
|
3932
3932
|
onDeselectObject={() => activeRuntime.deselectObject()}
|
|
3933
3933
|
onScopeAskAgent={(payload) => {
|
|
3934
|
-
// Resolve
|
|
3935
|
-
//
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
const
|
|
3939
|
-
const model = models.find((entry) => entry.scopeId === payload.scopeId);
|
|
3940
|
-
if (!model) return;
|
|
3941
|
-
const scopeSnapshot = activeRuntime.getWorkflowScopeSnapshot();
|
|
3942
|
-
const scopes = scopeSnapshot?.scopes ?? [];
|
|
3943
|
-
const scope = scopes.find((entry) => entry.scopeId === payload.scopeId);
|
|
3944
|
-
if (!scope) return;
|
|
3945
|
-
const anchor = scope.anchor;
|
|
3934
|
+
// Resolve scope card + story through the mounted UI API seam so
|
|
3935
|
+
// the shell root does not re-own workflow facet reads.
|
|
3936
|
+
const model = api.ui?.scope.card(payload.scopeId) ?? null;
|
|
3937
|
+
const bundle = api.ui?.scope.getBundle(payload.scopeId) ?? null;
|
|
3938
|
+
const anchor = payload.anchor ?? model?.anchor;
|
|
3946
3939
|
if (!anchor) return;
|
|
3947
3940
|
const requestId =
|
|
3948
3941
|
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
@@ -3957,8 +3950,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3957
3950
|
requestId,
|
|
3958
3951
|
scopeId: payload.scopeId,
|
|
3959
3952
|
anchor,
|
|
3960
|
-
selectionText: model
|
|
3961
|
-
...(scope.
|
|
3953
|
+
selectionText: model?.label ?? "",
|
|
3954
|
+
...(bundle?.scope.handle.storyTarget
|
|
3955
|
+
? { storyTarget: bundle.scope.handle.storyTarget }
|
|
3956
|
+
: {}),
|
|
3962
3957
|
};
|
|
3963
3958
|
onEventRef.current?.(eventPayload);
|
|
3964
3959
|
}}
|
|
@@ -162,7 +162,10 @@ export interface EditorShellViewProps {
|
|
|
162
162
|
groupId: string;
|
|
163
163
|
}) => void;
|
|
164
164
|
/** K2 — forwarded from workspace to WordReviewEditor. */
|
|
165
|
-
onScopeAskAgent?: (payload: {
|
|
165
|
+
onScopeAskAgent?: (payload: {
|
|
166
|
+
scopeId: string;
|
|
167
|
+
anchor?: import("../api/public-types.ts").EditorAnchorProjection;
|
|
168
|
+
}) => void;
|
|
166
169
|
/** N6 — deselects the currently grabbed object; wired to runtime.deselectObject(). */
|
|
167
170
|
onDeselectObject?: () => void;
|
|
168
171
|
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
@@ -28,6 +28,7 @@ import { TwScopeCardLayer } from "./tw-scope-card-layer";
|
|
|
28
28
|
import { TwPageStackChromeLayer, type PmPortalView } from "../page-stack/tw-page-stack-chrome-layer";
|
|
29
29
|
import { TwTableGripLayer } from "../chrome/tw-table-grip-layer";
|
|
30
30
|
import { TwObjectSelectionOverlay } from "./tw-object-selection-overlay";
|
|
31
|
+
import { useUiApi } from "../ui-api-context";
|
|
31
32
|
|
|
32
33
|
export interface TwChromeOverlayProps {
|
|
33
34
|
/** Layout facet the overlay layers read from (layout-semantic data). */
|
|
@@ -39,10 +40,9 @@ export interface TwChromeOverlayProps {
|
|
|
39
40
|
*/
|
|
40
41
|
geometryFacet: import("../../runtime/geometry/index.ts").GeometryFacet;
|
|
41
42
|
/**
|
|
42
|
-
* Workflow facet —
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* (e.g., during initial mount).
|
|
43
|
+
* Workflow facet — no-provider fallback for scope rail/card reads.
|
|
44
|
+
* Mounted editor paths prefer `api.ui.scope.*`; pass `null` when no
|
|
45
|
+
* runtime is attached (e.g., during initial mount).
|
|
46
46
|
*/
|
|
47
47
|
workflowFacet: import("../../runtime/workflow/rail/types.ts").WorkflowFacet | null;
|
|
48
48
|
/** Optional coordinate space override. Defaults to the overlay origin. */
|
|
@@ -242,6 +242,15 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
242
242
|
mediaPreviews,
|
|
243
243
|
activeBandRibbonProps,
|
|
244
244
|
}) => {
|
|
245
|
+
const ui = useUiApi();
|
|
246
|
+
const scopeRailSegments = React.useMemo(
|
|
247
|
+
() =>
|
|
248
|
+
ui?.scope.rail().segments ??
|
|
249
|
+
workflowFacet?.getAllRailSegments() ??
|
|
250
|
+
[],
|
|
251
|
+
[ui, workflowFacet, renderFrameRevision],
|
|
252
|
+
);
|
|
253
|
+
|
|
245
254
|
return (
|
|
246
255
|
<div
|
|
247
256
|
className="wre-chrome-overlay pointer-events-none absolute inset-0 z-30"
|
|
@@ -266,6 +275,7 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
266
275
|
<TwScopeRailLayer
|
|
267
276
|
geometryFacet={geometryFacet}
|
|
268
277
|
workflowFacet={workflowFacet}
|
|
278
|
+
scopeRailSegments={scopeRailSegments}
|
|
269
279
|
space={space}
|
|
270
280
|
activeScopeId={activeScopeId}
|
|
271
281
|
onStripeClick={onScopeStripeClick}
|
|
@@ -53,8 +53,6 @@ export interface TwScopeCardLayerProps {
|
|
|
53
53
|
*/
|
|
54
54
|
workflowFacet: WorkflowFacet | null;
|
|
55
55
|
activeScopeId: string | null;
|
|
56
|
-
/** Scope ids currently visible under the Workflow rail layer filters. */
|
|
57
|
-
visibleScopeIds?: ReadonlySet<string>;
|
|
58
56
|
onClose: () => void;
|
|
59
57
|
onModeChange: (scopeId: string, mode: WorkflowScopeMode) => void;
|
|
60
58
|
onIssueAction: (
|
|
@@ -94,7 +92,6 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
|
|
|
94
92
|
facet,
|
|
95
93
|
workflowFacet,
|
|
96
94
|
activeScopeId,
|
|
97
|
-
visibleScopeIds,
|
|
98
95
|
onClose,
|
|
99
96
|
onModeChange,
|
|
100
97
|
onIssueAction,
|
|
@@ -131,11 +128,10 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
|
|
|
131
128
|
const getVisibleScopeCardModel = React.useCallback(
|
|
132
129
|
(scopeId: string | null): ScopeCardModel | null => {
|
|
133
130
|
if (!scopeId) return null;
|
|
134
|
-
if (visibleScopeIds && !visibleScopeIds.has(scopeId)) return null;
|
|
135
131
|
if (ui) return ui.scope.card(scopeId);
|
|
136
132
|
return getWorkflowScopeCardModel(scopeId);
|
|
137
133
|
},
|
|
138
|
-
[getWorkflowScopeCardModel, ui
|
|
134
|
+
[getWorkflowScopeCardModel, ui],
|
|
139
135
|
);
|
|
140
136
|
|
|
141
137
|
// The effective scope is the pinned one if it still resolves to a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scope rail layer — renders workflow scopes as a thin color stripe in
|
|
3
|
-
* the reserved left-gutter lane plus a
|
|
4
|
-
*
|
|
3
|
+
* the reserved left-gutter lane plus a border-only line outline. The
|
|
4
|
+
* visible scope ownership border lives on the PM inline text decoration.
|
|
5
5
|
*
|
|
6
6
|
* Per runtime-rendering-and-chrome-phase.md §5 and
|
|
7
7
|
* docs/plans/scope-card-overlay.md P0, the rail is a projection over
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
projectRectToOverlay,
|
|
18
18
|
type OverlayCoordinateSpace,
|
|
19
19
|
} from "./chrome-overlay-projector";
|
|
20
|
+
import { useUiApi } from "../ui-api-context";
|
|
20
21
|
import type { RenderFrame, RenderFrameRect } from "../../api/public-types.ts";
|
|
21
22
|
import type { ScopeRailSegment, ScopeRailPosture } from "../../api/public-types.ts";
|
|
22
23
|
import type { WorkflowFacet } from "../../api/public-types.ts";
|
|
@@ -30,16 +31,21 @@ export interface TwScopeRailLayerProps {
|
|
|
30
31
|
/**
|
|
31
32
|
* Geometry facet — used for `getRenderFrame()`. Migrated from
|
|
32
33
|
* `facet: WordReviewEditorLayoutFacet` in refactor/05 cross-lane-coord
|
|
33
|
-
* §8.4 pass.
|
|
34
|
-
* `workflowFacet` per Layer-06 Slice 4's seam inversion.
|
|
34
|
+
* §8.4 pass. Mounted rail/card data flows through `api.ui.scope.*`.
|
|
35
35
|
*/
|
|
36
36
|
geometryFacet: GeometryFacet;
|
|
37
37
|
/**
|
|
38
|
-
* Workflow facet —
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Workflow facet — no-provider fallback for scope rail/card reads.
|
|
39
|
+
* Mounted editor paths prefer `api.ui.scope.*`; passing `null` makes
|
|
40
|
+
* fallback reads no-op.
|
|
41
41
|
*/
|
|
42
42
|
workflowFacet: WorkflowFacet | null;
|
|
43
|
+
/**
|
|
44
|
+
* Optional pre-read rail snapshot from `ui.scope.rail()`. When omitted,
|
|
45
|
+
* the layer reads the mounted UI API itself, then falls back to the
|
|
46
|
+
* workflow facet for no-provider paths.
|
|
47
|
+
*/
|
|
48
|
+
scopeRailSegments?: readonly ScopeRailSegment[];
|
|
43
49
|
/** Overlay's coordinate space. Defaults to the overlay's own origin. */
|
|
44
50
|
space?: OverlayCoordinateSpace;
|
|
45
51
|
/** Horizontal pad (px) the rail gutter occupies to the left of body. */
|
|
@@ -88,12 +94,13 @@ const POSTURE_STYLES: Record<ScopeRailPosture, PostureStyle> = {
|
|
|
88
94
|
|
|
89
95
|
const DEFAULT_RAIL_LANE_PX = 44;
|
|
90
96
|
const STRIPE_WIDTH_PX = 4;
|
|
91
|
-
const LABEL_WIDTH_PX =
|
|
97
|
+
const LABEL_WIDTH_PX = 28;
|
|
92
98
|
const STACK_OFFSET_PX = 6;
|
|
93
99
|
|
|
94
100
|
export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
95
101
|
geometryFacet,
|
|
96
102
|
workflowFacet,
|
|
103
|
+
scopeRailSegments,
|
|
97
104
|
space,
|
|
98
105
|
railLaneWidthPx = DEFAULT_RAIL_LANE_PX,
|
|
99
106
|
activeScopeId,
|
|
@@ -101,8 +108,13 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
|
101
108
|
onSegmentClick,
|
|
102
109
|
"data-testid": testId,
|
|
103
110
|
}) => {
|
|
111
|
+
const ui = useUiApi();
|
|
104
112
|
const frame = geometryFacet.getRenderFrame() ?? null;
|
|
105
|
-
const segments =
|
|
113
|
+
const segments =
|
|
114
|
+
scopeRailSegments ??
|
|
115
|
+
ui?.scope.rail().segments ??
|
|
116
|
+
workflowFacet?.getAllRailSegments() ??
|
|
117
|
+
[];
|
|
106
118
|
|
|
107
119
|
if (!frame || segments.length === 0) {
|
|
108
120
|
return null;
|
|
@@ -110,15 +122,23 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
|
110
122
|
|
|
111
123
|
// P2: which scopes currently have a `source: "ai"` candidate
|
|
112
124
|
// overlapping — drives the agent-pending shimmer class on their
|
|
113
|
-
// tints.
|
|
114
|
-
//
|
|
115
|
-
const cardModels = workflowFacet?.getAllScopeCardModels() ?? [];
|
|
125
|
+
// tints. Mounted surfaces read card projections through ui.scope.card;
|
|
126
|
+
// no-provider paths fall back to the workflow facet.
|
|
127
|
+
const cardModels = ui ? [] : workflowFacet?.getAllScopeCardModels() ?? [];
|
|
116
128
|
const agentPendingByScope = new Map<string, boolean>();
|
|
117
129
|
for (const model of cardModels) {
|
|
118
130
|
if (model.agentPending) {
|
|
119
131
|
agentPendingByScope.set(model.scopeId, true);
|
|
120
132
|
}
|
|
121
133
|
}
|
|
134
|
+
if (ui) {
|
|
135
|
+
for (const segment of segments) {
|
|
136
|
+
const model = ui.scope.card(segment.scopeId);
|
|
137
|
+
if (model?.agentPending) {
|
|
138
|
+
agentPendingByScope.set(segment.scopeId, true);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
122
142
|
|
|
123
143
|
// P3c: stack offsets for overlapping scopes. Two scopes whose
|
|
124
144
|
// offset ranges intersect on the same page render as stacked
|
|
@@ -222,17 +242,20 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
|
222
242
|
onKeyDown={handleStripeKey}
|
|
223
243
|
style={projectRectToOverlay(stripeRect, projectorSpace)}
|
|
224
244
|
/>
|
|
225
|
-
{/*
|
|
226
|
-
<
|
|
245
|
+
{/* Edit handle — revealed on stripe hover via CSS. */}
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
tabIndex={-1}
|
|
227
249
|
className={`wre-scope-rail-label wre-scope-rail-label-${style.railToken}`}
|
|
228
250
|
data-scope-id={segment.scopeId}
|
|
229
251
|
data-posture={segment.posture}
|
|
230
|
-
aria-
|
|
252
|
+
aria-label={`Edit scope${segment.label ? `: ${segment.label}` : ""}`}
|
|
253
|
+
onClick={handleActivate}
|
|
231
254
|
style={projectRectToOverlay(labelRect, projectorSpace)}
|
|
232
255
|
>
|
|
233
256
|
<span aria-hidden="true" className={`wre-scope-rail-icon wre-scope-rail-icon-${style.icon}`} />
|
|
234
257
|
<span className="wre-scope-rail-label-text">{style.labelText}</span>
|
|
235
|
-
</
|
|
258
|
+
</button>
|
|
236
259
|
</React.Fragment>
|
|
237
260
|
);
|
|
238
261
|
})}
|
|
@@ -402,13 +402,7 @@ function buildParagraph(
|
|
|
402
402
|
indentRight: paragraphLayout.indentation?.right ?? null,
|
|
403
403
|
indentFirstLine: paragraphLayout.indentation?.firstLine ?? null,
|
|
404
404
|
indentHanging: paragraphLayout.indentation?.hanging ?? null,
|
|
405
|
-
numberingMarkerWidth:
|
|
406
|
-
paragraphLayout.markerLane?.width ??
|
|
407
|
-
paragraphLayout.indentation?.hanging ??
|
|
408
|
-
(paragraphLayout.indentation?.firstLine !== undefined &&
|
|
409
|
-
paragraphLayout.indentation.firstLine < 0
|
|
410
|
-
? Math.abs(paragraphLayout.indentation.firstLine)
|
|
411
|
-
: null),
|
|
405
|
+
numberingMarkerWidth: paragraphLayout.markerLane?.width ?? null,
|
|
412
406
|
numberingMarkerStart: paragraphLayout.markerLane?.start ?? null,
|
|
413
407
|
numberingMarkerJustification: paragraphLayout.markerJustification ?? null,
|
|
414
408
|
numberingMarkerRunProperties: block.resolvedNumbering?.markerRunProperties ?? null,
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
cycleScopeIndex,
|
|
10
10
|
shouldHandleScopeNavKey,
|
|
11
11
|
} from "../chrome-overlay/scope-keyboard-cycle";
|
|
12
|
+
import { useUiApi } from "../ui-api-context.tsx";
|
|
12
13
|
|
|
13
14
|
export interface UseScopeCardStateOptions {
|
|
14
15
|
layoutFacet?: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
|
|
15
16
|
/**
|
|
16
|
-
* Layer-06 workflow facet —
|
|
17
|
-
*
|
|
18
|
-
* rail-seam inversion removed those methods from `layoutFacet`.
|
|
17
|
+
* Layer-06 workflow facet — no-provider fallback for scope card models.
|
|
18
|
+
* Mounted paths prefer `api.ui.scope.rail/card`.
|
|
19
19
|
*/
|
|
20
20
|
workflowFacet?: import("../../runtime/workflow/rail/types.ts").WorkflowFacet;
|
|
21
21
|
onScopeModeChangeRequested?: (payload: {
|
|
@@ -79,6 +79,31 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
|
|
|
79
79
|
void layoutFacet;
|
|
80
80
|
|
|
81
81
|
const [activeScopeId, setActiveScopeId] = useState<string | null>(null);
|
|
82
|
+
const ui = useUiApi();
|
|
83
|
+
|
|
84
|
+
const readScopeIds = useCallback((): string[] => {
|
|
85
|
+
if (ui) {
|
|
86
|
+
const ids: string[] = [];
|
|
87
|
+
const seen = new Set<string>();
|
|
88
|
+
for (const segment of ui.scope.rail().segments) {
|
|
89
|
+
if (seen.has(segment.scopeId)) continue;
|
|
90
|
+
seen.add(segment.scopeId);
|
|
91
|
+
ids.push(segment.scopeId);
|
|
92
|
+
}
|
|
93
|
+
return ids;
|
|
94
|
+
}
|
|
95
|
+
return workflowFacet?.getAllScopeCardModels().map((model) => model.scopeId) ?? [];
|
|
96
|
+
}, [ui, workflowFacet]);
|
|
97
|
+
|
|
98
|
+
const readScopeCard = useCallback(
|
|
99
|
+
(scopeId: string) => {
|
|
100
|
+
if (ui) return ui.scope.card(scopeId);
|
|
101
|
+
return workflowFacet
|
|
102
|
+
?.getAllScopeCardModels()
|
|
103
|
+
.find((m) => m.scopeId === scopeId) ?? null;
|
|
104
|
+
},
|
|
105
|
+
[ui, workflowFacet],
|
|
106
|
+
);
|
|
82
107
|
|
|
83
108
|
const handleScopeStripeClick = useCallback(
|
|
84
109
|
(segment: { scopeId: string }) => {
|
|
@@ -94,14 +119,13 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
|
|
|
94
119
|
}, []);
|
|
95
120
|
|
|
96
121
|
useEffect(() => {
|
|
97
|
-
if (!workflowFacet) {
|
|
122
|
+
if (!ui && !workflowFacet) {
|
|
98
123
|
return undefined;
|
|
99
124
|
}
|
|
100
125
|
const onKey = (event: KeyboardEvent) => {
|
|
101
126
|
if (!shouldHandleScopeNavKey(event)) return;
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
const ids = models.map((model) => model.scopeId);
|
|
127
|
+
const ids = readScopeIds();
|
|
128
|
+
if (ids.length === 0) return;
|
|
105
129
|
const key = event.key.toLowerCase();
|
|
106
130
|
if (key === "enter") {
|
|
107
131
|
if (!activeScopeId) {
|
|
@@ -117,7 +141,7 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
|
|
|
117
141
|
};
|
|
118
142
|
window.addEventListener("keydown", onKey);
|
|
119
143
|
return () => window.removeEventListener("keydown", onKey);
|
|
120
|
-
}, [workflowFacet, activeScopeId]);
|
|
144
|
+
}, [ui, workflowFacet, activeScopeId, readScopeIds]);
|
|
121
145
|
|
|
122
146
|
const handleScopeCardModeChange = useCallback(
|
|
123
147
|
(scopeId: string, mode: WorkflowScopeMode) => {
|
|
@@ -149,12 +173,10 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
|
|
|
149
173
|
|
|
150
174
|
const handleScopeCardAskAgent = useCallback(
|
|
151
175
|
(scopeId: string) => {
|
|
152
|
-
const cardModel =
|
|
153
|
-
?.getAllScopeCardModels()
|
|
154
|
-
.find((m) => m.scopeId === scopeId);
|
|
176
|
+
const cardModel = readScopeCard(scopeId);
|
|
155
177
|
onScopeAskAgent?.({ scopeId, anchor: cardModel?.anchor });
|
|
156
178
|
},
|
|
157
|
-
[onScopeAskAgent,
|
|
179
|
+
[onScopeAskAgent, readScopeCard],
|
|
158
180
|
);
|
|
159
181
|
|
|
160
182
|
return {
|
|
@@ -379,17 +379,31 @@
|
|
|
379
379
|
/*
|
|
380
380
|
* ─── Workflow inline text emphasis ───
|
|
381
381
|
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* wavy underline, active = thin outline). The rounded in-text background
|
|
387
|
-
* boxes that previously wrapped every run are gone — the overlay's flat
|
|
388
|
-
* tint handles that signal.
|
|
382
|
+
* Scopes should read as text ownership, not block selection. PM inline
|
|
383
|
+
* decorations carry the visible border on the scoped text, while the
|
|
384
|
+
* ChromeOverlay plane supplies the gutter/action rail. Keep this
|
|
385
|
+
* border-only: no filled boxes over document content.
|
|
389
386
|
*/
|
|
390
387
|
.prosemirror-surface .ProseMirror .wre-workflow-inline {
|
|
391
388
|
-webkit-box-decoration-break: clone;
|
|
392
389
|
box-decoration-break: clone;
|
|
390
|
+
border-radius: 2px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.prosemirror-surface .ProseMirror .wre-workflow-inline-edit {
|
|
394
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-accent) 52%, transparent);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.prosemirror-surface .ProseMirror .wre-workflow-inline-suggest {
|
|
398
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-warning) 56%, transparent);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.prosemirror-surface .ProseMirror .wre-workflow-inline-comment {
|
|
402
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-insert) 48%, transparent);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.prosemirror-surface .ProseMirror .wre-workflow-inline-view {
|
|
406
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-secondary) 46%, transparent);
|
|
393
407
|
}
|
|
394
408
|
|
|
395
409
|
.prosemirror-surface .ProseMirror .wre-workflow-inline-candidate {
|
|
@@ -408,31 +422,29 @@
|
|
|
408
422
|
|
|
409
423
|
/*
|
|
410
424
|
* Locked zone marker for inline runs: a subtle dotted right edge so the
|
|
411
|
-
* reader can tell where the locked range ends when the gutter
|
|
412
|
-
* out of view. The
|
|
425
|
+
* reader can tell where the locked range ends when the gutter handle scrolls
|
|
426
|
+
* out of view. The rail carries the action affordance.
|
|
413
427
|
*/
|
|
414
428
|
.prosemirror-surface .ProseMirror .wre-workflow-inline-locked-zone {
|
|
415
429
|
box-shadow: inset -1px 0 0 color-mix(in srgb, var(--color-danger) 35%, transparent);
|
|
416
430
|
}
|
|
417
431
|
|
|
418
432
|
/*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* fought with the overlay's flat tint. The class name is kept on the
|
|
423
|
-
* inline decoration as a data hook (no visual), and emphasis for the
|
|
424
|
-
* active scope now lives on the ChromeOverlay rail stripe + scope card.
|
|
433
|
+
* Active scope emphasis is a stronger text border plus the gutter handle.
|
|
434
|
+
* This keeps focus local to scoped text without reintroducing filled green
|
|
435
|
+
* rectangles.
|
|
425
436
|
*/
|
|
426
437
|
.prosemirror-surface .ProseMirror .wre-workflow-inline-active {
|
|
427
|
-
|
|
438
|
+
box-shadow:
|
|
439
|
+
0 0 0 1px color-mix(in srgb, var(--color-accent) 72%, transparent),
|
|
440
|
+
0 0 0 3px color-mix(in srgb, var(--color-accent) 12%, transparent);
|
|
428
441
|
}
|
|
429
442
|
|
|
430
443
|
/*
|
|
431
444
|
* ─── ChromeOverlay: scope rail layer ───
|
|
432
445
|
*
|
|
433
|
-
* The overlay sits above PM and paints
|
|
434
|
-
*
|
|
435
|
-
* kernel's anchor index, not DOM rects.
|
|
446
|
+
* The overlay sits above PM and paints gutter handles plus optional
|
|
447
|
+
* border-only line outlines. It must not fill document content.
|
|
436
448
|
*/
|
|
437
449
|
.wre-scope-rail-layer {
|
|
438
450
|
pointer-events: none;
|
|
@@ -471,58 +483,36 @@
|
|
|
471
483
|
border-radius: 0.2rem;
|
|
472
484
|
pointer-events: none;
|
|
473
485
|
z-index: 0;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
.wre-scope-rail-tint-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
.wre-scope-rail-tint-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
.wre-scope-rail-tint-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
.wre-scope-rail-tint-danger {
|
|
490
|
-
background: color-mix(in srgb, var(--color-danger) 14%, transparent);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/* §3.7 canonical scope families */
|
|
494
|
-
.wre-scope-rail-tint-blocked {
|
|
495
|
-
background: var(--color-scope-tint-blocked);
|
|
496
|
-
}
|
|
497
|
-
.wre-scope-rail-tint-in-scope {
|
|
498
|
-
background: var(--color-scope-tint-in-scope);
|
|
499
|
-
}
|
|
500
|
-
.wre-scope-rail-tint-suggest {
|
|
501
|
-
background: var(--color-scope-tint-suggest);
|
|
502
|
-
}
|
|
503
|
-
.wre-scope-rail-tint-comment {
|
|
504
|
-
background: var(--color-scope-tint-comment);
|
|
505
|
-
}
|
|
506
|
-
.wre-scope-rail-tint-scheduled {
|
|
507
|
-
background: var(--color-scope-tint-scheduled);
|
|
508
|
-
}
|
|
486
|
+
background: transparent;
|
|
487
|
+
transition: box-shadow 140ms ease-out;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.wre-scope-rail-tint-accent,
|
|
491
|
+
.wre-scope-rail-tint-warning,
|
|
492
|
+
.wre-scope-rail-tint-insert,
|
|
493
|
+
.wre-scope-rail-tint-secondary,
|
|
494
|
+
.wre-scope-rail-tint-danger,
|
|
495
|
+
.wre-scope-rail-tint-blocked,
|
|
496
|
+
.wre-scope-rail-tint-in-scope,
|
|
497
|
+
.wre-scope-rail-tint-suggest,
|
|
498
|
+
.wre-scope-rail-tint-comment,
|
|
499
|
+
.wre-scope-rail-tint-scheduled,
|
|
509
500
|
.wre-scope-rail-tint-proposed {
|
|
510
|
-
background:
|
|
501
|
+
background: transparent;
|
|
511
502
|
}
|
|
512
503
|
|
|
513
504
|
.wre-scope-rail-tint-active {
|
|
514
|
-
outline: 1px solid color-mix(in srgb, var(--color-accent)
|
|
505
|
+
outline: 1px solid color-mix(in srgb, var(--color-accent) 52%, transparent);
|
|
515
506
|
outline-offset: -1px;
|
|
516
507
|
}
|
|
517
508
|
|
|
518
509
|
/*
|
|
519
510
|
* ─── Agent-pending shimmer (K2 / scope-card-overlay P2) ───
|
|
520
511
|
*
|
|
521
|
-
* Painted on every scope
|
|
522
|
-
* with `source: "ai"`.
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* border so the posture is still readable.
|
|
512
|
+
* Painted on every scope outline that overlaps a WorkflowCandidateRange
|
|
513
|
+
* with `source: "ai"`. A soft 1.8s pulse signals the agent is thinking
|
|
514
|
+
* without competing with the active outline. Reduced-motion disables the
|
|
515
|
+
* animation and holds a static 60% opacity outline.
|
|
526
516
|
*/
|
|
527
517
|
@keyframes wre-agent-pulse {
|
|
528
518
|
0%, 100% { opacity: 0.4; }
|
|
@@ -544,9 +534,9 @@
|
|
|
544
534
|
* ─── Scope rail stripe ───
|
|
545
535
|
*
|
|
546
536
|
* The rail stripe is the rest-state representation of a scope: a 4px
|
|
547
|
-
* color stripe in the gutter lane.
|
|
548
|
-
* accent/warning/insert/secondary/danger tokens.
|
|
549
|
-
*
|
|
537
|
+
* color stripe in the gutter lane. Posture color comes from the
|
|
538
|
+
* accent/warning/insert/secondary/danger tokens. Hover widens the stripe
|
|
539
|
+
* via transform (zero layout cost) and reveals the edit handle.
|
|
550
540
|
*/
|
|
551
541
|
.wre-scope-rail-stripe {
|
|
552
542
|
position: absolute;
|
|
@@ -595,34 +585,31 @@
|
|
|
595
585
|
.wre-scope-rail-stripe.wre-scope-rail-tint-proposed { background: var(--color-scope-tint-proposed); }
|
|
596
586
|
|
|
597
587
|
/*
|
|
598
|
-
* ─── Scope rail
|
|
588
|
+
* ─── Scope rail edit handle ───
|
|
599
589
|
*
|
|
600
|
-
* Shown only on stripe hover (CSS-driven).
|
|
601
|
-
* stripe with
|
|
602
|
-
* of the scope.
|
|
590
|
+
* Shown only on stripe hover (CSS-driven). The handle overlays the
|
|
591
|
+
* stripe with a compact icon anchored to the first line of the scope.
|
|
603
592
|
*/
|
|
604
593
|
.wre-scope-rail-label {
|
|
605
594
|
position: absolute;
|
|
606
595
|
display: flex;
|
|
607
596
|
align-items: center;
|
|
608
597
|
justify-content: center;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
598
|
+
width: 24px;
|
|
599
|
+
height: 24px;
|
|
600
|
+
padding: 0;
|
|
601
|
+
border-radius: 999px;
|
|
612
602
|
border: 1px solid transparent;
|
|
613
603
|
background: var(--color-canvas, #fff);
|
|
614
604
|
box-shadow: var(--shadow-sm);
|
|
615
|
-
font
|
|
616
|
-
line-height: 1;
|
|
617
|
-
text-transform: uppercase;
|
|
618
|
-
letter-spacing: 0.06em;
|
|
619
|
-
font-weight: 600;
|
|
605
|
+
font: inherit;
|
|
620
606
|
cursor: pointer;
|
|
621
607
|
z-index: 2;
|
|
622
608
|
opacity: 0;
|
|
623
609
|
pointer-events: none;
|
|
624
610
|
transition: opacity 140ms ease-out, transform 140ms ease-out;
|
|
625
611
|
transform: translateX(-4px);
|
|
612
|
+
margin: 0;
|
|
626
613
|
}
|
|
627
614
|
|
|
628
615
|
.wre-scope-rail-stripe:hover + .wre-scope-rail-label,
|
|
@@ -697,8 +684,8 @@
|
|
|
697
684
|
|
|
698
685
|
.wre-scope-rail-icon {
|
|
699
686
|
display: inline-block;
|
|
700
|
-
width:
|
|
701
|
-
height:
|
|
687
|
+
width: 13px;
|
|
688
|
+
height: 13px;
|
|
702
689
|
background-color: currentColor;
|
|
703
690
|
mask-repeat: no-repeat;
|
|
704
691
|
mask-position: center;
|
|
@@ -708,6 +695,10 @@
|
|
|
708
695
|
-webkit-mask-size: contain;
|
|
709
696
|
}
|
|
710
697
|
|
|
698
|
+
.wre-scope-rail-label-text {
|
|
699
|
+
display: none;
|
|
700
|
+
}
|
|
701
|
+
|
|
711
702
|
/* Simple inline-SVG-as-mask icons so consumers don't need an icon font. */
|
|
712
703
|
.wre-scope-rail-icon-lock {
|
|
713
704
|
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 018 0v4"/></svg>');
|
|
@@ -283,13 +283,20 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
283
283
|
// changed, re-read any attached anchors" marker. Per-query
|
|
284
284
|
// invalidation fan-out is a Phase Q follow-up — the geometry
|
|
285
285
|
// facet does not yet expose per-kind invalidation events.
|
|
286
|
-
const
|
|
286
|
+
const uiApi = useUiApi();
|
|
287
287
|
const shellChannels = useUiShellChannels();
|
|
288
288
|
React.useEffect(() => {
|
|
289
|
-
if (!
|
|
290
|
-
shellChannels.viewport.emit(
|
|
289
|
+
if (!uiApi || !shellChannels) return;
|
|
290
|
+
shellChannels.viewport.emit(uiApi.viewport.get());
|
|
291
291
|
shellChannels.overlays.emit({ kind: "page", value: 0 });
|
|
292
|
-
}, [renderFrameRevision,
|
|
292
|
+
}, [renderFrameRevision, uiApi, shellChannels]);
|
|
293
|
+
const scopeRailSegments = useMemo(
|
|
294
|
+
() =>
|
|
295
|
+
uiApi?.scope.rail().segments ??
|
|
296
|
+
props.workflowFacet?.getAllRailSegments() ??
|
|
297
|
+
[],
|
|
298
|
+
[uiApi, props.workflowFacet, renderFrameRevision],
|
|
299
|
+
);
|
|
293
300
|
const headings = props.documentNavigation?.headings ?? [];
|
|
294
301
|
const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
|
|
295
302
|
const footerVariant = snapshot.pageLayout?.footerVariants[0]?.variant ?? "default";
|
|
@@ -1272,10 +1279,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1272
1279
|
onRejectRevision: props.onRejectRevision,
|
|
1273
1280
|
onAcceptAllChanges: props.onAcceptAllChanges,
|
|
1274
1281
|
onRejectAllChanges: props.onRejectAllChanges,
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
scopeRailSegments
|
|
1282
|
+
// Layer 11 closeout: mounted workspace chrome reads scope
|
|
1283
|
+
// rail data through `api.ui.scope.rail()`, with the workflow
|
|
1284
|
+
// facet retained as the no-provider fallback.
|
|
1285
|
+
scopeRailSegments,
|
|
1279
1286
|
activeScopeId,
|
|
1280
1287
|
onOpenScope: (segment) => {
|
|
1281
1288
|
handleScopeStripeClick({ scopeId: segment.scopeId });
|