@opendata-ai/openchart-core 8.3.1 → 8.4.1

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.
package/dist/index.js CHANGED
@@ -443,6 +443,274 @@ function findAccessibleColor(baseColor, bg, targetRatio = 4.5) {
443
443
  return baseColor;
444
444
  }
445
445
 
446
+ // src/styles/token-definitions.ts
447
+ var CSS_TOKEN_ROOT_SELECTORS = [
448
+ ".oc-root",
449
+ ".oc-chart-root",
450
+ ".oc-table-wrapper",
451
+ ".oc-table-root",
452
+ ".oc-graph-wrapper",
453
+ ".oc-graph-root",
454
+ ".oc-sankey-root",
455
+ ".oc-tilemap-root",
456
+ ".oc-barlist-root",
457
+ ".oc-map-root"
458
+ ];
459
+ var CSS_TOKENS = [
460
+ {
461
+ name: "--oc-font-family",
462
+ light: '\n "Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
463
+ },
464
+ {
465
+ name: "--oc-font-mono",
466
+ light: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace'
467
+ },
468
+ {
469
+ section: "Animation easing presets via CSS linear()",
470
+ name: "--oc-ease-smooth",
471
+ light: "linear(\n 0,\n 0.157,\n 0.438,\n 0.64,\n 0.766,\n 0.85,\n 0.906,\n 0.941,\n 0.964,\n 0.978,\n 0.988,\n 0.994,\n 0.998,\n 1\n )"
472
+ },
473
+ {
474
+ name: "--oc-ease-snappy",
475
+ light: "linear(\n 0,\n 0.012,\n 0.048,\n 0.108,\n 0.194,\n 0.302,\n 0.426,\n 0.559,\n 0.69,\n 0.808,\n 0.905,\n 0.973,\n 1.013,\n 1.028,\n 1.023,\n 1.006,\n 0.984,\n 0.966,\n 0.957,\n 0.957,\n 0.964,\n 0.975,\n 0.986,\n 0.995,\n 1,\n 1.003,\n 1.002,\n 1,\n 0.998,\n 0.998,\n 0.999,\n 1\n )",
476
+ consumers: "EASE_VAR_MAP in vanilla renderers"
477
+ },
478
+ { section: "Animation timing defaults", name: "--oc-animation-duration", light: "450ms" },
479
+ { name: "--oc-animation-stagger", light: "30ms" },
480
+ { name: "--oc-annotation-delay", light: "150ms" },
481
+ { section: "Typography scale (editorial design system)", name: "--oc-title-size", light: "26px" },
482
+ {
483
+ name: "--oc-title-weight",
484
+ light: "600",
485
+ comment: "one ladder: 400 regular / 500 medium / 600 semibold / 700 bold"
486
+ },
487
+ { name: "--oc-title-tracking", light: "-0.022em" },
488
+ { name: "--oc-subtitle-size", light: "14px" },
489
+ { name: "--oc-subtitle-weight", light: "400" },
490
+ { name: "--oc-source-size", light: "11px" },
491
+ { name: "--oc-source-weight", light: "400" },
492
+ { name: "--oc-body-size", light: "13px" },
493
+ { name: "--oc-eyebrow-size", light: "11px" },
494
+ { name: "--oc-eyebrow-weight", light: "500" },
495
+ { name: "--oc-eyebrow-tracking", light: "0.08em" },
496
+ { section: "Surfaces (light mode defaults)", name: "--oc-bg", light: "#ffffff", dark: "#09090b" },
497
+ { name: "--oc-card", light: "#ffffff", dark: "#111113" },
498
+ {
499
+ name: "--oc-secondary",
500
+ light: "#f4f4f5",
501
+ dark: "#27272a",
502
+ comment: "zinc-100, raised surface"
503
+ },
504
+ {
505
+ section: `Text levels. Names invert across modes: in light mode "muted" sits at
506
+ * a lighter zinc step than "subtle" because both are picked relative to
507
+ * the active background, not from a fixed lightness ladder. The intent
508
+ * is "muted = first step away from primary text"; "subtle = next step
509
+ * down"; etc.
510
+ *
511
+ * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,
512
+ * subtle=zinc-400, faint=zinc-300
513
+ * Dark mode (dark.css): inverts the surface tokens but keeps the same
514
+ * muted -> subtle -> faint progression away from primary.`,
515
+ name: "--oc-text",
516
+ light: "#09090b",
517
+ dark: "#f7f8f8"
518
+ },
519
+ { name: "--oc-text-secondary", light: "#3f3f46", dark: "#d0d6e0" },
520
+ { name: "--oc-text-muted", light: "#71717a", dark: "#a1a1aa" },
521
+ { name: "--oc-text-faint", light: "#d4d4d8", dark: "#52525b" },
522
+ {
523
+ section: `Derived neutral ramp. Named by the share of *text* in a text-toward-
524
+ * background mix: 100 is nearly the background, 800 is nearly the text.
525
+ * These static values are the mix against the default surfaces; mounts
526
+ * restamp them from ResolvedTheme.colors.neutral, which a custom warm or
527
+ * cool theme derives from its own text/background pair.`,
528
+ name: "--oc-gray-100",
529
+ light: "#f0f0f0",
530
+ dark: "#171719"
531
+ },
532
+ { name: "--oc-gray-200", light: "#e1e1e2", dark: "#262627" },
533
+ { name: "--oc-gray-300", light: "#c9c9c9", dark: "#3d3e3f" },
534
+ { name: "--oc-gray-400", light: "#9d9d9d", dark: "#68696a" },
535
+ { name: "--oc-gray-600", light: "#787879", dark: "#8c8c8d" },
536
+ { name: "--oc-gray-800", light: "#474748", dark: "#bcbcbd" },
537
+ {
538
+ section: "Lines",
539
+ name: "--oc-gridline",
540
+ light: "rgba(0, 0, 0, 0.08)",
541
+ dark: "rgba(255, 255, 255, 0.06)"
542
+ },
543
+ {
544
+ name: "--oc-axis",
545
+ light: "rgba(0, 0, 0, 0.14)",
546
+ dark: "rgba(255, 255, 255, 0.14)",
547
+ comment: "hairline = theme.colors.hairline, not the tick-label ink"
548
+ },
549
+ {
550
+ name: "--oc-border",
551
+ light: "rgba(0, 0, 0, 0.08)",
552
+ dark: "rgba(255, 255, 255, 0.1)"
553
+ },
554
+ {
555
+ name: "--oc-border-radius",
556
+ light: "8px",
557
+ comment: "containers and tooltips; marks use --oc-radius-sm"
558
+ },
559
+ { section: "Radius ladder", name: "--oc-radius-sm", light: "2px" },
560
+ { name: "--oc-radius-md", light: "6px" },
561
+ { name: "--oc-radius-lg", light: "var(--oc-border-radius)" },
562
+ { name: "--oc-radius-full", light: "999px" },
563
+ {
564
+ section: "Brand and semantic",
565
+ name: "--oc-accent",
566
+ light: "#06b6d4",
567
+ dark: "#06b6d4"
568
+ },
569
+ {
570
+ name: "--oc-accent-strong",
571
+ light: "#0891b2",
572
+ dark: "#06b6d4",
573
+ comment: "darker cyan for line strokes on light bg",
574
+ consumers: "stamped via adaptForLightLineStroke"
575
+ },
576
+ {
577
+ name: "--oc-positive",
578
+ light: "#15803d",
579
+ dark: "#4ade80",
580
+ comment: "kept equal to DEFAULT_THEME.colors.positive (token-theme-parity test)"
581
+ },
582
+ { name: "--oc-negative", light: "#dc2626", dark: "#f87171" },
583
+ {
584
+ name: "--oc-positive-tint",
585
+ light: "color-mix(in srgb, var(--oc-positive) 10%, var(--oc-bg))",
586
+ comment: "delta chip background"
587
+ },
588
+ {
589
+ name: "--oc-negative-tint",
590
+ light: "color-mix(in srgb, var(--oc-negative) 10%, var(--oc-bg))"
591
+ },
592
+ { name: "--oc-focus", light: "#3b82f6", dark: "#60a5fa" },
593
+ {
594
+ name: "--oc-focus-ring",
595
+ light: "rgba(59, 130, 246, 0.1)",
596
+ comment: "static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode"
597
+ },
598
+ {
599
+ name: "--oc-focus-ring-strong",
600
+ light: "rgba(59, 130, 246, 0.25)"
601
+ },
602
+ {
603
+ name: "--oc-editable-hover",
604
+ light: "rgba(79, 70, 229, 0.35)",
605
+ comment: "edit-mode hover outline, indigo one-off"
606
+ },
607
+ {
608
+ section: "Spacing scale (4px base) \u2014 only --oc-space-2/4 are consumed (JS-stamped)",
609
+ name: "--oc-space-2",
610
+ light: "8px",
611
+ consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
612
+ },
613
+ {
614
+ name: "--oc-space-4",
615
+ light: "16px",
616
+ consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
617
+ },
618
+ {
619
+ section: `Hover language. The hovered element goes to full ink; the rest of its
620
+ * group drops to --oc-hover-dim over --oc-hover-duration. Maps override the
621
+ * dim locally (3000 county paths at 0.3 reads as a blackout, not a hint).`,
622
+ name: "--oc-hover-dim",
623
+ light: "0.3"
624
+ },
625
+ { name: "--oc-hover-duration", light: "140ms" },
626
+ { name: "--oc-map-hover-dim", light: "0.75" },
627
+ {
628
+ section: "Interactive states",
629
+ name: "--oc-hover-bg",
630
+ light: "rgba(0, 0, 0, 0.025)",
631
+ dark: "rgba(255, 255, 255, 0.05)"
632
+ },
633
+ {
634
+ name: "--oc-tooltip-bg",
635
+ light: "rgba(255, 255, 255, 0.88)",
636
+ dark: "rgba(17, 17, 19, 0.92)"
637
+ },
638
+ {
639
+ name: "--oc-tooltip-border",
640
+ light: "rgba(0, 0, 0, 0.08)",
641
+ dark: "rgba(255, 255, 255, 0.08)"
642
+ },
643
+ {
644
+ name: "--oc-tooltip-shadow",
645
+ light: "0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12)",
646
+ dark: "0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4)"
647
+ },
648
+ { name: "--oc-tooltip-text", light: "#09090b", dark: "#f7f8f8" },
649
+ { name: "--oc-legend-text", light: "#3f3f46", dark: "#d0d6e0" }
650
+ ];
651
+ var TOKEN_MAP = new Map(CSS_TOKENS.map((t) => [t.name, t]));
652
+ function cssTokenDefault(name, mode) {
653
+ const token = TOKEN_MAP.get(name);
654
+ if (!token) throw new Error(`Unknown CSS token: ${name}`);
655
+ if (mode === "dark" && token.dark !== void 0) return token.dark;
656
+ return token.light;
657
+ }
658
+
659
+ // src/colors/neutral.ts
660
+ var STEPS = [
661
+ [100, 0.06],
662
+ [200, 0.12],
663
+ [300, 0.22],
664
+ [400, 0.4],
665
+ [600, 0.55],
666
+ [800, 0.75]
667
+ ];
668
+ function parseHex(color) {
669
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());
670
+ if (!m) return null;
671
+ const hex = m[1].length === 3 ? m[1].split("").map((c) => c + c).join("") : m[1];
672
+ return [
673
+ parseInt(hex.slice(0, 2), 16),
674
+ parseInt(hex.slice(2, 4), 16),
675
+ parseInt(hex.slice(4, 6), 16)
676
+ ];
677
+ }
678
+ function mix(text, bg, t) {
679
+ const c = (i) => Math.round(text[i] * t + bg[i] * (1 - t));
680
+ const hex = (n) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, "0");
681
+ return `#${hex(c(0))}${hex(c(1))}${hex(c(2))}`;
682
+ }
683
+ function staticRamp(mode) {
684
+ return {
685
+ 100: cssTokenDefault("--oc-gray-100", mode),
686
+ 200: cssTokenDefault("--oc-gray-200", mode),
687
+ 300: cssTokenDefault("--oc-gray-300", mode),
688
+ 400: cssTokenDefault("--oc-gray-400", mode),
689
+ 600: cssTokenDefault("--oc-gray-600", mode),
690
+ 800: cssTokenDefault("--oc-gray-800", mode),
691
+ secondary: cssTokenDefault("--oc-text-secondary", mode),
692
+ faint: cssTokenDefault("--oc-text-faint", mode),
693
+ border: cssTokenDefault("--oc-border", mode),
694
+ surface: cssTokenDefault("--oc-bg", mode)
695
+ };
696
+ }
697
+ function deriveNeutralRamp(text, bg, isDark = false) {
698
+ const mode = isDark ? "dark" : "light";
699
+ const surface = isOpaqueColor(bg) ? bg : cssTokenDefault("--oc-bg", mode);
700
+ if (!isOpaqueColor(bg)) return staticRamp(mode);
701
+ const t = parseHex(text);
702
+ const b = parseHex(bg);
703
+ if (!t || !b) return { ...staticRamp(mode), surface };
704
+ const ramp = Object.fromEntries(STEPS.map(([step, w]) => [step, mix(t, b, w)]));
705
+ return {
706
+ ...ramp,
707
+ secondary: ramp[800],
708
+ faint: ramp[300],
709
+ border: ramp[100],
710
+ surface
711
+ };
712
+ }
713
+
446
714
  // src/colors/palettes.ts
