@beyondwork/docx-react-component 1.0.53 → 1.0.54

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.
Files changed (86) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +35 -7
  3. package/src/io/docx-session.ts +30 -6
  4. package/src/runtime/collab/checkpoint-store.ts +1 -1
  5. package/src/runtime/collab/event-types.ts +4 -0
  6. package/src/runtime/collab/runtime-collab-sync.ts +1 -2
  7. package/src/runtime/document-runtime.ts +23 -9
  8. package/src/runtime/layout/inert-layout-facet.ts +1 -0
  9. package/src/runtime/layout/layout-engine-version.ts +58 -1
  10. package/src/runtime/layout/layout-invalidation.ts +150 -30
  11. package/src/runtime/layout/page-graph.ts +19 -0
  12. package/src/runtime/layout/paginated-layout-engine.ts +128 -19
  13. package/src/runtime/layout/project-block-fragments.ts +27 -0
  14. package/src/runtime/layout/public-facet.ts +27 -0
  15. package/src/runtime/render/render-frame-diff.ts +38 -2
  16. package/src/ui/WordReviewEditor.tsx +6 -3
  17. package/src/ui/headless/comment-decoration-model.ts +60 -5
  18. package/src/ui/headless/revision-decoration-model.ts +94 -6
  19. package/src/ui/shared/revision-filters.ts +16 -6
  20. package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
  21. package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
  22. package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
  23. package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
  24. package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
  25. package/src/ui-tailwind/chart/render/area.tsx +277 -0
  26. package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
  27. package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
  28. package/src/ui-tailwind/chart/render/combo.tsx +85 -0
  29. package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
  30. package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
  31. package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
  32. package/src/ui-tailwind/chart/render/line.tsx +363 -0
  33. package/src/ui-tailwind/chart/render/number-format.ts +120 -16
  34. package/src/ui-tailwind/chart/render/pie.tsx +275 -0
  35. package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
  36. package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
  37. package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
  38. package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
  39. package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
  40. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
  41. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
  42. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
  43. package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
  44. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
  45. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
  46. package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
  47. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
  48. package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
  49. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
  50. package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
  51. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
  52. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
  53. package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
  54. package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
  55. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
  56. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
  57. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
  58. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
  59. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
  60. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
  61. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
  62. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
  63. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
  64. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
  65. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
  66. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
  67. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
  68. package/src/ui-tailwind/index.ts +11 -0
  69. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +52 -2
  70. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +13 -0
  71. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +13 -0
  72. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
  73. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
  74. package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
  75. package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
  76. package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
  77. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
  78. package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
  79. package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
  80. package/src/ui-tailwind/theme/editor-theme.css +249 -22
  81. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
  82. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
  83. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
  84. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
  85. package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
  86. package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
