@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.
@@ -39,6 +39,12 @@ function getWorkflowInlineClass(
39
39
  isActiveWorkItem: boolean,
40
40
  isSelectionZone: boolean,
41
41
  ): string {
42
+ // Active-work-item emphasis lives on the ChromeOverlay rail stripe, not
43
+ // on the PM inline decoration. The per-run outline produced a halo
44
+ // around every text fragment (one box per wrapped span due to
45
+ // box-decoration-break: clone) — the `data-workflow-active` attribute
46
+ // remains the canonical hook for hit-testing and accessibility.
47
+ void isActiveWorkItem;
42
48
  const base =
43
49
  scope.mode === "edit"
44
50
  ? "wre-workflow-inline wre-workflow-inline-edit"
@@ -47,8 +53,7 @@ function getWorkflowInlineClass(
47
53
  : scope.mode === "comment"
48
54
  ? "wre-workflow-inline wre-workflow-inline-comment"
49
55
  : "wre-workflow-inline wre-workflow-inline-view";
50
- const withZone = isSelectionZone ? `${base} wre-workflow-inline-zone` : base;
51
- return isActiveWorkItem ? `${withZone} wre-workflow-inline-active` : withZone;
56
+ return isSelectionZone ? `${base} wre-workflow-inline-zone` : base;
52
57
  }
53
58
 
54
59
  function getWorkflowRailClass(
@@ -578,6 +578,23 @@ function buildSdtBlock(
578
578
  );
579
579
  }
580
580
 
581
+ /**
582
+ * Labels surface-projection emits for preserve-only complex fragments
583
+ * (charts, SmartArt, drawing shapes, WordArt, legacy VML). These have
584
+ * no first-class rendering — when the debug preview toggle is off, they
585
+ * collapse to a zero-dimension quiet marker so the reviewer's document
586
+ * view stays clean. Toggling `showUnsupportedObjectPreviews` on swaps
587
+ * them to the richer atom node specs that ship the preserved detail.
588
+ */
589
+ const UNSUPPORTED_COMPLEX_PREVIEW_LABELS = new Set<string>([
590
+ "Embedded chart",
591
+ "SmartArt diagram",
592
+ "Drawing shape",
593
+ "Text box",
594
+ "WordArt",
595
+ "Legacy VML drawing",
596
+ ]);
597
+
581
598
  /**
582
599
  * Map an opaque_inline surface segment to a dedicated complex-rendering PM atom
583
600
  * node when the label identifies a known complex content type, or fall back to
@@ -635,12 +652,20 @@ function buildOpaqueInlineOrComplexAtom(
635
652
  });
636
653
  }
637
654
 
655
+ // Preserve-only complex fragments without the debug toggle: collapse
656
+ // to a zero-dimension quiet marker so the document view matches the
657
+ // dev-drawer copy ("off by default"). The fragment stays in the
658
+ // canonical document, so export round-trips remain lossless.
659
+ const effectivePresentation =
660
+ segment.presentation ??
661
+ (UNSUPPORTED_COMPLEX_PREVIEW_LABELS.has(label) ? "quiet-marker" : "inline-chip");
662
+
638
663
  return editorSchema.nodes.opaque_inline.create({
639
664
  fragmentId: segment.fragmentId,
640
665
  warningId: segment.warningId,
641
666
  label,
642
667
  detail,
643
- presentation: segment.presentation ?? "inline-chip",
668
+ presentation: effectivePresentation,
644
669
  displayText: segment.displayText ?? null,
645
670
  });
646
671
  }
@@ -47,6 +47,11 @@ export { TwStatusBar } from "./status/tw-status-bar";
47
47
  // Chrome
48
48
  export { TwAlertBanner } from "./chrome/tw-alert-banner";
49
49
  export { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
50
+ export {
51
+ TwModeDock,
52
+ type TwModeDockAction,
53
+ type TwModeDockProps,
54
+ } from "./chrome/tw-mode-dock";
50
55
 
51
56
  // Chrome overlay plane (R3a — scope rail layer)
52
57
  export {
@@ -408,8 +408,16 @@
408
408
  box-shadow: inset -1px 0 0 color-mix(in srgb, var(--color-danger) 35%, transparent);
409
409
  }
410
410
 
411
+ /*
412
+ * `wre-workflow-inline-active` no longer emits a visual outline. The
413
+ * per-run inset box-shadow produced a halo around every text fragment
414
+ * (one box per run, due to box-decoration-break: clone above), which
415
+ * fought with the overlay's flat tint. The class name is kept on the
416
+ * inline decoration as a data hook (no visual), and emphasis for the
417
+ * active scope now lives on the ChromeOverlay rail stripe + scope card.
418
+ */
411
419
  .prosemirror-surface .ProseMirror .wre-workflow-inline-active {
412
- box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 30%, transparent);
420
+ /* intentionally empty visual emphasis handled by ChromeOverlay */
413
421
  }
