@beyondwork/docx-react-component 1.0.52 → 1.0.53

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 (29) hide show
  1. package/package.json +31 -40
  2. package/src/api/public-types.ts +32 -0
  3. package/src/io/chart-preview-resolver.ts +41 -0
  4. package/src/io/docx-session.ts +187 -17
  5. package/src/runtime/collab/runtime-collab-sync.ts +87 -6
  6. package/src/runtime/document-runtime.ts +159 -0
  7. package/src/runtime/layout/layout-engine-version.ts +40 -2
  8. package/src/runtime/layout/public-facet.ts +43 -1
  9. package/src/runtime/prerender/cache-envelope.ts +30 -0
  10. package/src/runtime/prerender/customxml-cache.ts +17 -3
  11. package/src/runtime/prerender/prerender-document.ts +17 -1
  12. package/src/runtime/render/render-kernel.ts +67 -19
  13. package/src/runtime/surface-projection.ts +28 -0
  14. package/src/runtime/table-schema.ts +27 -0
  15. package/src/runtime/table-style-resolver.ts +51 -0
  16. package/src/ui/editor-runtime-boundary.ts +39 -2
  17. package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
  18. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
  19. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
  20. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
  21. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +224 -0
  22. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +2 -2
  23. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +2 -2
  24. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +11 -147
  25. package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
  26. package/src/ui-tailwind/theme/editor-theme.css +26 -24
  27. package/src/ui-tailwind/theme/tokens.css +345 -0
  28. package/src/ui-tailwind/theme/tokens.ts +313 -0
  29. package/src/ui-tailwind/theme/use-density.ts +60 -0
@@ -77,11 +77,8 @@ import {
77
77
  resolvePageOverlayRects,
78
78
  type PageOverlayRect,
79
79
  } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
80
- import { TwPageHeaderBand } from "./tw-page-header-band.tsx";
81
- import { TwPageFooterBand } from "./tw-page-footer-band.tsx";
82
- import { TwFootnoteArea } from "./tw-footnote-area.tsx";
83
80
  import { TwEndnoteArea } from "./tw-endnote-area.tsx";
84
- import { FRAME_PX_PER_TWIP_AT_96DPI } from "../tw-review-workspace.tsx";
81
+ import { TwPageChromeEntry } from "./tw-page-chrome-entry.tsx";
85
82
 
