@beyondwork/docx-react-component 1.0.52 → 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 (103) hide show
  1. package/package.json +31 -40
  2. package/src/api/public-types.ts +67 -7
  3. package/src/io/chart-preview-resolver.ts +41 -0
  4. package/src/io/docx-session.ts +217 -23
  5. package/src/runtime/collab/checkpoint-store.ts +1 -1
  6. package/src/runtime/collab/event-types.ts +4 -0
  7. package/src/runtime/collab/runtime-collab-sync.ts +88 -8
  8. package/src/runtime/document-runtime.ts +182 -9
  9. package/src/runtime/layout/inert-layout-facet.ts +1 -0
  10. package/src/runtime/layout/layout-engine-version.ts +97 -2
  11. package/src/runtime/layout/layout-invalidation.ts +150 -30
  12. package/src/runtime/layout/page-graph.ts +19 -0
  13. package/src/runtime/layout/paginated-layout-engine.ts +128 -19
  14. package/src/runtime/layout/project-block-fragments.ts +27 -0
  15. package/src/runtime/layout/public-facet.ts +70 -1
  16. package/src/runtime/prerender/cache-envelope.ts +30 -0
  17. package/src/runtime/prerender/customxml-cache.ts +17 -3
  18. package/src/runtime/prerender/prerender-document.ts +17 -1
  19. package/src/runtime/render/render-frame-diff.ts +38 -2
  20. package/src/runtime/render/render-kernel.ts +67 -19
  21. package/src/runtime/surface-projection.ts +28 -0
  22. package/src/runtime/table-schema.ts +27 -0
  23. package/src/runtime/table-style-resolver.ts +51 -0
  24. package/src/ui/WordReviewEditor.tsx +6 -3
  25. package/src/ui/editor-runtime-boundary.ts +39 -2
  26. package/src/ui/headless/comment-decoration-model.ts +60 -5
  27. package/src/ui/headless/revision-decoration-model.ts +94 -6
  28. package/src/ui/shared/revision-filters.ts +16 -6
  29. package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
  30. package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
  31. package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
  32. package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
  33. package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
  34. package/src/ui-tailwind/chart/render/area.tsx +277 -0
  35. package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
  36. package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
  37. package/src/ui-tailwind/chart/render/combo.tsx +85 -0
  38. package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
  39. package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
  40. package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
  41. package/src/ui-tailwind/chart/render/line.tsx +363 -0
  42. package/src/ui-tailwind/chart/render/number-format.ts +120 -16
  43. package/src/ui-tailwind/chart/render/pie.tsx +275 -0
  44. package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
  45. package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
  46. package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
  47. package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
  48. package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
  49. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
  50. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
  51. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
  52. package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
  53. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
  54. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
  55. package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
  56. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
  57. package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
  58. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
  59. package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
  60. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
  61. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
  62. package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
  63. package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
  64. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
  65. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
  66. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
  67. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
  68. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
  69. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
  70. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
  71. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
  72. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
  73. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
  74. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
  75. package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
  76. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
  77. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
  78. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
  79. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
  80. package/src/ui-tailwind/index.ts +11 -0
  81. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
  82. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +274 -0
  83. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +15 -2
  84. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +15 -2
  85. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +19 -147
  86. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
  87. package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
  88. package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
  89. package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
  90. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
  91. package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
  92. package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
  93. package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
  94. package/src/ui-tailwind/theme/editor-theme.css +275 -46
  95. package/src/ui-tailwind/theme/tokens.css +345 -0
  96. package/src/ui-tailwind/theme/tokens.ts +313 -0
  97. package/src/ui-tailwind/theme/use-density.ts +60 -0
  98. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
  99. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
  100. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
  101. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
  102. package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
  103. package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
@@ -43,6 +43,13 @@ export interface ResolveSelectionToolPlacementInput {
43
43
  edgePaddingPx?: number;
44
44
  /** Gap between anchor and toolbar in px. Defaults to 12. */
45
45
  gapPx?: number;
46
+ /**
47
+ * Width of the review rail in px. When set, the available viewport
48
+ * width is reduced by this amount on the right side so the toolbar
49
+ * never overlaps the rail. A 1.5 rem (24px) margin is applied beyond
50
+ * the rail edge. Defaults to 0 (no rail).
51
+ */
52
+ railWidthPx?: number;
46
53
  }