447
715
  var ACHROMATIC_RAMP = {
448
716
  fg: "#f7f8f8",
@@ -464,23 +732,73 @@ var ACHROMATIC_RAMP = {
464
732
  };
465
733
  var CATEGORICAL_PALETTE = [
466
734
  "#06b6d4",
467
- // cyan, primary accent (sRGB literal, ~205°)
468
- "#ee4a73",
469
- // rose — oklch(65% 0.20 10)
470
- "#00b054",
471
- // emerald — oklch(65% 0.20 155)
472
- "#a46bf5",
473
- // violet — oklch(65% 0.20 300)
474
- "#e07d00",
475
- // amber — oklch(68% 0.19 70)
476
- "#0091ff",
477
- // sky — oklch(65% 0.20 250)
478
- "#f36000",
479
- // orange — oklch(67% 0.20 45)
480
- "#6f7dff",
481
- // indigo — oklch(65% 0.20 275)
482
- "#00afbf"
483
- // teal — oklch(65% 0.20 200)
735
+ // cyan — oklch(0.715 0.126 215) — sRGB literal, primary accent
736
+ "#e29e47",
737
+ // ochre — oklch(0.750 0.130 70)
738
+ "#4170cb",
739
+ // blue — oklch(0.559 0.150 262)
740
+ "#b2417f",
741
+ // rose — oklch(0.550 0.161 350)
742
+ "#3ca368",
743
+ // green — oklch(0.640 0.130 155)
744
+ "#a584dc"
745
+ // violet — oklch(0.679 0.130 300)
746
+ ];
747
+ var CATEGORICAL_FILL_PALETTE = [
748
+ "#51bccc",
749
+ // cyan — oklch(0.739 0.100 209)
750
+ "#dda96b",
751
+ // ochre — oklch(0.770 0.100 70)
752
+ "#587fc8",
753
+ // blue — oklch(0.601 0.120 262)
754
+ "#bf6291",
755
+ // rose — oklch(0.619 0.131 350)
756
+ "#62ab7d",
757
+ // green — oklch(0.681 0.101 155)
758
+ "#ae96da"
759
+ // violet — oklch(0.719 0.100 300)
760
+ ];
761
+ var CATEGORICAL_PALETTE_DARK = [
762
+ "#22d3ee",
763
+ // cyan — oklch(0.797 0.134 212) — sRGB literal
764
+ "#f9be78",
765
+ // ochre — oklch(0.840 0.111 70)
766
+ "#6591e1",
767
+ // blue — oklch(0.660 0.129 262)
768
+ "#d771a4",
769
+ // rose — oklch(0.680 0.140 350)
770
+ "#6ebf8c",
771
+ // green — oklch(0.739 0.109 155)
772
+ "#bca1ed"
773
+ // violet — oklch(0.760 0.110 300)
774
+ ];
775
+ var CATEGORICAL_FILL_PALETTE_DARK = [
776
+ "#72cedc",
777
+ // cyan — oklch(0.800 0.090 208)
778
+ "#f3c898",
779
+ // ochre — oklch(0.859 0.079 70)
780
+ "#7698d6",
781
+ // blue — oklch(0.679 0.100 262)
782
+ "#d281a8",
783
+ // rose — oklch(0.700 0.111 350)
784
+ "#82ba95",
785
+ // green — oklch(0.740 0.079 155)
786
+ "#c2afe7"
787
+ // violet — oklch(0.789 0.081 300)
788
+ ];
789
+ var CATEGORICAL_EXTENDED_PALETTE = [
790
+ "#8e8f19",
791
+ // olive — oklch(0.629 0.130 110)
792
+ "#b086eb",
793
+ // periwinkle— oklch(0.701 0.149 302)
794
+ "#ac3225",
795
+ // brick — oklch(0.501 0.161 30)
796
+ "#00bfc0",
797
+ // teal — oklch(0.729 0.124 195)
798
+ "#a5538c",
799
+ // mulberry — oklch(0.560 0.129 340)
800
+ "#65b1f6"
801
+ // sky — oklch(0.740 0.126 248)
484
802
  ];
485
803
  var SEQUENTIAL_BLUE = {
486
804
  name: "blue",
@@ -913,9 +1231,11 @@ function getBreakpoint(width) {
913
1231
  if (width <= BREAKPOINT_MEDIUM_MAX) return "medium";
914
1232
  return "full";
915
1233
  }
1234
+ var HEIGHT_MINIMAL_MAX = 100;
916
1235
  var HEIGHT_CRAMPED_MAX = 200;
917
1236
  var HEIGHT_SHORT_MAX = 350;
918
1237
  function getHeightClass(height) {
1238
+ if (height < HEIGHT_MINIMAL_MAX) return "minimal";
919
1239
  if (height < HEIGHT_CRAMPED_MAX) return "cramped";
920
1240
  if (height <= HEIGHT_SHORT_MAX) return "short";
921
1241
  return "normal";
@@ -953,10 +1273,10 @@ function getWidthStrategy(breakpoint) {
953
1273
  }
954
1274
  function applyHeightConstraints(strategy, heightClass) {
955
1275
  if (heightClass === "normal") return strategy;
956
- if (heightClass === "cramped") {
1276
+ if (heightClass === "minimal" || heightClass === "cramped") {
957
1277
  return {
958
1278
  ...strategy,
959
- chromeMode: "hidden",
1279
+ chromeMode: heightClass === "minimal" ? "hidden" : "compact",
960
1280
  legendMaxHeight: 0,
961
1281
  labelMode: "none",
962
1282
  annotationPosition: "tooltip-only"
@@ -972,6 +1292,16 @@ function getLayoutStrategy(breakpoint, heightClass = "normal") {
972
1292
  const base = getWidthStrategy(breakpoint);
973
1293
  return applyHeightConstraints(base, heightClass);
974
1294
  }
1295
+ var HEIGHT_NO_GRID_MAX = 150;
1296
+ var COMPACT_MAX_X_TICKS = 3;
1297
+ function resolveChromeEconomy(width, height) {
1298
+ const economy = {
1299
+ gridlines: height >= HEIGHT_NO_GRID_MAX,
1300
+ axes: height >= HEIGHT_CRAMPED_MAX
1301
+ };
1302
+ if (getBreakpoint(width) === "compact") economy.maxXTicks = COMPACT_MAX_X_TICKS;
1303
+ return economy;
1304
+ }
975
1305
 
976
1306
  // src/responsive/metrics.ts
977
1307
  var AXIS_TITLE_OFFSET_COMPACT = 35;
@@ -983,7 +1313,7 @@ var HPAD_COMPACT_FRACTION = 0.5;
983
1313
  var HPAD_COMPACT_MIN = 4;
984
1314
  var TICK_LABEL_OFFSET = 6;
985
1315
  var AXIS_TITLE_TRAILING_PAD = 4;
986
- var AXIS_TITLE_GAP = 7;
1316
+ var AXIS_TITLE_GAP = 3;
987
1317
  function axisTitleOffset(maxTickLabelWidth, titleFontSize, width, inline = false) {
988
1318
  const halfTitleGlyph = Math.ceil(titleFontSize / 2);
989
1319
  if (inline) {
@@ -1063,18 +1393,22 @@ function truncateRotatedLabel(label, tickAngle, tickFontSize, tickFontWeight, me
1063
1393
  var DEFAULT_THEME = {
1064
1394
  colors: {
1065
1395
  categorical: [...CATEGORICAL_PALETTE],
1396
+ categoricalFill: [...CATEGORICAL_FILL_PALETTE],
1066
1397
  sequential: SEQUENTIAL_PALETTES,
1067
1398
  diverging: DIVERGING_PALETTES,
1068
1399
  background: "transparent",
1069
1400
  text: "#09090b",
1070
- gridline: "rgba(0,0,0,0.1)",
1401
+ gridline: "rgba(0,0,0,0.08)",
1402
+ // Structural hairline: axis lines, separators. Deliberately heavier than
1403
+ // the gridline and lighter than any ink, so structure reads as structure.
1404
+ hairline: "rgba(0,0,0,0.14)",
1071
1405
  // Used for axis lines/ticks AND axis tick label fill. Must clear WCAG AA
1072
1406
  // contrast (4.5:1) on white because tick labels are rendered with this
1073
1407
  // color. Zinc-500 hits ~5.7:1.
1074
1408
  axis: "#71717a",
1075
1409
  annotationFill: "rgba(0,0,0,0.04)",
1076
1410
  annotationText: "#71717a",
1077
- positive: "#16a34a",
1411
+ positive: "#15803d",
1078
1412
  negative: "#dc2626"
1079
1413
  },
1080
1414
  fonts: {
@@ -1086,19 +1420,21 @@ var DEFAULT_THEME = {
1086
1420
  body: 13,
1087
1421
  small: 11,
1088
1422
  axisTick: 11,
1089
- metricLabel: 10,
1423
+ metricLabel: 11,
1090
1424
  metricValue: 22
1091
1425
  },
1426
+ // One ladder everywhere: 400 regular / 500 medium (eyebrow, labels, UI,
1427
+ // values) / 600 semibold (title, series names, annotation lede) / 700 bold.
1092
1428
  weights: {
1093
- normal: 450,
1094
- medium: 550,
1095
- semibold: 590,
1429
+ normal: 400,
1430
+ medium: 500,
1431
+ semibold: 600,
1096
1432
  bold: 700
1097
1433
  }
1098
1434
  },
1099
1435
  spacing: {
1100
1436
  padding: 20,
1101
- chromeGap: 4,
1437
+ chromeGap: 6,
1102
1438
  chromeToChart: 8,
1103
1439
  chartToFooter: 8,
1104
1440
  axisMargin: 6,
@@ -1110,17 +1446,18 @@ var DEFAULT_THEME = {
1110
1446
  // so the gap holds regardless of font size.
1111
1447
  xAxisLabelPadding: 4
1112
1448
  },
1113
- borderRadius: 2,
1449
+ /** Containers and tooltips. Marks carry their own 2px radius constant. */
1450
+ borderRadius: 8,
1114
1451
  chrome: {
1115
1452
  eyebrow: {
1116
1453
  fontSize: 11,
1117
- fontWeight: 510,
1454
+ fontWeight: 500,
1118
1455
  color: "#06b6d4",
1119
1456
  lineHeight: 1.4
1120
1457
  },
1121
1458
  title: {
1122
1459
  fontSize: 26,
1123
- fontWeight: 590,
1460
+ fontWeight: 600,
1124
1461
  color: "#09090b",
1125
1462
  lineHeight: 1.15
1126
1463
  },
@@ -1149,6 +1486,8 @@ var DEFAULT_THEME = {
1149
1486
  lineHeight: 1.4
1150
1487
  }
1151
1488
  },
1489
+ /** No editorial rule by default; the `broadsheet` preset sets one. */
1490
+ rule: null,
1152
1491
  seriesStrategy: "palette"
1153
1492
  };
1154
1493
 
@@ -1215,9 +1554,15 @@ function adaptTheme(theme) {
1215
1554
  const lightChrome = DEFAULT_THEME.chrome;
1216
1555
  const overridden = (current, lightDefault, darkDefault) => current === lightDefault ? darkDefault : current;
1217
1556
  const darkText = pairs?.["colors.text"] ? pairs["colors.text"].dark : overridden(theme.colors.text, light.text, DARK_TEXT);
1218
- const darkGridline = pairs?.["colors.gridline"] ? pairs["colors.gridline"].dark : overridden(theme.colors.gridline, light.gridline, "rgba(255,255,255,0.05)");
1557
+ const darkGridline = pairs?.["colors.gridline"] ? pairs["colors.gridline"].dark : overridden(theme.colors.gridline, light.gridline, "rgba(255,255,255,0.06)");
1558
+ const darkHairline = pairs?.["colors.hairline"] ? pairs["colors.hairline"].dark : overridden(theme.colors.hairline, light.hairline, "rgba(255,255,255,0.14)");
1219
1559
  const darkAxis = pairs?.["colors.axis"] ? pairs["colors.axis"].dark : overridden(theme.colors.axis, light.axis, "#a1a1aa");
1220
- const categorical = theme.colors.categorical;
1560
+ const categorical = overridden(theme.colors.categorical, light.categorical, [
1561
+ ...CATEGORICAL_PALETTE_DARK
1562
+ ]);
1563
+ const categoricalFill = overridden(theme.colors.categoricalFill, light.categoricalFill, [
1564
+ ...CATEGORICAL_FILL_PALETTE_DARK
1565
+ ]);
1221
1566
  return {
1222
1567
  ...theme,
1223
1568
  isDark: true,
@@ -1230,8 +1575,11 @@ function adaptTheme(theme) {
1230
1575
  annotationFill: pairs?.["colors.annotationFill"] ? pairs["colors.annotationFill"].dark : overridden(theme.colors.annotationFill, light.annotationFill, "rgba(255,255,255,0.06)"),
1231
1576
  annotationText: pairs?.["colors.annotationText"] ? pairs["colors.annotationText"].dark : overridden(theme.colors.annotationText, light.annotationText, darkMuted),
1232
1577
  categorical,
1233
- positive: pairs?.["colors.positive"] ? pairs["colors.positive"].dark : theme.colors.positive !== "#16a34a" ? theme.colors.positive : "#34d399",
1234
- negative: pairs?.["colors.negative"] ? pairs["colors.negative"].dark : theme.colors.negative !== "#dc2626" ? theme.colors.negative : "#f87171"
1578
+ categoricalFill,
1579
+ hairline: darkHairline,
1580
+ positive: pairs?.["colors.positive"] ? pairs["colors.positive"].dark : overridden(theme.colors.positive, light.positive, "#4ade80"),
1581
+ negative: pairs?.["colors.negative"] ? pairs["colors.negative"].dark : overridden(theme.colors.negative, light.negative, "#f87171"),
1582
+ neutral: deriveNeutralRamp(darkText, darkBg, true)
1235
1583
  },
1236
1584
  chrome: {
1237
1585
  // Eyebrow keeps its accent tint (cyan in both modes) unless the user
@@ -1261,7 +1609,10 @@ function adaptTheme(theme) {
1261
1609
  ...theme.chrome.footer,
1262
1610
  color: pairs?.["chrome.footer.color"] ? pairs["chrome.footer.color"].dark : overridden(theme.chrome.footer.color, lightChrome.footer.color, darkMuted)
1263
1611
  }
1264
- }
1612
+ },
1613
+ // The editorial rule is a brand device: it keeps its authored color unless
1614
+ // the theme gave an explicit dark half of the pair.
1615
+ rule: theme.rule && pairs?.["rule.color"] ? { ...theme.rule, color: pairs["rule.color"].dark } : theme.rule
1265
1616
  };
1266
1617
  }
1267
1618
  function adaptForLightLineStroke(color) {
@@ -1324,6 +1675,71 @@ var wire = {
1324
1675
  },
1325
1676
  borderRadius: 2
1326
1677
  };
1678
+ var broadsheet = {
1679
+ colors: {
1680
+ categorical: ["#1d5f8a", "#e3120b", "#7fb0d3", "#e0a63b", "#3a8a6e", "#8c8680"],
1681
+ categoricalFill: ["#3b7097", "#ee493a", "#97c2e2", "#efbb64", "#579a81", "#9e9893"],
1682
+ background: { light: "#fffdf9", dark: "#171513" },
1683
+ text: { light: "#1a1714", dark: "#f4f1ea" },
1684
+ gridline: { light: "rgba(26,23,20,0.08)", dark: "rgba(244,241,234,0.07)" },
1685
+ // Heavier than the default 0.14: newsprint rules are drawn to be seen.
1686
+ hairline: { light: "rgba(26,23,20,0.28)", dark: "rgba(244,241,234,0.28)" },
1687
+ axis: { light: "#6b645c", dark: "#a8a099" },
1688
+ annotationFill: { light: "rgba(26,23,20,0.05)", dark: "rgba(244,241,234,0.07)" },
1689
+ annotationText: { light: "#6b645c", dark: "#a8a099" }
1690
+ },
1691
+ fonts: {
1692
+ sizes: { title: 24, subtitle: 14, body: 13, small: 11 }
1693
+ },
1694
+ spacing: {
1695
+ padding: 20,
1696
+ chromeGap: 8,
1697
+ chromeToChart: 16,
1698
+ chartToFooter: 12
1699
+ },
1700
+ chrome: {
1701
+ eyebrow: { color: { light: "#e3120b", dark: "#f4463f" }, fontSize: 11, fontWeight: 700 },
1702
+ title: { fontSize: 24, fontWeight: 700, lineHeight: 1.18 },
1703
+ subtitle: { fontWeight: 400, lineHeight: 1.45 }
1704
+ },
1705
+ /** The masthead device: a short red bar above the eyebrow. */
1706
+ rule: { color: { light: "#e3120b", dark: "#f4463f" }, width: 40, thickness: 3 },
1707
+ borderRadius: 0
1708
+ };
1709
+ var terminal = {
1710
+ colors: {
1711
+ categorical: [...CATEGORICAL_PALETTE_DARK],
1712
+ categoricalFill: [...CATEGORICAL_FILL_PALETTE_DARK],
1713
+ background: { light: "#0b0f14", dark: "#0b0f14" },
1714
+ text: { light: "#e6edf3", dark: "#e6edf3" },
1715
+ gridline: { light: "rgba(230,237,243,0.06)", dark: "rgba(230,237,243,0.06)" },
1716
+ hairline: { light: "rgba(230,237,243,0.14)", dark: "rgba(230,237,243,0.14)" },
1717
+ // 6.25:1 on #0b0f14 — tick labels are drawn in this color too.
1718
+ axis: { light: "#8b949e", dark: "#8b949e" },
1719
+ annotationFill: { light: "rgba(230,237,243,0.06)", dark: "rgba(230,237,243,0.06)" },
1720
+ annotationText: { light: "#8b949e", dark: "#8b949e" },
1721
+ positive: { light: "#4ade80", dark: "#4ade80" },
1722
+ negative: { light: "#f87171", dark: "#f87171" }
1723
+ },
1724
+ fonts: {
1725
+ family: '"Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
1726
+ mono: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
1727
+ sizes: { title: 15, subtitle: 12, body: 12, small: 10, axisTick: 10 }
1728
+ },
1729
+ spacing: {
1730
+ padding: 12,
1731
+ chromeGap: 3,
1732
+ chromeToChart: 6,
1733
+ chartToFooter: 6
1734
+ },
1735
+ chrome: {
1736
+ eyebrow: { color: { light: "#22d3ee", dark: "#22d3ee" }, fontSize: 10, fontWeight: 500 },
1737
+ title: { fontSize: 15, fontWeight: 600, lineHeight: 1.3 },
1738
+ subtitle: { fontWeight: 400, lineHeight: 1.4 }
1739
+ },
1740
+ borderRadius: 4,
1741
+ seriesStrategy: "accent-neutral"
1742
+ };
1327
1743
 
1328
1744
  // src/theme/resolve.ts
1329
1745
  function deepMerge(target, source) {
@@ -1350,11 +1766,13 @@ function themeConfigToPartial(config) {
1350
1766
  colors.categorical = config.colors;
1351
1767
  } else {
1352
1768
  if (config.colors.categorical) colors.categorical = config.colors.categorical;
1769
+ if (config.colors.categoricalFill) colors.categoricalFill = config.colors.categoricalFill;
1353
1770
  if (config.colors.sequential) colors.sequential = config.colors.sequential;
1354
1771
  if (config.colors.diverging) colors.diverging = config.colors.diverging;
1355
1772
  if (config.colors.background) colors.background = resolveTokenLight(config.colors.background);
1356
1773
  if (config.colors.text) colors.text = resolveTokenLight(config.colors.text);
1357
1774
  if (config.colors.gridline) colors.gridline = resolveTokenLight(config.colors.gridline);
1775
+ if (config.colors.hairline) colors.hairline = resolveTokenLight(config.colors.hairline);
1358
1776
  if (config.colors.axis) colors.axis = resolveTokenLight(config.colors.axis);
1359
1777
  if (config.colors.annotationFill)
1360
1778
  colors.annotationFill = resolveTokenLight(config.colors.annotationFill);
@@ -1363,6 +1781,9 @@ function themeConfigToPartial(config) {
1363
1781
  if (config.colors.positive) colors.positive = resolveTokenLight(config.colors.positive);
1364
1782
  if (config.colors.negative) colors.negative = resolveTokenLight(config.colors.negative);
1365
1783
  }
1784
+ if (colors.categorical && !colors.categoricalFill) {
1785
+ colors.categoricalFill = colors.categorical;
1786
+ }
1366
1787
  partial.colors = colors;
1367
1788
  }
1368
1789
  if (config.fonts) {
@@ -1408,6 +1829,13 @@ function themeConfigToPartial(config) {
1408
1829
  }
1409
1830
  partial.chrome = chrome;
1410
1831
  }
1832
+ if (config.rule) {
1833
+ partial.rule = {
1834
+ color: resolveTokenLight(config.rule.color),
1835
+ width: config.rule.width,
1836
+ thickness: config.rule.thickness
1837
+ };
1838
+ }
1411
1839
  if (config.seriesStrategy) {
1412
1840
  partial.seriesStrategy = config.seriesStrategy;
1413
1841
  }
@@ -1420,6 +1848,7 @@ function collectTokenPairs(config) {
1420
1848
  "background",
1421
1849
  "text",
1422
1850
  "gridline",
1851
+ "hairline",
1423
1852
  "axis",
1424
1853
  "annotationFill",
1425
1854
  "annotationText",
@@ -1433,6 +1862,9 @@ function collectTokenPairs(config) {
1433
1862
  }
1434
1863
  }
1435
1864
  }
1865
+ if (config.rule?.color && typeof config.rule.color === "object" && "light" in config.rule.color) {
1866
+ pairs["rule.color"] = config.rule.color;
1867
+ }
1436
1868
  if (config.chrome) {
1437
1869
  for (const [element, value] of Object.entries(config.chrome)) {
1438
1870
  if (value && typeof value === "object" && "color" in value && value.color && typeof value.color === "object" && "light" in value.color) {
@@ -1491,9 +1923,9 @@ function adjustOpacity(hex, opacity) {
1491
1923
  const r = parseInt(m[1].slice(0, 2), 16);
1492
1924
  const g = parseInt(m[1].slice(2, 4), 16);
1493
1925
  const b = parseInt(m[1].slice(4, 6), 16);
1494
- const mix = (c) => Math.round(c * opacity + 128 * (1 - opacity));
1926
+ const mix2 = (c) => Math.round(c * opacity + 128 * (1 - opacity));
1495
1927
  const toHex = (n) => n.toString(16).padStart(2, "0");
1496
- return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
1928
+ return `#${toHex(mix2(r))}${toHex(mix2(g))}${toHex(mix2(b))}`;
1497
1929
  }
1498
1930
  function resolveTheme(userTheme, base = DEFAULT_THEME) {
1499
1931
  let merged = userTheme ? deepMerge(base, themeConfigToPartial(userTheme)) : { ...base };
@@ -1523,6 +1955,10 @@ function resolveTheme(userTheme, base = DEFAULT_THEME) {
1523
1955
  }
1524
1956
  return {
1525
1957
  ...merged,
1958
+ colors: {
1959
+ ...merged.colors,
1960
+ neutral: deriveNeutralRamp(merged.colors.text, merged.colors.background, dark)
1961
+ },
1526
1962
  isDark: dark,
1527
1963
  _tokenPairs: userTheme ? collectTokenPairs(userTheme) : void 0
1528
1964
  };
@@ -1540,10 +1976,11 @@ function scaleFontSize(baseFontSize, width) {
1540
1976
  const t = (width - 250) / 250;
1541
1977
  return Math.max(Math.round(baseFontSize * (0.72 + t * 0.28)), 10);
1542
1978
  }
1543
- function buildTextStyle(defaults, fontFamily, textColor, width, overrides) {
1979
+ var CRAMPED_TITLE_SCALE = 0.65;
1980
+ function buildTextStyle(defaults, fontFamily, textColor, width, overrides, extraScale = 1) {
1544
1981
  const hasExplicitSize = overrides?.fontSize !== void 0;
1545
1982
  const baseFontSize = overrides?.fontSize ?? defaults.fontSize;
1546
- const fontSize = hasExplicitSize ? baseFontSize : scaleFontSize(baseFontSize, width);
1983
+ const fontSize = hasExplicitSize ? baseFontSize : Math.max(Math.round(scaleFontSize(baseFontSize, width) * extraScale), 10);
1547
1984
  return {
1548
1985
  fontFamily: overrides?.fontFamily ?? fontFamily,
1549
1986
  fontSize,
@@ -1603,7 +2040,7 @@ function footnoteBandHeight(count, theme) {
1603
2040
  if (count <= 0) return 0;
1604
2041
  return count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;
1605
2042
  }
1606
- function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true, bottomLegendHeight = 0) {
2043
+ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true, bottomLegendHeight = 0, crampedTitle = false) {
1607
2044
  if (!chrome || chromeMode === "hidden") {
1608
2045
  if (watermark && width >= BRAND_MIN_WIDTH) {
1609
2046
  const hiddenPad = padding ?? theme.spacing.padding;
@@ -1626,6 +2063,19 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1626
2063
  let topY = pad;
1627
2064
  const topElements = {};
1628
2065
  const eyebrowNorm = chromeMode === "compact" ? null : normalizeChromeText(chrome.eyebrow);
2066
+ const titleNormPeek = normalizeChromeText(chrome.title);
2067
+ const subtitleNormPeek = chromeMode === "compact" ? null : normalizeChromeText(chrome.subtitle);
2068
+ let ruleElement;
2069
+ if (theme.rule && (eyebrowNorm || titleNormPeek || subtitleNormPeek)) {
2070
+ ruleElement = {
2071
+ x: pad,
2072
+ y: topY,
2073
+ width: theme.rule.width,
2074
+ thickness: theme.rule.thickness,
2075
+ color: theme.rule.color
2076
+ };
2077
+ topY += theme.rule.thickness + chromeGap;
2078
+ }
1629
2079
  if (eyebrowNorm) {
1630
2080
  const style = buildTextStyle(
1631
2081
  theme.chrome.eyebrow,
@@ -1647,14 +2097,15 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1647
2097
  topElements.eyebrow = element;
1648
2098
  topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;
1649
2099
  }
1650
- const titleNorm = normalizeChromeText(chrome.title);
2100
+ const titleNorm = titleNormPeek;
1651
2101
  if (titleNorm) {
1652
2102
  const style = buildTextStyle(
1653
2103
  theme.chrome.title,
1654
2104
  fontFamily,
1655
2105
  theme.chrome.title.color,
1656
2106
  width,
1657
- titleNorm.style
2107
+ titleNorm.style,
2108
+ crampedTitle ? CRAMPED_TITLE_SCALE : 1
1658
2109
  );
1659
2110
  const maxLines = titleNorm.style?.maxLines;
1660
2111
  const lineCount = estimateLineCount(titleNorm.text, style, maxWidth, measureText, maxLines);
@@ -1669,7 +2120,7 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1669
2120
  topElements.title = element;
1670
2121
  topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;
1671
2122
  }
1672
- const subtitleNorm = chromeMode === "compact" ? null : normalizeChromeText(chrome.subtitle);
2123
+ const subtitleNorm = subtitleNormPeek;
1673
2124
  if (subtitleNorm) {
1674
2125
  const style = buildTextStyle(
1675
2126
  theme.chrome.subtitle,
@@ -1705,6 +2156,7 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1705
2156
  return {
1706
2157
  topHeight,
1707
2158
  bottomHeight: compactBottom,
2159
+ ...ruleElement ? { rule: ruleElement } : {},
1708
2160
  ...topElements
1709
2161
  };
1710
2162
  }
@@ -1811,6 +2263,7 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1811
2263
  return {
1812
2264
  topHeight,
1813
2265
  bottomHeight,
2266
+ ...ruleElement ? { rule: ruleElement } : {},
1814
2267
  ...topElements,
1815
2268
  ...bottomElements,
1816
2269
  ...brandElement ? { brand: brandElement } : {}
@@ -2072,6 +2525,12 @@ var D3_FORMAT_SUFFIX_RE = /^(.*~?[efgsrdxXobcnp%])(.+)$/;
2072
2525
  function buildD3Formatter(formatStr) {
2073
2526
  if (!formatStr) return null;
2074
2527
  if (formatStr === "ordinal") return formatOrdinal;
2528
+ if (formatStr === "compact") {
2529
+ return (v) => {
2530
+ if (!Number.isFinite(v)) return String(v);
2531
+ return Math.abs(v) >= 1e3 ? abbreviateNumber(v) : formatNumber(v);
2532
+ };
2533
+ }
2075
2534
  try {
2076
2535
  const fmt = d3Format(formatStr);
2077
2536
  if (formatStr.includes("s")) {
@@ -2142,6 +2601,12 @@ function isYearContext(ctx) {
2142
2601
  function resolveNumberFormatter(formatStr, ctx) {
2143
2602
  if (!formatStr) return null;
2144
2603
  if (formatStr === "ordinal") return formatOrdinal;
2604
+ if (formatStr === "compact") {
2605
+ return (v) => {
2606
+ if (!Number.isFinite(v)) return String(v);
2607
+ return Math.abs(v) >= 1e3 ? abbreviateNumber(v) : formatNumber(v);
2608
+ };
2609
+ }
2145
2610
  if (formatStr === "percent") {
2146
2611
  const fraction = ctx?.extent ? Math.max(Math.abs(ctx.extent[0]), Math.abs(ctx.extent[1])) <= 1 : true;
2147
2612
  return (v) => formatPercent(v, { fraction });
@@ -2583,168 +3048,6 @@ function deepMergeSpec(target, source) {
2583
3048
  }
2584
3049
  return result;
2585
3050
  }
2586
-
2587
- // src/styles/token-definitions.ts
2588
- var CSS_TOKEN_ROOT_SELECTORS = [
2589
- ".oc-root",
2590
- ".oc-chart-root",
2591
- ".oc-table-wrapper",
2592
- ".oc-table-root",
2593
- ".oc-graph-wrapper",
2594
- ".oc-graph-root",
2595
- ".oc-sankey-root",
2596
- ".oc-tilemap-root",
2597
- ".oc-barlist-root",
2598
- ".oc-map-root"
2599
- ];
2600
- var CSS_TOKENS = [
2601
- {
2602
- name: "--oc-font-family",
2603
- light: '\n "Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
2604
- },
2605
- {
2606
- name: "--oc-font-mono",
2607
- light: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace'
2608
- },
2609
- {
2610
- section: "Animation easing presets via CSS linear()",
2611
- name: "--oc-ease-smooth",
2612
- light: "linear(\n 0,\n 0.157,\n 0.438,\n 0.64,\n 0.766,\n 0.85,\n 0.906,\n 0.941,\n 0.964,\n 0.978,\n 0.988,\n 0.994,\n 0.998,\n 1\n )"
2613
- },
2614
- {
2615
- name: "--oc-ease-snappy",
2616
- light: "linear(\n 0,\n 0.012,\n 0.048,\n 0.108,\n 0.194,\n 0.302,\n 0.426,\n 0.559,\n 0.69,\n 0.808,\n 0.905,\n 0.973,\n 1.013,\n 1.028,\n 1.023,\n 1.006,\n 0.984,\n 0.966,\n 0.957,\n 0.957,\n 0.964,\n 0.975,\n 0.986,\n 0.995,\n 1,\n 1.003,\n 1.002,\n 1,\n 0.998,\n 0.998,\n 0.999,\n 1\n )",
2617
- consumers: "EASE_VAR_MAP in vanilla renderers"
2618
- },
2619
- { section: "Animation timing defaults", name: "--oc-animation-duration", light: "500ms" },
2620
- { name: "--oc-animation-stagger", light: "80ms" },
2621
- { name: "--oc-annotation-delay", light: "200ms" },
2622
- { section: "Typography scale (editorial design system)", name: "--oc-title-size", light: "26px" },
2623
- { name: "--oc-title-weight", light: "590" },
2624
- { name: "--oc-title-tracking", light: "-0.022em" },
2625
- { name: "--oc-subtitle-size", light: "14px" },
2626
- { name: "--oc-subtitle-weight", light: "450" },
2627
- { name: "--oc-source-size", light: "11px" },
2628
- { name: "--oc-source-weight", light: "450" },
2629
- { name: "--oc-body-size", light: "13px" },
2630
- { name: "--oc-eyebrow-size", light: "11px" },
2631
- { name: "--oc-eyebrow-weight", light: "550" },
2632
- { name: "--oc-eyebrow-tracking", light: "0.08em" },
2633
- { section: "Surfaces (light mode defaults)", name: "--oc-bg", light: "#ffffff", dark: "#09090b" },
2634
- { name: "--oc-card", light: "#ffffff", dark: "#111113" },
2635
- {
2636
- name: "--oc-secondary",
2637
- light: "#f4f4f5",
2638
- dark: "#27272a",
2639
- comment: "zinc-100, raised surface"
2640
- },
2641
- {
2642
- section: `Text levels. Names invert across modes: in light mode "muted" sits at
2643
- * a lighter zinc step than "subtle" because both are picked relative to
2644
- * the active background, not from a fixed lightness ladder. The intent
2645
- * is "muted = first step away from primary text"; "subtle = next step
2646
- * down"; etc.
2647
- *
2648
- * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,
2649
- * subtle=zinc-400, faint=zinc-300
2650
- * Dark mode (dark.css): inverts the surface tokens but keeps the same
2651
- * muted -> subtle -> faint progression away from primary.`,
2652
- name: "--oc-text",
2653
- light: "#09090b",
2654
- dark: "#f7f8f8"
2655
- },
2656
- { name: "--oc-text-secondary", light: "#3f3f46", dark: "#d0d6e0" },
2657
- { name: "--oc-text-muted", light: "#71717a", dark: "#a1a1aa" },
2658
- { name: "--oc-text-faint", light: "#d4d4d8", dark: "#52525b" },
2659
- {
2660
- section: "Lines",
2661
- name: "--oc-gridline",
2662
- light: "rgba(0, 0, 0, 0.1)",
2663
- dark: "rgba(255, 255, 255, 0.05)"
2664
- },
2665
- {
2666
- name: "--oc-axis",
2667
- light: "rgba(0, 0, 0, 0.1)",
2668
- dark: "rgba(255, 255, 255, 0.1)"
2669
- },
2670
- {
2671
- name: "--oc-border",
2672
- light: "rgba(0, 0, 0, 0.08)",
2673
- dark: "rgba(255, 255, 255, 0.1)"
2674
- },
2675
- { name: "--oc-border-radius", light: "2px" },
2676
- {
2677
- section: "Brand and semantic",
2678
- name: "--oc-accent",
2679
- light: "#06b6d4",
2680
- dark: "#06b6d4"
2681
- },
2682
- {
2683
- name: "--oc-accent-strong",
2684
- light: "#0891b2",
2685
- dark: "#06b6d4",
2686
- comment: "darker cyan for line strokes on light bg",
2687
- consumers: "stamped via adaptForLightLineStroke"
2688
- },
2689
- { name: "--oc-positive", light: "#10b981", dark: "#34d399" },
2690
- { name: "--oc-negative", light: "#e11d48", dark: "#fb7185" },
2691
- { name: "--oc-focus", light: "#3b82f6", dark: "#60a5fa" },
2692
- {
2693
- name: "--oc-focus-ring",
2694
- light: "rgba(59, 130, 246, 0.1)",
2695
- comment: "static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode"
2696
- },
2697
- {
2698
- name: "--oc-focus-ring-strong",
2699
- light: "rgba(59, 130, 246, 0.25)"
2700
- },
2701
- {
2702
- name: "--oc-editable-hover",
2703
- light: "rgba(79, 70, 229, 0.35)",
2704
- comment: "edit-mode hover outline, indigo one-off"
2705
- },
2706
- {
2707
- section: "Spacing scale (4px base) \u2014 only --oc-space-2/4 are consumed (JS-stamped)",
2708
- name: "--oc-space-2",
2709
- light: "8px",
2710
- consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
2711
- },
2712
- {
2713
- name: "--oc-space-4",
2714
- light: "16px",
2715
- consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
2716
- },
2717
- {
2718
- section: "Interactive states",
2719
- name: "--oc-hover-bg",
2720
- light: "rgba(0, 0, 0, 0.025)",
2721
- dark: "rgba(255, 255, 255, 0.05)"
2722
- },
2723
- {
2724
- name: "--oc-tooltip-bg",
2725
- light: "rgba(255, 255, 255, 0.88)",
2726
- dark: "rgba(17, 17, 19, 0.92)"
2727
- },
2728
- {
2729
- name: "--oc-tooltip-border",
2730
- light: "rgba(0, 0, 0, 0.08)",
2731
- dark: "rgba(255, 255, 255, 0.08)"
2732
- },
2733
- {
2734
- name: "--oc-tooltip-shadow",
2735
- light: "0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12)",
2736
- dark: "0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4)"
2737
- },
2738
- { name: "--oc-tooltip-text", light: "#09090b", dark: "#f7f8f8" },
2739
- { name: "--oc-legend-text", light: "#3f3f46", dark: "#d0d6e0" }
2740
- ];
2741
- var TOKEN_MAP = new Map(CSS_TOKENS.map((t) => [t.name, t]));
2742
- function cssTokenDefault(name, mode) {
2743
- const token = TOKEN_MAP.get(name);
2744
- if (!token) throw new Error(`Unknown CSS token: ${name}`);
2745
- if (mode === "dark" && token.dark !== void 0) return token.dark;
2746
- return token.light;
2747
- }
2748
3051
  export {
2749
3052
  ACHROMATIC_RAMP,
2750
3053
  AXIS_TITLE_GAP,
@@ -2756,7 +3059,12 @@ export {
2756
3059
  BRAND_RESERVE_WIDTH,
2757
3060
  BREAKPOINT_COMPACT_MAX,
2758
3061
  BREAKPOINT_MEDIUM_MAX,
3062
+ CATEGORICAL_EXTENDED_PALETTE,
3063
+ CATEGORICAL_FILL_PALETTE,
3064
+ CATEGORICAL_FILL_PALETTE_DARK,
2759
3065
  CATEGORICAL_PALETTE,
3066
+ CATEGORICAL_PALETTE_DARK,
3067
+ COMPACT_MAX_X_TICKS,
2760
3068
  COMPACT_WIDTH,
2761
3069
  CSS_TOKENS,
2762
3070
  CSS_TOKEN_ROOT_SELECTORS,
@@ -2766,6 +3074,8 @@ export {
2766
3074
  FOOTNOTE_LINE_HEIGHT,
2767
3075
  GRAPH_ENCODING_RULES,
2768
3076
  HEIGHT_CRAMPED_MAX,
3077
+ HEIGHT_MINIMAL_MAX,
3078
+ HEIGHT_NO_GRID_MAX,
2769
3079
  HEIGHT_SHORT_MAX,
2770
3080
  HPAD_COMPACT_FRACTION,
2771
3081
  HPAD_COMPACT_MIN,
@@ -2799,6 +3109,7 @@ export {
2799
3109
  areaChart,
2800
3110
  axisTitleOffset,
2801
3111
  barChart,
3112
+ broadsheet,
2802
3113
  buildCompactStepFormatter,
2803
3114
  buildD3Formatter,
2804
3115
  buildTemporalFormatter,
@@ -2813,6 +3124,7 @@ export {
2813
3124
  dataTable,
2814
3125
  deepMergeSpec,
2815
3126
  defaultNumberFormatter,
3127
+ deriveNeutralRamp,
2816
3128
  detectCollision,
2817
3129
  donutChart,
2818
3130
  dotChart,
@@ -2861,6 +3173,7 @@ export {
2861
3173
  overlapArea,
2862
3174
  pickLabelColor,
2863
3175
  pieChart,
3176
+ resolveChromeEconomy,
2864
3177
  resolveCollisions,
2865
3178
  resolveMarkDef,
2866
3179
  resolveMarkType,
@@ -2870,6 +3183,7 @@ export {
2870
3183
  resolveTheme,
2871
3184
  scatterChart,
2872
3185
  simulateColorBlindness,
3186
+ terminal,
2873
3187
  textAscent,
2874
3188
  tileMap,
2875
3189
  truncateRotatedLabel,