@@ -51,6 +51,12 @@ export function CollabPresenceStrip({
51
51
  const rootClass = [
52
52
  "tw-collab-presence-strip",
53
53
  observer ? "tw-collab-presence-observer" : null,
54
+ // Lane 6b §6b.S6 — token-bound default styling. BEM classes above
55
+ // stay as host-CSS override hooks; the inline Tailwind below paints
56
+ // the calm presence strip and dims under observer posture.
57
+ "inline-flex items-center gap-2 px-2 py-1",
58
+ "text-[11px] text-[var(--color-text-tertiary)]",
59
+ observer ? "opacity-60" : null,
54
60
  className ?? null,
55
61
  ]
56
62
  .filter((value): value is string => value !== null)
@@ -64,14 +70,23 @@ export function CollabPresenceStrip({
64
70
  aria-label="Collaborators"
65
71
  data-observer={observer ? "true" : "false"}
66
72
  >
67
- <ul className="tw-collab-presence-strip__peers" aria-live="polite">
73
+ <ul
74
+ className="tw-collab-presence-strip__peers flex items-center gap-1 list-none m-0 p-0"
75
+ aria-live="polite"
76
+ >
68
77
  {tiles.map((peer) => (
69
78
  <CollabPresencePeerTile key={peer.userId} peer={peer} />
70
79
  ))}
71
80
  </ul>
72
81
  {overflow > 0 ? (
73
82
  <span
74
- className="tw-collab-presence-strip__overflow"
83
+ className={[
84
+ "tw-collab-presence-strip__overflow",
85
+ "inline-flex items-center justify-center",
86
+ "h-5 min-w-5 px-1 rounded-[var(--radius-pill)]",
87
+ "bg-[var(--color-bg-muted)] text-[var(--color-text-secondary)]",
88
+ "text-[10px] font-medium",
89
+ ].join(" ")}
75
90
  aria-label={`${overflow} additional peers`}
76
91
  data-testid="collab-presence-overflow"
77
92
  >
@@ -90,19 +105,38 @@ function CollabPresencePeerTile({ peer }: { peer: AwarenessPeer }) {
90
105
  const initials = computeInitials(peer.displayName);
91
106
  return (
92
107
  <li
93
- className="tw-collab-presence-strip__tile"
108
+ className={[
109
+ "tw-collab-presence-strip__tile",
110
+ "inline-flex items-center gap-1",
111
+ ].join(" ")}
94
112
  data-testid={`collab-presence-peer-${peer.userId}`}
95
113
  data-author-kind={peer.authorKind}
96
114
  data-active-story={peer.activeStoryId ?? ""}
97
115
  title={peer.displayName}
98
116
  >
99
- <span className="tw-collab-presence-strip__avatar" aria-hidden="true">
117
+ <span
118
+ className={[
119
+ "tw-collab-presence-strip__avatar",
120
+ "inline-flex h-5 w-5 items-center justify-center",
121
+ "rounded-[var(--radius-pill)]",
122
+ "bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]",
123
+ "text-[10px] font-semibold",
124
+ ].join(" ")}
125
+ aria-hidden="true"
126
+ >
100
127
  {initials}
101
128
  </span>
102
- <span className="tw-collab-presence-strip__name">{peer.displayName}</span>
129
+ <span className="tw-collab-presence-strip__name sr-only">
130
+ {peer.displayName}
131
+ </span>
103
132
  {peer.authorKind !== "human" ? (
104
133
  <span
105
- className="tw-collab-presence-strip__badge"
134
+ className={[
135
+ "tw-collab-presence-strip__badge",
136
+ "inline-flex items-center px-1 rounded-[var(--radius-pill)]",
137
+ "bg-[var(--color-bg-muted)] text-[var(--color-text-tertiary)]",
138
+ "text-[9px] font-medium uppercase tracking-wide",
139
+ ].join(" ")}
106
140
  data-testid={`collab-presence-peer-${peer.userId}-badge`}
107
141
  >
108
142
  {peer.authorKind}
@@ -112,6 +146,17 @@ function CollabPresencePeerTile({ peer }: { peer: AwarenessPeer }) {
112
146
  );
113
147
  }
114
148
 
149
+ /**
150
+ * Lane 6b §6b.S6 — transport chip tone binding.
151
+ *
152
+ * connected / attached → calm tertiary (no paint)
153
+ * syncing / detached → semantic-warning-soft + warning glyph
154
+ * offline → semantic-error-soft + error glyph
155
+ * none → muted (disconnected intent)
156
+ *
157
+ * Health is the silent default — only degraded states paint, so operators
158
+ * who glance at the strip only see colour when something is off.
159
+ */
115
160
  function CollabTransportChip({
116
161
  status,
117
162
  queuedLocalEvents,
@@ -123,9 +168,25 @@ function CollabTransportChip({
123
168
  status === "offline" && queuedLocalEvents > 0
124
169
  ? `offline (${queuedLocalEvents} queued)`
125
170
  : status;
171
+
172
+ const toneClass =
173
+ status === "offline"
174
+ ? "bg-[var(--color-semantic-error-soft)] text-[var(--color-semantic-error)]"
175
+ : status === "syncing"
176
+ ? "bg-[var(--color-semantic-warning-soft)] text-[var(--color-semantic-warning)]"
177
+ : // connected — calm tertiary
178
+ "text-[var(--color-text-tertiary)]";
179
+
126
180
  return (
127
181
  <span
128
- className={`tw-collab-presence-strip__transport tw-collab-presence-strip__transport--${status}`}
182
+ className={[
183
+ "tw-collab-presence-strip__transport",
184
+ `tw-collab-presence-strip__transport--${status}`,
185
+ "inline-flex items-center px-2 py-0.5 rounded-[var(--radius-pill)]",
186
+ "text-[10px] font-medium uppercase tracking-wide",
187
+ "transition-colors duration-[var(--motion-fast)]",
188
+ toneClass,
189
+ ].join(" ")}
129
190
  data-testid="collab-presence-transport"
130
191
  data-status={status}
131
192
  data-queued={queuedLocalEvents.toString()}
@@ -31,6 +31,14 @@ export function CollabRoleChip({
31
31
  "tw-collab-role-chip",
32
32
  `tw-collab-role-chip--${posture.role}`,
33
33
  offline ? "tw-collab-role-chip--offline" : null,
34
+ // Lane 6b §6b.S6 — calm token-bound chip. Offline dims via opacity;
35
+ // the BEM classes above stay as hooks for host CSS to tint the role.
36
+ "inline-flex items-center gap-1.5 px-2 py-0.5",
37
+ "rounded-[var(--radius-pill)]",
38
+ "bg-[var(--color-bg-muted)] text-[var(--color-text-secondary)]",
39
+ "text-[11px] font-medium",
40
+ "transition-colors duration-[var(--motion-fast)]",
41
+ offline ? "opacity-60" : null,
34
42
  className ?? null,
35
43
  ]
36
44
  .filter((v): v is string => v !== null)
@@ -53,8 +61,19 @@ export function CollabRoleChip({
53
61
  aria-label={`Role ${posture.role}, ${peerLabel}`}
54
62
  title={`${posture.role} · ${peerLabel}`}
55
63
  >
56
- <span className="tw-collab-role-chip__role">{posture.role}</span>
57
- <span className="tw-collab-role-chip__peers" aria-hidden="true">
64
+ <span className="tw-collab-role-chip__role capitalize">
65
+ {posture.role}
66
+ </span>
67
+ <span
68
+ className={[
69
+ "tw-collab-role-chip__peers",
70
+ "inline-flex items-center justify-center",
71
+ "h-4 min-w-4 px-1 rounded-[var(--radius-pill)]",
72
+ "bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]",
73
+ "text-[10px] font-semibold leading-none",
74
+ ].join(" ")}
75
+ aria-hidden="true"
76
+ >
58
77
  {posture.peers}
59
78
  </span>
60
79
  </span>
@@ -35,6 +35,12 @@ export function CollabTamperBanner({
35
35
 
36
36
  const rootClass = [
37
37
  "tw-collab-tamper-banner",
38
+ // Lane 6b §6b.S6 — single-action semantic-error banner. Designsystem
39
+ // §6.21 calls for the tamper surface to be the loudest collab chrome
40
+ // element (assertive) with a single recovery affordance.
41
+ "flex items-center gap-3 px-3 py-2 text-xs",
42
+ "bg-[var(--color-semantic-error-soft)] text-[var(--color-semantic-error)]",
43
+ "transition-colors duration-[var(--motion-fast)]",
38
44
  className ?? null,
39
45
  ]
40
46
  .filter((v): v is string => v !== null)
@@ -48,16 +54,27 @@ export function CollabTamperBanner({
48
54
  role="alert"
49
55
  aria-live="assertive"
50
56
  >
51
- <span className="tw-collab-tamper-banner__icon" aria-hidden="true">
57
+ <span
58
+ className="tw-collab-tamper-banner__icon shrink-0"
59
+ aria-hidden="true"
60
+ >
52
61
 
53
62
  </span>
54
- <span className="tw-collab-tamper-banner__message">
63
+ <span className="tw-collab-tamper-banner__message flex-1">
55
64
  Metadata integrity check failed. The workflow payload was modified
56
65
  outside the editor. Mutations are blocked until you acknowledge.
57
66
  </span>
58
67
  <button
59
68
  type="button"
60
- className="tw-collab-tamper-banner__ack"
69
+ className={[
70
+ "tw-collab-tamper-banner__ack",
71
+ "shrink-0 inline-flex items-center",
72
+ "rounded-[var(--radius-sm)] px-3 py-1 text-xs font-semibold",
73
+ "bg-[var(--color-semantic-error)] text-[var(--color-text-on-accent)]",
74
+ "hover:bg-[var(--color-semantic-error)]/90",
75
+ "transition-colors duration-[var(--motion-fast)]",
76
+ "focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
77
+ ].join(" ")}
61
78
  data-testid="collab-tamper-banner-ack"
62
79
  onClick={onAcknowledge}
63
80
  >
@@ -1,7 +1,10 @@
1
- import React from "react";
1
+ import React, { type ReactNode } from "react";
2
2
  import { AlertTriangle, XCircle } from "lucide-react";
3
3
 
4
- import type { RuntimeRenderSnapshot, WorkflowBlockedCommandReason } from "../../api/public-types";
4
+ import type {
5
+ RuntimeRenderSnapshot,
6
+ WorkflowBlockedCommandReason,
7
+ } from "../../api/public-types";
5
8
 
6
9
  export interface TwAlertBannerProps {
7
10
  snapshot: RuntimeRenderSnapshot;
@@ -9,55 +12,117 @@ export interface TwAlertBannerProps {
9
12
  workflowBlockedReasons?: WorkflowBlockedCommandReason[];
10
13
  }
11
14
 
12
- export function TwAlertBanner(props: TwAlertBannerProps) {
15
+ /**
16
+ * TwAlertBanner — single-row chrome strip above the toolbar (designsystem §6.17).
17
+ *
18
+ * Lane 6b §6b.S5 contract:
19
+ * (1) ONE banner at a time, highest severity wins.
20
+ * (2) Precedence: fatalError > blockExport > workflowBlocked > preserveOnly.
21
+ * (3) Severity tones bind Lane 6a `--color-semantic-*` tokens — no legacy
22
+ * `bg-danger-soft` / `bg-warning-soft` / `bg-amber-50` class names.
23
+ * (4) Error (fatal / blockExport) → `--color-semantic-error(-soft)`
24
+ * Warning (workflow / preserve) → `--color-semantic-warning(-soft)`
25
+ */
26
+
27
+ type BannerSeverity = "error" | "warning";
28
+
29
+ interface BannerRender {
30
+ severity: BannerSeverity;
31
+ icon: ReactNode;
32
+ message: ReactNode;
33
+ testid: string;
34
+ }
35
+
36
+ function renderBanner(variant: BannerRender): React.ReactElement {
37
+ const { severity, icon, message, testid } = variant;
38
+ const toneClass =
39
+ severity === "error"
40
+ ? "bg-[var(--color-semantic-error-soft)] text-[var(--color-semantic-error)]"
41
+ : "bg-[var(--color-semantic-warning-soft)] text-[var(--color-semantic-warning)]";
42
+ return (
43
+ <div
44
+ role="alert"
45
+ aria-live={severity === "error" ? "assertive" : "polite"}
46
+ data-testid={testid}
47
+ data-severity={severity}
48
+ className={[
49
+ "flex items-center gap-2 px-3 py-1.5 text-xs",
50
+ "transition-colors duration-[var(--motion-fast)]",
51
+ toneClass,
52
+ ].join(" ")}
53
+ >
54
+ {icon}
55
+ <span>{message}</span>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export function TwAlertBanner(
61
+ props: TwAlertBannerProps,
62
+ ): React.ReactElement | null {
13
63
  const { snapshot, preserveOnlyCount, workflowBlockedReasons = [] } = props;
14
64
 
65
+ // 1. Fatal runtime error — highest precedence.
15
66
  if (snapshot.fatalError) {
16
- return (
17
- <div className="flex items-center gap-2 px-3 py-1.5 bg-danger-soft text-danger text-xs">
18
- <XCircle className="h-3.5 w-3.5 shrink-0" />
19
- <span>{snapshot.fatalError.message}</span>
20
- </div>
21
- );
67
+ return renderBanner({
68
+ severity: "error",
69
+ icon: <XCircle className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />,
70
+ message: snapshot.fatalError.message,
71
+ testid: "tw-alert-banner__fatal",
72
+ });
22
73
  }
23
74
 
75
+ // 2. Export blocked — compatibility engine says we can't round-trip.
24
76
  if (snapshot.compatibility.blockExport) {
25
- return (
26
- <div className="flex items-center gap-2 px-3 py-1.5 bg-danger-soft text-danger text-xs">
27
- <XCircle className="h-3.5 w-3.5 shrink-0" />
28
- <span>
77
+ return renderBanner({
78
+ severity: "error",
79
+ icon: <XCircle className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />,
80
+ message: (
81
+ <>
29
82
  Export blocked &mdash;{" "}
30
- {snapshot.compatibility.blockExportReasons[0] ?? "unsupported content"}
31
- </span>
32
- </div>
33
- );
34
- }
35
-
36
- if (preserveOnlyCount > 0) {
37
- return (
38
- <div className="flex items-center gap-2 px-3 py-1.5 bg-warning-soft text-comment text-xs">
39
- <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
40
- <span>
41
- {preserveOnlyCount} preserve-only feature
42
- {preserveOnlyCount !== 1 ? "s" : ""} detected
43
- </span>
44
- </div>
45
- );
83
+ {snapshot.compatibility.blockExportReasons[0] ??
84
+ "unsupported content"}
85
+ </>
86
+ ),
87
+ testid: "tw-alert-banner__block-export",
88
+ });
46
89
  }
47
90
 
91
+ // 3. Workflow blocked — host policy refuses a command, per reasons.
48
92
  if (workflowBlockedReasons.length > 0) {
49
- const firstReason = workflowBlockedReasons[0];
50
- return (
51
- <div className="flex items-center gap-2 px-3 py-1.5 bg-amber-50 text-amber-700 text-xs">
52
- <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
53
- <span>
93
+ const firstReason = workflowBlockedReasons[0]!;
94
+ return renderBanner({
95
+ severity: "warning",
96
+ icon: (
97
+ <AlertTriangle className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
98
+ ),
99
+ message: (
100
+ <>
54
101
  {firstReason.message}
55
102
  {workflowBlockedReasons.length > 1
56
103
  ? ` (+${workflowBlockedReasons.length - 1} more)`
57
104
  : ""}
58
- </span>
59
- </div>
60
- );
105
+ </>
106
+ ),
107
+ testid: "tw-alert-banner__workflow-blocked",
108
+ });
109
+ }
110
+
111
+ // 4. Preserve-only features — lowest precedence warning.
112
+ if (preserveOnlyCount > 0) {
113
+ return renderBanner({
114
+ severity: "warning",
115
+ icon: (
116
+ <AlertTriangle className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
117
+ ),
118
+ message: (
119
+ <>
120
+ {preserveOnlyCount} preserve-only feature
121
+ {preserveOnlyCount !== 1 ? "s" : ""} detected
122
+ </>
123
+ ),
124
+ testid: "tw-alert-banner__preserve-only",
125
+ });
61
126
  }
62
127
 
63
128
  return null;