@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
@@ -587,6 +587,14 @@
587
587
  .wre-scope-rail-stripe.wre-scope-rail-label-secondary { color: var(--color-secondary); }
588
588
  .wre-scope-rail-stripe.wre-scope-rail-label-danger { color: var(--color-danger); }
589
589
 
590
+ /* §3.7 canonical scope families — stripe color */
591
+ .wre-scope-rail-stripe.wre-scope-rail-tint-blocked { background: var(--color-scope-tint-blocked); }
592
+ .wre-scope-rail-stripe.wre-scope-rail-tint-in-scope { background: var(--color-scope-tint-in-scope); }
593
+ .wre-scope-rail-stripe.wre-scope-rail-tint-suggest { background: var(--color-scope-tint-suggest); }
594
+ .wre-scope-rail-stripe.wre-scope-rail-tint-comment { background: var(--color-scope-tint-comment); }
595
+ .wre-scope-rail-stripe.wre-scope-rail-tint-scheduled { background: var(--color-scope-tint-scheduled); }
596
+ .wre-scope-rail-stripe.wre-scope-rail-tint-proposed { background: var(--color-scope-tint-proposed); }
597
+
590
598
  /*
591
599
  * ─── Scope rail label pill ───
592
600
  *
@@ -652,6 +660,38 @@
652
660
  background: color-mix(in srgb, var(--color-danger) 8%, var(--color-canvas, #fff));
653
661
  }
654
662
 
663
+ /* §3.7 canonical scope families — label pill */
664
+ .wre-scope-rail-label-blocked {
665
+ color: var(--color-semantic-error);
666
+ border-color: color-mix(in srgb, var(--color-semantic-error) 40%, transparent);
667
+ background: color-mix(in srgb, var(--color-semantic-error) 8%, var(--color-canvas, #fff));
668
+ }
669
+ .wre-scope-rail-label-suggest {
670
+ color: var(--color-semantic-warning);
671
+ border-color: color-mix(in srgb, var(--color-semantic-warning) 42%, transparent);
672
+ background: color-mix(in srgb, var(--color-semantic-warning) 8%, var(--color-canvas, #fff));
673
+ }
674
+ .wre-scope-rail-label-scheduled {
675
+ color: var(--color-semantic-info);
676
+ border-color: color-mix(in srgb, var(--color-semantic-info) 40%, transparent);
677
+ background: color-mix(in srgb, var(--color-semantic-info) 8%, var(--color-canvas, #fff));
678
+ }
679
+ .wre-scope-rail-label-comment {
680
+ color: var(--color-accent-primary);
681
+ border-color: color-mix(in srgb, var(--color-accent-primary) 40%, transparent);
682
+ background: color-mix(in srgb, var(--color-accent-primary) 8%, var(--color-canvas, #fff));
683
+ }
684
+ .wre-scope-rail-label-in-scope {
685
+ color: var(--color-accent-primary);
686
+ border-color: color-mix(in srgb, var(--color-accent-primary) 40%, transparent);
687
+ background: color-mix(in srgb, var(--color-accent-primary) 8%, var(--color-canvas, #fff));
688
+ }
689
+ .wre-scope-rail-label-proposed {
690
+ color: var(--color-accent-primary);
691
+ border-color: color-mix(in srgb, var(--color-accent-primary) 40%, transparent);
692
+ background: color-mix(in srgb, var(--color-accent-primary) 8%, var(--color-canvas, #fff));
693
+ }
694
+
655
695
  .wre-scope-rail-label-active {
656
696
  box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent);
657
697
  }
@@ -784,10 +824,14 @@
784
824
  * — consumers pick whichever matches their payload shape.
785
825
  */
786
826
  .wre-rail-card {
827
+ --wre-rail-card-edge: var(--color-border-default);
828
+ --wre-rail-card-eyebrow: var(--color-text-tertiary);
787
829
  position: relative;
788
- border-radius: var(--radius-card);
789
- background: var(--color-surface);
790
- border: 1px solid color-mix(in srgb, var(--color-border) 80%, transparent);
830
+ border-radius: var(--radius-lg);
831
+ background: var(--color-bg-elevated);
832
+ border: 1px solid var(--color-border-subtle);
833
+ border-left: 3px solid var(--wre-rail-card-edge);
834
+ box-shadow: var(--shadow-soft);
791
835
  padding: 14px 16px 16px;
792
836
  overflow: hidden;
793
837
  transition: background-color var(--motion-fast) ease-out,
@@ -812,34 +856,23 @@
812
856
  }
813
857
 
814
858
  .wre-rail-card[data-tone="inReview"] {
815
- --wre-rail-card-edge: var(--color-accent);
816
- --wre-rail-card-eyebrow: var(--color-accent);
817
- --wre-rail-card-progress-fill: var(--color-accent);
859
+ --wre-rail-card-edge: var(--color-semantic-warning);
860
+ --wre-rail-card-eyebrow: var(--color-semantic-warning);
818
861
  }
819
862
 
820
863
  .wre-rail-card[data-tone="blocked"] {
821
- --wre-rail-card-edge: var(--color-danger);
822
- --wre-rail-card-eyebrow: var(--color-danger);
823
- --wre-rail-card-progress-fill: var(--color-danger);
824
- background: color-mix(in srgb, var(--color-danger-soft) 75%, var(--color-surface));
864
+ --wre-rail-card-edge: var(--color-semantic-error);
865
+ --wre-rail-card-eyebrow: var(--color-semantic-error);
825
866
  }
826
867
 
827
868
  .wre-rail-card[data-tone="scheduled"] {
828
- --wre-rail-card-edge: var(--color-tertiary);
829
- --wre-rail-card-eyebrow: var(--color-tertiary);
830
- --wre-rail-card-progress-fill: var(--color-tertiary);
869
+ --wre-rail-card-edge: var(--color-semantic-info);
870
+ --wre-rail-card-eyebrow: var(--color-semantic-info);
831
871
  }
832
872
 
833
873
  .wre-rail-card[data-tone="resolved"] {
834
- --wre-rail-card-edge: var(--color-success);
835
- --wre-rail-card-eyebrow: var(--color-success);
836
- --wre-rail-card-progress-fill: var(--color-success);
837
- }
838
-
839
- .wre-rail-card[data-tone="neutral"] {
840
- --wre-rail-card-edge: var(--color-border-strong);
841
- --wre-rail-card-eyebrow: var(--color-tertiary);
842
- --wre-rail-card-progress-fill: var(--color-secondary);
874
+ --wre-rail-card-edge: var(--color-semantic-success);
875
+ --wre-rail-card-eyebrow: var(--color-semantic-success);
843
876
  }
844
877
 
845
878
  .wre-rail-card__eyebrow {
@@ -1009,3 +1042,197 @@
1009
1042
  color: var(--color-primary);
1010
1043
  background: color-mix(in srgb, var(--color-accent-soft) 80%, var(--color-subtle));
1011
1044
  }
1045
+
1046
+ /*
1047
+ * Lane 6d — Slice U1 (L8 Phase E): active H/F band chrome.
1048
+ *
1049
+ * Idle bands sit at 0.6 opacity so the header/footer content is
1050
+ * visible but doesn't compete with the body; hover bumps to 0.85; the
1051
+ * active band goes to full opacity and gains a 3 px accent ribbon
1052
+ * across its top edge plus a floating "Header — Section N" label in
1053
+ * the top-left corner so reviewers always know which band they are
1054
+ * editing when a section break changes the chrome.
1055
+ *
1056
+ * When any H/F story is active the chrome layer's root carries
1057
+ * `data-story-active="header|footer"` and the body PM surface dims to
1058
+ * 0.65 so the active band reads as the focal surface.
1059
+ */
1060
+ .wre-page-band {
1061
+ opacity: 0.6;
1062
+ transition: opacity var(--motion-fast, 120ms) ease-out;
1063
+ pointer-events: auto;
1064
+ }
1065
+
1066
+ .wre-page-band:hover {
1067
+ opacity: 0.85;
1068
+ }
1069
+
1070
+ .wre-page-band[data-active="true"] {
1071
+ opacity: 1;
1072
+ }
1073
+
1074
+ .wre-page-band[data-active="true"]::before {
1075
+ content: "";
1076
+ position: absolute;
1077
+ top: 0;
1078
+ left: 0;
1079
+ right: 0;
1080
+ height: 3px;
1081
+ background: var(--color-accent);
1082
+ border-radius: 0 0 var(--radius-pill) var(--radius-pill);
1083
+ pointer-events: none;
1084
+ }
1085
+
1086
+ .wre-page-band__label {
1087
+ position: absolute;
1088
+ top: 6px;
1089
+ left: 10px;
1090
+ font-size: 10px;
1091
+ letter-spacing: 0.08em;
1092
+ text-transform: uppercase;
1093
+ color: var(--color-accent);
1094
+ background: var(--color-surface);
1095
+ padding: 2px 8px;
1096
+ border-radius: var(--radius-pill);
1097
+ box-shadow: var(--shadow-soft);
1098
+ pointer-events: none;
1099
+ z-index: 1;
1100
+ }
1101
+
1102
+ [data-page-stack-chrome-layer][data-story-active="header"]
1103
+ ~ [data-pm-body-slot],
1104
+ [data-page-stack-chrome-layer][data-story-active="footer"]
1105
+ ~ [data-pm-body-slot] {
1106
+ opacity: 0.65;
1107
+ transition: opacity var(--motion-fast, 120ms) ease-out;
1108
+ }
1109
+
1110
+ /*
1111
+ * Lane 6d — Slice S2: print variant.
1112
+ *
1113
+ * At print time, collapse all editor chrome (toolbar, rails, overlays,
1114
+ * page-break spacers, dock chrome) and let the browser paginate the
1115
+ * paper cards one per physical sheet. Intentionally no `@page` rule —
1116
+ * the browser's print dialog owns paper size / margins so the host
1117
+ * application doesn't need to fight it.
1118
+ */
1119
+ @media print {
1120
+ .wre-toolbar,
1121
+ .wre-status-bar,
1122
+ .wre-review-rail,
1123
+ .wre-scope-rail-layer,
1124
+ .wre-page-chrome-widget,
1125
+ .wre-workspace-dock,
1126
+ .wre-mode-dock,
1127
+ [data-chrome-overlay],
1128
+ [data-pin-surface] {
1129
+ display: none !important;
1130
+ }
1131
+
1132
+ [data-paper-frame] {
1133
+ box-shadow: none !important;
1134
+ border: none !important;
1135
+ border-radius: 0 !important;
1136
+ margin: 0 !important;
1137
+ zoom: 1 !important;
1138
+ page-break-after: always;
1139
+ }
1140
+
1141
+ [data-paper-frame]:last-child {
1142
+ page-break-after: auto;
1143
+ }
1144
+
1145
+ .wre-page-surface {
1146
+ color: #000;
1147
+ background: #fff;
1148
+ }
1149
+ }
1150
+
1151
+ /*
1152
+ * §6.23 — Scrollbar grammar (Lane 6c.S10)
1153
+ * Thin track, thumb low-contrast until hover. Consistent across
1154
+ * rail, long popovers, and the health panel.
1155
+ * Scoped to the editor root class to avoid leaking into host chrome.
1156
+ */
1157
+ .wre-editor ::-webkit-scrollbar,
1158
+ .wre-editor *::-webkit-scrollbar {
1159
+ width: 6px;
1160
+ height: 6px;
1161
+ }
1162
+ .wre-editor ::-webkit-scrollbar-track,
1163
+ .wre-editor *::-webkit-scrollbar-track {
1164
+ background: transparent;
1165
+ }
1166
+ .wre-editor ::-webkit-scrollbar-thumb,
1167
+ .wre-editor *::-webkit-scrollbar-thumb {
1168
+ background: var(--color-border-default);
1169
+ border-radius: 9999px;
1170
+ }
1171
+ .wre-editor ::-webkit-scrollbar-thumb:hover,
1172
+ .wre-editor *::-webkit-scrollbar-thumb:hover {
1173
+ background: var(--color-border-strong);
1174
+ }
1175
+ .wre-editor,
1176
+ .wre-editor * {
1177
+ scrollbar-width: thin;
1178
+ scrollbar-color: var(--color-border-default) transparent;
1179
+ }
1180
+
1181
+ /*
1182
+ * §6.9 — Table grip hit area + affordance (Lane 6c.U8 / Commit C18)
1183
+ *
1184
+ * Visual stripe is 2 px; hit area is extended to 8 px via ::before pseudo-
1185
+ * element (3 px pad each side). On coarse-pointer devices the ::before extends
1186
+ * to 44 px total for WCAG 2.5.5 touch-target compliance.
1187
+ *
1188
+ * State cascade:
1189
+ * default → transparent background (invisible, pointer affordance only)
1190
+ * hover → --color-border-default (subtle stripe appears)
1191
+ * active → --color-accent-primary (vivid stripe during drag)
1192
+ */
1193
+ .wre-table-grip-col {
1194
+ position: absolute;
1195
+ width: 2px;
1196
+ background: transparent;
1197
+ cursor: col-resize;
1198
+ transition: background 75ms ease;
1199
+ }
1200
+ .wre-table-grip-col::before {
1201
+ content: "";
1202
+ position: absolute;
1203
+ inset: 0 -3px;
1204
+ }
1205
+ .wre-table-grip-col:hover {
1206
+ background: var(--color-border-default);
1207
+ }
1208
+ .wre-table-grip-col[data-active="true"] {
1209
+ background: var(--color-accent-primary);
1210
+ }
1211
+
1212
+ .wre-table-grip-row {
1213
+ position: absolute;
1214
+ height: 2px;
1215
+ background: transparent;
1216
+ cursor: row-resize;
1217
+ transition: background 75ms ease;
1218
+ }
1219
+ .wre-table-grip-row::before {
1220
+ content: "";
1221
+ position: absolute;
1222
+ inset: -3px 0;
1223
+ }
1224
+ .wre-table-grip-row:hover {
1225
+ background: var(--color-border-default);
1226
+ }
1227
+ .wre-table-grip-row[data-active="true"] {
1228
+ background: var(--color-accent-primary);
1229
+ }
1230
+
1231
+ @media (pointer: coarse) {
1232
+ .wre-table-grip-col::before {
1233
+ inset: 0 -21px; /* 44 px total touch hit area */
1234
+ }
1235
+ .wre-table-grip-row::before {
1236
+ inset: -21px 0; /* 44 px total touch hit area */
1237
+ }
1238
+ }
@@ -52,7 +52,20 @@ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-edi
52
52
  import { ROLE_ACTION_SETS } from "../chrome/role-action-sets";
53
53
  import { TwScopePostureMenu } from "./tw-scope-posture-menu";
54
54
 
55
- export type MarkupDisplayMode = "clean" | "simple" | "all";
55
+ /**
56
+ * Toolbar-local alias for the markup-display union. L6d.N2 widens
57
+ * this to match the `MarkupDisplay` surface in
58
+ * `comment-decoration-model.ts`, which now accepts Word's 4-mode
59
+ * grammar plus the legacy triple.
60
+ */
61
+ export type MarkupDisplayMode =
62
+ | "all-markup"
63
+ | "simple-markup"
64
+ | "no-markup"
65
+ | "original"
66
+ | "clean"
67
+ | "simple"
68
+ | "all";
56
69
 
57
70
  export interface WorkflowWorkItemSnapshot {
58
71
  workItemId: string;
@@ -4,15 +4,20 @@ import * as Tabs from "@radix-ui/react-tabs";
4
4
  /**
5
5
  * TwShellHeader — the top "app chrome" bar above the document canvas.
6
6
  *
7
- * Anatomy matches the editorial reference mock:
7
+ * Designsystem §6.1 three subregions on a single 48-px row:
8
8
  * ┌─────────────────────────────────────────────────────────────────────┐
9
- * │ [brand] Edit | Review | Workflow | More [⋯] [CTA]
9
+ * │ LEFT CENTER │ RIGHT │
10
+ * │ brand + slot │ mode tabs (always-on) │ icon actions + CTA │
10
11
  * └─────────────────────────────────────────────────────────────────────┘
11
12
  *
12
- * All three regions are optional slots hosts opt in by supplying the
13
- * corresponding prop. When nothing is supplied the header renders empty but
14
- * preserves layout height so the document canvas does not jump when a CTA
15
- * appears.
13
+ * Layout is CSS-grid `grid-cols-[1fr_auto_1fr]` so the center zone is
14
+ * always visually centred regardless of left / right slot width. The
15
+ * 4-mode switcher (edit / review / workflow / more) renders
16
+ * unconditionally — Lane 6b §6b.S1 flips it from opt-in to always-on.
17
+ *
18
+ * All colors, shadows, radius, and motion bind to the Lane 6a token
19
+ * substrate (`var(--color-*)` / `var(--shadow-*)` / `var(--radius-*)` /
20
+ * `var(--motion-*)`) — no hex literals or legacy Tailwind palette names.
16
21
  */
17
22
 
18
23
  export type ShellHeaderMode = "edit" | "review" | "workflow" | "more";
@@ -40,6 +45,10 @@ export interface ShellHeaderIconAction {
40
45
 
41
46
  export interface TwShellHeaderProps {
42
47
  brand?: ReactNode;
48
+ /**
49
+ * Mode tab options. When omitted, a default 4-mode set is rendered so
50
+ * the center subregion always has tabs per designsystem §6.1.
51
+ */
43
52
  modes?: readonly ShellHeaderModeOption[];
44
53
  activeMode?: ShellHeaderMode;
45
54
  onModeChange?: (mode: ShellHeaderMode) => void;
@@ -50,23 +59,33 @@ export interface TwShellHeaderProps {
50
59
  className?: string;
51
60
  }
52
61
 
53
- const focusRingClass =
54
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-canvas";
62
+ /**
63
+ * Default 4-mode set — designsystem §6.1. Exported so hosts can extend
64
+ * / relabel without reconstructing the whole list.
65
+ */
66
+ export const DEFAULT_SHELL_HEADER_MODES: readonly ShellHeaderModeOption[] = [
67
+ { id: "edit", label: "Edit" },
68
+ { id: "review", label: "Review" },
69
+ { id: "workflow", label: "Workflow" },
70
+ { id: "more", label: "More" },
71
+ ];
55
72
 
56
- export function TwShellHeader(props: TwShellHeaderProps) {
57
- const hasContent =
58
- props.brand ||
59
- (props.modes && props.modes.length > 0) ||
60
- (props.iconActions && props.iconActions.length > 0) ||
61
- props.primaryAction;
73
+ const focusRingClass =
74
+ "focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]";
62
75
 
63
- if (!hasContent) {
64
- return null;
65
- }
76
+ export function TwShellHeader(props: TwShellHeaderProps): React.ReactElement {
77
+ const modes =
78
+ props.modes && props.modes.length > 0
79
+ ? props.modes
80
+ : DEFAULT_SHELL_HEADER_MODES;
81
+ const activeMode: ShellHeaderMode = props.activeMode ?? modes[0]!.id;
66
82
 
67
83
  const className = [
68
- "flex h-12 shrink-0 items-center justify-between gap-4 px-4 bg-canvas/92 backdrop-blur-sm",
69
- props.isScrolled ? "border-b border-border" : "border-b border-transparent",
84
+ "grid h-12 shrink-0 grid-cols-[1fr_auto_1fr] items-center gap-2 px-4",
85
+ "bg-[var(--color-bg-chrome)]/92 backdrop-blur-sm",
86
+ props.isScrolled
87
+ ? "border-b border-[var(--color-border-subtle)]"
88
+ : "border-b border-transparent",
70
89
  "transition-colors duration-[var(--motion-fast)]",
71
90
  props.className,
72
91
  ]
@@ -75,10 +94,15 @@ export function TwShellHeader(props: TwShellHeaderProps) {
75
94
 
76
95
  return (
77
96
  <header className={className} data-testid="tw-shell-header">
78
- <div className="flex min-w-0 items-center gap-3">
97
+ {/* LEFT: brand + host-supplied slot */}
98
+ <div
99
+ className="flex min-w-0 items-center gap-3"
100
+ data-region="left"
101
+ data-testid="tw-shell-header__region-left"
102
+ >
79
103
  {props.brand ? (
80
104
  <div
81
- className="font-[family-name:var(--font-legal-serif)] text-[15px] font-semibold text-primary truncate"
105
+ className="truncate font-[family-name:var(--font-legal-serif)] text-[15px] font-semibold text-[var(--color-text-primary)]"
82
106
  data-testid="tw-shell-header__brand"
83
107
  >
84
108
  {props.brand}
@@ -86,9 +110,14 @@ export function TwShellHeader(props: TwShellHeaderProps) {
86
110
  ) : null}
87
111
  </div>
88
112
 
89
- {props.modes && props.modes.length > 0 && props.activeMode ? (
113
+ {/* CENTER: mode tabs (always on) */}
114
+ <div
115
+ className="flex items-center justify-center"
116
+ data-region="center"
117
+ data-testid="tw-shell-header__region-center"
118
+ >
90
119
  <Tabs.Root
91
- value={props.activeMode}
120
+ value={activeMode}
92
121
  onValueChange={(v: string) =>
93
122
  props.onModeChange?.(v as ShellHeaderMode)
94
123
  }
@@ -97,7 +126,7 @@ export function TwShellHeader(props: TwShellHeaderProps) {
97
126
  aria-label="Workspace modes"
98
127
  className="flex items-center gap-1"
99
128
  >
100
- {props.modes.map((mode) => (
129
+ {modes.map((mode) => (
101
130
  <Tabs.Trigger
102
131
  key={mode.id}
103
132
  value={mode.id}
@@ -110,17 +139,26 @@ export function TwShellHeader(props: TwShellHeaderProps) {
110
139
  ))}
111
140
  </Tabs.List>
112
141
  </Tabs.Root>
113
- ) : (
114
- <div aria-hidden="true" />
115
- )}
142
+ </div>
116
143
 
117
- <div className="flex items-center gap-1">
144
+ {/* RIGHT: icon actions + primary CTA */}
145
+ <div
146
+ className="flex items-center justify-end gap-1"
147
+ data-region="right"
148
+ data-testid="tw-shell-header__region-right"
149
+ >
118
150
  {props.iconActions?.map((action) => {
119
151
  const commonProps = {
120
152
  key: action.id,
121
153
  "aria-label": action.label,
122
154
  title: action.label,
123
- className: `inline-flex h-8 w-8 items-center justify-center rounded-sm text-secondary transition-colors hover:bg-surface-hover hover:text-primary ${focusRingClass}`,
155
+ className: [
156
+ "inline-flex h-8 w-8 items-center justify-center rounded-[var(--radius-sm)]",
157
+ "text-[var(--color-text-secondary)]",
158
+ "transition-colors duration-[var(--motion-fast)]",
159
+ "hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-primary)]",
160
+ focusRingClass,
161
+ ].join(" "),
124
162
  } as const;
125
163
 
126
164
  if (action.href) {
@@ -145,10 +183,13 @@ export function TwShellHeader(props: TwShellHeaderProps) {
145
183
  onClick={props.primaryAction.onClick}
146
184
  data-tone={props.primaryAction.tone ?? "accent"}
147
185
  className={[
148
- "ml-2 inline-flex h-8 items-center rounded-sm px-3 text-xs font-semibold transition-colors disabled:opacity-40",
186
+ "ml-2 inline-flex h-8 items-center rounded-[var(--radius-sm)] px-3",
187
+ "text-xs font-semibold",
188
+ "transition-colors duration-[var(--motion-fast)]",
189
+ "disabled:opacity-40",
149
190
  props.primaryAction.tone === "neutral"
150
- ? "bg-surface text-primary hover:bg-surface-hover"
151
- : "bg-accent text-white hover:bg-[color-mix(in_srgb,var(--color-accent)_85%,#000)]",
191
+ ? "bg-[var(--color-bg-muted)] text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)]"
192
+ : "bg-[var(--color-accent-primary)] text-[var(--color-text-on-accent)] hover:bg-[var(--color-accent-primary-hover)]",
152
193
  focusRingClass,
153
194
  ].join(" ")}
154
195
  data-testid="tw-shell-header__primary-action"
@@ -7,13 +7,31 @@ export interface TwToolbarIconButtonProps {
7
7
  icon: React.ComponentType<{ className?: string }>;
8
8
  label: string;
9
9
  disabled?: boolean;
10
+ /**
11
+ * Active / pressed state. Designsystem §6.2 + Lane 6b §6b.S2: active
12
+ * buttons paint an accent-soft tint with an accent border ring — NOT a
13
+ * filled accent CTA. The filled-CTA grammar is reserved for the shell
14
+ * header primary action only.
15
+ */
10
16
  active?: boolean;
17
+ /**
18
+ * Emphasis variant — used for "stand-out" toolbar items (e.g. a pinned
19
+ * AI action). Paints the accent glyph color at rest; still tints on
20
+ * hover and on active.
21
+ */
11
22
  emphasis?: boolean;
23
+ /**
24
+ * Lane 6b §6b.U6 — optional keyboard-shortcut hint rendered as a small
25
+ * `<kbd>` chip to the right of the label inside the tooltip. Use
26
+ * platform-agnostic symbols (⌘, ⇧, ⌥, ⌃) — callers format for macOS
27
+ * vs. Windows however they like.
28
+ */
29
+ shortcut?: string;
12
30
  onClick?: () => void;
13
31
  }
14
32
 
15
33
  const focusRingClass =
16
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
34
+ "focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]";
17
35
 
18
36
  export function TwToolbarIconButton(props: TwToolbarIconButtonProps) {
19
37
  return (
@@ -22,17 +40,22 @@ export function TwToolbarIconButton(props: TwToolbarIconButtonProps) {
22
40
  <button
23
41
  type="button"
24
42
  aria-label={props.label}
43
+ aria-pressed={props.active ?? undefined}
44
+ data-active={props.active ? "true" : undefined}
25
45
  disabled={props.disabled}
26
46
  onMouseDown={preserveEditorSelectionMouseDown}
27
47
  onClick={props.onClick}
28
48
  className={[
29
- "inline-flex h-6 w-6 items-center justify-center rounded-md border border-transparent transition-colors outline-none",
49
+ "inline-flex h-6 w-6 items-center justify-center rounded-[var(--radius-sm)]",
50
+ "border border-transparent outline-none",
51
+ "transition-colors duration-[var(--motion-fast)]",
30
52
  "disabled:opacity-30 disabled:cursor-not-allowed",
31
- props.emphasis
32
- ? "text-accent hover:border-border/60 hover:bg-surface"
33
- : props.active
34
- ? "border-border/70 bg-surface text-accent shadow-[0_4px_12px_-10px_var(--color-shadow-strong)]"
35
- : "text-secondary hover:border-border/60 hover:bg-surface hover:text-primary",
53
+ props.active
54
+ ? // Active = underline-tint grammar: accent-soft fill, accent-primary glyph, accent border ring.
55
+ "bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)] border-[var(--color-border-accent)]"
56
+ : props.emphasis
57
+ ? "text-[var(--color-accent-primary)] hover:bg-[var(--color-bg-hover)] hover:border-[var(--color-border-subtle)]"
58
+ : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-primary)] hover:border-[var(--color-border-subtle)]",
36
59
  focusRingClass,
37
60
  ].join(" ")}
38
61
  >
@@ -41,10 +64,27 @@ export function TwToolbarIconButton(props: TwToolbarIconButtonProps) {
41
64
  </Tooltip.Trigger>
42
65
  <Tooltip.Portal>
43
66
  <Tooltip.Content
44
- className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
67
+ className={[
68
+ "inline-flex items-center gap-2 rounded-[var(--radius-sm)] px-2 py-1 text-xs z-50",
69
+ "bg-[var(--color-text-primary)] text-[var(--color-text-inverse)]",
70
+ "shadow-[var(--shadow-soft)]",
71
+ ].join(" ")}
45
72
  sideOffset={6}
46
73
  >
47
- {props.label}
74
+ <span>{props.label}</span>
75
+ {props.shortcut ? (
76
+ <kbd
77
+ className={[
78
+ "inline-flex items-center rounded-[var(--radius-sm)]",
79
+ "px-1 py-0.5 font-sans text-[10px] font-medium",
80
+ "border border-[var(--color-border-subtle)]/40",
81
+ "bg-[var(--color-bg-overlay)] text-[var(--color-text-inverse)]/80",
82
+ ].join(" ")}
83
+ data-testid="tw-toolbar-icon-button__shortcut"
84
+ >
85
+ {props.shortcut}
86
+ </kbd>
87
+ ) : null}
48
88
  </Tooltip.Content>
49
89
  </Tooltip.Portal>
50
90
  </Tooltip.Root>