@beyondwork/docx-react-component 1.0.48 → 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 (45) hide show
  1. package/README.md +16 -11
  2. package/package.json +30 -41
  3. package/src/api/public-types.ts +84 -12
  4. package/src/core/commands/index.ts +9 -1
  5. package/src/core/commands/text-commands.ts +3 -1
  6. package/src/core/selection/anchor-conversion.ts +112 -0
  7. package/src/core/selection/review-anchors.ts +108 -3
  8. package/src/core/state/text-transaction.ts +86 -2
  9. package/src/internal/harness-debug-ports.ts +168 -0
  10. package/src/io/chart-preview-resolver.ts +32 -1
  11. package/src/io/export/serialize-main-document.ts +9 -0
  12. package/src/io/export/serialize-paragraph-formatting.ts +8 -0
  13. package/src/io/export/serialize-run-formatting.ts +10 -1
  14. package/src/io/ooxml/chart/chart-style-table.ts +543 -0
  15. package/src/io/ooxml/chart/color-palette.ts +101 -0
  16. package/src/io/ooxml/chart/compose-series-color.ts +147 -0
  17. package/src/io/ooxml/chart/parse-chart-space.ts +118 -46
  18. package/src/io/ooxml/chart/parse-series.ts +76 -11
  19. package/src/io/ooxml/chart/resolve-color.ts +16 -6
  20. package/src/io/ooxml/chart/types.ts +30 -11
  21. package/src/io/ooxml/parse-complex-content.ts +6 -3
  22. package/src/io/ooxml/parse-main-document.ts +41 -0
  23. package/src/io/ooxml/parse-paragraph-formatting.ts +46 -0
  24. package/src/io/ooxml/parse-run-formatting.ts +49 -0
  25. package/src/io/ooxml/property-grab-bag.ts +211 -0
  26. package/src/model/canonical-document.ts +69 -3
  27. package/src/runtime/collab/index.ts +7 -0
  28. package/src/runtime/collab/runtime-collab-sync.ts +51 -0
  29. package/src/runtime/collab/workflow-shared.ts +247 -0
  30. package/src/runtime/document-locations.ts +1 -9
  31. package/src/runtime/document-outline.ts +1 -9
  32. package/src/runtime/document-runtime.ts +74 -49
  33. package/src/runtime/hyperlink-color-resolver.ts +119 -0
  34. package/src/runtime/surface-projection.ts +94 -36
  35. package/src/runtime/theme-color-resolver.ts +188 -0
  36. package/src/runtime/workflow-markup.ts +7 -18
  37. package/src/ui/WordReviewEditor.tsx +18 -2
  38. package/src/ui/editor-runtime-boundary.ts +36 -0
  39. package/src/ui/headless/selection-helpers.ts +10 -23
  40. package/src/ui/unsupported-previews-policy.ts +23 -0
  41. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +10 -0
  42. package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
  43. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +47 -0
  44. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +88 -0
  45. package/src/ui-tailwind/tw-review-workspace.tsx +16 -1
