@beyondwork/docx-react-component 1.0.28 → 1.0.30

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 (92) hide show
  1. package/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -277,6 +277,69 @@ function requestTableLayoutSync(start: HTMLElement): void {
277
277
  table?.dispatchEvent(new Event(TABLE_LAYOUT_SYNC_EVENT));
278
278
  }
279
279
 
280
+ function applyTableAttrs(table: HTMLTableElement, node: PMNode): void {
281
+ table.className = "border-collapse w-full my-2 text-sm";
282
+ table.setAttribute("data-pm-table-root", "true");
283
+ table.style.marginLeft = "";
284
+ table.style.marginRight = "";
285
+ const alignment = node.attrs.alignment as string | null | undefined;
286
+ if (alignment === "center") {
287
+ table.style.marginLeft = "auto";
288
+ table.style.marginRight = "auto";
289
+ } else if (alignment === "right") {
290
+ table.style.marginLeft = "auto";
291
+ }
292
+ }
293
+
294
+ function applyRowAttrs(row: HTMLTableRowElement, node: PMNode): void {
295
+ row.style.height = "";
296
+ row.style.minHeight = "";
297
+ const height = node.attrs.height as number | null | undefined;
298
+ const heightRule = node.attrs.heightRule as string | null | undefined;
299
+ if (typeof height !== "number" || height <= 0) {
300
+ return;
301
+ }
302
+ const points = `${height / 20}pt`;
303
+ if (heightRule === "exact") {
304
+ row.style.height = points;
305
+ } else if (heightRule === "atLeast") {
306
+ row.style.minHeight = points;
307
+ }
308
+ }
309
+
310
+ function applyCellAttrs(cell: HTMLTableCellElement, node: PMNode, isHeader: boolean): void {
311
+ cell.className = isHeader
312
+ ? "border border-primary/20 p-2 align-top font-semibold bg-surface-raised"
313
+ : "border border-primary/20 p-2 align-top";
314
+
315
+ const backgroundColor = node.attrs.backgroundColor as string | null | undefined;
316
+ const verticalAlign = node.attrs.verticalAlign as "top" | "center" | "bottom" | null | undefined;
317
+ const borderTop = node.attrs.borderTop as string | null | undefined;
318
+ const borderRight = node.attrs.borderRight as string | null | undefined;
319
+ const borderBottom = node.attrs.borderBottom as string | null | undefined;
320
+ const borderLeft = node.attrs.borderLeft as string | null | undefined;
321
+
322
+ if (backgroundColor) cell.setAttribute("data-cell-background", backgroundColor);
323
+ else cell.removeAttribute("data-cell-background");
324
+ if (verticalAlign && verticalAlign !== "top") cell.setAttribute("data-vertical-align", verticalAlign);
325
+ else cell.removeAttribute("data-vertical-align");
326
+ if (borderTop) cell.setAttribute("data-border-top", borderTop);
327
+ else cell.removeAttribute("data-border-top");
328
+ if (borderRight) cell.setAttribute("data-border-right", borderRight);
329
+ else cell.removeAttribute("data-border-right");
330
+ if (borderBottom) cell.setAttribute("data-border-bottom", borderBottom);
331
+ else cell.removeAttribute("data-border-bottom");
332
+ if (borderLeft) cell.setAttribute("data-border-left", borderLeft);
333
+ else cell.removeAttribute("data-border-left");
334
+
335
+ cell.style.backgroundColor = backgroundColor ?? "";
336
+ cell.style.verticalAlign = verticalAlign === "center" ? "middle" : (verticalAlign ?? "");
337
+ cell.style.borderTop = borderTop ?? "";
338
+ cell.style.borderRight = borderRight ?? "";
339
+ cell.style.borderBottom = borderBottom ?? "";
340
+ cell.style.borderLeft = borderLeft ?? "";
341
+ }
342
+
280
343
  /**
281
344
  * NodeView for the table node.
282
345
  * Renders as <table><tbody>...</tbody></table>.
@@ -293,8 +356,7 @@ export class TableNodeView {
293
356
  this.node = node;
294
357
 
295
358
  const table = document.createElement("table");
296
- table.className = "border-collapse w-full my-2 text-sm";
297
- table.setAttribute("data-pm-table-root", "true");
359
+ applyTableAttrs(table, node);
298
360
 
299
361
  const tbody = document.createElement("tbody");
300
362
  table.appendChild(tbody);
@@ -314,6 +376,7 @@ export class TableNodeView {
314
376
  }
315
377
 
316
378
  this.node = node;
379
+ applyTableAttrs(this.dom as HTMLTableElement, node);
317
380
  this.scheduleLayoutSync();
318
381
  return true;
319
382
  }
@@ -357,11 +420,17 @@ export class TableRowNodeView {
357
420
  dom: HTMLElement;
358
421
  contentDOM: HTMLElement;
359
422
 
360
- constructor(_node: PMNode) {
423
+ constructor(node: PMNode) {
361
424
  const tr = document.createElement("tr");
425
+ applyRowAttrs(tr, node);
362
426
  this.dom = tr;
363
427
  this.contentDOM = tr;
364
428
  }
429
+
430
+ update(node: PMNode): boolean {
431
+ applyRowAttrs(this.dom as HTMLTableRowElement, node);
432
+ return true;
433
+ }
365
434
  }
366
435
 
367
436
  /**
@@ -377,10 +446,7 @@ export class TableCellNodeView {
377
446
  constructor(node: PMNode) {
378
447
  const isHeader = (node.type.spec as { tableRole?: string }).tableRole === "header_cell";
379
448
  const cell = document.createElement(isHeader ? "th" : "td");
380
-
381
- cell.className = isHeader
382
- ? "border border-primary/20 p-2 align-top font-semibold bg-surface-raised"
383
- : "border border-primary/20 p-2 align-top";
449
+ applyCellAttrs(cell, node, isHeader);
384
450
 
385
451
  const colspan = resolveRenderedColspan(node);
386
452
  const rowspan = resolveRenderedRowspan(node);
@@ -409,6 +475,7 @@ export class TableCellNodeView {
409
475
  const colspan = resolveRenderedColspan(node);
410
476
  const rowspan = resolveRenderedRowspan(node);
411
477
  const cell = this.dom as HTMLTableCellElement;
478
+ applyCellAttrs(cell, node, isHeader);
412
479
  cell.colSpan = colspan > 1 ? colspan : 1;
413
480
  cell.rowSpan = rowspan > 1 ? rowspan : 1;
414
481
  cell.style.display = "";
@@ -22,7 +22,7 @@ export function TwCommentSidebar(props: TwCommentSidebarProps) {
22
22
 
23
23
  return (
24
24
  <div className="outline-none">
25
- <div className="mb-2 flex items-center gap-2 text-[10px] text-tertiary">
25
+ <div className="mb-3 flex items-center gap-2 text-[11px] text-tertiary">
26
26
  <span>{comments.openCommentIds.length} open</span>
27
27
  <span className="text-border">·</span>
28
28
  <span>{comments.resolvedCommentIds.length} resolved</span>
@@ -34,7 +34,7 @@ export function TwCommentSidebar(props: TwCommentSidebarProps) {
34
34
  )}
35
35
  </div>
36
36
  {comments.threads.length > 0 ? (
37
- <div className="space-y-1.5">
37
+ <div className="space-y-2">
38
38
  {comments.threads.map((thread) => (
39
39
  <CommentThreadCard
40
40
  key={thread.commentId}
@@ -50,7 +50,7 @@ export function TwCommentSidebar(props: TwCommentSidebarProps) {
50
50
  ))}
51
51
  </div>
52
52
  ) : (
53
- <div className="rounded-lg border border-dashed border-border bg-surface/60 px-2.5 py-3 text-[10px] leading-4 text-tertiary">
53
+ <div className="rounded-lg bg-surface px-3 py-3 text-[11px] leading-5 text-tertiary ring-1 ring-border/40">
54
54
  No comment threads yet. Select text and add one from the toolbar.
55
55
  </div>
56
56
  )}
@@ -93,11 +93,11 @@ function CommentThreadCard(props: {
93
93
  role="button"
94
94
  tabIndex={0}
95
95
  className={[
96
- "cursor-pointer rounded-lg border px-2 py-1.5 transition-colors",
96
+ "cursor-pointer rounded-md bg-surface/90 px-3 py-2.5 transition-colors ring-1 ring-border/40",
97
97
  focusRingClass,
98
98
  isActive
99
- ? "border-accent/25 bg-accent-soft/35"
100
- : "border-border bg-canvas hover:border-border-strong hover:bg-surface/70",
99
+ ? "bg-accent-soft/40 ring-accent/25"
100
+ : "hover:bg-surface",
101
101
  thread.status === "detached" ? "opacity-70" : "",
102
102
  ].join(" ")}
103
103
  onClick={() => props.onOpenComment?.(thread)}
@@ -109,8 +109,8 @@ function CommentThreadCard(props: {
109
109
  }}
110
110
  >
111
111
  {/* Header row: avatar + author + date + status */}
