basalt-ui 1.20.0 → 1.21.0

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 (87) hide show
  1. package/AGENTS.md +9 -5
  2. package/CHANGELOG.md +19 -0
  3. package/MIGRATING.md +147 -49
  4. package/README.md +42 -8
  5. package/agent/rules/basalt-app.md +41 -0
  6. package/agent/rules/basalt-charts.md +15 -12
  7. package/agent/rules/basalt-content.md +4 -0
  8. package/agent/rules/basalt-router.md +57 -5
  9. package/agent/rules/basalt-state.md +21 -3
  10. package/agent/rules/basalt-tokens.md +73 -16
  11. package/agent/skills/basalt-app/SKILL.md +4 -1
  12. package/agent/skills/basalt-charts/SKILL.md +5 -3
  13. package/agent/templates/CLAUDE-block.md.tpl +11 -5
  14. package/configs/lefthook.yml +48 -15
  15. package/configs/oxlint-plugin.js +181 -68
  16. package/configs/oxlint-plugin.test.ts +215 -9
  17. package/dist/charts/kinds/Bars.d.ts +1 -1
  18. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  19. package/dist/charts/kinds/Donut.d.ts +1 -1
  20. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  21. package/dist/charts/kinds/DualPanel.d.ts +1 -1
  22. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  23. package/dist/charts/kinds/Heatmap.d.ts +1 -1
  24. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  25. package/dist/charts/kinds/MultiLine.d.ts +1 -1
  26. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  27. package/dist/charts/kinds/StackedArea.d.ts +1 -1
  28. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  29. package/dist/charts/kinds/ZonedLine.d.ts +1 -1
  30. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  31. package/dist/cli/index.d.ts +48 -14
  32. package/dist/cli/index.d.ts.map +1 -1
  33. package/dist/cli/index.js +328 -66
  34. package/dist/cli/waiver-audit.test.d.ts +2 -0
  35. package/dist/cli/waiver-audit.test.d.ts.map +1 -0
  36. package/dist/guard/index.d.ts +47 -2
  37. package/dist/guard/index.d.ts.map +1 -1
  38. package/dist/guard/index.js +176 -31
  39. package/dist/router-tanstack/multi-search-param-store.d.ts +13 -0
  40. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  41. package/dist/router-tanstack/multi-search-param-store.js +4 -1
  42. package/dist/router-tanstack/search-param-store.d.ts +29 -5
  43. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  44. package/dist/router-tanstack/search-param-store.js +31 -3
  45. package/dist/router-tanstack/search-param-store.test.d.ts +2 -0
  46. package/dist/router-tanstack/search-param-store.test.d.ts.map +1 -0
  47. package/dist/shell/index.d.ts +12 -5
  48. package/dist/shell/index.d.ts.map +1 -1
  49. package/dist/shell/index.js +24 -6
  50. package/dist/tokens/index.d.ts +3 -1
  51. package/dist/tokens/index.d.ts.map +1 -1
  52. package/dist/tokens/index.js +5 -1
  53. package/dist/tokens/palette.d.ts +2 -2
  54. package/dist/tokens/palette.js +2 -2
  55. package/dist/tokens/text-scale.test.d.ts +2 -0
  56. package/dist/tokens/text-scale.test.d.ts.map +1 -0
  57. package/dist/tokens.css +4 -2
  58. package/dist/vite.d.ts +24 -2
  59. package/dist/vite.d.ts.map +1 -1
  60. package/dist/vite.js +54 -13
  61. package/llms.txt +2 -2
  62. package/package.json +1 -1
  63. package/src/charts/kinds/Bars.tsx +1 -1
  64. package/src/charts/kinds/Donut.tsx +1 -1
  65. package/src/charts/kinds/DualPanel.tsx +2 -2
  66. package/src/charts/kinds/Heatmap.tsx +1 -1
  67. package/src/charts/kinds/MultiLine.tsx +1 -1
  68. package/src/charts/kinds/StackedArea.tsx +1 -1
  69. package/src/charts/kinds/ZonedLine.tsx +1 -1
  70. package/src/cli/index.ts +607 -128
  71. package/src/cli/init-sync.test.ts +2 -0
  72. package/src/cli/template-ledger.test.ts +1 -1
  73. package/src/cli/toolchain-wiring.test.ts +162 -0
  74. package/src/cli/waiver-audit.test.ts +131 -0
  75. package/src/guard/check-source.test.ts +441 -1
  76. package/src/guard/index.ts +430 -60
  77. package/src/router-tanstack/multi-search-param-store.ts +16 -1
  78. package/src/router-tanstack/search-param-store.test.ts +135 -0
  79. package/src/router-tanstack/search-param-store.ts +90 -8
  80. package/src/shell/app-mobile-nav.tsx +1 -1
  81. package/src/shell/index.test.tsx +52 -0
  82. package/src/shell/index.tsx +64 -13
  83. package/src/tokens/index.ts +24 -1
  84. package/src/tokens/palette.ts +2 -2
  85. package/src/tokens/text-scale.test.ts +51 -0
  86. package/src/vite.test.ts +125 -6
  87. package/src/vite.ts +148 -20