@@ -0,0 +1,543 @@
1
+ /**
2
+ * OOXML chart-style table (styles 1-48).
3
+ *
4
+ * Chart styles are Word's legacy mechanism for setting per-chart visual
5
+ * properties by a single integer id. They pre-date the 2013+ per-chart
6
+ * `style1.xml` + `colors1.xml` sidecar parts, but Word still emits
7
+ * `c:chart/c:style val="N"` (or `c14:style val="10N"` as the MC choice
8
+ * wrapper) for every chart, and the legacy interpretation is the
9
+ * rendering fallback when no sidecar is present.
10
+ *
11
+ * Microsoft does not publish a machine-readable table for styles 1-48.
12
+ * The semantics below were reconstructed from:
13
+ * - ECMA-376 Part 1 Annex K (chart-style IDs)
14
+ * - Microsoft's visual references in the XlsxWriter project
15
+ * - LibreOffice's `chart2/source/tools/ChartTypeTemplate.cxx`
16
+ * - Observed behavior of Word 2016 / M365 when a chart carries only
17
+ * a c:style element with no sidecar parts.
18
+ *
19
+ * Scope: styles 1-12. The remaining 13-48 are listed as "default" here
20
+ * and filled in by follow-up slices. A chart that references a style
21
+ * not yet in the table falls back to style 2 (the Office default) so
22
+ * the renderer always has a usable value.
23
+ *
24
+ * This file is pure data. No DOM, no React, no rendering code — any
25
+ * stage (Stage 3 SVG primitives, Stage 4 per-type renderers) can import
26
+ * from here without creating a layering cycle.
27
+ */
28
+
29
+ import type { FillSpec, StrokeSpec, TextProperties } from "./types.ts";
30
+
31
+ /**
32
+ * How each series gets a color.
33
+ *
34
+ * - `accent1To6`: round-robin through accent1..accent6, wrapping back
35
+ * to accent1 on series 7+. Default for styles 1-4 + most charts.
36
+ * - `monochromaticAccentN`: single accent hue with per-series tint/shade
37
+ * variations (first series at full strength, subsequent series
38
+ * lightened or darkened by fixed steps). Styles 5-10.
39
+ */
40
+ export type SeriesColorMode =
41
+ | "accent1To6"
42
+ | "monochromaticAccent1"
43
+ | "monochromaticAccent2"
44
+ | "monochromaticAccent3"
45
+ | "monochromaticAccent4"
46
+ | "monochromaticAccent5"
47
+ | "monochromaticAccent6";
48
+
49
+ export interface ChartStyle {
50
+ /** How to assign a color to each series index. */
51
+ seriesColorMode: SeriesColorMode;
52
+ /** Background fill of the entire chart card (outside the plot area). */
53
+ backgroundFill?: FillSpec;
54
+ /** Plot area fill (behind the bars / lines / slices). */
55
+ plotAreaFill?: FillSpec;
56
+ /** Default title text properties. */
57
+ titleTxPr: TextProperties;
58
+ /** Default axis-label text properties. */
59
+ axisTxPr: TextProperties;
60
+ /** Default data-label text properties. */
61
+ dataLabelTxPr: TextProperties;
62
+ /** Default stroke around each series shape (bar/column/slice outline). */
63
+ seriesOutline?: StrokeSpec;
64
+ /** Default major gridline stroke. */
65
+ gridlineStroke?: StrokeSpec;
66
+ }
67
+
68
+ /**
69
+ * Style 2 is the Office default — it's what you get if you insert a
70
+ * chart in Word without touching the style picker. We use it as the
71
+ * fallback for unknown ids and as the anchor for derived styles.
72
+ */
73
+ export const DEFAULT_CHART_STYLE_ID = 2;
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Common building blocks
77
+ // ---------------------------------------------------------------------------
78
+
79
+ const TEXT_BODY: TextProperties = {
80
+ fontFamily: "+mn-lt", // minor latin font from theme
81
+ fontSizePt: 9,
82
+ color: { kind: "scheme", value: "tx1", mods: [
83
+ { kind: "lumMod", value: 65000 },
84
+ { kind: "lumOff", value: 35000 },
85
+ ] },
86
+ };
87
+
88
+ const TEXT_TITLE: TextProperties = {
89
+ fontFamily: "+mn-lt",
90
+ fontSizePt: 14,
91
+ bold: false,
92
+ color: { kind: "scheme", value: "tx1", mods: [
93
+ { kind: "lumMod", value: 65000 },
94
+ { kind: "lumOff", value: 35000 },
95
+ ] },
96
+ };
97
+
98
+ const GRIDLINE_STROKE_SUBTLE: StrokeSpec = {
99
+ color: { kind: "scheme", value: "tx1", mods: [
100
+ { kind: "lumMod", value: 15000 },
101
+ { kind: "lumOff", value: 85000 },
102
+ ] },
103
+ widthEmu: 9525, // 0.75 pt
104
+ dash: "solid",
105
+ };
106
+
107
+ const SERIES_OUTLINE_NONE: StrokeSpec = {
108
+ noFill: true,
109
+ };
110
+
111
+ const SERIES_OUTLINE_LIGHT: StrokeSpec = {
112
+ color: { kind: "scheme", value: "lt1" },
113
+ widthEmu: 9525,
114
+ dash: "solid",
115
+ };
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Styles 1-12
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Style 1 — "Plain". Flat accent colors, no background, no outline,
123
+ * no gridlines. Used for minimal presentation.
124
+ */
125
+ const STYLE_1: ChartStyle = {
126
+ seriesColorMode: "accent1To6",
127
+ backgroundFill: { kind: "none" },
128
+ plotAreaFill: { kind: "none" },
129
+ titleTxPr: TEXT_TITLE,
130
+ axisTxPr: TEXT_BODY,
131
+ dataLabelTxPr: TEXT_BODY,
132
+ seriesOutline: SERIES_OUTLINE_NONE,
133
+ };
134
+
135
+ /**
136
+ * Style 2 — Office default. Flat accent colors, subtle gridlines,
137
+ * minor outline. This is what `c:style val="2"` means.
138
+ */
139
+ const STYLE_2: ChartStyle = {
140
+ seriesColorMode: "accent1To6",
141
+ backgroundFill: { kind: "none" },
142
+ plotAreaFill: { kind: "none" },
143
+ titleTxPr: TEXT_TITLE,
144
+ axisTxPr: TEXT_BODY,
145
+ dataLabelTxPr: TEXT_BODY,
146
+ seriesOutline: SERIES_OUTLINE_NONE,
147
+ gridlineStroke: GRIDLINE_STROKE_SUBTLE,
148
+ };
149
+
150
+ /**
151
+ * Style 3 — Outlined variant of style 2. Subtle white outline on each
152
+ * series shape for card-style separation.
153
+ */
154
+ const STYLE_3: ChartStyle = {
155
+ seriesColorMode: "accent1To6",
156
+ backgroundFill: { kind: "none" },
157
+ plotAreaFill: { kind: "none" },
158
+ titleTxPr: TEXT_TITLE,
159
+ axisTxPr: TEXT_BODY,
160
+ dataLabelTxPr: TEXT_BODY,
161
+ seriesOutline: SERIES_OUTLINE_LIGHT,
162
+ gridlineStroke: GRIDLINE_STROKE_SUBTLE,
163
+ };
164
+
165
+ /**
166
+ * Style 4 — Colorful with a light plot-area background.
167
+ */
168
+ const STYLE_4: ChartStyle = {
169
+ seriesColorMode: "accent1To6",
170
+ backgroundFill: { kind: "none" },
171
+ plotAreaFill: {
172
+ kind: "solid",
173
+ color: { kind: "scheme", value: "lt1", mods: [
174
+ { kind: "lumMod", value: 95000 },
175
+ ] },
176
+ },
177
+ titleTxPr: TEXT_TITLE,
178
+ axisTxPr: TEXT_BODY,
179
+ dataLabelTxPr: TEXT_BODY,
180
+ seriesOutline: SERIES_OUTLINE_NONE,
181
+ gridlineStroke: GRIDLINE_STROKE_SUBTLE,
182
+ };
183
+
184
+ /**
185
+ * Styles 5-10 — Monochromatic. Each uses a single accent hue with
186
+ * per-series luminance variations. Style 5 = accent1, 6 = accent2, etc.
187
+ * ECMA-376 Annex K documents this six-way split.
188
+ */
189
+ function monochromaticStyle(mode: SeriesColorMode): ChartStyle {
190
+ return {
191
+ seriesColorMode: mode,
192
+ backgroundFill: { kind: "none" },
193
+ plotAreaFill: { kind: "none" },
194
+ titleTxPr: TEXT_TITLE,
195
+ axisTxPr: TEXT_BODY,
196
+ dataLabelTxPr: TEXT_BODY,
197
+ seriesOutline: SERIES_OUTLINE_LIGHT,
198
+ gridlineStroke: GRIDLINE_STROKE_SUBTLE,
199
+ };
200
+ }
201
+
202
+ const STYLE_5 = monochromaticStyle("monochromaticAccent1");
203
+ const STYLE_6 = monochromaticStyle("monochromaticAccent2");
204
+ const STYLE_7 = monochromaticStyle("monochromaticAccent3");
205
+ const STYLE_8 = monochromaticStyle("monochromaticAccent4");
206
+ const STYLE_9 = monochromaticStyle("monochromaticAccent5");
207
+ const STYLE_10 = monochromaticStyle("monochromaticAccent6");
208
+
209
+ /**
210
+ * Styles 11-12 — Colorful with darker plot-area background variations.
211
+ * These are less common in practice; rendered as decorated accent1To6.
212
+ */
213
+ const STYLE_11: ChartStyle = {
214
+ seriesColorMode: "accent1To6",
215
+ backgroundFill: { kind: "none" },
216
+ plotAreaFill: {
217
+ kind: "solid",
218
+ color: { kind: "scheme", value: "tx1", mods: [
219
+ { kind: "lumMod", value: 5000 },
220
+ { kind: "lumOff", value: 95000 },
221
+ ] },
222
+ },
223
+ titleTxPr: TEXT_TITLE,
224
+ axisTxPr: TEXT_BODY,
225
+ dataLabelTxPr: TEXT_BODY,
226
+ seriesOutline: SERIES_OUTLINE_NONE,
227
+ gridlineStroke: GRIDLINE_STROKE_SUBTLE,
228
+ };
229
+
230
+ const STYLE_12: ChartStyle = {
231
+ seriesColorMode: "accent1To6",
232
+ backgroundFill: { kind: "none" },
233
+ plotAreaFill: {
234
+ kind: "solid",
235
+ color: { kind: "scheme", value: "tx1", mods: [
236
+ { kind: "lumMod", value: 15000 },
237
+ { kind: "lumOff", value: 85000 },
238
+ ] },
239
+ },
240
+ titleTxPr: TEXT_TITLE,
241
+ axisTxPr: TEXT_BODY,
242
+ dataLabelTxPr: TEXT_BODY,
243
+ seriesOutline: SERIES_OUTLINE_NONE,
244
+ gridlineStroke: GRIDLINE_STROKE_SUBTLE,
245
+ };
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Styles 13-48
249
+ //
250
+ // Semantics below follow ECMA-376 Annex K groupings. The table mirrors the
251
+ // visual grid from Microsoft's XlsxWriter reference:
252
+ // 13-16: accent1To6 with decorated plot-area or darker gridlines
253
+ // 17-22: accent1To6 with various plot-area backgrounds
254
+ // 23-28: accent1To6 with dark (tx1-tinted) plot-area backgrounds
255
+ // 29-34: monochromatic on accent1..accent6 (second pass with decoration)
256
+ // 35-40: monochromatic on accent1..accent6 (third pass, darker)
257
+ // 41-48: accent1To6 with mixed backgrounds/decorations
258
+ //
259
+ // Undocumented decoration details (gradient stops, precise luminance
260
+ // ratios) are approximated. Fidelity refinement happens per-fixture once
261
+ // the renderer lands in Stage 3-4.
262
+ // ---------------------------------------------------------------------------
263
+
264
+ function accentDecoratedStyle(options: {
265
+ plotAreaFill?: FillSpec;
266
+ seriesOutline?: StrokeSpec;
267
+ gridlineStroke?: StrokeSpec;
268
+ } = {}): ChartStyle {
269
+ return {
270
+ seriesColorMode: "accent1To6",
271
+ backgroundFill: { kind: "none" },
272
+ plotAreaFill: options.plotAreaFill ?? { kind: "none" },
273
+ titleTxPr: TEXT_TITLE,
274
+ axisTxPr: TEXT_BODY,
275
+ dataLabelTxPr: TEXT_BODY,
276
+ seriesOutline: options.seriesOutline ?? SERIES_OUTLINE_NONE,
277
+ gridlineStroke: options.gridlineStroke ?? GRIDLINE_STROKE_SUBTLE,
278
+ };
279
+ }
280
+
281
+ const PLOT_AREA_FILL_LIGHT: FillSpec = {
282
+ kind: "solid",
283
+ color: { kind: "scheme", value: "lt1", mods: [
284
+ { kind: "lumMod", value: 95000 },
285
+ ] },
286
+ };
287
+
288
+ const PLOT_AREA_FILL_MEDIUM: FillSpec = {
289
+ kind: "solid",
290
+ color: { kind: "scheme", value: "tx1", mods: [
291
+ { kind: "lumMod", value: 25000 },
292
+ { kind: "lumOff", value: 75000 },
293
+ ] },
294
+ };
295
+
296
+ const PLOT_AREA_FILL_DARK: FillSpec = {
297
+ kind: "solid",
298
+ color: { kind: "scheme", value: "tx1", mods: [
299
+ { kind: "lumMod", value: 50000 },
300
+ { kind: "lumOff", value: 50000 },
301
+ ] },
302
+ };
303
+
304
+ const GRIDLINE_STROKE_LIGHT: StrokeSpec = {
305
+ color: { kind: "scheme", value: "lt1", mods: [
306
+ { kind: "lumMod", value: 80000 },
307
+ ] },
308
+ widthEmu: 9525,
309
+ dash: "solid",
310
+ };
311
+
312
+ // Styles 13-16: decorated accent1To6 with outline variants.
313
+ const STYLE_13 = accentDecoratedStyle({ seriesOutline: SERIES_OUTLINE_LIGHT });
314
+ const STYLE_14 = accentDecoratedStyle({
315
+ seriesOutline: SERIES_OUTLINE_LIGHT,
316
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
317
+ });
318
+ const STYLE_15 = accentDecoratedStyle({ plotAreaFill: PLOT_AREA_FILL_LIGHT });
319
+ const STYLE_16 = accentDecoratedStyle({
320
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
321
+ seriesOutline: SERIES_OUTLINE_NONE,
322
+ });
323
+
324
+ // Styles 17-22: accent1To6 with progressively darker plot-area fills.
325
+ const STYLE_17 = accentDecoratedStyle({ plotAreaFill: PLOT_AREA_FILL_MEDIUM });
326
+ const STYLE_18 = accentDecoratedStyle({
327
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
328
+ seriesOutline: SERIES_OUTLINE_LIGHT,
329
+ });
330
+ const STYLE_19 = accentDecoratedStyle({
331
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
332
+ gridlineStroke: GRIDLINE_STROKE_LIGHT,
333
+ });
334
+ const STYLE_20 = accentDecoratedStyle({
335
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
336
+ gridlineStroke: GRIDLINE_STROKE_LIGHT,
337
+ });
338
+ const STYLE_21 = accentDecoratedStyle({
339
+ plotAreaFill: PLOT_AREA_FILL_DARK,
340
+ seriesOutline: SERIES_OUTLINE_LIGHT,
341
+ gridlineStroke: GRIDLINE_STROKE_LIGHT,
342
+ });
343
+ const STYLE_22 = accentDecoratedStyle({ plotAreaFill: PLOT_AREA_FILL_DARK });
344
+
345
+ // Styles 23-28: dark theme — dark plot-area background with light text.
346
+ const TEXT_BODY_LIGHT: TextProperties = {
347
+ fontFamily: "+mn-lt",
348
+ fontSizePt: 9,
349
+ color: { kind: "scheme", value: "lt1" },
350
+ };
351
+
352
+ const TEXT_TITLE_LIGHT: TextProperties = {
353
+ fontFamily: "+mn-lt",
354
+ fontSizePt: 14,
355
+ color: { kind: "scheme", value: "lt1" },
356
+ };
357
+
358
+ function darkThemeStyle(mode: SeriesColorMode): ChartStyle {
359
+ return {
360
+ seriesColorMode: mode,
361
+ backgroundFill: { kind: "none" },
362
+ plotAreaFill: PLOT_AREA_FILL_DARK,
363
+ titleTxPr: TEXT_TITLE_LIGHT,
364
+ axisTxPr: TEXT_BODY_LIGHT,
365
+ dataLabelTxPr: TEXT_BODY_LIGHT,
366
+ seriesOutline: SERIES_OUTLINE_NONE,
367
+ gridlineStroke: GRIDLINE_STROKE_LIGHT,
368
+ };
369
+ }
370
+
371
+ const STYLE_23 = darkThemeStyle("accent1To6");
372
+ const STYLE_24 = darkThemeStyle("accent1To6");
373
+ const STYLE_25 = darkThemeStyle("accent1To6");
374
+ const STYLE_26 = darkThemeStyle("accent1To6");
375
+ const STYLE_27 = darkThemeStyle("accent1To6");
376
+ const STYLE_28 = darkThemeStyle("accent1To6");
377
+
378
+ // Styles 29-34: monochromatic second pass (accent1..accent6 with decoration).
379
+ const STYLE_29: ChartStyle = {
380
+ ...monochromaticStyle("monochromaticAccent1"),
381
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
382
+ };
383
+ const STYLE_30: ChartStyle = {
384
+ ...monochromaticStyle("monochromaticAccent2"),
385
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
386
+ };
387
+ const STYLE_31: ChartStyle = {
388
+ ...monochromaticStyle("monochromaticAccent3"),
389
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
390
+ };
391
+ const STYLE_32: ChartStyle = {
392
+ ...monochromaticStyle("monochromaticAccent4"),
393
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
394
+ };
395
+ const STYLE_33: ChartStyle = {
396
+ ...monochromaticStyle("monochromaticAccent5"),
397
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
398
+ };
399
+ const STYLE_34: ChartStyle = {
400
+ ...monochromaticStyle("monochromaticAccent6"),
401
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
402
+ };
403
+
404
+ // Styles 35-40: monochromatic third pass (darker, with medium background).
405
+ const STYLE_35: ChartStyle = {
406
+ ...monochromaticStyle("monochromaticAccent1"),
407
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
408
+ };
409
+ const STYLE_36: ChartStyle = {
410
+ ...monochromaticStyle("monochromaticAccent2"),
411
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
412
+ };
413
+ const STYLE_37: ChartStyle = {
414
+ ...monochromaticStyle("monochromaticAccent3"),
415
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
416
+ };
417
+ const STYLE_38: ChartStyle = {
418
+ ...monochromaticStyle("monochromaticAccent4"),
419
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
420
+ };
421
+ const STYLE_39: ChartStyle = {
422
+ ...monochromaticStyle("monochromaticAccent5"),
423
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
424
+ };
425
+ const STYLE_40: ChartStyle = {
426
+ ...monochromaticStyle("monochromaticAccent6"),
427
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
428
+ };
429
+
430
+ // Styles 41-48: accent1To6 mixed decoration.
431
+ const STYLE_41 = accentDecoratedStyle();
432
+ const STYLE_42 = accentDecoratedStyle({ seriesOutline: SERIES_OUTLINE_LIGHT });
433
+ const STYLE_43 = accentDecoratedStyle({ plotAreaFill: PLOT_AREA_FILL_LIGHT });
434
+ const STYLE_44 = accentDecoratedStyle({
435
+ plotAreaFill: PLOT_AREA_FILL_LIGHT,
436
+ seriesOutline: SERIES_OUTLINE_LIGHT,
437
+ });
438
+ const STYLE_45 = accentDecoratedStyle({ plotAreaFill: PLOT_AREA_FILL_MEDIUM });
439
+ const STYLE_46 = accentDecoratedStyle({
440
+ plotAreaFill: PLOT_AREA_FILL_MEDIUM,
441
+ seriesOutline: SERIES_OUTLINE_LIGHT,
442
+ });
443
+ const STYLE_47 = accentDecoratedStyle({ plotAreaFill: PLOT_AREA_FILL_DARK });
444
+ const STYLE_48: ChartStyle = {
445
+ seriesColorMode: "accent1To6",
446
+ backgroundFill: { kind: "none" },
447
+ plotAreaFill: PLOT_AREA_FILL_DARK,
448
+ titleTxPr: TEXT_TITLE_LIGHT,
449
+ axisTxPr: TEXT_BODY_LIGHT,
450
+ dataLabelTxPr: TEXT_BODY_LIGHT,
451
+ seriesOutline: SERIES_OUTLINE_LIGHT,
452
+ gridlineStroke: GRIDLINE_STROKE_LIGHT,
453
+ };
454
+
455
+ // ---------------------------------------------------------------------------
456
+ // Exported lookup
457
+ // ---------------------------------------------------------------------------
458
+
459
+ /**
460
+ * Chart-style lookup keyed by the numeric style id. Styles not yet in
461
+ * the table are absent; use `getChartStyle()` for safe default fallback.
462
+ *
463
+ * Styles 13-48 are intentionally absent for now. They'll land in
464
+ * follow-up slices as we encounter fixtures that reference them.
465
+ */
466
+ export const CHART_STYLES: Readonly<Record<number, ChartStyle>> = {
467
+ 1: STYLE_1,
468
+ 2: STYLE_2,
469
+ 3: STYLE_3,
470
+ 4: STYLE_4,
471
+ 5: STYLE_5,
472
+ 6: STYLE_6,
473
+ 7: STYLE_7,
474
+ 8: STYLE_8,
475
+ 9: STYLE_9,
476
+ 10: STYLE_10,
477
+ 11: STYLE_11,
478
+ 12: STYLE_12,
479
+ 13: STYLE_13,
480
+ 14: STYLE_14,
481
+ 15: STYLE_15,
482
+ 16: STYLE_16,
483
+ 17: STYLE_17,
484
+ 18: STYLE_18,
485
+ 19: STYLE_19,
486
+ 20: STYLE_20,
487
+ 21: STYLE_21,
488
+ 22: STYLE_22,
489
+ 23: STYLE_23,
490
+ 24: STYLE_24,
491
+ 25: STYLE_25,
492
+ 26: STYLE_26,
493
+ 27: STYLE_27,
494
+ 28: STYLE_28,
495
+ 29: STYLE_29,
496
+ 30: STYLE_30,
497
+ 31: STYLE_31,
498
+ 32: STYLE_32,
499
+ 33: STYLE_33,
500
+ 34: STYLE_34,
501
+ 35: STYLE_35,
502
+ 36: STYLE_36,
503
+ 37: STYLE_37,
504
+ 38: STYLE_38,
505
+ 39: STYLE_39,
506
+ 40: STYLE_40,
507
+ 41: STYLE_41,
508
+ 42: STYLE_42,
509
+ 43: STYLE_43,
510
+ 44: STYLE_44,
511
+ 45: STYLE_45,
512
+ 46: STYLE_46,
513
+ 47: STYLE_47,
514
+ 48: STYLE_48,
515
+ };
516
+
517
+ /**
518
+ * Look up a chart style by id. Falls back to the Office default (style 2)
519
+ * when the id is undefined or not in the table.
520
+ */
521
+ export function getChartStyle(id: number | undefined): ChartStyle {
522
+ if (id === undefined) return CHART_STYLES[DEFAULT_CHART_STYLE_ID]!;
523
+ return CHART_STYLES[id] ?? CHART_STYLES[DEFAULT_CHART_STYLE_ID]!;
524
+ }
525
+
526
+ /**
527
+ * Resolve a raw chart-style id as it appears in OOXML to the canonical
528
+ * 1-48 range.
529
+ *
530
+ * Word 2013+ emits the newer scheme `c14:style val="10N"` (where 10N =
531
+ * 100 + legacy id) inside an `mc:AlternateContent/Choice` block, with
532
+ * the legacy `c:style val="N"` in the `mc:Fallback`. When a chart has
533
+ * only the c14 choice value and no sidecar `style1.xml`, we map
534
+ * 101-148 back to the legacy 1-48 range.
535
+ *
536
+ * Any value outside 1-48 or 101-148 falls back to the Office default.
537
+ */
538
+ export function resolveChartStyleId(raw: number | undefined): number {
539
+ if (raw === undefined) return DEFAULT_CHART_STYLE_ID;
540
+ if (Number.isInteger(raw) && raw >= 1 && raw <= 48) return raw;
541
+ if (Number.isInteger(raw) && raw >= 101 && raw <= 148) return raw - 100;
542
+ return DEFAULT_CHART_STYLE_ID;
543
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Default series color palette.
3
+ *
4
+ * Given a `SeriesColorMode` (from the resolved chart style) and a series
5
+ * index, produce a `ColorRef` describing the color Word would use for
6
+ * that series when no explicit `c:ser/spPr` fill is declared.
7
+ *
8
+ * This layer is purely symbolic — it returns `ColorRef` values (scheme
9
+ * references with optional OOXML modifiers), not concrete hex. Downstream
10
+ * renderer stages compose `paletteColorRef(...)` with `resolveColor(ref,
11
+ * theme)` to produce a final sRGB string.
12
+ *
13
+ * Two modes:
14
+ *
15
+ * - **accent1To6** — round-robin across accent1..accent6, wrapping at 6.
16
+ * Matches Word's default palette for styles 1-4, 11-22, 41-48.
17
+ *
18
+ * - **monochromaticAccentN** — single accent hue with per-series tint/
19
+ * shade variation. Series 0 is the accent at full strength; subsequent
20
+ * series lighten via (lumMod, lumOff) then darken via shade, producing
21
+ * a 6-step tint/shade ramp per accent. Matches Word's monochromatic
22
+ * palette for styles 5-10, 29-40.
23
+ */
24
+
25
+ import type { SeriesColorMode } from "./chart-style-table.ts";
26
+ import type { ColorMod, ColorRef } from "./types.ts";
27
+
28
+ /**
29
+ * Luminance modifier ladder used for monochromatic palettes. Index 0 is
30
+ * the full-strength accent; subsequent entries are progressively lighter
31
+ * tints and then darker shades. Values are in OOXML's parts-per-100,000
32
+ * convention. The ladder matches Word's observed output for monochromatic
33
+ * styles 5-10 on real-world charts.
34
+ */
35
+ const MONOCHROMATIC_LADDER: Array<ColorMod[] | undefined> = [
36
+ undefined, // series 0: accent at full strength
37
+ [
38
+ { kind: "lumMod", value: 60000 },
39
+ { kind: "lumOff", value: 40000 },
40
+ ], // series 1: 40% tint
41
+ [
42
+ { kind: "lumMod", value: 80000 },
43
+ { kind: "lumOff", value: 20000 },
44
+ ], // series 2: 20% tint
45
+ [{ kind: "shade", value: 75000 }], // series 3: 25% shade
46
+ [{ kind: "shade", value: 50000 }], // series 4: 50% shade
47
+ [
48
+ { kind: "lumMod", value: 40000 },
49
+ { kind: "lumOff", value: 60000 },
50
+ ], // series 5: 60% tint (very light)
51
+ ];
52
+
53
+ /**
54
+ * Return the ColorRef for a given palette mode and series index. Pure
55
+ * function — no theme lookup. Callers compose with `resolveColor`.
56
+ */
57
+ export function paletteColorRef(
58
+ mode: SeriesColorMode,
59
+ seriesIdx: number,
60
+ ): ColorRef {
61
+ const idx = Math.max(0, Math.floor(seriesIdx));
62
+ if (mode === "accent1To6") {
63
+ const slot = `accent${(idx % 6) + 1}`;
64
+ return { kind: "scheme", value: slot };
65
+ }
66
+ // monochromaticAccentN
67
+ const accentSlot = monochromaticAccentSlot(mode);
68
+ const mods = MONOCHROMATIC_LADDER[idx % MONOCHROMATIC_LADDER.length];
69
+ const ref: ColorRef = { kind: "scheme", value: accentSlot };
70
+ if (mods && mods.length > 0) ref.mods = mods;
71
+ return ref;
72
+ }
73
+
74
+ /**
75
+ * Factory variant — returns a closure for callers that want to bind a
76
+ * mode once and call `palette(seriesIdx)` repeatedly in a render loop.
77
+ */
78
+ export function makePalette(
79
+ mode: SeriesColorMode,
80
+ ): (seriesIdx: number) => ColorRef {
81
+ return (seriesIdx: number) => paletteColorRef(mode, seriesIdx);
82
+ }
83
+
84
+ function monochromaticAccentSlot(mode: SeriesColorMode): string {
85
+ switch (mode) {
86
+ case "monochromaticAccent1":
87
+ return "accent1";
88
+ case "monochromaticAccent2":
89
+ return "accent2";
90
+ case "monochromaticAccent3":
91
+ return "accent3";
92
+ case "monochromaticAccent4":
93
+ return "accent4";
94
+ case "monochromaticAccent5":
95
+ return "accent5";
96
+ case "monochromaticAccent6":
97
+ return "accent6";
98
+ default:
99
+ return "accent1";
100
+ }
101
+ }