112
- <div className="mb-1 flex items-center gap-1.5">
113
- <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-accent/10 text-[8px] font-semibold text-accent">
112
+ <div className="mb-1.5 flex items-center gap-1.5">
113
+ <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-subtle text-[8px] font-semibold text-secondary">
114
114
  {thread.createdBy.charAt(0).toUpperCase()}
115
115
  </span>
116
116
  <span className="truncate text-[10px] font-medium text-primary">{thread.createdBy}</span>
@@ -125,7 +125,7 @@ function CommentThreadCard(props: {
125
125
 
126
126
  {/* Excerpt — anchored text from document */}
127
127
  {showExcerpt ? (
128
- <p className="mb-1 rounded-md border-l-2 border-comment/25 bg-comment-soft/30 px-2 py-1 text-[9px] leading-4 text-comment/80 italic whitespace-pre-wrap break-words line-clamp-2">
128
+ <p className="mb-1.5 rounded-md bg-comment-soft px-2 py-1 text-[9px] leading-4 text-secondary italic whitespace-pre-wrap break-words line-clamp-2">
129
129
  {thread.excerpt}
130
130
  </p>
131
131
  ) : null}
@@ -159,7 +159,7 @@ function CommentThreadCard(props: {
159
159
 
160
160
  {/* Reply entries (compact) */}
161
161
  {thread.entries.slice(1).map((entry) => (
162
- <div key={entry.entryId} className="mt-1.5 ml-4 border-l border-border/50 pl-2.5">
162
+ <div key={entry.entryId} className="mt-2 ml-4 border-l border-border/50 pl-2.5">
163
163
  <div className="mb-0.5 flex items-center gap-1">
164
164
  <span className="text-[9px] font-medium text-secondary">{entry.authorId}</span>
165
165
  <span className="text-[9px] text-tertiary">{formatCommentDate(entry.createdAt)}</span>
@@ -180,12 +180,12 @@ function CommentThreadCard(props: {
180
180
  ) : null}
181
181
 
182
182
  {/* Inline actions — compact, horizontal */}
183
- <div className="mt-1 flex items-center gap-0.5">
183
+ <div className="mt-2 flex items-center gap-1">
184
184
  {thread.status === "open" && (
185
185
  <>
186
186
  <button
187
187
  type="button"
188
- className="inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[9px] font-medium text-insert hover:bg-insert-soft transition-colors"
188
+ className="inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[9px] font-medium text-accent hover:bg-accent-soft transition-colors"
189
189
  onClick={(e) => { e.stopPropagation(); props.onResolveComment?.(thread.commentId); }}
190
190
  >
191
191
  <Check className="h-2 w-2" /> Resolve
@@ -198,7 +198,7 @@ function CommentThreadCard(props: {
198
198
  {thread.status === "resolved" && (
199
199
  <button
200
200
  type="button"
201
- className="inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[9px] font-medium text-secondary hover:bg-surface transition-colors"
201
+ className="inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[9px] font-medium text-secondary hover:bg-surface-hover transition-colors"
202
202
  data-comment-thread-action="reopen"
203
203
  onClick={(e) => { e.stopPropagation(); props.onReopenComment?.(thread.commentId); }}
204
204
  >
@@ -233,7 +233,7 @@ function InlineEditableBody(props: {
233
233
  if (!isEditing) {
234
234
  return (
235
235
  <p
236
- className={`cursor-text rounded px-1 text-[10px] leading-[1.15rem] -mx-1 transition-colors hover:bg-surface ${props.body ? "text-secondary" : "text-tertiary italic"}`}
236
+ className={`-mx-1 cursor-text rounded px-1 text-[10px] leading-[1.15rem] transition-colors hover:bg-surface ${props.body ? "text-secondary" : "text-tertiary italic"}`}
237
237
  onClick={(e) => {
238
238
  e.stopPropagation();
239
239
  setDraft(props.body);
@@ -255,7 +255,7 @@ function InlineEditableBody(props: {
255
255
  ) : null}
256
256
  <textarea
257
257
  ref={textareaRef}
258
- className="w-full resize-none rounded-md border border-border bg-surface px-2 py-1.5 text-[10px] leading-4 text-primary placeholder:text-tertiary focus:outline-none focus:ring-1 focus:ring-accent"
258
+ className="w-full resize-none rounded-md bg-surface px-2 py-1.5 text-[10px] leading-4 text-primary placeholder:text-tertiary focus:outline-none focus:ring-1 focus:ring-accent ring-1 ring-border/50"
259
259
  rows={2}
260
260
  value={draft}
261
261
  placeholder="Type your comment..."
@@ -301,7 +301,7 @@ function ReplyInput(props: { commentId: string; onAddReply: (commentId: string,
301
301
  return (
302
302
  <button
303
303
  type="button"
304
- className="inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] font-medium text-tertiary hover:text-secondary hover:bg-surface transition-colors"
304
+ className="inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] font-medium text-tertiary hover:bg-surface-hover hover:text-secondary transition-colors"
305
305
  onClick={(e) => {
306
306
  e.stopPropagation();
307
307
  setIsOpen(true);
@@ -316,7 +316,7 @@ function ReplyInput(props: { commentId: string; onAddReply: (commentId: string,
316
316
  <div className="w-full mt-1.5" onClick={(e) => e.stopPropagation()}>
317
317
  <textarea
318
318
  ref={inputRef}
319
- className="w-full rounded border border-border bg-surface px-2 py-1 text-[11px] text-primary placeholder:text-tertiary resize-none focus:outline-none focus:ring-1 focus:ring-accent"
319
+ className="w-full resize-none rounded bg-surface px-2 py-1 text-[11px] text-primary placeholder:text-tertiary focus:outline-none focus:ring-1 focus:ring-accent ring-1 ring-border/50"
320
320
  rows={2}
321
321
  placeholder="Reply..."
322
322
  value={body}
@@ -8,13 +8,13 @@ import type {
8
8
  CommentSidebarThreadSnapshot,
9
9
  CompatibilityPanelSnapshot,
10
10
  EditorWarning,
11
+ RuntimeContextAnalyticsSnapshot,
11
12
  TrackedChangesSnapshot,
12
13
  TrackedChangeEntrySnapshot,
13
14
  } from "../../api/public-types";
14
15
  import type { MarkupDisplay } from "../../ui/headless/comment-decoration-model";
15
16
  import { TwCommentSidebar } from "./tw-comment-sidebar";
16
17
  import { TwRevisionSidebar } from "./tw-revision-sidebar";
17
- import { TwHealthPanel } from "./tw-health-panel";
18
18
 
19
19
  export type ReviewRailTab = "comments" | "changes";
20
20
 
@@ -26,6 +26,7 @@ export interface TwReviewRailProps {
26
26
  compatibility: CompatibilityPanelSnapshot;
27
27
  warnings: EditorWarning[];
28
28
  markupDisplay: MarkupDisplay;
29
+ contextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
29
30
  activeCommentId?: string;
30
31
  activeRevisionId?: string;
31
32
  onActiveTabChange: (tab: ReviewRailTab) => void;
@@ -45,41 +46,44 @@ const focusRingClass =
45
46
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
46
47
 
47
48
  export function TwReviewRail(props: TwReviewRailProps) {
48
- const warningCount = props.compatibility.featureEntries.filter(
49
- (e) => e.featureClass !== "supported-roundtrip",
50
- ).length + props.warnings.length;
51
-
52
49
  return (
53
50
  <aside
54
51
  aria-label="Review rail"
55
- className="flex w-[304px] shrink-0 flex-col border-l border-border bg-canvas"
52
+ className="flex w-[336px] shrink-0 flex-col border-l border-border/60 bg-[var(--color-sidebar-tint)]"
56
53
  >
57
54
  <Tabs.Root
58
55
  value={props.activeTab}
59
56
  onValueChange={(v: string) => props.onActiveTabChange(v as ReviewRailTab)}
60
57
  className="flex flex-1 flex-col min-h-0"
61
58
  >
62
- <Tabs.List className="flex shrink-0 border-b border-border px-2">
59
+ <Tabs.List className="flex shrink-0 border-b border-border/60 px-3 py-2">
63
60
  <Tabs.Trigger
64
61
  value="comments"
65
- className={`flex-1 py-2 text-xs text-tertiary font-medium transition-colors data-[state=active]:text-primary data-[state=active]:font-semibold data-[state=active]:shadow-[inset_0_-2px_0_var(--color-accent)] outline-none ${focusRingClass}`}
62
+ className={`flex-1 rounded-lg px-3 py-1.5 text-xs font-medium text-tertiary transition-colors data-[state=active]:bg-surface data-[state=active]:text-primary data-[state=active]:shadow-[0_1px_0_var(--color-shadow)] outline-none ${focusRingClass}`}
66
63
  >
67
64
  Comments{" "}
68
- <span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary">{props.comments.totalCount}</span>
65
+ {props.comments.totalCount > 0 ? (
66
+ <span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary">
67
+ {props.comments.totalCount}
68
+ </span>
69
+ ) : null}
69
70
  </Tabs.Trigger>
70
71
  <Tabs.Trigger
71
72
  value="changes"
72
- className={`flex-1 py-2 text-xs text-tertiary font-medium transition-colors data-[state=active]:text-primary data-[state=active]:font-semibold data-[state=active]:shadow-[inset_0_-2px_0_var(--color-accent)] outline-none ${focusRingClass}`}
73
+ className={`flex-1 rounded-lg px-3 py-1.5 text-xs font-medium text-tertiary transition-colors data-[state=active]:bg-surface data-[state=active]:text-primary data-[state=active]:shadow-[0_1px_0_var(--color-shadow)] outline-none ${focusRingClass}`}
73
74
  >
74
75
  Changes{" "}
75
- <span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary">{props.trackedChanges.totalCount}</span>
76
+ {props.trackedChanges.totalCount > 0 ? (
77
+ <span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary">
78
+ {props.trackedChanges.totalCount}
79
+ </span>
80
+ ) : null}
76
81
  </Tabs.Trigger>
77
- {/* Health moved to toolbar popover */}
78
82
  </Tabs.List>
79
83
 
80
84
  <ScrollArea.Root className="flex-1 min-h-0">
81
85
  <ScrollArea.Viewport className="h-full w-full">
82
- <Tabs.Content value="comments" className="p-2.5 outline-none">
86
+ <Tabs.Content value="comments" className="p-3 outline-none">
83
87
  <TwCommentSidebar
84
88
  currentUserId={props.currentUserId}
85
89
  comments={props.comments}
@@ -92,7 +96,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
92
96
  />
93
97
  </Tabs.Content>
94
98
 
95
- <Tabs.Content value="changes" className="p-2.5 outline-none">
99
+ <Tabs.Content value="changes" className="p-3 outline-none">
96
100
  <TwRevisionSidebar
97
101
  trackedChanges={props.trackedChanges}
98
102
  markupDisplay={props.markupDisplay}
@@ -104,14 +108,12 @@ export function TwReviewRail(props: TwReviewRailProps) {
104
108
  onRejectAllChanges={props.onRejectAllChanges}
105
109
  />
106
110
  </Tabs.Content>
107
-
108
- {/* Health panel moved to toolbar popover */}
109
111
  </ScrollArea.Viewport>
110
112
  <ScrollArea.Scrollbar
111
113
  orientation="vertical"
112
114
  className="flex w-1.5 touch-none select-none p-0.5"
113
115
  >
114
- <ScrollArea.Thumb className="relative flex-1 rounded-full bg-black/[0.12]" />
116
+ <ScrollArea.Thumb className="relative flex-1 rounded-full bg-[color:color-mix(in_srgb,var(--color-secondary)_18%,transparent)]" />
115
117
  </ScrollArea.Scrollbar>
116
118
  </ScrollArea.Root>
117
119
  </Tabs.Root>
@@ -28,12 +28,12 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
28
28
 
29
29
  return (
30
30
  <div className="outline-none">
31
- <p className="mb-2 text-[10px] text-tertiary">
31
+ <p className="mb-3 text-[11px] text-tertiary">
32
32
  {trackedChanges.pendingChangeIds.length} active · {trackedChanges.acceptedChangeIds.length} accepted · {trackedChanges.preserveOnlyChangeIds.length} preserve-only
33
33
  </p>
34
34
 
35
35
  {/* Bulk actions */}
36
- <div className="mb-2 flex gap-1">
36
+ <div className="mb-3 flex gap-1.5">
37
37
  <button
38
38
  type="button"
39
39
  disabled={actionablePendingCount === 0}
@@ -45,7 +45,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
45
45
  <button
46
46
  type="button"
47
47
  disabled={actionablePendingCount === 0}
48
- className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-[10px] text-secondary hover:bg-surface transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
48
+ className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-[10px] text-secondary hover:bg-surface-hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
49
49
  onClick={props.onRejectAllChanges}
50
50
  >
51
51
  Reject all
@@ -53,7 +53,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
53
53
  </div>
54
54
 
55
55
  {visibleRevisions.length > 0 ? (
56
- <div className="space-y-1">
56
+ <div className="space-y-2">
57
57
  {visibleRevisions.map((rev) => {
58
58
  const isActive = activeRevisionId === rev.revisionId;
59
59
 
@@ -62,7 +62,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
62
62
  key={rev.revisionId}
63
63
  role="button"
64
64
  tabIndex={0}
65
- className={`flex rounded-lg transition-colors cursor-pointer ${focusRingClass} ${isActive ? "bg-accent-soft" : "hover:bg-surface"}`}
65
+ className={`flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border/40 ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
66
66
  onClick={() => props.onOpenRevision?.(rev)}
67
67
  onKeyDown={(event) => {
68
68
  if (event.key === "Enter" || event.key === " ") {
@@ -71,7 +71,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
71
71
  }
72
72
  }}
73
73
  >
74
- <div className={`w-0.5 shrink-0 rounded-l-lg ${
74
+ <div className={`w-0.5 shrink-0 rounded-l-md ${
75
75
  rev.kind === "insertion" ? "bg-insert"
76
76
  : rev.kind === "deletion" ? "bg-danger"
77
77
  : "bg-tertiary"
@@ -96,13 +96,13 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
96
96
  {rev.detail ? (
97
97
  <p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
98
98
  ) : null}
99
- <div className="mt-1.5 flex gap-1">
99
+ <div className="mt-2 flex gap-1.5">
100
100
  {rev.actionability === "actionable" ? (
101
101
  <>
102
102
  <button
103
103
  type="button"
104
104
  disabled={!rev.canAccept || rev.status === "accepted"}
105
- className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-insert hover:bg-insert-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
105
+ className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
106
106
  onClick={(e) => {
107
107
  e.stopPropagation();
108
108
  props.onAcceptRevision?.(rev.revisionId);
@@ -113,7 +113,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
113
113
  <button
114
114
  type="button"
115
115
  disabled={!rev.canReject || rev.status === "rejected"}
116
- className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-delete-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
116
+ className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-danger-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
117
117
  onClick={(e) => {
118
118
  e.stopPropagation();
119
119
  props.onRejectRevision?.(rev.revisionId);
@@ -132,7 +132,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
132
132
  })}
133
133
  </div>
134
134
  ) : (
135
- <p className="text-xs text-tertiary py-4">
135
+ <p className="rounded-lg bg-surface px-3 py-4 text-xs text-tertiary ring-1 ring-border/40">
136
136
  {trackedChanges.totalCount > 0
137
137
  ? "Switch to Full markup to see all tracked changes."
138
138
  : "Tracked change cards will appear here when present."}
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
2
 
3
+ import type { RuntimeContextAnalyticsSnapshot } from "../../api/public-types";
4
+
3
5
  export interface TwStatusBarProps {
4
6
  isDirty: boolean;
5
7
  isExportBlocked: boolean;
@@ -7,6 +9,7 @@ export interface TwStatusBarProps {
7
9
  commentCount: number;
8
10
  changeCount: number;
9
11
  sessionId: string;
12
+ contextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
10
13
  }
11
14
 
12
15
  export function TwStatusBar(props: TwStatusBarProps) {
@@ -20,11 +23,10 @@ export function TwStatusBar(props: TwStatusBarProps) {
20
23
  : props.preserveOnlyCount > 0
21
24
  ? "Warnings"
22
25
  : "Ready";
23
-
24
26
  return (
25
27
  <footer
26
28
  data-testid="status-bar"
27
- className="flex h-7 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-tertiary"
29
+ className="flex h-7 shrink-0 items-center gap-3 border-t border-border/60 bg-surface/72 px-3 text-[11px] text-tertiary"
28
30
  >
29
31
  <span className="flex items-center gap-1.5">
30
32
  <span
@@ -33,7 +35,7 @@ export function TwStatusBar(props: TwStatusBarProps) {
33
35
  ? "bg-danger"
34
36
  : props.isDirty
35
37
  ? "bg-comment"
36
- : "bg-insert"
38
+ : "bg-accent"
37
39
  }`}
38
40
  />
39
41
  {saveState}
@@ -44,8 +46,8 @@ export function TwStatusBar(props: TwStatusBarProps) {
44
46
  props.isExportBlocked
45
47
  ? "bg-danger"
46
48
  : props.preserveOnlyCount > 0
47
- ? "bg-comment"
48
- : "bg-insert"
49
+ ? "bg-warning"
50
+ : "bg-accent"
49
51
  }`}
50
52
  />
51
53
  Export {exportState.toLowerCase()}
@@ -55,7 +57,9 @@ export function TwStatusBar(props: TwStatusBarProps) {
55
57
  {props.changeCount} change{props.changeCount !== 1 ? "s" : ""}
56
58
  </span>
57
59
  <span className="flex-1" />
58
- <span>{props.sessionId}</span>
60
+ <span className="truncate text-[10px] uppercase tracking-[0.12em] text-tertiary/80">
61
+ Session active
62
+ </span>
59
63
  </footer>
60
64
  );
61
65
  }
@@ -17,14 +17,15 @@
17
17
 
18
18
  @theme {
19
19
  /* Backgrounds */
20
- --color-surface: #f7f7f5;
21
- --color-surface-hover: #ebebea;
22
- --color-surface-active: #e3e3e1;
23
- --color-subtle: #f0f0ee;
20
+ --color-surface: #fafbf9;
21
+ --color-surface-hover: #f1f4f1;
22
+ --color-surface-active: #e8eeea;
23
+ --color-subtle: #eff3ef;
24
24
  --color-canvas: #ffffff;
25
- --color-shell-bg: #f3efe7;
26
- --color-card: #faf8f2;
27
- --color-editor-frame: #f6f3ee;
25
+ --color-shell-bg: #f4f8f5;
26
+ --color-card: #ffffff;
27
+ --color-editor-frame: #f7faf7;
28
+ --color-sidebar-tint: rgba(247, 250, 247, 0.92);
28
29
 
29
30
  /* Text */
30
31
  --color-primary: #1f1f1f;
@@ -32,29 +33,31 @@
32
33
  --color-tertiary: #616161;
33
34
 
34
35
  /* Accent */
35
- --color-accent: #1660a8;
36
- --color-accent-soft: rgba(22, 96, 168, 0.08);
36
+ --color-accent: #18b394;
37
+ --color-accent-soft: rgba(24, 179, 148, 0.1);
38
+ --color-workflow: #7557d8;
39
+ --color-workflow-soft: rgba(117, 87, 216, 0.12);
37
40
 
38
41
  /* Review — comments */
39
- --color-comment: #7a4f00;
40
- --color-comment-soft: rgba(255, 212, 0, 0.14);
41
- --color-comment-strong: rgba(255, 212, 0, 0.25);
42
+ --color-comment: #7557d8;
43
+ --color-comment-soft: rgba(117, 87, 216, 0.12);
44
+ --color-comment-strong: rgba(117, 87, 216, 0.18);
42
45
 
43
46
  /* Review — insertions */
44
- --color-insert: #1a7f37;
45
- --color-insert-soft: rgba(26, 127, 55, 0.08);
47
+ --color-insert: #2f6b4f;
48
+ --color-insert-soft: rgba(47, 107, 79, 0.1);
46
49
 
47
50
  /* Review — deletions */
48
- --color-delete: #cf222e;
49
- --color-delete-soft: rgba(207, 34, 46, 0.06);
51
+ --color-delete: #9b4f49;
52
+ --color-delete-soft: rgba(155, 79, 73, 0.08);
50
53
 
51
54
  /* Semantic */
52
- --color-warning: #7a4f00;
53
- --color-warning-soft: rgba(154, 103, 0, 0.08);
54
- --color-danger: #cf222e;
55
- --color-danger-soft: rgba(207, 34, 46, 0.08);
56
- --color-success: #1a7f37;
57
- --color-success-soft: rgba(26, 127, 55, 0.08);
55
+ --color-warning: #7557d8;
56
+ --color-warning-soft: rgba(117, 87, 216, 0.12);
57
+ --color-danger: #9b4f49;
58
+ --color-danger-soft: rgba(155, 79, 73, 0.1);
59
+ --color-success: #2f6b4f;
60
+ --color-success-soft: rgba(47, 107, 79, 0.1);
58
61
 
59
62
  /* Borders */
60
63
  --color-border: rgba(0, 0, 0, 0.06);
@@ -85,28 +88,31 @@
85
88
  --color-shell-bg: #171514;
86
89
  --color-card: #23211f;
87
90
  --color-editor-frame: #1d1b19;
91
+ --color-sidebar-tint: rgba(29, 27, 25, 0.94);
88
92
 
89
93
  --color-primary: #e4e4e4;
90
94
  --color-secondary: #a0a0a0;
91
95
  --color-tertiary: #6e6e6e;
92
96
 
93
- --color-accent: #4ca6f0;
94
- --color-accent-soft: rgba(76, 166, 240, 0.12);
95
-
96
- --color-comment: #d4a020;
97
- --color-comment-soft: rgba(212, 160, 32, 0.16);
98
- --color-comment-strong: rgba(212, 160, 32, 0.28);
99
- --color-insert: #3fb950;
100
- --color-insert-soft: rgba(63, 185, 80, 0.12);
101
- --color-delete: #f85149;
102
- --color-delete-soft: rgba(248, 81, 73, 0.12);
103
-
104
- --color-warning: #d4a020;
105
- --color-warning-soft: rgba(212, 160, 32, 0.12);
106
- --color-danger: #f85149;
107
- --color-danger-soft: rgba(248, 81, 73, 0.12);
108
- --color-success: #3fb950;
109
- --color-success-soft: rgba(63, 185, 80, 0.12);
97
+ --color-accent: #5edec1;
98
+ --color-accent-soft: rgba(94, 222, 193, 0.14);
99
+ --color-workflow: #c7b6ff;
100
+ --color-workflow-soft: rgba(199, 182, 255, 0.16);
101
+
102
+ --color-comment: #c7b6ff;
103
+ --color-comment-soft: rgba(199, 182, 255, 0.18);
104
+ --color-comment-strong: rgba(199, 182, 255, 0.24);
105
+ --color-insert: #8fd2a9;
106
+ --color-insert-soft: rgba(143, 210, 169, 0.18);
107
+ --color-delete: #d78d84;
108
+ --color-delete-soft: rgba(215, 141, 132, 0.14);
109
+
110
+ --color-warning: #c7b6ff;
111
+ --color-warning-soft: rgba(199, 182, 255, 0.16);
112
+ --color-danger: #d78d84;
113
+ --color-danger-soft: rgba(215, 141, 132, 0.16);
114
+ --color-success: #8fd2a9;
115
+ --color-success-soft: rgba(143, 210, 169, 0.18);
110
116
 
111
117
  --color-border: rgba(255, 255, 255, 0.08);
112
118
  --color-border-strong: rgba(255, 255, 255, 0.14);
@@ -184,7 +190,7 @@
184
190
  background: var(--color-page-bg);
185
191
  border: 1px solid var(--color-page-border);
186
192
  border-radius: 2px;
187
- box-shadow: 0 1px 4px var(--color-page-shadow);
193
+ box-shadow: 0 8px 24px -20px var(--color-page-shadow);
188
194
  }
189
195
 
190
196
  /* Canvas-mode typography — lighter, review-first baseline */
@@ -193,6 +199,12 @@
193
199
  font-size: 15px;
194
200
  line-height: 1.6;
195
201
  color: var(--color-primary);
202
+ width: min(100%, 920px);
203
+ min-height: 100%;
204
+ background: var(--color-page-bg);
205
+ border: 1px solid var(--color-page-border);
206
+ border-radius: 2px;
207
+ box-shadow: 0 6px 18px -14px var(--color-page-shadow);
196
208
  -webkit-font-smoothing: antialiased;
197
209
  -moz-osx-font-smoothing: grayscale;
198
210
  text-rendering: optimizeLegibility;
@@ -248,6 +260,7 @@
248
260
  cursor: text;
249
261
  border: none;
250
262
  box-shadow: none;
263
+ white-space: pre-wrap;
251
264
  }
252
265
 
253
266
  .prosemirror-surface .ProseMirror:focus {
@@ -294,6 +307,11 @@
294
307
  text-underline-offset: 0.18em;
295
308
  }
296
309
 
310
+ .prosemirror-surface .ProseMirror .wre-workflow-inline-metadata {
311
+ background: color-mix(in srgb, var(--wre-workflow-metadata-color, var(--color-accent)) 12%, transparent);
312
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--wre-workflow-metadata-color, var(--color-accent)) 22%, transparent);
313
+ }
314
+
297
315
  .prosemirror-surface .ProseMirror .wre-workflow-inline-preserve-only {
298
316
  background: color-mix(in srgb, var(--color-danger) 7%, transparent);
299
317
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-danger) 18%, transparent);