47
54
 
48
55
  const DEFAULT_EDGE_PADDING = 16;
@@ -79,6 +86,11 @@ export function resolveSelectionToolPlacement(
79
86
  ),
80
87
  );
81
88
 
89
+ // Rail-aware right boundary: viewport width minus rail width minus 1.5rem (24px) margin.
90
+ const RAIL_MARGIN = 24; // 1.5rem
91
+ const railWidthPx = input.railWidthPx ?? 0;
92
+ const effectiveRightEdge = container.widthPx - railWidthPx - (railWidthPx > 0 ? RAIL_MARGIN : 0);
93
+
82
94
  const anchorLeft = anchor.leftPx;
83
95
  const anchorRight = anchor.leftPx + anchor.widthPx;
84
96
  const anchorTop = anchor.topPx;
@@ -86,14 +98,14 @@ export function resolveSelectionToolPlacement(
86
98
  const centerX = anchorLeft + anchor.widthPx / 2;
87
99
  const centerY = anchorTop + anchor.heightPx / 2;
88
100
 
89
- const rightClearance = container.widthPx - anchorRight - gapPx - edgePadding;
101
+ const rightClearance = effectiveRightEdge - anchorRight - gapPx - edgePadding;
90
102
  const leftClearance = anchorLeft - gapPx - edgePadding;
91
103
 
92
104
  const clampedCenterX = Math.max(
93
105
  edgePadding,
94
106
  Math.min(
95
107
  centerX,
96
- Math.max(edgePadding, container.widthPx - edgePadding),
108
+ Math.max(edgePadding, effectiveRightEdge - edgePadding),
97
109
  ),
98
110
  );
99
111
  const clampedCenterY = Math.max(
@@ -106,7 +118,7 @@ export function resolveSelectionToolPlacement(
106
118
  ),
107
119
  ),
108
120
  );
109
- const maxWidthPx = Math.max(220, container.widthPx - edgePadding * 2);
121
+ const maxWidthPx = Math.max(220, effectiveRightEdge - edgePadding * 2);
110
122
 