414
422
 
415
423
  /*
@@ -423,9 +431,37 @@
423
431
  pointer-events: none;
424
432
  }
425
433
 
434
+ /*
435
+ * ─── Gutter lane tokens ───
436
+ *
437
+ * The scope rail and scope card chrome live in a reserved lane to the
438
+ * left of the page frame so they visibly read as chrome (not document
439
+ * content). Page surfaces use 64px, canvas surfaces 48px. Host apps
440
+ * can override via CSS custom property.
441
+ */
442
+ :root {
443
+ --wre-gutter-lane-width: 64px;
444
+ --wre-gutter-lane-pad: 8px;
445
+ }
446
+
447
+ .wre-canvas-surface {
448
+ --wre-gutter-lane-width: 48px;
449
+ }
450
+
451
+ /*
452
+ * The page-with-gutter grid shell allocates the gutter as its own
453
+ * column so absolute-positioned chrome overlays (which fill inset 0 of
454
+ * this shell) land INSIDE the gutter, not over the shell background.
455
+ */
456
+ .wre-page-with-gutter {
457
+ display: grid;
458
+ grid-template-columns: var(--wre-gutter-lane-width) 1fr;
459
+ position: relative;
460
+ }
461
+
426
462
  .wre-scope-rail-tint {
427
463
  position: absolute;
428
- border-radius: 0.35rem;
464
+ border-radius: 0.2rem;
429
465
  pointer-events: none;
430
466
  z-index: 0;
431
467
  transition: background 140ms ease-out;
@@ -452,32 +488,89 @@
452
488
  outline-offset: -1px;
453
489
  }
454
490
 