@@ -14,6 +14,8 @@ import { deriveRadius } from "./palette.js";
14
14
  import { deriveSpacing } from "./palette.js";
15
15
  const alpha = (token, a) => `color-mix(in srgb, ${token} ${Math.round(a * 100)}%, transparent)`;
16
16
  const TEXT = {
17
+ nano: 10,
18
+ // engraved/graphic label INSIDE a drawn object (a plate face, a dial, a chip) — never UI copy or prose
17
19
  micro: 11,
18
20
  // mono uppercase micro-label (table th, Menu.Label, sidebar/section headers, axis ticks)
19
21
  xs: 12.5,
@@ -30,8 +32,10 @@ const TEXT = {
30
32
  // article-density Prose h2 (docs/CONTENT-SPEC.md §5)
31
33
  kpi: 24,
32
34
  // the StatCard hero numeral (density pass: 31 → 24, tighter/less shouty)
33
- h1: 26
35
+ h1: 26,
34
36
  // article-density Prose h1 (docs/CONTENT-SPEC.md §5)
37
+ display: 30
38
+ // a numeral read at arm's length (a keypad readout, a timer, a kiosk figure) — not a heading
35
39
  };
36
40
  const VX = {
37
41
  // Non-color sizing constants
@@ -112,11 +112,11 @@ export declare const SHADOW: {
112
112
  * ever needs more, raise the DROP. Do not reintroduce an edge; both shapes have been tried.
113
113
  */
114
114
  readonly raised: {
115
- readonly light: "0 1px 2px rgba(28, 25, 23, 0.10)";
115
+ readonly light: "0 1px 2px rgba(28, 25, 23, 0.1)";
116
116
  readonly dark: "0 1px 2px rgba(0, 0, 0, 0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent)";
117
117
  };
118
118
  readonly overlay: {
119
- readonly light: "0 8px 24px rgba(28, 25, 23, 0.10)";
119
+ readonly light: "0 8px 24px rgba(28, 25, 23, 0.1)";
120
120
  readonly dark: "0 8px 24px rgba(0, 0, 0, 0.5)";
121
121
  };
122
122
  };
@@ -72,7 +72,7 @@ const SHADOW = {
72
72
  * ever needs more, raise the DROP. Do not reintroduce an edge; both shapes have been tried.
73
73
  */
74
74
  raised: {
75
- light: "0 1px 2px rgba(28, 25, 23, 0.10)",
75
+ light: "0 1px 2px rgba(28, 25, 23, 0.1)",
76
76
  dark: "0 1px 2px rgba(0, 0, 0, 0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent)"
77
77
  },
78
78
  // Floating-layer elevation (menus, popovers, tooltips, modals, drawers) — a REAL drop shadow, a
@@ -80,7 +80,7 @@ const SHADOW = {
80
80
  // Unlike `card`, no embedded ring: floating surfaces carry a real 1px `--vx-surface-border` so
81
81
  // Mantine's rotated-square arrow inherits a proper edge.
82
82
  overlay: {
83
- light: "0 8px 24px rgba(28, 25, 23, 0.10)",
83
+ light: "0 8px 24px rgba(28, 25, 23, 0.1)",
84
84
  dark: "0 8px 24px rgba(0, 0, 0, 0.5)"
85
85
  }
86
86
  };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=text-scale.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-scale.test.d.ts","sourceRoot":"","sources":["../../src/tokens/text-scale.test.ts"],"names":[],"mappings":""}
package/dist/tokens.css CHANGED
@@ -138,6 +138,7 @@
138
138
  --vx-fill-hover-lime: color-mix(in srgb, var(--vx-fill-lime) 88%, #000);
139
139
  --vx-fill-hover-yellow: color-mix(in srgb, var(--vx-fill-yellow) 88%, #000);
140
140
  --vx-fill-hover-orange: color-mix(in srgb, var(--vx-fill-orange) 88%, #000);
141
+ --vx-text-nano: 10px;
141
142
  --vx-text-micro: 11px;
142
143
  --vx-text-xs: 12.5px;
143
144
  --vx-text-sm: 13.5px;
@@ -147,6 +148,7 @@
147
148
  --vx-text-h2: 21px;
148
149
  --vx-text-kpi: 24px;
149
150
  --vx-text-h1: 26px;
151
+ --vx-text-display: 30px;
150
152
  --vx-area-top: 22%;
151
153
  --vx-area-bottom: 1%;
152
154
  --vx-good: color-mix(in srgb, var(--vx-good-solid) 18%, transparent);
@@ -274,8 +276,8 @@ html[data-mantine-color-scheme='light'] {
274
276
  --vx-divider: color-mix(in srgb, #e5e5e5 65%, transparent);
275
277
  --vx-shadow-card: 0 1px 2px rgba(28, 25, 23, 0.05), 0 0 0 1px var(--vx-surface-hairline);
276
278
  --vx-shadow-ctrl: 0 1px 2px rgba(28, 25, 23, 0.12);
277
- --vx-shadow-raised: 0 1px 2px rgba(28, 25, 23, 0.10);
278
- --vx-shadow-overlay: 0 8px 24px rgba(28, 25, 23, 0.10);
279
+ --vx-shadow-raised: 0 1px 2px rgba(28, 25, 23, 0.1);
280
+ --vx-shadow-overlay: 0 8px 24px rgba(28, 25, 23, 0.1);
279
281
  --vx-surface-bg: #f2f2f5;
280
282
  --vx-surface-panel: #fafafd;
281
283
  --vx-surface-panel-hover: #ffffff;
package/dist/vite.d.ts CHANGED
@@ -49,7 +49,21 @@ export type BasaltAppOptions = {
49
49
  light: string;
50
50
  dark: string;
51
51
  };
52
- /** Manifest `background_color` + anti-FOUC paint color. Default: the resolved dark theme color. */
52
+ /**
53
+ * Which scheme the anti-FOUC `<style>` paints before the app boots. Default `'dark'` —
54
+ * `BasaltProvider`'s own default. `'light'` paints the light surface, `'auto'` declares
55
+ * `color-scheme: light dark` and picks the background off `prefers-color-scheme`, and `false`
56
+ * paints the background but declares no `color-scheme` at all (leave native controls to Mantine).
57
+ *
58
+ * Set this to whatever the app passes as `defaultColorScheme` — a mismatch is a visible flash of
59
+ * the wrong surface, not a correctness bug: the boot rule stops applying the moment Mantine
60
+ * writes `data-mantine-color-scheme` on `<html>` (see `BOOT_SCOPE`).
61
+ */
62
+ colorScheme?: 'dark' | 'light' | 'auto' | false;
63
+ /**
64
+ * Manifest `background_color` + anti-FOUC paint color. Default: the theme color matching
65
+ * `colorScheme` (the dark one for `'dark'` / `'auto'` / `false`, the light one for `'light'`).
66
+ */
53
67
  backgroundColor?: string;
54
68
  /** Manifest `display` mode. Default: `'standalone'`. */
55
69
  display?: 'standalone' | 'minimal-ui' | 'fullscreen' | 'browser';
@@ -104,13 +118,21 @@ export type BasaltAppOptions = {
104
118
  * manifest `theme_color` both need a FLAT color, so the flattening step is the contract this
105
119
  * plugin owes its output regardless of which shape the palette hands it.
106
120
  *
121
+ * Two behaviours worth knowing before adopting it, both of which the plugin owns because the
122
+ * consumer cannot reach them:
123
+ * • It HOISTS the shell's `<meta charset>` ahead of everything it injects, so the encoding
124
+ * declaration stays inside the spec's first 1024 bytes (see `hoistCharset`).
125
+ * • The anti-FOUC `<style>` expires the moment Mantine resolves the real scheme, and `colorScheme`
126
+ * selects which surface it paints until then (see `BOOT_SCOPE` / `buildBootStyle`).
127
+ *
107
128
  * @example
108
129
  * // vite.config.ts
109
130
  * import { basaltAppPlugin, basaltViteConfig } from 'basalt-ui/vite'
110
131
  *
111
132
  * export default {
112
133
  * ...basaltViteConfig({ port: 5173 }),
113
- * plugins: [react(), ...basaltAppPlugin({ name: 'Argo' })],
134
+ * // colorScheme mirrors <BasaltProvider defaultColorScheme> — omit it for the dark default.
135
+ * plugins: [react(), ...basaltAppPlugin({ name: 'Argo', colorScheme: 'auto' })],
114
136
  * }
115
137
  */
116
138
  export declare function basaltAppPlugin(options: BasaltAppOptions): PluginOption[];
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAA6B,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAI/E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AA6CD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,UAAU,CAgFpE;AAgBD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrD,mGAAmG;IACnG,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wDAAwD;IACxD,OAAO,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAA;IAChE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mGAAmG;IACnG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,wFAAwF;IACxF,KAAK,CAAC,EAAE,KAAK,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC3B,oFAAoF;IACpF,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,SAAS,GAAG,qBAAqB,CAAA;KAAE,CAAA;IACvF,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,KAAK,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClD,CAAA;AAuZD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAMzE"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAA6B,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAI/E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AA6CD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,UAAU,CAgFpE;AAgBD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrD;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;IAC/C;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wDAAwD;IACxD,OAAO,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAA;IAChE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mGAAmG;IACnG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,wFAAwF;IACxF,KAAK,CAAC,EAAE,KAAK,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC3B,oFAAoF;IACpF,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,SAAS,GAAG,qBAAqB,CAAA;KAAE,CAAA;IACvF,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,KAAK,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClD,CAAA;AAigBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAMzE"}
package/dist/vite.js CHANGED
@@ -140,6 +140,29 @@ const VIEWPORT_TAG = {
140
140
  tag: "meta",
141
141
  attrs: { name: "viewport", content: "width=device-width, initial-scale=1.0, viewport-fit=cover" }
142
142
  };
143
+ const BOOT_SCOPE = "html:not([data-mantine-color-scheme])";
144
+ const BOOT_COLOR_SCHEME = { dark: "dark", light: "light", auto: "light dark" };
145
+ function buildBootStyle(input) {
146
+ const { colorScheme, themeColor, backgroundColor, explicitBackground } = input;
147
+ if (colorScheme === false) return `${BOOT_SCOPE}{background-color:${backgroundColor}}`;
148
+ const scheme = BOOT_COLOR_SCHEME[colorScheme];
149
+ if (colorScheme !== "auto" || explicitBackground) {
150
+ return `${BOOT_SCOPE}{background-color:${backgroundColor};color-scheme:${scheme}}`;
151
+ }
152
+ return `${BOOT_SCOPE}{background-color:${themeColor.light};color-scheme:${scheme}}@media(prefers-color-scheme:dark){${BOOT_SCOPE}{background-color:${themeColor.dark}}}`;
153
+ }
154
+ const CHARSET_META_RE = /[ \t]*<meta\s+charset\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>=]+))\s*\/?>[ \t]*\r?\n?/i;
155
+ const HTTP_EQUIV_CHARSET_RE = /<meta\s[^>]*http-equiv\s*=\s*["']?content-type["']?[^>]*>/i;
156
+ const DEFAULT_CHARSET = "UTF-8";
157
+ function hoistCharset(html) {
158
+ if (HTTP_EQUIV_CHARSET_RE.test(html)) return { html, tag: void 0 };
159
+ const match = CHARSET_META_RE.exec(html);
160
+ const charset = match?.[1] ?? match?.[2] ?? match?.[3] ?? DEFAULT_CHARSET;
161
+ return {
162
+ html: match ? html.replace(CHARSET_META_RE, "") : html,
163
+ tag: { tag: "meta", attrs: { charset } }
164
+ };
165
+ }
143
166
  function buildSeoTags(input) {
144
167
  const { name, description, seo } = input;
145
168
  const tags = [];
@@ -183,7 +206,7 @@ function buildStaticTags(input) {
183
206
  options,
184
207
  shortName,
185
208
  themeColor,
186
- backgroundColor,
209
+ bootStyle,
187
210
  iconsEnabled,
188
211
  iconsDir,
189
212
  darkreader,
@@ -210,10 +233,7 @@ function buildStaticTags(input) {
210
233
  }
211
234
  }
212
235
  );
213
- tags.push({
214
- tag: "style",
215
- children: `html{background-color:${backgroundColor};color-scheme:dark}`
216
- });
236
+ tags.push({ tag: "style", children: bootStyle });
217
237
  if (iconsEnabled) {
218
238
  tags.push(
219
239
  {
@@ -274,7 +294,7 @@ function buildManifestJson(input) {
274
294
  scope,
275
295
  id,
276
296
  display,
277
- themeColor,
297
+ schemeColor,
278
298
  backgroundColor,
279
299
  iconsDir,
280
300
  base
@@ -287,7 +307,9 @@ function buildManifestJson(input) {
287
307
  start_url: startUrl,
288
308
  scope,
289
309
  display,
290
- theme_color: themeColor.dark,
310
+ // A manifest carries ONE theme color, so it follows the boot scheme rather than the dual
311
+ // `<meta name="theme-color">` pair. Unchanged for the default (`'dark'`) and for `'auto'`.
312
+ theme_color: schemeColor,
291
313
  background_color: backgroundColor,
292
314
  icons: [
293
315
  {
@@ -313,7 +335,15 @@ function createMainPlugin(options) {
313
335
  const iconsDir = options.icons === false ? void 0 : options.icons?.dir;
314
336
  const manifestEnabled = options.manifest !== false;
315
337
  const themeColor = resolveThemeColors(options.themeColor);
316
- const backgroundColor = options.backgroundColor ?? themeColor.dark;
338
+ const colorScheme = options.colorScheme ?? "dark";
339
+ const schemeColor = colorScheme === "light" ? themeColor.light : themeColor.dark;
340
+ const backgroundColor = options.backgroundColor ?? schemeColor;
341
+ const bootStyle = buildBootStyle({
342
+ colorScheme,
343
+ themeColor,
344
+ backgroundColor,
345
+ explicitBackground: options.backgroundColor !== void 0
346
+ });
317
347
  let manifestJson = "";
318
348
  let manifestHref = "/site.webmanifest";
319
349
  let staticTags = [];
@@ -332,7 +362,7 @@ function createMainPlugin(options) {
332
362
  scope,
333
363
  id,
334
364
  display,
335
- themeColor,
365
+ schemeColor,
336
366
  backgroundColor,
337
367
  iconsDir,
338
368
  base
@@ -341,7 +371,7 @@ function createMainPlugin(options) {
341
371
  options,
342
372
  shortName,
343
373
  themeColor,
344
- backgroundColor,
374
+ bootStyle,
345
375
  iconsEnabled,
346
376
  iconsDir,
347
377
  darkreader,
@@ -350,9 +380,20 @@ function createMainPlugin(options) {
350
380
  manifestHref
351
381
  });
352
382
  },
353
- transformIndexHtml(html) {
354
- const tags = hasViewportMeta(html) ? staticTags : [VIEWPORT_TAG, ...staticTags];
355
- return { html, tags };
383
+ transformIndexHtml: {
384
+ // `pre` so this plugin's tags are the FIRST head-prepend contributions Vite collects — that
385
+ // ordering is what puts the hoisted <meta charset> at the very top of <head> rather than
386
+ // behind another plugin's tags. Vite injects all collected tags after every hook has run.
387
+ order: "pre",
388
+ handler(html) {
389
+ const hoisted = hoistCharset(html);
390
+ const tags = [
391
+ ...hoisted.tag ? [hoisted.tag] : [],
392
+ ...hasViewportMeta(html) ? [] : [VIEWPORT_TAG],
393
+ ...staticTags
394
+ ];
395
+ return { html: hoisted.html, tags };
396
+ }
356
397
  },
357
398
  generateBundle() {
358
399
  if (!manifestEnabled) return;
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.20.0
1
+ # basalt-ui v1.21.0
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,7 +19,7 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.20.0
22
+ Version: 1.21.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -125,7 +125,7 @@ export type BarsProps<T> = {
125
125
  /** Legend config forwarded to `CartesianChart`; `false` disables the legend (sparkline escape).
126
126
  * Default `{ placement: 'bottom' }`. */
127
127
  legend?: ChartLegendConfig | false
128
- /** Accessible text alternative, forwarded as `aria-label` (+ `role="img"`). */
128
+ /** Accessible text alternative, forwarded as `aria-label` (+ `role="group"`). */
129
129
  ariaLabel?: string
130
130
  /** Forwarded to `CartesianChart` — see `ChartPending`'s JSDoc for the three-state rationale. */
131
131
  isPending?: boolean
@@ -50,7 +50,7 @@ export type DonutProps<K extends string = SeriesKey> = {
50
50
  centerContent?: ReactNode
51
51
  innerRatio?: number
52
52
  padAngle?: number
53
- /** Accessible text alternative, forwarded to `ChartFrame` as `aria-label` (+ `role="img"`). */
53
+ /** Accessible text alternative, forwarded to `ChartFrame` as `aria-label` (+ `role="group"`). */
54
54
  ariaLabel?: string
55
55
  /** Forwarded to `ChartFrame` — see `ChartPending`'s JSDoc for the three-state rationale. */
56
56
  isPending?: boolean
@@ -94,7 +94,7 @@ export type DualPanelProps<T> = {
94
94
  /** Legend config forwarded to `ChartFrame`; `false` disables the legend (sparkline escape).
95
95
  * Default `{ placement: 'bottom' }`. */
96
96
  legend?: ChartLegendConfig | false
97
- /** Accessible text alternative, forwarded to `ChartFrame` as `aria-label` (+ `role="img"`). */
97
+ /** Accessible text alternative, forwarded to `ChartFrame` as `aria-label` (+ `role="group"`). */
98
98
  ariaLabel?: string
99
99
  /** Forwarded to `ChartFrame` — see `ChartPending`'s JSDoc for the three-state rationale. */
100
100
  isPending?: boolean
@@ -521,7 +521,7 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
521
521
  </>
522
522
  )}
523
523
 
524
- {/* theme-allow basalt/hand-rolled-plot: TWO panes over one x scale is not a single
524
+ {/* theme-allow-file basalt/hand-rolled-plot: TWO panes over one x scale is not a single
525
525
  cartesian plot, so `CartesianChart` (one plot rect, one or two y axes) cannot express
526
526
  it. This file therefore assembles the plot itself — but from the SAME parts every
527
527
  other chart gets: `ChartFrame`, `autoMargin` + `probeAxisLabels`, `useChartCursor`,
@@ -53,7 +53,7 @@ export type HeatmapProps<T> = {
53
53
  renderTooltip?: (cell: HeatmapCell) => ReactNode
54
54
  /** Optional gradient legend strip below the grid (faint → solid color). */
55
55
  legend?: { min: string; max: string }
56
- /** Accessible text alternative, forwarded to `ChartFrame` as `aria-label` (+ `role="img"`). */
56
+ /** Accessible text alternative, forwarded to `ChartFrame` as `aria-label` (+ `role="group"`). */
57
57
  ariaLabel?: string
58
58
  /** Forwarded to `ChartFrame` — see `ChartPending`'s JSDoc for the three-state rationale. */
59
59
  isPending?: boolean
@@ -52,7 +52,7 @@ export type MultiLineProps<T> = {
52
52
  /** Legend config forwarded to `CartesianChart`; `false` disables the legend (sparkline escape).
53
53
  * Default `{ placement: 'bottom' }`. */
54
54
  legend?: ChartLegendConfig | false
55
- /** Accessible text alternative, forwarded to `CartesianChart` as `aria-label` (+ `role="img"`). */
55
+ /** Accessible text alternative, forwarded to `CartesianChart` as `aria-label` (+ `role="group"`). */
56
56
  ariaLabel?: string
57
57
  /** Forwarded to `CartesianChart` — see `ChartPending`'s JSDoc for the three-state rationale. */
58
58
  isPending?: boolean
@@ -33,7 +33,7 @@ export type StackedAreaProps<T> = {
33
33
  /** Legend config forwarded to `CartesianChart`; `false` disables the legend (sparkline escape).
34
34
  * Default `{ placement: 'bottom' }`. */
35
35
  legend?: ChartLegendConfig | false
36
- /** Accessible text alternative, forwarded as `aria-label` (+ `role="img"`). */
36
+ /** Accessible text alternative, forwarded as `aria-label` (+ `role="group"`). */
37
37
  ariaLabel?: string
38
38
  /** Forwarded to `CartesianChart` — see `ChartPending`'s JSDoc for the three-state rationale. */
39
39
  isPending?: boolean
@@ -73,7 +73,7 @@ export type ZonedLineProps<T> = {
73
73
  /** Legend config forwarded to `CartesianChart`; `false` disables the legend (sparkline escape).
74
74
  * Default `{ placement: 'bottom' }`. */
75
75
  legend?: ChartLegendConfig | false
76
- /** Accessible text alternative, forwarded to `CartesianChart` as `aria-label` (+ `role="img"`). */
76
+ /** Accessible text alternative, forwarded to `CartesianChart` as `aria-label` (+ `role="group"`). */
77
77
  ariaLabel?: string
78
78
  /** Forwarded to `CartesianChart` — see `ChartPending`'s JSDoc for the three-state rationale. */
79
79
  isPending?: boolean