@beyondwork/docx-react-component 1.0.47 → 1.0.49

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 (80) hide show
  1. package/README.md +16 -11
  2. package/package.json +30 -41
  3. package/src/api/public-types.ts +199 -13
  4. package/src/compare/diff-engine.ts +4 -0
  5. package/src/core/commands/add-scope.ts +257 -0
  6. package/src/core/commands/formatting-commands.ts +2 -0
  7. package/src/core/commands/index.ts +9 -1
  8. package/src/core/commands/text-commands.ts +3 -1
  9. package/src/core/schema/text-schema.ts +95 -1
  10. package/src/core/selection/anchor-conversion.ts +112 -0
  11. package/src/core/selection/review-anchors.ts +108 -3
  12. package/src/core/state/text-transaction.ts +103 -7
  13. package/src/internal/harness-debug-ports.ts +168 -0
  14. package/src/io/chart-preview-resolver.ts +59 -1
  15. package/src/io/docx-session.ts +226 -38
  16. package/src/io/export/serialize-main-document.ts +46 -0
  17. package/src/io/export/serialize-paragraph-formatting.ts +8 -0
  18. package/src/io/export/serialize-run-formatting.ts +10 -1
  19. package/src/io/export/serialize-settings.ts +421 -0
  20. package/src/io/export/serialize-styles.ts +10 -0
  21. package/src/io/normalize/normalize-text.ts +1 -0
  22. package/src/io/ooxml/chart/chart-style-table.ts +543 -0
  23. package/src/io/ooxml/chart/color-palette.ts +101 -0
  24. package/src/io/ooxml/chart/compose-series-color.ts +147 -0
  25. package/src/io/ooxml/chart/parse-axis.ts +277 -0
  26. package/src/io/ooxml/chart/parse-chart-space.ts +885 -0
  27. package/src/io/ooxml/chart/parse-series.ts +635 -0
  28. package/src/io/ooxml/chart/resolve-color.ts +261 -0
  29. package/src/io/ooxml/chart/types.ts +439 -0
  30. package/src/io/ooxml/parse-block-structure.ts +99 -0
  31. package/src/io/ooxml/parse-complex-content.ts +90 -2
  32. package/src/io/ooxml/parse-main-document.ts +156 -1
  33. package/src/io/ooxml/parse-paragraph-formatting.ts +46 -0
  34. package/src/io/ooxml/parse-run-formatting.ts +49 -0
  35. package/src/io/ooxml/parse-scope-markers.ts +184 -0
  36. package/src/io/ooxml/parse-settings-blueprint.ts +349 -0
  37. package/src/io/ooxml/parse-settings.ts +97 -1
  38. package/src/io/ooxml/parse-styles.ts +65 -0
  39. package/src/io/ooxml/parse-theme.ts +2 -127
  40. package/src/io/ooxml/property-grab-bag.ts +211 -0
  41. package/src/io/ooxml/xml-attr-helpers.ts +59 -1
  42. package/src/io/ooxml/xml-parser.ts +142 -0
  43. package/src/model/canonical-document.ts +160 -0
  44. package/src/model/scope-markers.ts +144 -0
  45. package/src/runtime/collab/base-doc-fingerprint.ts +99 -0
  46. package/src/runtime/collab/checkpoint-election.ts +75 -0
  47. package/src/runtime/collab/checkpoint-scheduler.ts +204 -0
  48. package/src/runtime/collab/checkpoint-store.ts +115 -0
  49. package/src/runtime/collab/event-types.ts +27 -0
  50. package/src/runtime/collab/index.ts +29 -0
  51. package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
  52. package/src/runtime/collab/runtime-collab-sync.ts +330 -0
  53. package/src/runtime/collab/workflow-shared.ts +247 -0
  54. package/src/runtime/document-locations.ts +1 -9
  55. package/src/runtime/document-outline.ts +1 -9
  56. package/src/runtime/document-runtime.ts +288 -65
  57. package/src/runtime/editor-surface/capabilities.ts +63 -50
  58. package/src/runtime/hyperlink-color-resolver.ts +119 -0
  59. package/src/runtime/layout/layout-engine-version.ts +8 -1
  60. package/src/runtime/prerender/cache-envelope.ts +19 -7
  61. package/src/runtime/prerender/cache-key.ts +25 -14
  62. package/src/runtime/prerender/canonical-document-hash.ts +63 -0
  63. package/src/runtime/prerender/customxml-cache.ts +211 -0
  64. package/src/runtime/prerender/customxml-probe.ts +78 -0
  65. package/src/runtime/prerender/prerender-document.ts +74 -7
  66. package/src/runtime/scope-resolver.ts +148 -0
  67. package/src/runtime/scope-tag-registry.ts +10 -0
  68. package/src/runtime/surface-projection.ts +102 -37
  69. package/src/runtime/theme-color-resolver.ts +188 -0
  70. package/src/runtime/workflow-markup.ts +7 -18
  71. package/src/ui/WordReviewEditor.tsx +48 -2
  72. package/src/ui/editor-runtime-boundary.ts +42 -1
  73. package/src/ui/headless/selection-helpers.ts +10 -23
  74. package/src/ui/runtime-shortcut-dispatch.ts +12 -7
  75. package/src/ui/unsupported-previews-policy.ts +23 -0
  76. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +10 -0
  77. package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
  78. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +47 -0
  79. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +88 -0
  80. package/src/ui-tailwind/tw-review-workspace.tsx +16 -1
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Resolve a `ColorRef` into a concrete sRGB hex string.
3
+ *
4
+ * Implements the second layer of the OOXML chart-color cascade
5
+ * (docs/plans/lane-5-charts.md § 4 Task 2.2): scheme-color and
6
+ * themeLinked references are looked up in `ResolvedTheme.colors`, and
7
+ * OOXML color modifiers (`lumMod`, `lumOff`, `shade`, `tint`, `satMod`,
8
+ * `hueMod`) are applied in declaration order via HSL color math per
9
+ * ECMA-376 § 20.1.2.3.x.
10
+ *
11
+ * The output is always a valid `#RRGGBB` uppercase hex string. Bad or
12
+ * missing inputs fall through to a legible default (#808080) rather than
13
+ * throwing — renderers must never see `undefined` for a color slot.
14
+ *
15
+ * Alpha modifiers are intentionally NOT applied to the sRGB output;
16
+ * callers that want opacity must emit it as a separate channel. This
17
+ * keeps the return type a pure color and makes the resolver idempotent.
18
+ */
19
+
20
+ import type { ColorMod, ColorRef } from "./types.ts";
21
+ import type { ResolvedTheme } from "../../../model/canonical-document.ts";
22
+
23
+ const FALLBACK_COLOR = "#808080";
24
+ const OOXML_UNIT = 100_000; // modifier val is in parts per 100,000
25
+
26
+ /**
27
+ * Scheme-alias map. Word often emits aliases that refer to the primary
28
+ * color slots: `tx1` → `dk1`, `bg1` → `lt1`, `tx2` → `dk2`, `bg2` → `lt2`.
29
+ * The mapping is standardized by DrawingML.
30
+ *
31
+ * `phClr` is deliberately NOT aliased — it's a DrawingML "placeholder"
32
+ * color (ECMA-376 §20.1.4.1.22) that the caller is expected to substitute
33
+ * from its own context (e.g. the surrounding shape's accent). When the
34
+ * caller has no substitution, resolving phClr should fall through to the
35
+ * fallback color rather than silently pretend it meant accent1. Prior
36
+ * behavior aliased `phClr` → `accent1`, which silently miscolored every
37
+ * chart that used a placeholder color in its sidecar colors.xml.
38
+ */
39
+ const SCHEME_ALIASES: Record<string, string> = {
40
+ tx1: "dk1",
41
+ bg1: "lt1",
42
+ tx2: "dk2",
43
+ bg2: "lt2",
44
+ };
45
+
46
+ export function resolveColor(ref: ColorRef, theme: ResolvedTheme): string {
47
+ const baseHex = baseColor(ref, theme);
48
+ const rgb = hexToRgb(baseHex);
49
+ const withMods = applyMods(rgb, "mods" in ref ? ref.mods : undefined);
50
+ return rgbToHex(withMods);
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Base-color lookup (before modifiers)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function baseColor(ref: ColorRef, theme: ResolvedTheme): string {
58
+ switch (ref.kind) {
59
+ case "srgb":
60
+ return normalizeHex(ref.value);
61
+ case "scheme":
62
+ case "themeLinked": {
63
+ const slot = SCHEME_ALIASES[ref.value] ?? ref.value;
64
+ const hex = theme.colors[slot];
65
+ return hex ? normalizeHex(hex) : FALLBACK_COLOR;
66
+ }
67
+ default:
68
+ return FALLBACK_COLOR;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Normalize any `#RRGGBB` or `RRGGBB` string to uppercase with a leading
74
+ * `#`. Returns the fallback color on any malformed input so the output
75
+ * contract (`/^#[0-9A-F]{6}$/`) is always satisfied.
76
+ */
77
+ function normalizeHex(raw: string): string {
78
+ const m = /^#?([0-9A-Fa-f]{6})$/.exec(raw);
79
+ if (!m) return FALLBACK_COLOR;
80
+ return `#${m[1]!.toUpperCase()}`;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Modifier pipeline
85
+ // ---------------------------------------------------------------------------
86
+
87
+ interface Rgb {
88
+ r: number;
89
+ g: number;
90
+ b: number;
91
+ }
92
+
93
+ function applyMods(rgb: Rgb, mods: readonly ColorMod[] | undefined): Rgb {
94
+ if (!mods || mods.length === 0) return rgb;
95
+ let current = rgb;
96
+ for (const mod of mods) {
97
+ current = applyMod(current, mod);
98
+ }
99
+ return current;
100
+ }
101
+
102
+ function applyMod(rgb: Rgb, mod: ColorMod): Rgb {
103
+ const frac = mod.value / OOXML_UNIT;
104
+ switch (mod.kind) {
105
+ case "lumMod": {
106
+ // Scale luminance.
107
+ const hsl = rgbToHsl(rgb);
108
+ hsl.l = clamp01(hsl.l * frac);
109
+ return hslToRgb(hsl);
110
+ }
111
+ case "lumOff": {
112
+ // Add to luminance.
113
+ const hsl = rgbToHsl(rgb);
114
+ hsl.l = clamp01(hsl.l + frac);
115
+ return hslToRgb(hsl);
116
+ }
117
+ case "satMod": {
118
+ // Scale saturation.
119
+ const hsl = rgbToHsl(rgb);
120
+ hsl.s = clamp01(hsl.s * frac);
121
+ return hslToRgb(hsl);
122
+ }
123
+ case "hueMod": {
124
+ // Multiplicative scaling of the hue angle — NOT an additive
125
+ // rotation. ECMA-376 §20.1.2.3.14 and LibreOffice
126
+ // (oox/source/drawingml/color.cxx `lclModValue`) apply `hueMod` as
127
+ // `hue' = hue * (val/100000)`, stored in the hue dimension of HSL
128
+ // and clamped into the valid angle range. We wrap via mod-360 for
129
+ // numeric resilience when the source value exceeds 100% (a legal
130
+ // "expand hue" case).
131
+ const hsl = rgbToHsl(rgb);
132
+ hsl.h = ((hsl.h * frac) % 360 + 360) % 360;
133
+ return hslToRgb(hsl);
134
+ }
135
+ case "shade": {
136
+ // Scale channels toward black. Per ECMA-376 § 20.1.2.3.31, shade is
137
+ // applied in the sRGB color space, not HSL — this is important
138
+ // because it preserves hue and saturation while darkening.
139
+ return {
140
+ r: clamp255(rgb.r * frac),
141
+ g: clamp255(rgb.g * frac),
142
+ b: clamp255(rgb.b * frac),
143
+ };
144
+ }
145
+ case "tint": {
146
+ // Scale channels toward white. Per ECMA-376 § 20.1.2.3.34, tint is
147
+ // applied in sRGB space: C' = C * frac + 255 * (1 - frac).
148
+ // Note: this is equivalent to "lighten by (1-frac)".
149
+ const inv = 1 - frac;
150
+ return {
151
+ r: clamp255(rgb.r * frac + 255 * inv),
152
+ g: clamp255(rgb.g * frac + 255 * inv),
153
+ b: clamp255(rgb.b * frac + 255 * inv),
154
+ };
155
+ }
156
+ case "alpha":
157
+ // Opacity — not applied to sRGB output. Callers that need opacity
158
+ // must query the mods list directly.
159
+ return rgb;
160
+ }
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Color-space conversions
165
+ // ---------------------------------------------------------------------------
166
+
167
+ function hexToRgb(hex: string): Rgb {
168
+ const n = Number.parseInt(hex.slice(1), 16);
169
+ return {
170
+ r: (n >>> 16) & 0xff,
171
+ g: (n >>> 8) & 0xff,
172
+ b: n & 0xff,
173
+ };
174
+ }
175
+
176
+ function rgbToHex(rgb: Rgb): string {
177
+ const r = Math.round(clamp255(rgb.r));
178
+ const g = Math.round(clamp255(rgb.g));
179
+ const b = Math.round(clamp255(rgb.b));
180
+ return `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`;
181
+ }
182
+
183
+ function toHex2(n: number): string {
184
+ return n.toString(16).padStart(2, "0").toUpperCase();
185
+ }
186
+
187
+ interface Hsl {
188
+ /** Hue in degrees [0, 360). */
189
+ h: number;
190
+ /** Saturation in [0, 1]. */
191
+ s: number;
192
+ /** Luminance in [0, 1]. */
193
+ l: number;
194
+ }
195
+
196
+ function rgbToHsl(rgb: Rgb): Hsl {
197
+ const r = rgb.r / 255;
198
+ const g = rgb.g / 255;
199
+ const b = rgb.b / 255;
200
+ const max = Math.max(r, g, b);
201
+ const min = Math.min(r, g, b);
202
+ const l = (max + min) / 2;
203
+ let h = 0;
204
+ let s = 0;
205
+ if (max !== min) {
206
+ const delta = max - min;
207
+ s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
208
+ if (max === r) h = ((g - b) / delta) % 6;
209
+ else if (max === g) h = (b - r) / delta + 2;
210
+ else h = (r - g) / delta + 4;
211
+ h *= 60;
212
+ if (h < 0) h += 360;
213
+ }
214
+ return { h, s, l };
215
+ }
216
+
217
+ function hslToRgb(hsl: Hsl): Rgb {
218
+ const { h, s, l } = hsl;
219
+ const c = (1 - Math.abs(2 * l - 1)) * s;
220
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
221
+ const m = l - c / 2;
222
+ let r1 = 0;
223
+ let g1 = 0;
224
+ let b1 = 0;
225
+ if (h < 60) {
226
+ r1 = c;
227
+ g1 = x;
228
+ } else if (h < 120) {
229
+ r1 = x;
230
+ g1 = c;
231
+ } else if (h < 180) {
232
+ g1 = c;
233
+ b1 = x;
234
+ } else if (h < 240) {
235
+ g1 = x;
236
+ b1 = c;
237
+ } else if (h < 300) {
238
+ r1 = x;
239
+ b1 = c;
240
+ } else {
241
+ r1 = c;
242
+ b1 = x;
243
+ }
244
+ return {
245
+ r: (r1 + m) * 255,
246
+ g: (g1 + m) * 255,
247
+ b: (b1 + m) * 255,
248
+ };
249
+ }
250
+
251
+ function clamp01(x: number): number {
252
+ if (x < 0) return 0;
253
+ if (x > 1) return 1;
254
+ return x;
255
+ }
256
+
257
+ function clamp255(x: number): number {
258
+ if (x < 0) return 0;
259
+ if (x > 255) return 255;
260
+ return x;
261
+ }
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Chart data-model types (Stage 1).
3
+ *
4
+ * Pure type declarations. No runtime code, no imports from runtime modules.
5
+ * `ChartModel` is a discriminated union (by `kind`) covering every chart
6
+ * family we parse or intentionally mark as unsupported. Parsed models retain
7
+ * the source XML in `rawXml` so the export path (which re-emits the
8
+ * original drawing verbatim) stays byte-identical regardless of renderer
9
+ * coverage.
10
+ *
11
+ * Stage 2 will add the theme/colors/style cascade that turns `ColorRef`
12
+ * values into concrete sRGB strings. Until then, colors are declared as
13
+ * references; consumers of Stage 1 models should treat color resolution as
14
+ * "not yet available."
15
+ *
16
+ * See docs/plans/lane-5-charts.md §3 Task 1.1 for the full specification.
17
+ */
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Top-level union
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export type ChartModel =
24
+ | BarChartModel
25
+ | LineChartModel
26
+ | PieChartModel
27
+ | AreaChartModel
28
+ | ScatterChartModel
29
+ | BubbleChartModel
30
+ | ComboChartModel
31
+ | UnsupportedChartModel;
32
+
33
+ export interface ChartCommon {
34
+ /** Chart title, if present. */
35
+ title?: Title;
36
+ /** Legend configuration; absent if the chart has no legend. */
37
+ legend?: Legend;
38
+ /** Whether to plot only visible cells (c:plotVisOnly). */
39
+ plotVisOnly: boolean;
40
+ /** How to render blank/null values (c:dispBlanksAs). */
41
+ dispBlanksAs: "gap" | "zero" | "span";
42
+ /** Legacy chart style id (c:style val, 1–48), if present. */
43
+ styleId?: number;
44
+ /** Theme color scheme slot names available to this chart (accent1..6 etc.). */
45
+ themeColorScheme?: string[];
46
+ /**
47
+ * The raw chart-space XML that produced this model. Preserved so the
48
+ * export path can re-emit the original drawing without depending on the
49
+ * renderer's coverage of a given chart family.
50
+ */
51
+ rawXml: string;
52
+ }
53
+
54
+ /**
55
+ * Category-like axis binding for bar/line/area charts. Date axes are a
56
+ * legal substitute for a category axis at the XML level (c:dateAx replaces
57
+ * c:catAx) and we preserve the kind here so the renderer can format
58
+ * category labels as dates when the source authored them that way.
59
+ */
60
+ export type CategoryLikeAxis = CategoryAxis | DateAxis;
61
+
62
+ export interface BarChartModel extends ChartCommon {
63
+ kind: "bar";
64
+ /** "bar" = horizontal; "column" = vertical. */
65
+ direction: "bar" | "column";
66
+ grouping: "clustered" | "stacked" | "percentStacked" | "standard";
67
+ /** Gap between categories in percent of bar width (c:gapWidth). */
68
+ gapWidth: number;
69
+ /** Overlap within a category in percent, -100..100 (c:overlap). */
70
+ overlap: number;
71
+ series: Series[];
72
+ categoryAxis: CategoryLikeAxis;
73
+ valueAxis: ValueAxis;
74
+ /** Present when any series/group targeted the secondary axis. */
75
+ secondaryValueAxis?: ValueAxis;
76
+ }
77
+
78
+ export interface LineChartModel extends ChartCommon {
79
+ kind: "line";
80
+ grouping: "standard" | "stacked" | "percentStacked";
81
+ /** Global smooth flag; per-series values can override. */
82
+ smooth: boolean;
83
+ /** Global marker flag; per-series values can override. */
84
+ marker: boolean;
85
+ series: LineSeries[];
86
+ categoryAxis: CategoryLikeAxis;
87
+ valueAxis: ValueAxis;
88
+ secondaryValueAxis?: ValueAxis;
89
+ }
90
+
91
+ export interface PieChartModel extends ChartCommon {
92
+ kind: "pie";
93
+ doughnut: boolean;
94
+ /** Doughnut hole size in percent, typical 10–75 (c:holeSize). */
95
+ holeSizePercent?: number;
96
+ /** Clockwise degrees from 12 o'clock, 0–360 (c:firstSliceAngle / 60000). */
97
+ firstSliceAngle: number;
98
+ varyColors: boolean;
99
+ /** Almost always one series; pie-of-pie is length 2 (deferred). */
100
+ series: PieSeries[];
101
+ /** Category labels (from the first series' c:cat). */
102
+ categoryLabels: string[];
103
+ }
104
+
105
+ export interface AreaChartModel extends ChartCommon {
106
+ kind: "area";
107
+ grouping: "standard" | "stacked" | "percentStacked";
108
+ series: Series[];
109
+ categoryAxis: CategoryLikeAxis;
110
+ valueAxis: ValueAxis;
111
+ secondaryValueAxis?: ValueAxis;
112
+ }
113
+
114
+ export interface ScatterChartModel extends ChartCommon {
115
+ kind: "scatter";
116
+ style: "line" | "lineMarker" | "marker" | "smooth" | "smoothMarker";
117
+ series: ScatterSeries[];
118
+ /** Scatter uses a numeric x-axis, not a category axis. */
119
+ xAxis: ValueAxis;
120
+ yAxis: ValueAxis;
121
+ }
122
+
123
+ export interface BubbleChartModel extends ChartCommon {
124
+ kind: "bubble";
125
+ bubble3D: boolean;
126
+ series: BubbleSeries[];
127
+ xAxis: ValueAxis;
128
+ yAxis: ValueAxis;
129
+ }
130
+
131
+ export interface ComboChartModel extends ChartCommon {
132
+ kind: "combo";
133
+ /**
134
+ * Each type-group (bar/line/area) contributes its own series list. Axes
135
+ * may be shared or split into primary/secondary pairs.
136
+ */
137
+ groups: Array<BarChartModel | LineChartModel | AreaChartModel>;
138
+ /** True if any group targeted a secondary value axis. */
139
+ hasSecondaryAxis: boolean;
140
+ }
141
+
142
+ export interface UnsupportedChartModel extends ChartCommon {
143
+ kind: "unsupported";
144
+ reason: UnsupportedReason;
145
+ /** Human-readable detail (e.g. "Chart family lineChart not yet implemented"). */
146
+ detail: string;
147
+ }
148
+
149
+ /**
150
+ * Discriminator values for `UnsupportedChartModel.reason`.
151
+ *
152
+ * - `not-yet-implemented`: the parser recognises the chart family but a
153
+ * subsequent slice is responsible for implementing it (line/pie/area/
154
+ * scatter/bubble/combo in this slice).
155
+ * - `pivot`/`stock`/`surface`/…: families that are intentionally deferred
156
+ * indefinitely per the plan's non-goals (rare in legal/finance corpora,
157
+ * disproportionate renderer cost).
158
+ * - `no-plot-area`: the chart XML had no c:plotArea child.
159
+ * - `parse-error`: unexpected structure or exception during parse.
160
+ */
161
+ export type UnsupportedReason =
162
+ | "not-yet-implemented"
163
+ | "pivot"
164
+ | "stock"
165
+ | "surface"
166
+ | "treemap"
167
+ | "sunburst"
168
+ | "histogram"
169
+ | "waterfall"
170
+ | "funnel"
171
+ | "map"
172
+ | "no-plot-area"
173
+ | "parse-error";
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Series
177
+ // ---------------------------------------------------------------------------
178
+
179
+ export interface SeriesBase {
180
+ /** c:idx — render order. */
181
+ idx: number;
182
+ /** c:order — data order. */
183
+ order: number;
184
+ /** Series display name (c:tx/c:strRef/c:strCache or c:v). */
185
+ name?: string;
186
+ /** Explicit fill/stroke overrides from c:spPr. */
187
+ spPr?: ShapeProperties;
188
+ }
189
+
190
+ export interface Series extends SeriesBase {
191
+ /** Category labels; string-form even when c:numRef was used. */
192
+ categories: string[];
193
+ /** Numeric values; `null` for blank/missing points (sparse cache). */
194
+ values: Array<number | null>;
195
+ dataLabels?: DataLabelsSpec;
196
+ /** Per-data-point overrides (c:dPt). */
197
+ dataPoints?: DataPointOverride[];
198
+ }
199
+
200
+ export interface LineSeries extends Series {
201
+ /** Per-series smooth override. */
202
+ smooth?: boolean;
203
+ marker?: MarkerSpec;
204
+ }
205
+
206
+ export interface PieSeries extends SeriesBase {
207
+ values: Array<number | null>;
208
+ /** Per-series default explosion percent. */
209
+ explosion?: number;
210
+ dataPoints?: DataPointOverride[];
211
+ }
212
+
213
+ export interface ScatterSeries extends SeriesBase {
214
+ xValues: Array<number | null>;
215
+ yValues: Array<number | null>;
216
+ smooth?: boolean;
217
+ marker?: MarkerSpec;
218
+ }
219
+
220
+ export interface BubbleSeries extends SeriesBase {
221
+ xValues: Array<number | null>;
222
+ yValues: Array<number | null>;
223
+ sizes: Array<number | null>;
224
+ bubbleScale?: number;
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Axes
229
+ // ---------------------------------------------------------------------------
230
+
231
+ export type Axis = CategoryAxis | ValueAxis | DateAxis | SeriesAxis;
232
+
233
+ export interface AxisBase {
234
+ /** c:axId — unique within the plot area. */
235
+ id: string;
236
+ /** c:crossAx — the id of the perpendicular axis this one crosses. */
237
+ crossAxisId?: string;
238
+ /** Value at which the perpendicular axis crosses this one (c:crossesAt). */
239
+ crossesAt?: number | "autoZero" | "max" | "min";
240
+ /** c:axPos: b = bottom, t = top, l = left, r = right. */
241
+ position: "b" | "t" | "l" | "r";
242
+ /**
243
+ * Whether the axis is visible. Note: c:delete has inverted semantics —
244
+ * `val="1"` means invisible; we store the normal-sense boolean here.
245
+ */
246
+ visible: boolean;
247
+ title?: Title;
248
+ majorGridlines?: boolean;
249
+ minorGridlines?: boolean;
250
+ majorUnit?: number;
251
+ minorUnit?: number;
252
+ /** c:numFmt/@formatCode. */
253
+ numberFormat?: string;
254
+ }
255
+
256
+ export interface CategoryAxis extends AxisBase {
257
+ kind: "category";
258
+ /** c:auto — whether category width is chosen automatically. */
259
+ auto: boolean;
260
+ labelAlign?: "ctr" | "l" | "r";
261
+ labelOffset?: number;
262
+ tickMark?: "none" | "in" | "out" | "cross";
263
+ tickLabelSkip?: number;
264
+ tickMarkSkip?: number;
265
+ /** Category labels resolved from the first series' c:cat cache. */
266
+ categoryLabels: string[];
267
+ }
268
+
269
+ export interface ValueAxis extends AxisBase {
270
+ kind: "value";
271
+ min?: number;
272
+ max?: number;
273
+ logBase?: number;
274
+ /** True when c:scaling/c:orientation val="maxMin". */
275
+ reverse: boolean;
276
+ crossBetween?: "between" | "midCat";
277
+ }
278
+
279
+ export interface DateAxis extends AxisBase {
280
+ kind: "date";
281
+ baseTimeUnit?: "days" | "months" | "years";
282
+ /** Serial date (days since 1899-12-30). */
283
+ min?: number;
284
+ max?: number;
285
+ majorTimeUnit?: "days" | "months" | "years";
286
+ minorTimeUnit?: "days" | "months" | "years";
287
+ }
288
+
289
+ export interface SeriesAxis extends AxisBase {
290
+ kind: "series";
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Other chart elements
295
+ // ---------------------------------------------------------------------------
296
+
297
+ export interface Title {
298
+ /**
299
+ * Plain-text rendering of the title. Rich-text formatting is not preserved
300
+ * at Stage 1; Stage 2/3 may extend this with per-run styling.
301
+ */
302
+ text?: string;
303
+ overlay: boolean;
304
+ spPr?: ShapeProperties;
305
+ txPr?: TextProperties;
306
+ }
307
+
308
+ export interface Legend {
309
+ position: "b" | "t" | "l" | "r" | "tr";
310
+ overlay: boolean;
311
+ spPr?: ShapeProperties;
312
+ txPr?: TextProperties;
313
+ }
314
+
315
+ export interface DataLabelsSpec {
316
+ showVal: boolean;
317
+ showCatName: boolean;
318
+ showSerName: boolean;
319
+ showPercent: boolean;
320
+ showBubbleSize: boolean;
321
+ showLegendKey: boolean;
322
+ position?: "b" | "ctr" | "l" | "r" | "t" | "bestFit" | "inBase" | "inEnd" | "outEnd";
323
+ separator?: string;
324
+ numberFormat?: string;
325
+ txPr?: TextProperties;
326
+ }
327
+
328
+ export interface MarkerSpec {
329
+ symbol:
330
+ | "circle"
331
+ | "square"
332
+ | "diamond"
333
+ | "triangle"
334
+ | "x"
335
+ | "star"
336
+ | "dot"
337
+ | "dash"
338
+ | "plus"
339
+ | "picture"
340
+ | "none"
341
+ | "auto";
342
+ /** Point size 2..72. */
343
+ size?: number;
344
+ spPr?: ShapeProperties;
345
+ }
346
+
347
+ export interface DataPointOverride {
348
+ /** Index of the data point within the series. */
349
+ idx: number;
350
+ spPr?: ShapeProperties;
351
+ marker?: MarkerSpec;
352
+ /** Pie only: per-slice explosion percent. */
353
+ explosion?: number;
354
+ /** Bar only: render negative values with inverted fill. */
355
+ invertIfNegative?: boolean;
356
+ /** Bubble only: 3D bubble flag (rendered as 2D regardless). */
357
+ bubble3D?: boolean;
358
+ }
359
+
360
+ // ---------------------------------------------------------------------------
361
+ // Shape / fill / stroke / color primitives
362
+ // ---------------------------------------------------------------------------
363
+
364
+ export interface ShapeProperties {
365
+ fill?: FillSpec;
366
+ stroke?: StrokeSpec;
367
+ }
368
+
369
+ export type FillSpec =
370
+ | { kind: "solid"; color: ColorRef }
371
+ | { kind: "gradient"; stops: Array<{ pos: number; color: ColorRef }>; angle?: number }
372
+ | { kind: "none" }
373
+ | { kind: "auto" };
374
+
375
+ export interface StrokeSpec {
376
+ color?: ColorRef;
377
+ /** Stroke width in EMU (English Metric Units). */
378
+ widthEmu?: number;
379
+ dash?: "solid" | "dash" | "dashDot" | "lgDash" | "lgDashDot" | "sysDash" | "sysDashDot";
380
+ noFill?: boolean;
381
+ }
382
+
383
+ /**
384
+ * An unresolved color reference. Stage 2 adds `resolveColor(ref, theme)`
385
+ * which turns this into a concrete sRGB string via the chart-color cascade
386
+ * (explicit sRGB → scheme color → theme color scheme).
387
+ */
388
+ export type ColorRef =
389
+ | { kind: "srgb"; value: string } // "#RRGGBB"
390
+ | { kind: "scheme"; value: string; mods?: ColorMod[] }
391
+ | { kind: "themeLinked"; value: string; mods?: ColorMod[] };
392
+
393
+ export interface ColorMod {
394
+ kind: "lumMod" | "lumOff" | "shade" | "tint" | "satMod" | "hueMod" | "alpha";
395
+ /** Parts per 100,000 (OOXML convention). */
396
+ value: number;
397
+ }
398
+
399
+ export interface TextProperties {
400
+ fontFamily?: string;
401
+ fontSizePt?: number;
402
+ bold?: boolean;
403
+ italic?: boolean;
404
+ color?: ColorRef;
405
+ }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // Exhaustiveness canary (compile-time only)
409
+ // ---------------------------------------------------------------------------
410
+
411
+ /**
412
+ * Compile-time assertion that `ChartModel["kind"]` equals the expected
413
+ * literal-union below. If a new variant is added to `ChartModel` without
414
+ * being listed in `_ExpectedKinds`, `_Equals` resolves to `false` and the
415
+ * subsequent `true` assignment fails at compile time. The previous
416
+ * dual-assign-cast pattern did not actually enforce union equality — any
417
+ * new kind silently passed through.
418
+ *
419
+ * Reference: the standard "exact type equality" trick using contravariance
420
+ * of conditional-type generic functions.
421
+ */
422
+ type _Equals<X, Y> =
423
+ (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
424
+ ? true
425
+ : false;
426
+
427
+ type _ExpectedKinds =
428
+ | "bar"
429
+ | "line"
430
+ | "pie"
431
+ | "area"
432
+ | "scatter"
433
+ | "bubble"
434
+ | "combo"
435
+ | "unsupported";
436
+
437
+ type _ChartKindCheck = _Equals<ChartModel["kind"], _ExpectedKinds>;
438
+ const _kindExhaustive: _ChartKindCheck = true;
439
+ void _kindExhaustive;