86
83
  /**
87
84
  * Minimal structural type for the PM `EditorView` handle consumed by
@@ -369,115 +366,18 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
369
366
  {rects.map((rect, pageIndex) => {
370
367
  const page = facet.getPage(pageIndex);
371
368
  if (!page) return null;
372
-
373
- // L7 Phase 2.8 — viewport cull. Pages outside the visible range
374
- // (plus overscan) render an empty frame wrapper: the
375
- // `data-page-chrome-frame` + `data-page-index` attributes stay
376
- // put so downstream DOM queries (portal-slot, test hooks) keep
377
- // working, but the four heavy React subtrees below do not mount.
378
- const frameHeightPxForCull = rect.bottomPx - rect.topPx;
379
- if (
380
- visiblePageIndexRange &&
381
- (pageIndex < visiblePageIndexRange.start ||
382
- pageIndex >= visiblePageIndexRange.end)
383
- ) {
384
- return (
385
- <div
386
- key={`page-chrome-${rect.pageId}`}
387
- data-page-chrome-frame=""
388
- data-page-index={pageIndex}
389
- data-page-chrome-culled=""
390
- style={{
391
- position: "absolute",
392
- top: `${rect.topPx}px`,
393
- left: 0,
394
- width: "100%",
395
- height: `${frameHeightPxForCull}px`,
396
- pointerEvents: "none",
397
- }}
398
- />
399
- );
400
- }
401
-
402
- const layout = page.layout;
403
- const headerStory = page.stories.header;
404
- const footerStory = page.stories.footer;
405
- const headerRegion = page.regions.header;
406
- const footerRegion = page.regions.footer;
407
- const footnoteRegion = page.regions.footnotes?.[0];
408
-
409
- const headerBlocks = headerStory
410
- ? facet
411
- .getStoryBlocksForRegion(pageIndex, "header")
412
- .map((b) => b.blockSnapshot)
413
- : [];
414
- const footerBlocks = footerStory
415
- ? facet
416
- .getStoryBlocksForRegion(pageIndex, "footer")
417
- .map((b) => b.blockSnapshot)
418
- : [];
419
- const footnoteBlocks = footnoteRegion
420
- ? facet
421
- .getStoryBlocksForRegion(pageIndex, "footnote-area")
422
- .map((b) => b.blockSnapshot)
423
- : [];
424
-
425
- const px = (twips: number): number => twips * FRAME_PX_PER_TWIP_AT_96DPI;
426
- const bandWidthPx = px(
427
- layout.pageWidth - layout.marginLeft - layout.marginRight,
428
- );
429
- const bandLeftPx = px(layout.marginLeft);
430
- const frameHeightPx = rect.bottomPx - rect.topPx;
431
-
432
369
  return (
433
- <div
370
+ <TwPageChromeEntry
434
371
  key={`page-chrome-${rect.pageId}`}
435
- data-page-chrome-frame=""
436
- data-page-index={pageIndex}
437
- style={{
438
- position: "absolute",
439
- top: `${rect.topPx}px`,
440
- left: 0,
441
- width: "100%",
442
- height: `${frameHeightPx}px`,
443
- pointerEvents: "none",
444
- }}
445
- >
446
- {headerRegion && headerStory ? (
447
- <TwPageHeaderBand
448
- pageIndex={pageIndex}
449
- blocks={headerBlocks}
450
- topPx={px(layout.headerMargin ?? 720)}
451
- leftPx={bandLeftPx}
452
- widthPx={bandWidthPx}
453
- bandHeightPx={px(headerRegion.heightTwips)}
454
- isActiveSlot={isActiveStoryMatch(activeStory, headerStory)}
455
- onClick={() => onOpenStory?.(headerStory)}
456
- />
457
- ) : null}
458
- {footerRegion && footerStory ? (
459
- <TwPageFooterBand
460
- pageIndex={pageIndex}
461
- blocks={footerBlocks}
462
- bottomPx={px(layout.footerMargin ?? 720)}
463
- leftPx={bandLeftPx}
464
- widthPx={bandWidthPx}
465
- bandHeightPx={px(footerRegion.heightTwips)}
466
- isActiveSlot={isActiveStoryMatch(activeStory, footerStory)}
467
- onClick={() => onOpenStory?.(footerStory)}
468
- />
469
- ) : null}
470
- {footnoteRegion ? (
471
- <TwFootnoteArea
472
- pageIndex={pageIndex}
473
- blocks={footnoteBlocks}
474
- topPx={px(footnoteRegion.originTwips - layout.marginTop)}
475
- leftPx={bandLeftPx}
476
- widthPx={px(footnoteRegion.widthTwips)}
477
- heightPx={px(footnoteRegion.heightTwips)}
478
- />
479
- ) : null}
480
- </div>
372
+ rect={rect}
373
+ pageIndex={pageIndex}
374
+ page={page}
375
+ facet={facet}
376
+ activeStory={activeStory}
377
+ onOpenStory={onOpenStory}
378
+ visiblePageIndexRange={visiblePageIndexRange}
379
+ renderFrameRevision={renderFrameRevision}
380
+ />
481
381
  );
482
382
  })}
483
383
  <TwEndnoteArea blocks={endnoteBlocks} />
@@ -485,40 +385,4 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
485
385
  );
486
386
  };
487
387
 
488
- /**
489
- * Strict equality for two `EditorStoryTarget` values — used to decide
490
- * whether a given band should render in active-slot mode.
491
- *
492
- * For `"main"` the kinds must match; for header/footer the triple
493
- * `(relationshipId, variant, sectionIndex)` must match; for footnote /
494
- * endnote the `noteId` must match. Any mismatch returns false.
495
- */
496
- function isActiveStoryMatch(
497
- active: EditorStoryTarget,
498
- candidate: EditorStoryTarget,
499
- ): boolean {
500
- if (active.kind !== candidate.kind) return false;
501
- if (active.kind === "main" || candidate.kind === "main") {
502
- return active.kind === candidate.kind;
503
- }
504
- if (active.kind === "footnote" && candidate.kind === "footnote") {
505
- return active.noteId === candidate.noteId;
506
- }
507
- if (active.kind === "endnote" && candidate.kind === "endnote") {
508
- return active.noteId === candidate.noteId;
509
- }
510
- // header / footer — triple match.
511
- if (
512
- (active.kind === "header" && candidate.kind === "header") ||
513
- (active.kind === "footer" && candidate.kind === "footer")
514
- ) {
515
- return (
516
- active.relationshipId === candidate.relationshipId &&
517
- active.variant === candidate.variant &&
518
- active.sectionIndex === candidate.sectionIndex
519
- );
520
- }
521
- return false;
522
- }
523
-
524
388
  export default TwPageStackChromeLayer;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Runtime chart-palette adapter.
