@beyondwork/docx-react-component 1.0.95 → 1.0.97
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 +33 -19
- package/src/api/v3/ui/_types.ts +11 -21
- package/src/api/v3/ui/chrome.ts +8 -9
- package/src/api/v3/ui/debug.ts +15 -77
- package/src/api/v3/ui/overlays-visibility.ts +9 -10
- package/src/api/v3/ui/overlays.ts +8 -75
- package/src/io/ooxml/parse-main-document.ts +30 -0
- package/src/io/ooxml/parse-picture.ts +14 -0
- package/src/io/ooxml/parse-shapes.ts +41 -1
- package/src/model/canonical-document.ts +17 -0
- package/src/runtime/document-runtime.ts +46 -1
- package/src/runtime/layout/layout-engine-version.ts +8 -1
- package/src/runtime/layout/page-story-resolver.ts +1 -0
- package/src/runtime/layout/paginated-layout-engine.ts +26 -10
- package/src/runtime/surface-projection.ts +114 -12
- package/src/runtime/workflow/rail/compose.ts +5 -0
- package/src/ui/WordReviewEditor.tsx +6 -10
- package/src/ui/editor-command-bag.ts +2 -0
- package/src/ui/ui-controller-factory.ts +2 -2
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +12 -41
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +3 -7
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +22 -228
- package/src/ui-tailwind/debug/README.md +12 -50
- package/src/ui-tailwind/debug/tw-debug-overlay.tsx +6 -6
- package/src/ui-tailwind/debug/tw-debug-presentation.tsx +9 -20
- package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +5 -6
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +1 -4
- package/src/ui-tailwind/editor-surface/picture-effects.ts +96 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +89 -62
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +205 -0
- package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +190 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +53 -53
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +83 -20
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +114 -4
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +5 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +26 -0
- package/src/ui-tailwind/theme/editor-theme.css +82 -84
- package/src/ui-tailwind/tw-review-workspace.tsx +15 -0
|
@@ -18,8 +18,15 @@ import {
|
|
|
18
18
|
} from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
19
19
|
import {
|
|
20
20
|
collectFloatingImageOverlayItems,
|
|
21
|
+
type FloatingImageOverlayItem,
|
|
21
22
|
type FloatingImagePreviewDescriptor,
|
|
22
23
|
} from "./floating-image-overlay-model.ts";
|
|
24
|
+
import { buildSegmentStyle } from "../editor-surface/tw-page-block-view.helpers.ts";
|
|
25
|
+
import { buildPictureFilterCss } from "../editor-surface/picture-effects.ts";
|
|
26
|
+
|
|
27
|
+
const EMU_PER_PX = 9525;
|
|
28
|
+
type FloatingShapeBody = NonNullable<FloatingImageOverlayItem["shape"]>["body"];
|
|
29
|
+
type FloatingShapeBodyAnchor = NonNullable<FloatingShapeBody>["anchor"];
|
|
23
30
|
|
|
24
31
|
export interface TwFloatingImageLayerProps {
|
|
25
32
|
facet: WordReviewEditorLayoutFacet;
|
|
@@ -221,9 +228,9 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
221
228
|
data-floating-image-count={items.length}
|
|
222
229
|
>
|
|
223
230
|
{items.map((item) => {
|
|
224
|
-
const interactive =
|
|
231
|
+
const interactive =
|
|
232
|
+
!item.shape && plane === "front" && typeof onActivateFloatingImage === "function";
|
|
225
233
|
const commonProps = {
|
|
226
|
-
key: item.key,
|
|
227
234
|
className: interactive
|
|
228
235
|
? "pointer-events-auto absolute m-0 border-0 bg-transparent p-0"
|
|
229
236
|
: "pointer-events-none absolute m-0 border-0 bg-transparent p-0",
|
|
@@ -234,9 +241,21 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
234
241
|
left: `${item.leftPx}px`,
|
|
235
242
|
width: `${item.widthPx}px`,
|
|
236
243
|
height: `${item.heightPx}px`,
|
|
244
|
+
...(item.zIndex !== undefined ? { zIndex: item.zIndex } : {}),
|
|
237
245
|
},
|
|
238
246
|
} as const;
|
|
239
|
-
const
|
|
247
|
+
const imageFilter = buildPictureFilterCss(item.pictureEffects);
|
|
248
|
+
const content = item.shape ? (
|
|
249
|
+
<div
|
|
250
|
+
className="flex h-full w-full overflow-hidden"
|
|
251
|
+
title={item.detail ?? item.shape.label}
|
|
252
|
+
style={buildFloatingShapeBoxStyle(item)}
|
|
253
|
+
>
|
|
254
|
+
<span style={buildFloatingShapeTextStyle(item)}>
|
|
255
|
+
{item.shape.text}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
) : item.src ? (
|
|
240
259
|
<img
|
|
241
260
|
src={item.src}
|
|
242
261
|
alt={interactive ? item.altText ?? "" : ""}
|
|
@@ -247,6 +266,8 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
247
266
|
width: "100%",
|
|
248
267
|
height: "100%",
|
|
249
268
|
objectFit: "fill",
|
|
269
|
+
...(imageFilter ? { filter: imageFilter } : {}),
|
|
270
|
+
...(item.pictureEffects?.srcRect ? { clipPath: srcRectToInset(item.pictureEffects.srcRect) } : {}),
|
|
250
271
|
}}
|
|
251
272
|
/>
|
|
252
273
|
) : (
|
|
@@ -259,13 +280,14 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
259
280
|
);
|
|
260
281
|
if (!interactive) {
|
|
261
282
|
return (
|
|
262
|
-
<div {...commonProps} aria-hidden="true">
|
|
283
|
+
<div key={item.key} {...commonProps} aria-hidden="true">
|
|
263
284
|
{content}
|
|
264
285
|
</div>
|
|
265
286
|
);
|
|
266
287
|
}
|
|
267
288
|
return (
|
|
268
289
|
<button
|
|
290
|
+
key={item.key}
|
|
269
291
|
{...commonProps}
|
|
270
292
|
type="button"
|
|
271
293
|
tabIndex={-1}
|
|
@@ -287,4 +309,92 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
287
309
|
);
|
|
288
310
|
};
|
|
289
311
|
|
|
312
|
+
function buildFloatingShapeBoxStyle(
|
|
313
|
+
item: FloatingImageOverlayItem,
|
|
314
|
+
): React.CSSProperties {
|
|
315
|
+
const fill = item.shape?.fill;
|
|
316
|
+
const line = item.shape?.line;
|
|
317
|
+
const style: React.CSSProperties = {
|
|
318
|
+
boxSizing: "border-box",
|
|
319
|
+
alignItems: bodyAnchorToAlignItems(item.shape?.body?.anchor),
|
|
320
|
+
justifyContent: inferTextBoxJustification(item.shape?.text),
|
|
321
|
+
padding: shapeBodyPadding(item.shape?.body),
|
|
322
|
+
textAlign: inferTextBoxTextAlign(item.shape?.text),
|
|
323
|
+
backgroundColor: "transparent",
|
|
324
|
+
border: "none",
|
|
325
|
+
};
|
|
326
|
+
if (fill?.kind === "solid" && fill.colorType === "srgbClr" && isSafeHexColor(fill.color)) {
|
|
327
|
+
style.backgroundColor = `#${fill.color}`;
|
|
328
|
+
}
|
|
329
|
+
if (line && !line.noLine && line.color && isSafeHexColor(line.color)) {
|
|
330
|
+
style.border = `${Math.max(1, Math.round((line.widthEmu ?? 9525) / EMU_PER_PX))}px solid #${line.color}`;
|
|
331
|
+
}
|
|
332
|
+
return style;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function bodyAnchorToAlignItems(
|
|
336
|
+
anchor: FloatingShapeBodyAnchor,
|
|
337
|
+
): React.CSSProperties["alignItems"] {
|
|
338
|
+
switch (anchor) {
|
|
339
|
+
case "t":
|
|
340
|
+
return "flex-start";
|
|
341
|
+
case "b":
|
|
342
|
+
return "flex-end";
|
|
343
|
+
case "ctr":
|
|
344
|
+
default:
|
|
345
|
+
return "center";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function shapeBodyPadding(
|
|
350
|
+
body: FloatingShapeBody,
|
|
351
|
+
): string {
|
|
352
|
+
const top = emuInsetToPx(body?.insetTopEmu, 0);
|
|
353
|
+
const right = emuInsetToPx(body?.insetRightEmu, 4);
|
|
354
|
+
const bottom = emuInsetToPx(body?.insetBottomEmu, 0);
|
|
355
|
+
const left = emuInsetToPx(body?.insetLeftEmu, 4);
|
|
356
|
+
return `${top}px ${right}px ${bottom}px ${left}px`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function emuInsetToPx(value: number | undefined, fallbackPx: number): number {
|
|
360
|
+
return value === undefined ? fallbackPx : Math.max(0, Math.round(value / EMU_PER_PX));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildFloatingShapeTextStyle(
|
|
364
|
+
item: FloatingImageOverlayItem,
|
|
365
|
+
): React.CSSProperties {
|
|
366
|
+
const style = buildSegmentStyle(item.shape?.marks, item.shape?.markAttrs);
|
|
367
|
+
if (item.shape?.markAttrs?.fontSize) {
|
|
368
|
+
style.fontSize = `${item.shape.markAttrs.fontSize / 2}pt`;
|
|
369
|
+
}
|
|
370
|
+
const rawColor = item.shape?.markAttrs?.textColor;
|
|
371
|
+
if (rawColor && isSafeHexColor(rawColor)) {
|
|
372
|
+
style.color = `#${rawColor}`;
|
|
373
|
+
}
|
|
374
|
+
if (!style.fontSize) {
|
|
375
|
+
style.fontSize = `${Math.max(12, Math.min(56, Math.round(item.heightPx * 0.42)))}px`;
|
|
376
|
+
}
|
|
377
|
+
if (!style.lineHeight) {
|
|
378
|
+
style.lineHeight = 1.05;
|
|
379
|
+
}
|
|
380
|
+
style.whiteSpace = "pre-wrap";
|
|
381
|
+
return style;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function srcRectToInset(srcRect: { top: number; right: number; bottom: number; left: number }): string {
|
|
385
|
+
return `inset(${(srcRect.top / 1000).toFixed(3)}% ${(srcRect.right / 1000).toFixed(3)}% ${(srcRect.bottom / 1000).toFixed(3)}% ${(srcRect.left / 1000).toFixed(3)}%)`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function inferTextBoxJustification(text: string | undefined): React.CSSProperties["justifyContent"] {
|
|
389
|
+
return text?.startsWith("[") ? "flex-end" : "center";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function inferTextBoxTextAlign(text: string | undefined): React.CSSProperties["textAlign"] {
|
|
393
|
+
return text?.startsWith("[") ? "right" : "center";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isSafeHexColor(value: string): boolean {
|
|
397
|
+
return /^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(value);
|
|
398
|
+
}
|
|
399
|
+
|
|
290
400
|
export default TwFloatingImageLayer;
|
|
@@ -38,6 +38,7 @@ export interface TwPageChromeEntryProps {
|
|
|
38
38
|
activeStoryPageIndex?: number | null;
|
|
39
39
|
onOpenStory?: (target: EditorStoryTarget, pageIndex: number) => void;
|
|
40
40
|
visiblePageIndexRange?: { start: number; end: number } | null;
|
|
41
|
+
plainPageBands?: boolean;
|
|
41
42
|
renderFrameRevision: number;
|
|
42
43
|
/** Preview catalog threaded into header/footer/footnote region renderers
|
|
43
44
|
* so images (CCEP logos on 7-of-8 CCEP docs) render as real <img>s. */
|
|
@@ -62,6 +63,7 @@ function TwPageChromeEntryInner({
|
|
|
62
63
|
activeStoryPageIndex,
|
|
63
64
|
onOpenStory,
|
|
64
65
|
visiblePageIndexRange,
|
|
66
|
+
plainPageBands = false,
|
|
65
67
|
renderFrameRevision,
|
|
66
68
|
mediaPreviews,
|
|
67
69
|
activeBandRibbonProps,
|
|
@@ -208,6 +210,7 @@ function TwPageChromeEntryInner({
|
|
|
208
210
|
widthPx={bandWidthPx}
|
|
209
211
|
bandHeightPx={px(headerRegion.heightTwips)}
|
|
210
212
|
isActiveSlot={Boolean(headerActive)}
|
|
213
|
+
chromeMode={plainPageBands ? "plain" : "normal"}
|
|
211
214
|
sectionLabel={headerActive ? headerSectionLabel : undefined}
|
|
212
215
|
onDoubleClick={handleHeaderDoubleClick}
|
|
213
216
|
mediaPreviews={mediaPreviews}
|
|
@@ -223,6 +226,7 @@ function TwPageChromeEntryInner({
|
|
|
223
226
|
widthPx={bandWidthPx}
|
|
224
227
|
bandHeightPx={px(footerRegion.heightTwips)}
|
|
225
228
|
isActiveSlot={Boolean(footerActive)}
|
|
229
|
+
chromeMode={plainPageBands ? "plain" : "normal"}
|
|
226
230
|
sectionLabel={footerActive ? footerSectionLabel : undefined}
|
|
227
231
|
onDoubleClick={handleFooterDoubleClick}
|
|
228
232
|
mediaPreviews={mediaPreviews}
|
|
@@ -267,6 +271,7 @@ function propsAreEqual(
|
|
|
267
271
|
prev.activeStoryPageIndex === next.activeStoryPageIndex &&
|
|
268
272
|
prev.onOpenStory === next.onOpenStory &&
|
|
269
273
|
prev.visiblePageIndexRange === next.visiblePageIndexRange &&
|
|
274
|
+
prev.plainPageBands === next.plainPageBands &&
|
|
270
275
|
prev.renderFrameRevision === next.renderFrameRevision &&
|
|
271
276
|
prev.rect.topPx === next.rect.topPx &&
|
|
272
277
|
prev.rect.bottomPx === next.rect.bottomPx &&
|
|
@@ -32,6 +32,7 @@ export interface TwPageFooterBandProps {
|
|
|
32
32
|
widthPx: number;
|
|
33
33
|
/** True when this band is the active-story slot. Renders portal target instead of read-only DOM. */
|
|
34
34
|
isActiveSlot: boolean;
|
|
35
|
+
chromeMode?: "normal" | "plain";
|
|
35
36
|
/**
|
|
36
37
|
* Lane 6d.U1 — section label for the active-band ribbon (e.g. "Footer — Section 1").
|
|
37
38
|
* Only rendered when `isActiveSlot` is true.
|
|
@@ -56,6 +57,7 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
56
57
|
leftPx,
|
|
57
58
|
widthPx,
|
|
58
59
|
isActiveSlot,
|
|
60
|
+
chromeMode = "normal",
|
|
59
61
|
sectionLabel,
|
|
60
62
|
onDoubleClick,
|
|
61
63
|
"data-testid": testId,
|
|
@@ -66,6 +68,7 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
66
68
|
<div
|
|
67
69
|
className="wre-page-band"
|
|
68
70
|
data-page-band="footer"
|
|
71
|
+
data-page-band-chrome={chromeMode === "plain" ? "plain" : undefined}
|
|
69
72
|
data-page-index={pageIndex}
|
|
70
73
|
data-active={isActiveSlot ? "true" : undefined}
|
|
71
74
|
data-testid={testId}
|
|
@@ -33,6 +33,7 @@ export interface TwPageHeaderBandProps {
|
|
|
33
33
|
widthPx: number;
|
|
34
34
|
/** True when this band is the active-story slot. Renders portal target instead of read-only DOM. */
|
|
35
35
|
isActiveSlot: boolean;
|
|
36
|
+
chromeMode?: "normal" | "plain";
|
|
36
37
|
/**
|
|
37
38
|
* Lane 6d.U1 — section label for the active-band ribbon (e.g. "Header — Section 1").
|
|
38
39
|
* Only rendered when `isActiveSlot` is true.
|
|
@@ -62,6 +63,7 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
62
63
|
leftPx,
|
|
63
64
|
widthPx,
|
|
64
65
|
isActiveSlot,
|
|
66
|
+
chromeMode = "normal",
|
|
65
67
|
sectionLabel,
|
|
66
68
|
onDoubleClick,
|
|
67
69
|
"data-testid": testId,
|
|
@@ -72,6 +74,7 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
72
74
|
<div
|
|
73
75
|
className="wre-page-band"
|
|
74
76
|
data-page-band="header"
|
|
77
|
+
data-page-band-chrome={chromeMode === "plain" ? "plain" : undefined}
|
|
75
78
|
data-page-index={pageIndex}
|
|
76
79
|
data-active={isActiveSlot ? "true" : undefined}
|
|
77
80
|
data-testid={testId}
|
|
@@ -148,6 +148,11 @@ export interface TwPageStackChromeLayerProps {
|
|
|
148
148
|
* reported.
|
|
149
149
|
*/
|
|
150
150
|
visiblePageIndexRange?: { start: number; end: number } | null;
|
|
151
|
+
/**
|
|
152
|
+
* Chrome-less visual fidelity mode: keep per-page story rendering active
|
|
153
|
+
* while suppressing editor-only inactive band tint/border.
|
|
154
|
+
*/
|
|
155
|
+
plainPageBands?: boolean;
|
|
151
156
|
/** Optional test id applied to the layer root. */
|
|
152
157
|
"data-testid"?: string;
|
|
153
158
|
/** Preview catalog threaded through to per-page chrome bands so images
|
|
@@ -173,6 +178,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
173
178
|
pmSurfaceElement,
|
|
174
179
|
pmView,
|
|
175
180
|
visiblePageIndexRange,
|
|
181
|
+
plainPageBands = false,
|
|
176
182
|
"data-testid": testId,
|
|
177
183
|
mediaPreviews,
|
|
178
184
|
activeBandRibbonProps,
|
|
@@ -461,6 +467,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
461
467
|
activeStoryPageIndex={activeStoryPageIndex}
|
|
462
468
|
onOpenStory={handleOpenStoryForPage}
|
|
463
469
|
visiblePageIndexRange={visiblePageIndexRange}
|
|
470
|
+
plainPageBands={plainPageBands}
|
|
464
471
|
renderFrameRevision={renderFrameRevision}
|
|
465
472
|
mediaPreviews={mediaPreviews}
|
|
466
473
|
activeBandRibbonProps={activeBandRibbonProps}
|
|
@@ -516,6 +523,7 @@ function propsAreEqual(
|
|
|
516
523
|
prev.pmSurfaceElement === next.pmSurfaceElement &&
|
|
517
524
|
prev.pmView === next.pmView &&
|
|
518
525
|
rangeEqual(prev.visiblePageIndexRange, next.visiblePageIndexRange) &&
|
|
526
|
+
prev.plainPageBands === next.plainPageBands &&
|
|
519
527
|
prev["data-testid"] === next["data-testid"]
|
|
520
528
|
);
|
|
521
529
|
}
|
|
@@ -167,6 +167,28 @@ function renderSegment(
|
|
|
167
167
|
/>
|
|
168
168
|
);
|
|
169
169
|
}
|
|
170
|
+
case "shape": {
|
|
171
|
+
if (shouldRenderAbsoluteFloatingImageInPageOverlay(seg.anchor)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (!seg.isTextBox || !seg.txbxText) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const style = buildSegmentStyle(seg.txbxMarks, seg.txbxMarkAttrs);
|
|
178
|
+
if (seg.txbxMarkAttrs?.textColor && isSafeHexColor(seg.txbxMarkAttrs.textColor)) {
|
|
179
|
+
style.color = `#${seg.txbxMarkAttrs.textColor}`;
|
|
180
|
+
}
|
|
181
|
+
return (
|
|
182
|
+
<span
|
|
183
|
+
key={seg.segmentId}
|
|
184
|
+
data-node-type="shape"
|
|
185
|
+
style={style}
|
|
186
|
+
title={seg.detail}
|
|
187
|
+
>
|
|
188
|
+
{seg.txbxText ?? seg.label}
|
|
189
|
+
</span>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
170
192
|
case "field_ref":
|
|
171
193
|
return (
|
|
172
194
|
<span
|
|
@@ -202,6 +224,10 @@ function renderSegment(
|
|
|
202
224
|
}
|
|
203
225
|
}
|
|
204
226
|
|
|
227
|
+
function isSafeHexColor(value: string): boolean {
|
|
228
|
+
return /^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(value);
|
|
229
|
+
}
|
|
230
|
+
|
|
205
231
|
// ---------------------------------------------------------------------------
|
|
206
232
|
// Block renderers
|
|
207
233
|
// ---------------------------------------------------------------------------
|
|
@@ -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,43 +422,43 @@
|
|
|
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
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
446
|
+
* Product scope affordance now lives directly on the scoped inline text.
|
|
447
|
+
* Keep the legacy rail layer hidden if an old host mounts it directly:
|
|
448
|
+
* no gutter markers, no rail handles, no overlay tints.
|
|
436
449
|
*/
|
|
437
450
|
.wre-scope-rail-layer {
|
|
451
|
+
display: none;
|
|
438
452
|
pointer-events: none;
|
|
439
453
|
}
|
|
440
454
|
|
|
441
455
|
/*
|
|
442
456
|
* ─── Gutter lane tokens ───
|
|
443
457
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
458
|
+
* Kept for legacy gutter consumers and page-card composition. Product
|
|
459
|
+
* scope rails are hidden; scoped text is the visible scope affordance.
|
|
460
|
+
* Page surfaces use 64px, canvas surfaces 48px. Host apps can override
|
|
461
|
+
* via CSS custom property.
|
|
448
462
|
*/
|
|
449
463
|
:root {
|
|
450
464
|
--wre-gutter-lane-width: 64px;
|
|
@@ -471,58 +485,36 @@
|
|
|
471
485
|
border-radius: 0.2rem;
|
|
472
486
|
pointer-events: none;
|
|
473
487
|
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
|
-
}
|
|
488
|
+
background: transparent;
|
|
489
|
+
transition: box-shadow 140ms ease-out;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.wre-scope-rail-tint-accent,
|
|
493
|
+
.wre-scope-rail-tint-warning,
|
|
494
|
+
.wre-scope-rail-tint-insert,
|
|
495
|
+
.wre-scope-rail-tint-secondary,
|
|
496
|
+
.wre-scope-rail-tint-danger,
|
|
497
|
+
.wre-scope-rail-tint-blocked,
|
|
498
|
+
.wre-scope-rail-tint-in-scope,
|
|
499
|
+
.wre-scope-rail-tint-suggest,
|
|
500
|
+
.wre-scope-rail-tint-comment,
|
|
501
|
+
.wre-scope-rail-tint-scheduled,
|
|
509
502
|
.wre-scope-rail-tint-proposed {
|
|
510
|
-
background:
|
|
503
|
+
background: transparent;
|
|
511
504
|
}
|
|
512
505
|
|
|
513
506
|
.wre-scope-rail-tint-active {
|
|
514
|
-
outline: 1px solid color-mix(in srgb, var(--color-accent)
|
|
507
|
+
outline: 1px solid color-mix(in srgb, var(--color-accent) 52%, transparent);
|
|
515
508
|
outline-offset: -1px;
|
|
516
509
|
}
|
|
517
510
|
|
|
518
511
|
/*
|
|
519
512
|
* ─── Agent-pending shimmer (K2 / scope-card-overlay P2) ───
|
|
520
513
|
*
|
|
521
|
-
* Painted on every scope
|
|
522
|
-
* with `source: "ai"`.
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* border so the posture is still readable.
|
|
514
|
+
* Painted on every scope outline that overlaps a WorkflowCandidateRange
|
|
515
|
+
* with `source: "ai"`. A soft 1.8s pulse signals the agent is thinking
|
|
516
|
+
* without competing with the active outline. Reduced-motion disables the
|
|
517
|
+
* animation and holds a static 60% opacity outline.
|
|
526
518
|
*/
|
|
527
519
|
@keyframes wre-agent-pulse {
|
|
528
520
|
0%, 100% { opacity: 0.4; }
|
|
@@ -541,12 +533,11 @@
|
|
|
541
533
|
}
|
|
542
534
|
|
|
543
535
|
/*
|
|
544
|
-
* ───
|
|
536
|
+
* ─── Legacy scope rail stripe ───
|
|
545
537
|
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* stripe via transform (zero layout cost) and reveals the label pill.
|
|
538
|
+
* Hidden by .wre-scope-rail-layer. These rules are retained only so old
|
|
539
|
+
* direct mounts degrade consistently if they have not yet removed the
|
|
540
|
+
* rail DOM.
|
|
550
541
|
*/
|
|
551
542
|
.wre-scope-rail-stripe {
|
|
552
543
|
position: absolute;
|
|
@@ -595,34 +586,31 @@
|
|
|
595
586
|
.wre-scope-rail-stripe.wre-scope-rail-tint-proposed { background: var(--color-scope-tint-proposed); }
|
|
596
587
|
|
|
597
588
|
/*
|
|
598
|
-
* ─── Scope rail
|
|
589
|
+
* ─── Scope rail edit handle ───
|
|
599
590
|
*
|
|
600
|
-
* Shown only on stripe hover (CSS-driven).
|
|
601
|
-
* stripe with
|
|
602
|
-
* of the scope.
|
|
591
|
+
* Shown only on stripe hover (CSS-driven). The handle overlays the
|
|
592
|
+
* stripe with a compact icon anchored to the first line of the scope.
|
|
603
593
|
*/
|
|
604
594
|
.wre-scope-rail-label {
|
|
605
595
|
position: absolute;
|
|
606
596
|
display: flex;
|
|
607
597
|
align-items: center;
|
|
608
598
|
justify-content: center;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
599
|
+
width: 24px;
|
|
600
|
+
height: 24px;
|
|
601
|
+
padding: 0;
|
|
602
|
+
border-radius: 999px;
|
|
612
603
|
border: 1px solid transparent;
|
|
613
604
|
background: var(--color-canvas, #fff);
|
|
614
605
|
box-shadow: var(--shadow-sm);
|
|
615
|
-
font
|
|
616
|
-
line-height: 1;
|
|
617
|
-
text-transform: uppercase;
|
|
618
|
-
letter-spacing: 0.06em;
|
|
619
|
-
font-weight: 600;
|
|
606
|
+
font: inherit;
|
|
620
607
|
cursor: pointer;
|
|
621
608
|
z-index: 2;
|
|
622
609
|
opacity: 0;
|
|
623
610
|
pointer-events: none;
|
|
624
611
|
transition: opacity 140ms ease-out, transform 140ms ease-out;
|
|
625
612
|
transform: translateX(-4px);
|
|
613
|
+
margin: 0;
|
|
626
614
|
}
|
|
627
615
|
|
|
628
616
|
.wre-scope-rail-stripe:hover + .wre-scope-rail-label,
|
|
@@ -697,8 +685,8 @@
|
|
|
697
685
|
|
|
698
686
|
.wre-scope-rail-icon {
|
|
699
687
|
display: inline-block;
|
|
700
|
-
width:
|
|
701
|
-
height:
|
|
688
|
+
width: 13px;
|
|
689
|
+
height: 13px;
|
|
702
690
|
background-color: currentColor;
|
|
703
691
|
mask-repeat: no-repeat;
|
|
704
692
|
mask-position: center;
|
|
@@ -708,6 +696,10 @@
|
|
|
708
696
|
-webkit-mask-size: contain;
|
|
709
697
|
}
|
|
710
698
|
|
|
699
|
+
.wre-scope-rail-label-text {
|
|
700
|
+
display: none;
|
|
701
|
+
}
|
|
702
|
+
|
|
711
703
|
/* Simple inline-SVG-as-mask icons so consumers don't need an icon font. */
|
|
712
704
|
.wre-scope-rail-icon-lock {
|
|
713
705
|
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>');
|
|
@@ -1085,6 +1077,12 @@
|
|
|
1085
1077
|
background-color: var(--color-bg-hover);
|
|
1086
1078
|
}
|
|
1087
1079
|
|
|
1080
|
+
.wre-page-band[data-page-band-chrome="plain"],
|
|
1081
|
+
.wre-page-band[data-page-band-chrome="plain"]:hover {
|
|
1082
|
+
background-color: transparent;
|
|
1083
|
+
border-color: transparent;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1088
1086
|
.wre-page-band[data-active="true"] {
|
|
1089
1087
|
background-color: var(--color-accent-soft);
|
|
1090
1088
|
border: 1px solid var(--color-border-accent);
|
|
@@ -243,6 +243,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
243
243
|
chromeOptions,
|
|
244
244
|
chromeVisibility: props.chromeVisibility,
|
|
245
245
|
});
|
|
246
|
+
const plainPageBands =
|
|
247
|
+
isPageWorkspace &&
|
|
248
|
+
chromeVisibility.pageChrome &&
|
|
249
|
+
!chromeVisibility.toolbar &&
|
|
250
|
+
!chromeVisibility.alerts &&
|
|
251
|
+
!chromeVisibility.selectionOverlay &&
|
|
252
|
+
!chromeVisibility.contextToolbars &&
|
|
253
|
+
!chromeVisibility.contextAnalytics &&
|
|
254
|
+
!chromeVisibility.statusBar &&
|
|
255
|
+
!chromeVisibility.reviewRail &&
|
|
256
|
+
!chromeVisibility.shellHeader;
|
|
246
257
|
const reviewRailAvailable = chromeVisibility.reviewRail && (caps?.reviewRailVisible ?? true);
|
|
247
258
|
const { viewportWidth, viewportHeight } = useViewportDimensions();
|
|
248
259
|
const { reviewRailOpen, setReviewRailOpen } = useReviewRailState({
|
|
@@ -1059,6 +1070,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1059
1070
|
...(pageShellMetrics.frameHeightPx
|
|
1060
1071
|
? { minHeight: `${pageShellMetrics.frameHeightPx}px` }
|
|
1061
1072
|
: {}),
|
|
1073
|
+
...(pageShellMetrics.frameHeightPx
|
|
1074
|
+
? { "--wre-page-frame-height-px": `${pageShellMetrics.frameHeightPx}px` }
|
|
1075
|
+
: {}),
|
|
1062
1076
|
...(zoomScale !== 1 ? { zoom: zoomScale } : {}),
|
|
1063
1077
|
}
|
|
1064
1078
|
: zoomScale !== 1
|
|
@@ -1209,6 +1223,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1209
1223
|
onOpenStory={handleOpenPageModeStory}
|
|
1210
1224
|
pmSurfaceElement={pmSurfaceElement}
|
|
1211
1225
|
visiblePageIndexRange={visiblePageIndexRange}
|
|
1226
|
+
plainPageBands={plainPageBands}
|
|
1212
1227
|
mediaPreviews={props.mediaPreviews}
|
|
1213
1228
|
activeBandRibbonProps={activeBandRibbonProps}
|
|
1214
1229
|
/>
|