111
123
  if (rightClearance >= toolbarWidth) {
112
124
  return {
@@ -17,6 +17,7 @@ export interface TwSelectionToolbarProps {
17
17
  onSetTextColor?: (color: string) => void;
18
18
  onSetHighlightColor?: (color: string | null) => void;
19
19
  onAddComment?: () => void;
20
+ density?: "micro" | "full";
20
21
  }
21
22
 
22
23
  const focusRingClass =
@@ -33,6 +34,7 @@ const DEFAULT_HIGHLIGHT_COLOR = "#ffff00";
33
34
 
34
35
  export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarProps>(function TwSelectionToolbar(props, ref) {
35
36
  const { model } = props;
37
+ const density = props.density ?? "full";
36
38
  const addCommentDisabled = !model.canAddComment;
37
39
  const formattingDisabled = !model.canToggleFormatting;
38
40
  const contextLabel = summarizeSelectionContext(model);
@@ -44,7 +46,13 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
44
46
  <div
45
47
  ref={ref}
46
48
  data-testid="selection-toolbar"
47
- className="inline-flex max-w-[min(22rem,calc(100vw-1.5rem))] items-center gap-1 rounded-lg border border-border/80 bg-canvas px-1 py-1 shadow-md ring-1 ring-border/70"
49
+ className="inline-flex max-w-[min(22rem,calc(100vw-1.5rem))] items-center gap-1 rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)]/80 bg-[var(--color-bg-canvas)] px-1.5 py-1 shadow-[var(--shadow-float)] ring-1 ring-[var(--color-border-subtle)]/70"
50
+ style={{
51
+ paddingLeft: "calc(6px * var(--space-density-multiplier))",
52
+ paddingRight: "calc(6px * var(--space-density-multiplier))",
53
+ paddingTop: "calc(4px * var(--space-density-multiplier))",
54
+ paddingBottom: "calc(4px * var(--space-density-multiplier))",
55
+ }}
48
56
  role="toolbar"
49
57
  aria-label="Selection actions"
50
58
  onFocusCapture={props.onFocusCapture}
@@ -71,24 +79,28 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
71
79
  disabled={formattingDisabled}
72
80
  onClick={props.onToggleUnderline}
73
81
  />
74
- <ToolbarActionButton
75
- icon={<Baseline className="h-3.5 w-3.5" />}
76
- label={`Apply ${model.textColorDefault ?? DEFAULT_TEXT_COLOR}`}
77
- pressed={false}
78
- disabled={formattingDisabled}
79
- onClick={() => props.onSetTextColor?.(model.textColorDefault ?? DEFAULT_TEXT_COLOR)}
80
- />
81
- <ToolbarActionButton
82
- icon={<Highlighter className="h-3.5 w-3.5" />}
83
- label={`Apply ${model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR} highlight`}
84
- pressed={false}
85
- disabled={formattingDisabled}
86
- onClick={() =>
87
- props.onSetHighlightColor?.(model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR)
88
- }
89
- />
90
82
 
91
- <div className="mx-0.5 h-4 w-px bg-border" />
83
+ {density === "full" && (
84
+ <>
85
+ <ToolbarActionButton
86
+ icon={<Baseline className="h-3.5 w-3.5" />}
87
+ label={`Apply ${model.textColorDefault ?? DEFAULT_TEXT_COLOR}`}
88
+ pressed={false}
89
+ disabled={formattingDisabled}
90
+ onClick={() => props.onSetTextColor?.(model.textColorDefault ?? DEFAULT_TEXT_COLOR)}
91
+ />
92
+ <ToolbarActionButton
93
+ icon={<Highlighter className="h-3.5 w-3.5" />}
94
+ label={`Apply ${model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR} highlight`}
95
+ pressed={false}
96
+ disabled={formattingDisabled}
97
+ onClick={() =>
98
+ props.onSetHighlightColor?.(model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR)
99
+ }
100
+ />
101
+ <div className="mx-0.5 h-4 w-px bg-[var(--color-border-subtle)]" />
102
+ </>
103
+ )}
92
104
 
93
105
  <Tooltip.Root>
94
106
  <Tooltip.Trigger asChild>
@@ -115,7 +127,7 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
115
127
 
116
128
  {model.previewText ? (
117
129
  <>
118
- <div className="mx-0.5 h-4 w-px bg-border" />
130
+ <div className="mx-0.5 h-4 w-px bg-[var(--color-border-subtle)]" />
119
131
  <span className="max-w-[7rem] truncate text-[10px] text-secondary">
120
132
  {model.previewText}
121
133
  </span>
@@ -124,7 +136,7 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
124
136
 
125
137
  {contextLabel ? (
126
138
  <>
127
- {!model.previewText ? <div className="mx-0.5 h-4 w-px bg-border" /> : null}
139
+ {!model.previewText ? <div className="mx-0.5 h-4 w-px bg-[var(--color-border-subtle)]" /> : null}
128
140
  <span
129
141
  className={`min-w-0 max-w-[9rem] truncate rounded-full px-1.5 py-0.5 text-[9px] font-medium tracking-[0.08em] ${
130
142
  model.badges.some((badge) => badge.tone === "accent")
@@ -0,0 +1,68 @@
1
+ import * as React from "react";
2
+
3
+ export type ShortcutKey =
4
+ | "Cmd" | "Ctrl" | "Shift" | "Alt" | "Enter" | "Esc" | "Tab" | "Backspace"
5
+ | string; // fall-through for single letters/digits
6
+
7
+ export interface TwShortcutHintProps {
8
+ keys: ShortcutKey[];
9
+ platform?: "mac" | "win";
10
+ className?: string;
11
+ }
12
+
13
+ const MAC_SYMBOL: Record<string, string> = {
14
+ Cmd: "⌘",
15
+ Shift: "⇧",
16
+ Alt: "⌥",
17
+ Ctrl: "⌃",
18
+ Enter: "↵",
19
+ Esc: "Esc",
20
+ Tab: "⇥",
21
+ Backspace: "⌫",
22
+ };
23
+
24
+ function detectPlatform(): "mac" | "win" {
25
+ if (typeof navigator === "undefined") return "win";
26
+ return /Mac|iPhone|iPad/.test(navigator.platform) ? "mac" : "win";
27
+ }
28
+
29
+ export function TwShortcutHint(props: TwShortcutHintProps): React.JSX.Element {
30
+ const { keys, className } = props;
31
+ const platform = props.platform ?? detectPlatform();
32
+
33
+ const mapped = keys.map((k) =>
34
+ platform === "mac"
35
+ ? (MAC_SYMBOL[k] ?? k)
36
+ : (k === "Cmd" ? "Ctrl" : k)
37
+ );
38
+
39
+ const containerClass = [
40
+ "inline-flex items-center gap-0.5",
41
+ "text-[9px] uppercase tracking-[0.06em]",
42
+ "text-[var(--color-text-tertiary)] opacity-75",
43
+ className,
44
+ ]
45
+ .filter(Boolean)
46
+ .join(" ");
47
+
48
+ const chipClass = [
49
+ "rounded-[var(--radius-sm)]",
50
+ "border border-[var(--color-border-default)]",
51
+ "bg-[var(--color-bg-muted)]",
52
+ "px-1 py-px leading-none font-medium",
53
+ ].join(" ");
54
+
55
+ return (
56
+ <span className={containerClass} data-testid="tw-shortcut-hint">
57
+ {mapped.map((label, i) => (
58
+ <kbd
59
+ key={i}
60
+ className={chipClass}
61
+ data-testid="tw-shortcut-hint__chip"
62
+ >
63
+ {label}
64
+ </kbd>
65
+ ))}
66
+ </span>
67
+ );
68
+ }
@@ -29,18 +29,18 @@ export function TwSuggestionCard(props: TwSuggestionCardProps) {
29
29
  return (
30
30
  <div
31
31
  data-testid="suggestion-card"
32
- className="inline-flex max-w-[min(24rem,calc(100vw-1.5rem))] flex-col gap-1.5 rounded-xl border border-border/80 bg-canvas px-2.5 py-1.5 shadow-lg ring-1 ring-border/75"
32
+ className="inline-flex max-w-[min(24rem,calc(100vw-1.5rem))] flex-col gap-1.5 rounded-[var(--radius-xl)] border border-[var(--color-border-subtle)]/80 bg-[var(--color-bg-canvas)] px-[10px] py-[10px] shadow-[var(--shadow-float)] ring-1 ring-[var(--color-border-subtle)]/75"
33
33
  onFocusCapture={props.onFocusCapture}
34
34
  onBlurCapture={props.onBlurCapture}
35
- role="group"
36
- aria-label="Suggestion actions"
35
+ role="region"
36
+ aria-label="Suggestion"
37
37
  >
38
38
  <div className="flex items-start justify-between gap-2">
39
39
  <div className="min-w-0">
40
- <div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-warning">
40
+ <div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--color-semantic-warning)]">
41
41
  {props.model.kindLabel}
42
42
  </div>
43
- <div className="mt-0.5 max-w-[14rem] truncate text-[13px] text-primary">
43
+ <div className="mt-0.5 max-w-[14rem] truncate text-[13px] text-[var(--color-text-primary)]">
44
44
  {props.model.previewText}
45
45
  </div>
46
46
  </div>
@@ -81,7 +81,7 @@ export function TwSuggestionCard(props: TwSuggestionCardProps) {
81
81
  disabled={commentDisabled}
82
82
  onMouseDown={preserveEditorSelectionMouseDown}
83
83
  onClick={props.onAddComment}
84
- className={`inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-[11px] font-medium text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
84
+ className={`inline-flex h-7 items-center gap-1 rounded-md border border-[var(--color-border-default)] px-2 text-[11px] font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-hover)] disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
85
85
  >
86
86
  <MessageSquare className="h-3 w-3" />
87
87
  Comment
@@ -118,10 +118,10 @@ function SuggestionActionButton(props: {
118
118
  onClick?: () => void;
119
119
  }) {
120
120
  const toneClass = props.tone === "accept"
121
- ? "border-success/35 bg-canvas text-success hover:bg-surface"
121
+ ? "border border-transparent bg-[var(--color-accent-primary)] text-[var(--color-text-on-accent)] hover:bg-[var(--color-accent-primary-hover)]"
122
122
  : props.tone === "reject"
123
- ? "border-danger/35 bg-canvas text-danger hover:bg-surface"
124
- : "border-border text-secondary hover:bg-surface";
123
+ ? "border border-[var(--color-semantic-error)]/35 bg-[var(--color-bg-canvas)] text-[var(--color-semantic-error)] hover:bg-[var(--color-semantic-error-soft)]"
124
+ : "border border-[var(--color-border-default)] text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)]";
125
125
 
126
126
  return (
127
127
  <button
@@ -130,7 +130,7 @@ function SuggestionActionButton(props: {
130
130
  disabled={props.disabled}
131
131
  onMouseDown={preserveEditorSelectionMouseDown}
132
132
  onClick={props.onClick}
133
- className={`inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${toneClass} ${focusRingClass}`}
133
+ className={`inline-flex h-7 items-center gap-1 rounded-md px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${toneClass} ${focusRingClass}`}
134
134
  >
135
135
  {props.icon}
136
136
  {props.label.replace(" suggestion", "").replace(" on suggestion", "")}
@@ -108,9 +108,9 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
108
108
  <div
109
109
  data-testid="table-context-toolbar"
110
110
  data-tier={tier}
111
- className={`flex ${widthCap} flex-wrap items-start gap-1.5 rounded-lg border border-border bg-canvas px-2.5 py-1.5 shadow-sm`}
111
+ className={`flex ${widthCap} flex-wrap items-start gap-[4px] rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)] bg-[var(--color-bg-canvas)] px-2.5 py-1.5 shadow-[var(--shadow-float)]`}
112
112
  >
113
- <span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
113
+ <span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]">
114
114
  {tierLabel(tier)}
115
115
  </span>
116
116
  {tableSizeLabel ? <ToolbarBadge>{tableSizeLabel}</ToolbarBadge> : null}
@@ -120,6 +120,7 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
120
120
  ) : null}
121
121
 
122
122
  {/* T5 whole-table: table alignment */}
123
+ {tier === "whole-table" ? <GroupDivider /> : null}
123
124
  {tier === "whole-table" ? (
124
125
  <ToolbarSection label="Align">
125
126
  {(["left", "center", "right"] as const).map((align) => (
@@ -166,6 +167,9 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
166
167
  </ToolbarSection>
167
168
  ) : null}
168
169
 
170
+ {/* separator before row/column structure group */}
171
+ <GroupDivider />
172
+
169
173
  {/* T2 / T4a row-selected: row ops */}
170
174
  {(tier === "caret-in-cell" || tier === "row-selected") ? (
171
175
  <ToolbarSection label="Rows">
@@ -261,6 +265,11 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
261
265
  </ToolbarSection>
262
266
  ) : null}
263
267
 
268
+ {/* separator before merge/split group (multi-cell, row, column) */}
269
+ {tier === "multi-cell" || tier === "row-selected" || tier === "column-selected" ? (
270
+ <GroupDivider />
271
+ ) : null}
272
+
264
273
  {/* T3 multi-cell: merge/split */}
265
274
  {tier === "multi-cell" ||
266
275
  tier === "row-selected" ||
@@ -333,6 +342,9 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
333
342
  </ToolbarSection>
334
343
  ) : null}
335
344
 
345
+ {/* separator before delete table (whole-table only) */}
346
+ {tier === "whole-table" ? <GroupDivider /> : null}
347
+
336
348
  {/* T5 only: delete table (danger) */}
337
349
  {tier === "whole-table" ? (
338
350
  <ToolbarSection label="Table">
@@ -399,6 +411,15 @@ function tierWidthCap(tier: TableTier): string {
399
411
  }
400
412
  }
401
413
 
414
+ function GroupDivider() {
415
+ return (
416
+ <div
417
+ className="mx-0.5 h-5 w-px bg-[var(--color-border-subtle)]"
418
+ aria-hidden="true"
419
+ />
420
+ );
421
+ }
422
+
402
423
  function ToolbarBadge(props: {
403
424
  children: React.ReactNode;
404
425
  tone?: "neutral" | "accent";
@@ -422,8 +443,8 @@ function ToolbarSection(props: {
422
443
  children: React.ReactNode;
423
444
  }) {
424
445
  return (
425
- <div className="flex flex-wrap items-center gap-1 rounded-md bg-surface/60 px-1.5 py-1 ring-1 ring-border/35">
426
- <span className="text-[9px] font-semibold uppercase tracking-[0.08em] text-tertiary">
446
+ <div className="flex flex-wrap items-center gap-1 rounded-md bg-[var(--color-bg-muted)]/60 px-1.5 py-1 ring-1 ring-[var(--color-border-subtle)]/35">
447
+ <span className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[var(--color-text-tertiary)]">
427
448
  {props.label}
428
449
  </span>
429
450
  <div className="flex flex-wrap items-center gap-1">{props.children}</div>
@@ -455,7 +476,7 @@ function ToolbarButton(props: {
455
476
  props.active
456
477
  ? "bg-accent/15 text-accent"
457
478
  : props.danger
458
- ? "text-danger hover:bg-danger/10"
479
+ ? "text-[var(--color-semantic-error)] hover:bg-[var(--color-semantic-error-soft)]"
459
480
  : "text-primary hover:bg-surface"
460
481
  }`}
461
482
  >
@@ -21,7 +21,7 @@
21
21
  * grip so PM still receives the click and places the caret.
22
22
  */
23
23
 
24
- import React, { useCallback, useEffect, useRef } from "react";
24
+ import React, { useCallback, useEffect, useRef, useState } from "react";
25
25
 
26
26
  import type {
27
27
  TableStructureContextSnapshot,
@@ -32,7 +32,8 @@ import { projectRectToOverlay } from "../chrome-overlay/chrome-overlay-projector
32
32
  import { DEFAULT_PX_PER_TWIP } from "../../runtime/render/render-frame-types";
33
33
  import { forwardNonDragClick } from "./forward-non-drag-click";
34
34
 
35
- const GRIP_PX = 6;
35
+ const GRIP_PX = 2;
36
+ const GRIP_HIT_PX = 8; // total hit area (2 px visual + 3 px pad each side)
36
37
  const DRAG_THRESHOLD_PX = 3;
37
38
  const MIN_COLUMN_TWIPS = 720;
38
39
  const MIN_ROW_TWIPS = 120;
@@ -143,6 +144,7 @@ function ColResizeGrip({
143
144
  dragStarted: boolean;
144
145
  gripEl: HTMLElement;
145
146
  } | null>(null);
147
+ const [isActive, setIsActive] = useState(false);
146
148
 
147
149
  const handleMouseDown = useCallback(
148
150
  (e: React.MouseEvent<HTMLElement>) => {
@@ -164,14 +166,17 @@ function ColResizeGrip({
164
166
  if (!drag.dragStarted) {
165
167
  if (Math.abs(e.clientX - drag.startX) < DRAG_THRESHOLD_PX) return;
166
168
  drag.dragStarted = true;
169
+ setIsActive(true);
167
170
  }
168
171
  e.preventDefault();
169
172
  };
170
173
  const handleUp = (e: MouseEvent) => {
171
174
  const drag = dragRef.current;
172
175
  if (!drag) return;
176
+ const wasDragging = drag.dragStarted;
173
177
  dragRef.current = null;
174
- if (!drag.dragStarted) {
178
+ setIsActive(false);
179
+ if (!wasDragging) {
175
180
  forwardNonDragClick(drag.gripEl, e);
176
181
  return;
177
182
  }
@@ -196,18 +201,17 @@ function ColResizeGrip({
196
201
  aria-orientation="vertical"
197
202
  aria-label={`Resize column ${colIndex + 1}`}
198
203
  data-testid={`col-resize-grip-${colIndex}`}
204
+ data-active={isActive ? "true" : "false"}
199
205
  className={[
200
- "pointer-events-auto absolute",
201
- disabled
202
- ? "cursor-default opacity-0"
203
- : "cursor-col-resize opacity-0 hover:opacity-100",
204
- "transition-opacity",
205
- "bg-accent",
206
- ].join(" ")}
206
+ "wre-table-grip-col pointer-events-auto absolute",
207
+ disabled ? "opacity-0 cursor-default" : "",
208
+ ]
209
+ .filter(Boolean)
210
+ .join(" ")}
207
211
  style={{
208
- left: `calc(${pos.left} - ${GRIP_PX / 2}px)`,
212
+ left: `calc(${pos.left} - ${GRIP_HIT_PX / 2}px)`,
209
213
  top: pos.top,
210
- width: `${GRIP_PX}px`,
214
+ width: `${GRIP_HIT_PX}px`,
211
215
  height: pos.height,
212
216
  }}
213
217
  onMouseDown={handleMouseDown}
@@ -243,6 +247,7 @@ function RowResizeGrip({
243
247
  dragStarted: boolean;
244
248
  gripEl: HTMLElement;
245
249
  } | null>(null);
250
+ const [isActive, setIsActive] = useState(false);
246
251
 
247
252
  const handleMouseDown = useCallback(
248
253
  (e: React.MouseEvent<HTMLElement>) => {
@@ -263,14 +268,17 @@ function RowResizeGrip({
263
268
  if (!drag.dragStarted) {
264
269
  if (Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD_PX) return;
265
270
  drag.dragStarted = true;
271
+ setIsActive(true);
266
272
  }
267
273
  e.preventDefault();
268
274
  };
269
275
  const handleUp = (e: MouseEvent) => {
270
276
  const drag = dragRef.current;
271
277
  if (!drag) return;
278
+ const wasDragging = drag.dragStarted;
272
279
  dragRef.current = null;
273
- if (!drag.dragStarted) {
280
+ setIsActive(false);
281
+ if (!wasDragging) {
274
282
  forwardNonDragClick(drag.gripEl, e);
275
283
  return;
276
284
  }
@@ -296,19 +304,18 @@ function RowResizeGrip({
296
304
  aria-orientation="horizontal"
297
305
  aria-label={`Resize row ${rowIndex + 1}`}
298
306
  data-testid={`row-resize-grip-${rowIndex}`}
307
+ data-active={isActive ? "true" : "false"}
299
308
  className={[
300
- "pointer-events-auto absolute",
301
- disabled
302
- ? "cursor-default opacity-0"
303
- : "cursor-row-resize opacity-0 hover:opacity-100",
304
- "transition-opacity",
305
- "bg-accent",
306
- ].join(" ")}
309
+ "wre-table-grip-row pointer-events-auto absolute",
310
+ disabled ? "opacity-0 cursor-default" : "",
311
+ ]
312
+ .filter(Boolean)
313
+ .join(" ")}
307
314
  style={{
308
315
  left: pos.left,
309
- top: `calc(${pos.top} - ${GRIP_PX / 2}px)`,
316
+ top: `calc(${pos.top} - ${GRIP_HIT_PX / 2}px)`,
310
317
  width: pos.width,
311
- height: `${GRIP_PX}px`,
318
+ height: `${GRIP_HIT_PX}px`,
312
319
  }}
313
320
  onMouseDown={handleMouseDown}
314
321
  />
@@ -8,28 +8,76 @@ export interface TwUnsavedModalProps {
8
8
  onCancel: () => void;
9
9
  }
10
10
 
11
+ /**
12
+ * TwUnsavedModal — blocking confirmation modal for close-with-unsaved-changes
13
+ * (designsystem §6.18). Lane 6b §6b.S7 rebinds every surface, shadow,
14
+ * radius, and colour to the Lane 6a token vocabulary:
15
+ *
16
+ * backdrop → bg-[var(--color-bg-overlay)] + backdrop-blur
17
+ * card → bg-[var(--color-bg-elevated)]
18
+ * shadow-[var(--shadow-float)]
19
+ * ring-1 ring-[var(--color-border-subtle)]
20
+ * rounded-[var(--radius-sm)]
21
+ * icon → bg-[var(--color-semantic-warning-soft)] / text-semantic-warning
22
+ * cancel → hover:bg-[var(--color-bg-hover)]
23
+ * discard → bg-[var(--color-semantic-error)] + hover shade
24
+ *
25
+ * Every transition binds `duration-[var(--motion-fast)]` so the reduced-
26
+ * motion media override in tokens.css zeroes out appropriately.
27
+ */
11
28
  export function TwUnsavedModal(props: TwUnsavedModalProps) {
12
29
  if (!props.open) return null;
13
30
 
14
31
  return (
15
- <div className="fixed inset-0 z-50 flex items-center justify-center">
32
+ <div
33
+ className="fixed inset-0 z-50 flex items-center justify-center"
34
+ role="dialog"
35
+ aria-modal="true"
36
+ aria-labelledby="tw-unsaved-modal__title"
37
+ data-testid="tw-unsaved-modal"
38
+ >
16
39
  {/* Backdrop */}
17
40
  <div
18
- className="absolute inset-0 bg-black/30 backdrop-blur-sm"
41
+ className={[
42
+ "absolute inset-0 bg-[var(--color-bg-overlay)] backdrop-blur-sm",
43
+ "transition-opacity duration-[var(--motion-fast)]",
44
+ ].join(" ")}
19
45
  onClick={props.onCancel}
46
+ data-testid="tw-unsaved-modal__backdrop"
20
47
  />
21
48
 
22
- {/* Modal */}
23
- <div className="relative mx-4 w-full max-w-md rounded-xl bg-canvas p-6 shadow-lg ring-1 ring-border">
49
+ {/* Card */}
50
+ <div
51
+ className={[
52
+ "relative mx-4 w-full max-w-md p-6",
53
+ "rounded-[var(--radius-sm)]",
54
+ "bg-[var(--color-bg-elevated)]",
55
+ "shadow-[var(--shadow-float)]",
56
+ "ring-1 ring-[var(--color-border-subtle)]",
57
+ ].join(" ")}
58
+ data-testid="tw-unsaved-modal__card"
59
+ >
24
60
  <div className="flex items-start gap-3">
25
- <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-warning-soft">
26
- <AlertTriangle className="h-5 w-5 text-warning" />
61
+ <div
62
+ className={[
63
+ "flex h-9 w-9 shrink-0 items-center justify-center",
64
+ "rounded-[var(--radius-pill)]",
65
+ "bg-[var(--color-semantic-warning-soft)]",
66
+ ].join(" ")}
67
+ >
68
+ <AlertTriangle
69
+ className="h-5 w-5 text-[var(--color-semantic-warning)]"
70
+ aria-hidden="true"
71
+ />
27
72
  </div>
28
73
  <div className="flex-1">
29
- <h3 className="text-base font-semibold text-primary">
74
+ <h3
75
+ id="tw-unsaved-modal__title"
76
+ className="text-base font-semibold text-[var(--color-text-primary)]"
77
+ >
30
78
  Unsaved changes
31
79
  </h3>
32
- <p className="mt-1.5 text-sm text-secondary leading-relaxed">
80
+ <p className="mt-1.5 text-sm leading-relaxed text-[var(--color-text-secondary)]">
33
81
  {props.message ??
34
82
  "You have unsaved changes that will be lost. Your work is being autosaved, but the latest edits may not be saved yet."}
35
83
  </p>
@@ -40,14 +88,28 @@ export function TwUnsavedModal(props: TwUnsavedModalProps) {
40
88
  <button
41
89
  type="button"
42
90
  onClick={props.onCancel}
43
- className="rounded-lg px-4 py-2 text-sm font-medium text-secondary hover:bg-surface transition-colors"
91
+ className={[
92
+ "rounded-[var(--radius-sm)] px-4 py-2 text-sm font-medium",
93
+ "text-[var(--color-text-secondary)]",
94
+ "hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-primary)]",
95
+ "transition-colors duration-[var(--motion-fast)]",
96
+ "focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
97
+ ].join(" ")}
98
+ data-testid="tw-unsaved-modal__cancel"
44
99
  >
45
100
  Keep editing
46
101
  </button>
47
102
  <button
48
103
  type="button"
49
104
  onClick={props.onDiscard}
50
- className="rounded-lg bg-danger px-4 py-2 text-sm font-medium text-white hover:bg-danger/90 transition-colors"
105
+ className={[
106
+ "rounded-[var(--radius-sm)] px-4 py-2 text-sm font-medium",
107
+ "bg-[var(--color-semantic-error)] text-[var(--color-text-on-accent)]",
108
+ "hover:bg-[var(--color-semantic-error)]/90",
109
+ "transition-colors duration-[var(--motion-fast)]",
110
+ "focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
111
+ ].join(" ")}
112
+ data-testid="tw-unsaved-modal__discard"
51
113
  >
52
114
  Discard changes
53
115
  </button>