3
+ *
4
+ * Reads live CSS variable values from the document root so that the palette
5
+ * automatically tracks light/dark mode and any host accent overrides applied
6
+ * at the `:root` level. No React dependency — pure module.
7
+ *
8
+ * See `docs/wiki/design-tokens.md` → "Chart palette" section.
9
+ */
10
+
11
+ const CATEGORICAL_COUNT = 8;
12
+ const SEQUENTIAL_COUNT = 6;
13
+
14
+ function cssVar(name: string): string {
15
+ return getComputedStyle(document.documentElement)
16
+ .getPropertyValue(name)
17
+ .trim();
18
+ }
19
+
20
+ /**
21
+ * Returns the CSS value for the categorical series slot at `index`.
22
+ * Wraps around after 8 slots (0-indexed).
23
+ */
24
+ export function getCategoricalAt(index: number): string {
25
+ const slot = (((index % CATEGORICAL_COUNT) + CATEGORICAL_COUNT) % CATEGORICAL_COUNT) + 1;
26
+ return cssVar(`--color-chart-categorical-${slot}`);
27
+ }
28
+
29
+ /**
30
+ * Returns all 6 sequential ramp values in order (lightest → darkest in light
31
+ * mode; darkest → lightest in dark mode — always index 1..6).
32
+ */
33
+ export function getSequentialRamp(): string[] {
34
+ return Array.from({ length: SEQUENTIAL_COUNT }, (_, i) =>
35
+ cssVar(`--color-chart-sequential-${i + 1}`),
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Returns the five diverging scale stops. Keys match the token names so
41
+ * consumers can reference them by semantic intent.
42
+ */
43
+ export function getDivergingScale(): {
44
+ negStrong: string;
45
+ neg: string;
46
+ neutral: string;
47
+ pos: string;
48
+ posStrong: string;
49
+ } {
50
+ return {
51
+ negStrong: cssVar("--color-chart-diverging-neg-strong"),
52
+ neg: cssVar("--color-chart-diverging-neg"),
53
+ neutral: cssVar("--color-chart-diverging-neutral"),
54
+ pos: cssVar("--color-chart-diverging-pos"),
55
+ posStrong: cssVar("--color-chart-diverging-pos-strong"),
56
+ };
57
+ }
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  @import "tailwindcss";
17
+ @import "./tokens.css";
17
18
 
18
19
  @theme {
19
20
  /* Backgrounds */
@@ -32,9 +33,8 @@
32
33
  --color-secondary: #6b6b6b;
33
34
  --color-tertiary: #616161;
34
35
 
35
- /* Accent */
36
- --color-accent: #18b394;
37
- --color-accent-soft: rgba(24, 179, 148, 0.1);
36
+ /* Accent — 6a.S1 forest-green flip. Values resolve through tokens.css. */
37
+ --color-accent: var(--color-accent-primary);
38
38
  --color-workflow: #7557d8;
39
39
  --color-workflow-soft: rgba(117, 87, 216, 0.12);
40
40
 
@@ -123,8 +123,7 @@
123
123
  * the .dark block below so the transform lives in one place.
124
124
  */
125
125
  --shadow-hairline: 0 0 0 1px var(--color-border);
126
- --shadow-soft: 0 4px 14px -10px rgba(16, 24, 40, 0.14);
127
- --shadow-float: 0 12px 28px -20px rgba(16, 24, 40, 0.18);
126
+ /* --shadow-soft / --shadow-float / --shadow-focus owned by tokens.css (§3.3/§3.4). */
128
127
 
129
128
  /*
130
129
  * Tailwind shadow-sm / shadow-md / shadow-lg / shadow-xl follow the runtime
@@ -170,26 +169,10 @@
170
169
  --color-secondary: #A5B7AC;
171
170
  --color-tertiary: #7F9187;
172
171
 
173
- --color-accent: #53B487;
174
- --color-accent-soft: rgba(83, 180, 135, 0.16);
172
+ /* Accent / semantic / change / comment dark values owned by tokens.css. */
175
173
  --color-workflow: #c7b6ff;
176
174
  --color-workflow-soft: rgba(199, 182, 255, 0.16);
177
175
 
178
- --color-comment: #c7b6ff;
179
- --color-comment-soft: rgba(199, 182, 255, 0.18);
180
- --color-comment-strong: rgba(199, 182, 255, 0.24);
181
- --color-insert: #8fd2a9;
182
- --color-insert-soft: rgba(143, 210, 169, 0.18);
183
- --color-delete: #d78d84;
184
- --color-delete-soft: rgba(215, 141, 132, 0.14);
185
-
186
- --color-warning: #c7b6ff;
187
- --color-warning-soft: rgba(199, 182, 255, 0.16);
188
- --color-danger: #d78d84;
189
- --color-danger-soft: rgba(215, 141, 132, 0.16);
190
- --color-success: #8fd2a9;
191
- --color-success-soft: rgba(143, 210, 169, 0.18);
192
-
193
176
  --color-border: rgba(255, 255, 255, 0.09);
194
177
  --color-border-strong: rgba(255, 255, 255, 0.16);
195
178
 
@@ -202,8 +185,7 @@
202
185
  --color-page-ruler: color-mix(in srgb, var(--color-border) 70%, transparent);
203
186
  --color-workspace-canvas: #111827;
204
187
 
205
- --shadow-soft: 0 6px 18px -10px rgba(0, 0, 0, 0.55);
206
- --shadow-float: 0 18px 40px -22px rgba(0, 0, 0, 0.7);
188
+ /* --shadow-soft / --shadow-float / --shadow-focus owned by tokens.css. */
207
189
  }
208
190
 
209
191
  /*
@@ -509,6 +491,26 @@
509
491
  background: color-mix(in srgb, var(--color-danger) 14%, transparent);
510
492
  }
511
493
 
494
+ /* §3.7 canonical scope families */
495
+ .wre-scope-rail-tint-blocked {
496
+ background: var(--color-scope-tint-blocked);
497
+ }
498
+ .wre-scope-rail-tint-in-scope {
499
+ background: var(--color-scope-tint-in-scope);
500
+ }
501
+ .wre-scope-rail-tint-suggest {
502
+ background: var(--color-scope-tint-suggest);
503
+ }
504
+ .wre-scope-rail-tint-comment {
505
+ background: var(--color-scope-tint-comment);
506
+ }
507
+ .wre-scope-rail-tint-scheduled {
508
+ background: var(--color-scope-tint-scheduled);
509
+ }
510
+ .wre-scope-rail-tint-proposed {
511
+ background: var(--color-scope-tint-proposed);
512
+ }
513
+
512
514
  .wre-scope-rail-tint-active {
513
515
  outline: 1px solid color-mix(in srgb, var(--color-accent) 40%, transparent);
514
516
  outline-offset: -1px;
@@ -0,0 +1,345 @@
1
+ /*
2
+ * @beyondwork/docx-react-component — canonical design tokens
3
+ *
4
+ * Source of truth for every color, shadow, radius, space, and motion value
5
+ * consumed by the chrome and document surface. Values are copied verbatim
6
+ * from `docs/reference/designsystem.md` §10 (starter CSS). Any hex change
7
+ * happens HERE and in `tokens.ts` — never in a component file.
8
+ *
9
+ * Two-layer model:
10
+ * 1. Product names (TypeScript) — `BRAND_TOKENS.color.accent.primary`
11
+ * 2. CSS variables (this file) — `var(--color-accent-primary)`
12
+ *
13
+ * See `docs/wiki/design-tokens.md` for narrative + eviction policy.
14
+ *
15
+ * Host overrides:
16
+ * Allowed — accent family, `--color-bg-app`, `--radius-*`, `--motion-*`
17
+ * Locked — semantic, change, comment, canvas, focus-ring
18
+ * See `docs/reference/designsystem.md` §8.5 + §9.
19
+ */
20
+
21
+ :root {
22
+ /* Backgrounds */
23
+ --color-bg-app: #F4F7F5;
24
+ --color-bg-chrome: #FAFCFA;
25
+ --color-bg-sidebar: #F1F6F2;
26
+ --color-bg-canvas: #FFFFFF;
27
+ --color-bg-elevated: #FFFFFF;
28
+ --color-bg-hover: #EAF6EF;
29
+ --color-bg-selected: #E2F2E8;
30
+ --color-bg-muted: #F7FAF8;
31
+ --color-bg-overlay: rgba(21, 26, 23, 0.08);
32
+
33
+ /* Borders */
34
+ --color-border-subtle: #E2EAE4;
35
+ --color-border-default: #D3DED6;
36
+ --color-border-strong: #BECDBF;
37
+ --color-border-accent: #8FC9AD;
38
+
39
+ /* Text */
40
+ --color-text-primary: #151A17;
41
+ --color-text-secondary: #5E6D63;
42
+ --color-text-tertiary: #8A978F;
43
+ --color-text-quiet: #A4AEA8;
44
+ --color-text-inverse: #F7FAF8;
45
+ --color-text-on-accent: #F7FAF8;
46
+ --color-text-on-soft-accent: #18563F;
47
+
48
+ /* Accent (host-overridable) */
49
+ --color-accent-primary: #1F6B4F;
50
+ --color-accent-primary-hover: #18563F;
51
+ --color-accent-primary-active: #124432;
52
+ --color-accent-secondary: #72D6AE;
53
+ --color-accent-secondary-hover: #5FC79C;
54
+ --color-accent-secondary-active: #49B785;
55
+ --color-accent-soft: #DDF1E4;
56
+ --color-accent-soft-hover: #CEEAD8;
57
+
58
+ /* Semantic (locked) */
59
+ --color-semantic-success: #1F8A5B;
60
+ --color-semantic-success-soft: #DDF3E8;
61
+ --color-semantic-warning: #D58B1E;
62
+ --color-semantic-warning-soft: #FBEFD8;
63
+ --color-semantic-error: #D95C67;
64
+ --color-semantic-error-soft: #FBE3E6;
65
+ --color-semantic-info: #2F8FCE;
66
+ --color-semantic-info-soft: #DCEFFC;
67
+
68
+ /* Field */
69
+ --color-field-fill: #F6FAF7;
70
+ --color-field-editable: #EEF8F0;
71
+ --color-field-focus: #DDF1E4;
72
+ --color-field-invalid: #FBE3E6;
73
+ --color-field-warning: #FBEFD8;
74
+
75
+ /* Status */
76
+ --color-status-ready: #1F8A5B;
77
+ --color-status-in-progress: #2F8FCE;
78
+ --color-status-paused: #8A978F;
79
+ --color-status-blocked: #D95C67;
80
+ --color-status-review: #1F6B4F;
81
+ --color-status-draft: #72907E;
82
+
83
+ /* Comment (locked) */
84
+ --color-comment-marker: #1F6B4F;
85
+ --color-comment-marker-hover: #18563F;
86
+ --color-comment-thread-bg: #F3FAF6;
87
+
88
+ /* Change (locked) */
89
+ --color-change-insertion: #DDF3E8;
90
+ --color-change-deletion: #FBE3E6;
91
+ --color-change-comment: #E8F4EC;
92
+ --color-change-selection: #DDF1E4;
93
+
94
+ /* Chart — categorical */
95
+ --color-chart-categorical-1: #1F6B4F;
96
+ --color-chart-categorical-2: #72D6AE;
97
+ --color-chart-categorical-3: #2F8FCE;
98
+ --color-chart-categorical-4: #D58B1E;
99
+ --color-chart-categorical-5: #7A6AF0;
100
+ --color-chart-categorical-6: #D95C67;
101
+ --color-chart-categorical-7: #5E8E7A;
102
+ --color-chart-categorical-8: #A7D9C1;
103
+
104
+ /* Chart — sequential */
105
+ --color-chart-sequential-1: #EAF6EF;
106
+ --color-chart-sequential-2: #D4EEDD;
107
+ --color-chart-sequential-3: #B4DFC8;
108
+ --color-chart-sequential-4: #8FC9AD;
109
+ --color-chart-sequential-5: #5FA985;
110
+ --color-chart-sequential-6: #1F6B4F;
111
+
112
+ /* Chart — diverging */
113
+ --color-chart-diverging-neg-strong: #C94B57;
114
+ --color-chart-diverging-neg: #E9979F;
115
+ --color-chart-diverging-neutral: #E9EFEA;
116
+ --color-chart-diverging-pos: #97D1B3;
117
+ --color-chart-diverging-pos-strong: #1F6B4F;
118
+
119
+ /* Scope tints (§3.7.1) */
120
+ --color-scope-tint-blocked: #FBE3E6;
121
+ --color-scope-tint-in-scope: #E2F2E8;
122
+ --color-scope-tint-suggest: #FBEFD8;
123
+ --color-scope-tint-comment: #F3FAF6;
124
+ --color-scope-tint-scheduled: #DCEFFC;
125
+ --color-scope-tint-proposed: #EAF6EF;
126
+
127
+ /* Shadows */
128
+ --shadow-soft: 0 6px 20px rgba(21, 26, 23, 0.06);
129
+ --shadow-float: 0 12px 32px rgba(21, 26, 23, 0.12);
130
+ --shadow-focus: 0 0 0 3px rgba(114, 214, 174, 0.28);
131
+
132
+ /* Radius (§4.3) */
133
+ --radius-sm: 10px;
134
+ --radius-md: 12px;
135
+ --radius-lg: 14px;
136
+ --radius-xl: 16px;
137
+ --radius-pill: 9999px;
138
+
139
+ /* Spacing (4px base) */
140
+ --space-1: 4px;
141
+ --space-2: 8px;
142
+ --space-3: 12px;
143
+ --space-4: 16px;
144
+ --space-5: 20px;
145
+ --space-6: 24px;
146
+ --space-8: 32px;
147
+ --space-10: 40px;
148
+ --space-12: 48px;
149
+ --space-16: 64px;
150
+
151
+ /* Motion (host-overridable) */
152
+ --motion-fast: 120ms;
153
+ --motion-default: 160ms;
154
+ --motion-slow: 220ms;
155
+ }
156
+
157
+ /*
158
+ * Dark palette (designsystem §3.4).
159
+ *
160
+ * Canonical selector is `[data-theme="dark"]`; `.dark` class is retained
161
+ * as a compat selector for one release cycle (existing harness markup
162
+ * uses it alongside the attribute). Remove `.dark` after an explicit
163
+ * consumer audit.
164
+ */
165
+ [data-theme="dark"],
166
+ .dark {
167
+ /* Backgrounds */
168
+ --color-bg-app: #0E1411;
169
+ --color-bg-chrome: #131B17;
170
+ --color-bg-sidebar: #16211B;
171
+ --color-bg-canvas: #1B2620;
172
+ --color-bg-elevated: #202E27;
173
+ --color-bg-hover: #21342A;
174
+ --color-bg-selected: #274234;
175
+ --color-bg-muted: #17211C;
176
+ --color-bg-overlay: rgba(0, 0, 0, 0.32);
177
+
178
+ /* Borders */
179
+ --color-border-subtle: #26352C;
180
+ --color-border-default: #32463A;
181
+ --color-border-strong: #405847;
182
+ --color-border-accent: #4F8E70;
183
+
184
+ /* Text */
185
+ --color-text-primary: #EFF7F2;
186
+ --color-text-secondary: #A5B7AC;
187
+ --color-text-tertiary: #7F9187;
188
+ --color-text-quiet: #67786F;
189
+ --color-text-inverse: #111613;
190
+ --color-text-on-accent: #0F1713;
191
+ --color-text-on-soft-accent: #DDF7E9;
192
+
193
+ /* Accent */
194
+ --color-accent-primary: #53B487;
195
+ --color-accent-primary-hover: #69C598;
196
+ --color-accent-primary-active: #84D8AE;
197
+ --color-accent-secondary: #9AE7C7;
198
+ --color-accent-secondary-hover: #B2F0D5;
199
+ --color-accent-secondary-active: #C7F6E0;
200
+ --color-accent-soft: #294235;
201
+ --color-accent-soft-hover: #31503F;
202
+
203
+ /* Semantic */
204
+ --color-semantic-success: #45C787;
205
+ --color-semantic-success-soft: #1E3A2C;
206
+ --color-semantic-warning: #E7B04D;
207
+ --color-semantic-warning-soft: #3E3117;
208
+ --color-semantic-error: #F07A84;
209
+ --color-semantic-error-soft: #3D2024;
210
+ --color-semantic-info: #64B6E8;
211
+ --color-semantic-info-soft: #183444;
212
+
213
+ /* Field */
214
+ --color-field-fill: #202D25;
215
+ --color-field-editable: #23372B;
216
+ --color-field-focus: #294235;
217
+ --color-field-invalid: #3D2024;
218
+ --color-field-warning: #3E3117;
219
+
220
+ /* Status */
221
+ --color-status-ready: #45C787;
222
+ --color-status-in-progress: #64B6E8;
223
+ --color-status-paused: #7F9187;
224
+ --color-status-blocked: #F07A84;
225
+ --color-status-review: #53B487;
226
+ --color-status-draft: #7DA38F;
227
+
228
+ /* Comment */
229
+ --color-comment-marker: #53B487;
230
+ --color-comment-marker-hover: #69C598;
231
+ --color-comment-thread-bg: #1A2A22;
232
+
233
+ /* Change */
234
+ --color-change-insertion: #1E3A2C;
235
+ --color-change-deletion: #3D2024;
236
+ --color-change-comment: #21342A;
237
+ --color-change-selection: #294235;
238
+
239
+ /* Chart — categorical */
240
+ --color-chart-categorical-1: #53B487;
241
+ --color-chart-categorical-2: #9AE7C7;
242
+ --color-chart-categorical-3: #64B6E8;
243
+ --color-chart-categorical-4: #E7B04D;
244
+ --color-chart-categorical-5: #A996FF;
245
+ --color-chart-categorical-6: #F07A84;
246
+ --color-chart-categorical-7: #7DA38F;
247
+ --color-chart-categorical-8: #355746;
248
+
249
+ /* Chart — sequential */
250
+ --color-chart-sequential-1: #16211B;
251
+ --color-chart-sequential-2: #1D2E25;
252
+ --color-chart-sequential-3: #274234;
253
+ --color-chart-sequential-4: #35654F;
254
+ --color-chart-sequential-5: #53B487;
255
+ --color-chart-sequential-6: #9AE7C7;
256
+
257
+ /* Chart — diverging */
258
+ --color-chart-diverging-neg-strong: #F07A84;
259
+ --color-chart-diverging-neg: #C76069;
260
+ --color-chart-diverging-neutral: #2A3530;
261
+ --color-chart-diverging-pos: #69C598;
262
+ --color-chart-diverging-pos-strong: #9AE7C7;
263
+
264
+ /* Scope tints (§3.7.2) */
265
+ --color-scope-tint-blocked: #3D2024;
266
+ --color-scope-tint-in-scope: #274234;
267
+ --color-scope-tint-suggest: #3E3117;
268
+ --color-scope-tint-comment: #1A2A22;
269
+ --color-scope-tint-scheduled: #183444;
270
+ --color-scope-tint-proposed: #21342A;
271
+
272
+ /* Shadows */
273
+ --shadow-soft: 0 8px 24px rgba(0, 0, 0, 0.28);
274
+ --shadow-float: 0 16px 40px rgba(0, 0, 0, 0.4);
275
+ --shadow-focus: 0 0 0 3px rgba(154, 231, 199, 0.22);
276
+ }
277
+
278
+ /*
279
+ * Reduced-motion contract (designsystem §4.6 / §7.3).
280
+ * Collapses every motion token so any transition consuming `var(--motion-*)`
281
+ * becomes functionally instant. Components MUST read these tokens — never
282
+ * hardcode a numeric duration.
283
+ */
284
+ @media (prefers-reduced-motion: reduce) {
285
+ :root {
286
+ --motion-fast: 1ms;
287
+ --motion-default: 1ms;
288
+ --motion-slow: 1ms;
289
+ }
290
+ }
291
+
292
+ /*
293
+ * Density scale (designsystem §4.2 / Lane 6b.U5 consumer wiring).
294
+ *
295
+ * Components multiply spacing by `--space-density-multiplier`.
296
+ * Standard (1.0) is the default; compact (0.85) and comfortable (1.15)
297
+ * are opt-in via `data-density` on the root element.
298
+ * The hook `use-density.ts` reads and writes this attribute.
299
+ */
300
+ :root {
301
+ --space-density-multiplier: 1;
302
+ }
303
+ :root[data-density="compact"] {
304
+ --space-density-multiplier: 0.85;
305
+ }
306
+ :root[data-density="standard"] {
307
+ --space-density-multiplier: 1;
308
+ }
309
+ :root[data-density="comfortable"] {
310
+ --space-density-multiplier: 1.15;
311
+ }
312
+
313
+ /*
314
+ * Back-compat aliases (temporary — remove after full consumer sweep).
315
+ *
316
+ * These bind pre-6a names to their post-6a equivalents so that the
317
+ * existing @theme {} block in editor-theme.css and already-shipped
318
+ * component markup continue to resolve during the phased migration.
319
+ */
320
+ :root {
321
+ /* Legacy accent seed (was teal) → forest-green primary family */
322
+ --color-accent: var(--color-accent-primary);
323
+ /* Legacy single-name semantic tokens → canonical semantic family */
324
+ --color-danger: var(--color-semantic-error);
325
+ --color-danger-soft: var(--color-semantic-error-soft);
326
+ --color-warning: var(--color-semantic-warning);
327
+ --color-warning-soft: var(--color-semantic-warning-soft);
328
+ --color-success: var(--color-semantic-success);
329
+ --color-success-soft: var(--color-semantic-success-soft);
330
+ --color-info: var(--color-semantic-info);
331
+ --color-info-soft: var(--color-semantic-info-soft);
332
+ /* Legacy review markup names → change family */
333
+ --color-insert: var(--color-accent-primary);
334
+ --color-insert-soft: var(--color-change-insertion);
335
+ --color-delete: var(--color-semantic-error);
336
+ --color-delete-soft: var(--color-change-deletion);
337
+ --color-comment: var(--color-comment-marker);
338
+ --color-comment-soft: var(--color-change-comment);
339
+ --color-comment-strong: var(--color-comment-marker-hover);
340
+ /* Legacy L8 Phase A radius names → §4.3 scale */
341
+ --radius-panel: var(--radius-md);
342
+ --radius-card: var(--radius-sm);
343
+ --radius-control: var(--radius-sm);
344
+ --radius-page: 2px;
345
+ }