491
+ /*
492
+ * ─── Scope rail stripe ───
493
+ *
494
+ * The rail stripe is the rest-state representation of a scope: a 4px
495
+ * color stripe in the gutter lane. Posture color comes from the
496
+ * accent/warning/insert/secondary/danger tokens. Hover widens the
497
+ * stripe via transform (zero layout cost) and reveals the label pill.
498
+ */
499
+ .wre-scope-rail-stripe {
500
+ position: absolute;
501
+ width: 4px;
502
+ border-radius: 2px;
503
+ background: currentColor;
504
+ pointer-events: auto;
505
+ cursor: pointer;
506
+ z-index: 1;
507
+ transform-origin: left center;
508
+ transition: transform 120ms ease-out, opacity 120ms ease-out;
509
+ opacity: 0.75;
510
+ /* Reset button defaults. */
511
+ border: none;
512
+ padding: 0;
513
+ margin: 0;
514
+ font: inherit;
515
+ color: inherit;
516
+ background-clip: padding-box;
517
+ }
518
+
519
+ .wre-scope-rail-stripe:hover,
520
+ .wre-scope-rail-stripe:focus-visible {
521
+ transform: scaleX(1.5);
522
+ opacity: 1;
523
+ outline: none;
524
+ }
525
+
526
+ .wre-scope-rail-stripe-active {
527
+ opacity: 1;
528
+ transform: scaleX(1.75);
529
+ }
530
+
531
+ .wre-scope-rail-stripe.wre-scope-rail-label-accent { color: var(--color-accent); }
532
+ .wre-scope-rail-stripe.wre-scope-rail-label-warning { color: var(--color-warning); }
533
+ .wre-scope-rail-stripe.wre-scope-rail-label-insert { color: var(--color-insert); }
534
+ .wre-scope-rail-stripe.wre-scope-rail-label-secondary { color: var(--color-secondary); }
535
+ .wre-scope-rail-stripe.wre-scope-rail-label-danger { color: var(--color-danger); }
536
+
537
+ /*
538
+ * ─── Scope rail label pill ───
539
+ *
540
+ * Shown only on stripe hover (CSS-driven). The pill overlays the
541
+ * stripe with icon + short posture label, anchored to the first line
542
+ * of the scope.
543
+ */
455
544
  .wre-scope-rail-label {
456
545
  position: absolute;
457
546
  display: flex;
458
- flex-direction: column;
459
547
  align-items: center;
460
548
  justify-content: center;
461
- gap: 0.15rem;
462
- padding: 0.25rem 0.5rem;
463
- border-radius: 0.375rem;
549
+ gap: 0.2rem;
550
+ padding: 0.15rem 0.3rem;
551
+ border-radius: var(--radius-sm);
464
552
  border: 1px solid transparent;
465
553
  background: var(--color-canvas, #fff);
466
- box-shadow: 0 2px 6px -3px rgba(0, 0, 0, 0.18);
467
- font-size: 10px;
554
+ box-shadow: var(--shadow-sm);
555
+ font-size: 9.5px;
468
556
  line-height: 1;
469
557
  text-transform: uppercase;
470
- letter-spacing: 0.08em;
558
+ letter-spacing: 0.06em;
471
559
  font-weight: 600;
472
560
  cursor: pointer;
473
- pointer-events: auto;
474
- z-index: 1;
475
- transition: transform 120ms ease-out, box-shadow 120ms ease-out;
561
+ z-index: 2;
562
+ opacity: 0;
563
+ pointer-events: none;
564
+ transition: opacity 140ms ease-out, transform 140ms ease-out;
565
+ transform: translateX(-4px);
476
566
  }
477
567
 
478
- .wre-scope-rail-label:hover {
479
- transform: translateY(-1px);
480
- box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.22);
568
+ .wre-scope-rail-stripe:hover + .wre-scope-rail-label,
569
+ .wre-scope-rail-label:hover,
570
+ .wre-scope-rail-stripe:focus-visible + .wre-scope-rail-label {
571
+ opacity: 1;
572
+ pointer-events: auto;
573
+ transform: translateX(0);
481
574
  }
482
575
 
483
576
  .wre-scope-rail-label-accent {
@@ -67,6 +67,7 @@ import {
67
67
  import type { EditorCommandBag } from "../ui/editor-command-bag.ts";
68
68
  import { preserveEditorSelectionMouseDown } from "../ui/headless/preserve-editor-selection";
69
69
  import { TwAlertBanner } from "./chrome/tw-alert-banner";
70
+ import { TwModeDock } from "./chrome/tw-mode-dock";
70
71
  import { TwLayoutPanel } from "./chrome/tw-layout-panel";
71
72
  import { TwPageRuler } from "./chrome/tw-page-ruler";
72
73
  import {
@@ -129,6 +130,15 @@ export interface TwReviewWorkspaceProps {
129
130
  searchLabel?: string;
130
131
  helpLabel?: string;
131
132
  };
133
+ /**
134
+ * Opt-in floating mode dock pinned to the bottom of the workspace.
135
+ * Hosts pass the derived label / icon / actions; defaults to hidden.
136
+ */
137
+ modeDock?: {
138
+ label: string;
139
+ icon?: ReactNode;
140
+ actions?: readonly import("./chrome/tw-mode-dock").TwModeDockAction[];
141
+ };
132
142
  document: ReactNode;
133
143
  workspaceMode: WorkspaceMode;
134
144
  zoomLevel?: ZoomLevel;
@@ -314,6 +324,25 @@ export interface TwReviewWorkspaceProps {
314
324
  * re-renders with the new per-role action set.
315
325
  */
316
326
  onEditorRoleChange?: (role: import("../api/public-types.ts").EditorRole) => void;
327
+ /**
328
+ * Scope card mode selector fired a mode change. Wire to the host's
329
+ * existing overlay-apply path (or an equivalent CCEP workflow
330
+ * endpoint). The card never mutates runtime state directly.
331
+ */
332
+ onScopeModeChangeRequested?: (payload: {
333
+ scopeId: string;
334
+ mode: import("../api/public-types.ts").WorkflowScopeMode;
335
+ }) => void;
336
+ /**
337
+ * Scope card issue row fired an action (resolve/waive/escalate/
338
+ * acknowledge). Host updates the attached `IssueMetadataValue`
339
+ * state and re-pushes via `setWorkflowMetadataEntries`.
340
+ */
341
+ onScopeIssueActionRequested?: (payload: {
342
+ scopeId: string;
343
+ issueId: string;
344
+ action: import("../api/public-types.ts").ScopeIssueAction;
345
+ }) => void;
317
346
  }
318
347
 
319
348
  export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
@@ -328,6 +357,40 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
328
357
  const markupDisplay = props.markupDisplay;
329
358
  const [navOpen, setNavOpen] = useState(false);
330
359
  const [layoutToolsOpen, setLayoutToolsOpen] = useState(false);
360
+
361
+ // Scope card state — tracks which scope's card is currently open so
362
+ // the ChromeOverlay's card layer renders the right one. The card
363
+ // closes on click-outside, Escape, or a repeat click on its stripe.
364
+ const [activeScopeId, setActiveScopeId] = useState<string | null>(null);
365
+ const handleScopeStripeClick = useCallback(
366
+ (segment: { scopeId: string }) => {
367
+ setActiveScopeId((current) =>
368
+ current === segment.scopeId ? null : segment.scopeId,
369
+ );
370
+ },
371
+ [],
372
+ );
373
+ const handleScopeCardClose = useCallback(() => {
374
+ setActiveScopeId(null);
375
+ }, []);
376
+ const onScopeModeChangeRequested = props.onScopeModeChangeRequested;
377
+ const handleScopeCardModeChange = useCallback(
378
+ (scopeId: string, mode: import("../api/public-types.ts").WorkflowScopeMode) => {
379
+ onScopeModeChangeRequested?.({ scopeId, mode });
380
+ },
381
+ [onScopeModeChangeRequested],
382
+ );
383
+ const onScopeIssueActionRequested = props.onScopeIssueActionRequested;
384
+ const handleScopeCardIssueAction = useCallback(
385
+ (
386
+ scopeId: string,
387
+ issueId: string,
388
+ action: import("../api/public-types.ts").ScopeIssueAction,
389
+ ) => {
390
+ onScopeIssueActionRequested?.({ scopeId, issueId, action });
391
+ },
392
+ [onScopeIssueActionRequested],
393
+ );
331
394
  const zoomLevel = props.zoomLevel ?? 100;
332
395
  const zoomScale = typeof zoomLevel === "number" ? zoomLevel / 100 : 1;
333
396
  const pageZoomBucket =
@@ -1144,6 +1207,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1144
1207
  tableContext={props.tableContext}
1145
1208
  onSetColumnWidth={props.onSetColumnWidth}
1146
1209
  onSetRowHeight={props.onSetRowHeight}
1210
+ activeScopeId={activeScopeId}
1211
+ onScopeStripeClick={handleScopeStripeClick}
1212
+ onScopeCardClose={handleScopeCardClose}
1213
+ onScopeCardModeChange={handleScopeCardModeChange}
1214
+ onScopeCardIssueAction={handleScopeCardIssueAction}
1147
1215
  />
1148
1216
  ) : null}
1149
1217
  </div>
@@ -1280,6 +1348,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1280
1348
  </div>
1281
1349
  ) : null}
1282
1350
  </div>
1351
+ {props.modeDock ? (
1352
+ <TwModeDock
1353
+ label={props.modeDock.label}
1354
+ icon={props.modeDock.icon}
1355
+ actions={props.modeDock.actions}
1356
+ />
1357
+ ) : null}
1283
1358
  </div>
1284
1359
  </Tooltip.Provider>
1285
1360
  );