basalt-ui 1.21.0 → 1.23.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 (85) hide show
  1. package/AGENTS.md +11 -1
  2. package/CHANGELOG.md +33 -0
  3. package/MIGRATING.md +205 -9
  4. package/README.md +63 -16
  5. package/agent/rules/basalt-app.md +39 -9
  6. package/agent/rules/basalt-charts.md +41 -6
  7. package/agent/rules/basalt-router.md +1 -1
  8. package/agent/rules/basalt-state.md +4 -4
  9. package/agent/rules/basalt-tokens.md +25 -4
  10. package/agent/skills/basalt-app/SKILL.md +4 -3
  11. package/agent/skills/basalt-charts/SKILL.md +23 -8
  12. package/agent/templates/DESIGN.md.tpl +3 -1
  13. package/configs/oxlint-plugin.js +86 -9
  14. package/configs/oxlint-plugin.test.ts +173 -0
  15. package/dist/charts/hooks/useBandPlot.d.ts +138 -0
  16. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
  17. package/dist/charts/hooks/useBandPlot.js +127 -0
  18. package/dist/charts/index.d.ts +4 -0
  19. package/dist/charts/index.d.ts.map +1 -1
  20. package/dist/charts/index.js +14 -0
  21. package/dist/charts/kinds/BandStrip.d.ts +113 -0
  22. package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
  23. package/dist/charts/kinds/BandStrip.js +218 -0
  24. package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
  25. package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
  26. package/dist/charts/kinds/Bars.d.ts +5 -1
  27. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  28. package/dist/charts/kinds/Bars.js +2 -0
  29. package/dist/charts/kinds/MirroredBars.d.ts +93 -0
  30. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
  31. package/dist/charts/kinds/MirroredBars.js +315 -0
  32. package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
  33. package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
  34. package/dist/charts/kinds/MultiLine.d.ts +5 -1
  35. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  36. package/dist/charts/kinds/MultiLine.js +2 -0
  37. package/dist/charts/kinds/StackedArea.d.ts +5 -1
  38. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  39. package/dist/charts/kinds/StackedArea.js +2 -0
  40. package/dist/charts/kinds/ZonedLine.d.ts +5 -1
  41. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  42. package/dist/charts/kinds/ZonedLine.js +2 -0
  43. package/dist/charts/primitives/CartesianChart.d.ts +18 -2
  44. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  45. package/dist/charts/primitives/CartesianChart.js +5 -2
  46. package/dist/charts/primitives/HatchPattern.d.ts +31 -0
  47. package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
  48. package/dist/charts/primitives/HatchPattern.js +31 -0
  49. package/dist/cli/index.d.ts +9 -1
  50. package/dist/cli/index.d.ts.map +1 -1
  51. package/dist/cli/index.js +275 -48
  52. package/dist/cli/shipped-versions.test.d.ts +2 -0
  53. package/dist/cli/shipped-versions.test.d.ts.map +1 -0
  54. package/dist/guard/index.d.ts +67 -0
  55. package/dist/guard/index.d.ts.map +1 -1
  56. package/dist/guard/index.js +70 -11
  57. package/dist/vite.d.ts +44 -2
  58. package/dist/vite.d.ts.map +1 -1
  59. package/dist/vite.js +71 -26
  60. package/llms.txt +2 -2
  61. package/package.json +1 -1
  62. package/src/charts/hooks/useBandPlot.ts +302 -0
  63. package/src/charts/index.ts +14 -0
  64. package/src/charts/kinds/BandStrip.test.tsx +200 -0
  65. package/src/charts/kinds/BandStrip.tsx +364 -0
  66. package/src/charts/kinds/Bars.tsx +7 -1
  67. package/src/charts/kinds/DualPanel.test.tsx +10 -4
  68. package/src/charts/kinds/MirroredBars.test.tsx +193 -0
  69. package/src/charts/kinds/MirroredBars.tsx +455 -0
  70. package/src/charts/kinds/MultiLine.tsx +7 -1
  71. package/src/charts/kinds/StackedArea.tsx +7 -1
  72. package/src/charts/kinds/ZonedLine.tsx +7 -1
  73. package/src/charts/primitives/CartesianChart.test.tsx +86 -4
  74. package/src/charts/primitives/CartesianChart.tsx +28 -3
  75. package/src/charts/primitives/HatchPattern.tsx +55 -0
  76. package/src/cli/check-theme.test.ts +35 -0
  77. package/src/cli/help.test.ts +5 -1
  78. package/src/cli/index.ts +552 -79
  79. package/src/cli/shipped-versions.test.ts +83 -0
  80. package/src/cli/toolchain-wiring.test.ts +362 -6
  81. package/src/cli/waiver-audit.test.ts +88 -4
  82. package/src/guard/check-source.test.ts +293 -0
  83. package/src/guard/index.ts +209 -20
  84. package/src/vite.test.ts +112 -0
  85. package/src/vite.ts +147 -31
@@ -108,7 +108,7 @@ const RADIUS_PROP_RE = /\bradius=(?:\{[0-9]+\}|"[0-9]+")/g;
108
108
  const MOTION_TRANSITION_NUMERIC = /\btransition\s*=\s*\{\{[^}]*\b(?:duration|stiffness|damping|mass)\s*:\s*-?\d/g;
109
109
  const MOTION_TRANSITION_EASE_ARRAY = /\btransition\s*=\s*\{\{[^}]*\bease\s*:\s*\[/g;
110
110
  const RAW_CHART_LEGEND_ARRAY = /<ChartLegend\b[^>]*?\bitems\s*=\s*\{\s*\[/g;
111
- const CHART_ENTRY_POINT_TAG = /<(?:MultiLine|Bars|Donut|DualPanel|Heatmap|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g;
111
+ const CHART_ENTRY_POINT_TAG = /<(?:MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g;
112
112
  const HAS_ARIA_LABEL_PROP = /\bariaLabel\s*=/;
113
113
  const RAW_FORM_CONTROL = /<(?:input|select|textarea)\b/g;
114
114
  const RAW_FORM_CONTROL_TAG = /<(?:input|select|textarea)\b(?:=>|[^>])*?>/g;
@@ -250,10 +250,24 @@ function parseAllowAnnotation(rest, scope) {
250
250
  return { rules, unknownRules, hasReason: reason.length >= MIN_ALLOW_REASON_LENGTH, scope };
251
251
  }
252
252
  const MAX_CSS_CONTINUATION_LINES = 8;
253
- const MAX_COMMENT_BLOCK_LINES = 8;
253
+ function commentBlockEnd(lines, index, tokenAt, syntax) {
254
+ const line = lines[index] ?? "";
255
+ if (syntax === "ts" && /\/\/\s*$/.test(line.slice(0, tokenAt))) return index;
256
+ const closer = syntax === "markup" ? "-->" : "*/";
257
+ if (line.slice(tokenAt).includes(closer)) return index;
258
+ for (let ahead = index + 1; ahead < lines.length; ahead++) {
259
+ if ((lines[ahead] ?? "").includes(closer)) return ahead;
260
+ }
261
+ return index;
262
+ }
254
263
  const COMMENT_OPENER_BEFORE = /(?:\/\/|\/\*|<!--)\s*$/;
255
264
  const COMMENT_ONLY_LINE = /^\s*(?:\{\s*\/\*|\/\/|\/\*|\*|<!--)/;
256
- const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*|<!--|^\s*\*)\s*$/;
265
+ const JSX_COMMENT_CLOSE = /\*\/\s*\}\s*$/;
266
+ function isCommentOnly(raw, stripped) {
267
+ const code = stripped.trim();
268
+ return code === "" || COMMENT_ONLY_LINE.test(raw) || code === "}" && JSX_COMMENT_CLOSE.test(raw);
269
+ }
270
+ const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*\*?|<!--|^\s*\*)\s*$/;
257
271
  function escapeRegExp(value) {
258
272
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
259
273
  }
@@ -272,7 +286,7 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
272
286
  list.push(annotation);
273
287
  byLine.set(index, list);
274
288
  };
275
- const record = (index, annotation, trailing) => {
289
+ const record = (index, annotation, trailing, tokenAt) => {
276
290
  declared.push({ line: index + 1, annotation });
277
291
  if (annotation.scope === "file") {
278
292
  fileScoped.push(annotation);
@@ -280,11 +294,14 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
280
294
  }
281
295
  add(index, annotation);
282
296
  if (!trailing) {
283
- for (let ahead = 1; ahead <= MAX_COMMENT_BLOCK_LINES; ahead++) {
284
- const raw = lines[index + ahead] ?? "";
285
- add(index + ahead, annotation);
286
- if (raw.trim() !== "" && (codeLines[index + ahead] ?? "").trim() !== "") break;
297
+ const closesAt = commentBlockEnd(lines, index, tokenAt, syntax);
298
+ for (let ahead = 1; index + ahead < lines.length; ahead++) {
299
+ const at = index + ahead;
300
+ add(at, annotation);
301
+ if (at <= closesAt) continue;
302
+ const raw = lines[at] ?? "";
287
303
  if (raw.trim() === "") break;
304
+ if (!isCommentOnly(raw, codeLines[at] ?? "")) break;
288
305
  }
289
306
  return;
290
307
  }
@@ -300,10 +317,15 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
300
317
  for (let i = 0; i < lines.length; i++) {
301
318
  const line = lines[i] ?? "";
302
319
  const stripped = codeLines[i] ?? "";
303
- const isCommentOnlyLine = stripped.trim() === "" || COMMENT_ONLY_LINE.test(line);
320
+ const isCommentOnlyLine = isCommentOnly(line, stripped);
304
321
  if (syntax === "markup") {
305
322
  for (const m of line.matchAll(jsonKey)) {
306
- record(i, parseAllowAnnotation(m[2] ?? "", m[1] === void 0 ? "line" : "file"), false);
323
+ record(
324
+ i,
325
+ parseAllowAnnotation(m[2] ?? "", m[1] === void 0 ? "line" : "file"),
326
+ false,
327
+ m.index
328
+ );
307
329
  }
308
330
  }
309
331
  for (const m of line.matchAll(token)) {
@@ -315,7 +337,8 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
315
337
  record(
316
338
  i,
317
339
  parseAllowAnnotation(rest, m[1] === void 0 ? "line" : "file"),
318
- !isCommentOnlyLine
340
+ !isCommentOnlyLine,
341
+ at
319
342
  );
320
343
  }
321
344
  }
@@ -997,13 +1020,49 @@ function checkSource(text, relPath, cfg) {
997
1020
  severity: severityOf(f.kind, cfg)
998
1021
  }));
999
1022
  }
1023
+ function findAllowAnnotations(text, relPath, cfg) {
1024
+ const lines = text.split("\n");
1025
+ const syntax = guardSyntaxFor(relPath);
1026
+ const codeText = syntax === "markup" ? stripMarkupComments(text) : stripComments(text, syntax);
1027
+ const { declared } = collectAllowAnnotations(
1028
+ lines,
1029
+ codeText.split("\n"),
1030
+ cfg.allowComment,
1031
+ syntax
1032
+ );
1033
+ return declared.map(({ line, annotation }) => ({
1034
+ line,
1035
+ scope: annotation.scope,
1036
+ rules: [...annotation.rules],
1037
+ guardKinds: annotation.rules.filter((id) => Object.hasOwn(GUARD_RULES, id)),
1038
+ pluginRules: annotation.rules.filter(
1039
+ (id) => !Object.hasOwn(GUARD_RULES, id) && PLUGIN_RULE_IDS.has(id)
1040
+ ),
1041
+ unknownRules: [...annotation.unknownRules],
1042
+ hasReason: annotation.hasReason,
1043
+ bare: annotation.rules.length === 0 && annotation.unknownRules.length === 0,
1044
+ text: lines[line - 1] ?? ""
1045
+ }));
1046
+ }
1047
+ const NEUTRALIZED_ALLOW_TOKEN = "basalt-audit-neutralized";
1048
+ function neutralizeAllowAnnotation(text, line, cfg) {
1049
+ const lines = text.split("\n");
1050
+ const target = lines[line - 1];
1051
+ if (target === void 0) return text;
1052
+ lines[line - 1] = target.replaceAll(cfg.allowComment, NEUTRALIZED_ALLOW_TOKEN);
1053
+ return lines.join("\n");
1054
+ }
1000
1055
  export {
1001
1056
  DEFAULT_GUARD_CONFIG,
1002
1057
  GENERATED_HEADER_LINE,
1003
1058
  GUARD_RULES,
1059
+ NEUTRALIZED_ALLOW_TOKEN,
1060
+ PLUGIN_RULE_IDS,
1004
1061
  TOKENS_ONLY_DISABLED_KINDS,
1005
1062
  checkSource,
1063
+ findAllowAnnotations,
1006
1064
  guardKindRemedy,
1007
1065
  guardWaiverHint,
1066
+ neutralizeAllowAnnotation,
1008
1067
  unmatchedExemptPatterns
1009
1068
  };
package/dist/vite.d.ts CHANGED
@@ -33,6 +33,29 @@ export type BasaltViteOptions = {
33
33
  enforcementNotice?: boolean;
34
34
  };
35
35
  export declare function basaltViteConfig(opts: BasaltViteOptions): UserConfig;
36
+ /**
37
+ * One icon a consumer names itself — a manifest `icons` member, plus optionally the head `<link>`
38
+ * that points at the same file.
39
+ *
40
+ * The field names are the manifest's own, verbatim, so what a consumer writes here is what ships in
41
+ * `site.webmanifest`. `src` is root-relative (`'/favicon.svg'` or `'favicon.svg'`, both resolve the
42
+ * same) and is prefixed with Vite's `base` exactly like basalt's built-in set.
43
+ */
44
+ export type BasaltAppIcon = {
45
+ /** Root-relative path under `public/`. Prefixed with Vite's resolved `base`. */
46
+ src: string;
47
+ /** Manifest `sizes`. `'any'` for a scalable SVG; `'192x192'` for a raster. */
48
+ sizes?: string;
49
+ /** MIME type, e.g. `'image/svg+xml'`. */
50
+ type?: string;
51
+ /** Manifest `purpose`, e.g. `'any maskable'`. */
52
+ purpose?: string;
53
+ /**
54
+ * Emit a head `<link rel="…">` for this icon too. Omitted → manifest only, which is what an app
55
+ * that links its own favicon from `index.html` wants.
56
+ */
57
+ rel?: 'icon' | 'shortcut icon' | 'apple-touch-icon' | 'mask-icon';
58
+ };
36
59
  export type BasaltAppOptions = {
37
60
  /** App display name — used verbatim as the manifest `name` and (unless overridden) OG title. */
38
61
  name: string;
@@ -80,10 +103,29 @@ export type BasaltAppOptions = {
80
103
  scope?: string;
81
104
  /** Manifest `id`. Default: the resolved `startUrl` (default- or explicitly-derived, per above). */
82
105
  id?: string;
83
- /** Icon links + manifest icon paths. `false` skips the head `<link>` icons entirely. */
106
+ /**
107
+ * Which icons the plugin knows about — three forms, one question each.
108
+ *
109
+ * - **omitted / `{ dir }`** — basalt's own six-filename set (the realfavicongenerator layout argo
110
+ * ships), optionally under a subdirectory of `public/`. Unchanged default.
111
+ * - **`false`** — no head `<link>` icons and no manifest `icons` member, so a manifest is never
112
+ * emitted pointing at files that 404.
113
+ * - **an array** — the icons this app ACTUALLY has, named by the consumer.
114
+ *
115
+ * The array is the form for every app that was not scaffolded from basalt's icon set, which is
116
+ * most of them. `{ dir }` can only move the six fixed filenames; it cannot rename one, so an app
117
+ * whose single icon is `favicon.svg` had no way to say so and had to keep a hand-written manifest
118
+ * beside the generated head — the last thing standing between rb and deleting a file carrying a
119
+ * permanent `theme-allow-file`. One SVG at `sizes: 'any'` is a complete, installable icon set.
120
+ *
121
+ * Every entry becomes a manifest icon; an entry becomes a head `<link>` only when it names a
122
+ * {@link BasaltAppIcon.rel}. That split is deliberate — an app that already links its favicon
123
+ * from its own `index.html` wants the manifest fixed without a duplicate tag, and an app that
124
+ * wants the plugin to own the head says so per icon. An empty array reads as `false`.
125
+ */
84
126
  icons?: false | {
85
127
  dir?: string;
86
- };
128
+ } | readonly BasaltAppIcon[];
87
129
  /** Emits `<meta name="darkreader-lock">` when `'lock'` (default). Pass `false` to omit it. */
88
130
  darkreader?: 'lock' | false;
89
131
  /** Site-wide SEO metadata — only the keys provided are emitted, no per-page SEO. */
@@ -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;;;;;;;;;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"}
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;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAA;IACX,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,kBAAkB,GAAG,WAAW,CAAA;CAClE,CAAA;AAED,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;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,aAAa,EAAE,CAAA;IAC3D,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;AA0kBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAMzE"}
package/dist/vite.js CHANGED
@@ -133,6 +133,18 @@ function withIconPath(dir, file) {
133
133
  function withBase(base, path) {
134
134
  return base + path.slice(1);
135
135
  }
136
+ function isIconList(icons) {
137
+ return Array.isArray(icons);
138
+ }
139
+ function resolveIcons(icons) {
140
+ if (icons === false) return { mode: "none" };
141
+ if (icons === void 0) return { mode: "default", dir: void 0 };
142
+ if (!isIconList(icons)) return { mode: "default", dir: icons.dir };
143
+ return icons.length === 0 ? { mode: "none" } : { mode: "explicit", icons };
144
+ }
145
+ function iconHref(base, src) {
146
+ return withBase(base, src.startsWith("/") ? src : `/${src}`);
147
+ }
136
148
  function hasViewportMeta(html) {
137
149
  return /<meta\s+[^>]*name=["']viewport["']/i.test(html);
138
150
  }
@@ -207,8 +219,7 @@ function buildStaticTags(input) {
207
219
  shortName,
208
220
  themeColor,
209
221
  bootStyle,
210
- iconsEnabled,
211
- iconsDir,
222
+ icons,
212
223
  darkreader,
213
224
  manifestEnabled,
214
225
  base,
@@ -234,13 +245,13 @@ function buildStaticTags(input) {
234
245
  }
235
246
  );
236
247
  tags.push({ tag: "style", children: bootStyle });
237
- if (iconsEnabled) {
248
+ if (icons.mode === "default") {
238
249
  tags.push(
239
250
  {
240
251
  tag: "link",
241
252
  attrs: {
242
253
  rel: "shortcut icon",
243
- href: withBase(base, withIconPath(iconsDir, ICON_FILES.favicon))
254
+ href: withBase(base, withIconPath(icons.dir, ICON_FILES.favicon))
244
255
  }
245
256
  },
246
257
  {
@@ -248,7 +259,7 @@ function buildStaticTags(input) {
248
259
  attrs: {
249
260
  rel: "icon",
250
261
  type: "image/svg+xml",
251
- href: withBase(base, withIconPath(iconsDir, ICON_FILES.svg))
262
+ href: withBase(base, withIconPath(icons.dir, ICON_FILES.svg))
252
263
  }
253
264
  },
254
265
  {
@@ -256,7 +267,7 @@ function buildStaticTags(input) {
256
267
  attrs: {
257
268
  rel: "icon",
258
269
  sizes: "96x96",
259
- href: withBase(base, withIconPath(iconsDir, ICON_FILES.png96))
270
+ href: withBase(base, withIconPath(icons.dir, ICON_FILES.png96))
260
271
  }
261
272
  },
262
273
  {
@@ -264,11 +275,25 @@ function buildStaticTags(input) {
264
275
  attrs: {
265
276
  rel: "apple-touch-icon",
266
277
  sizes: "180x180",
267
- href: withBase(base, withIconPath(iconsDir, ICON_FILES.appleTouch))
278
+ href: withBase(base, withIconPath(icons.dir, ICON_FILES.appleTouch))
268
279
  }
269
280
  }
270
281
  );
271
282
  }
283
+ if (icons.mode === "explicit") {
284
+ for (const icon of icons.icons) {
285
+ if (icon.rel === void 0) continue;
286
+ tags.push({
287
+ tag: "link",
288
+ attrs: {
289
+ rel: icon.rel,
290
+ ...icon.type === void 0 ? {} : { type: icon.type },
291
+ ...icon.sizes === void 0 ? {} : { sizes: icon.sizes },
292
+ href: iconHref(base, icon.src)
293
+ }
294
+ });
295
+ }
296
+ }
272
297
  tags.push(
273
298
  { tag: "meta", attrs: { name: "apple-mobile-web-app-title", content: shortName } },
274
299
  { tag: "meta", attrs: { name: "apple-mobile-web-app-capable", content: "yes" } },
@@ -286,6 +311,34 @@ function buildStaticTags(input) {
286
311
  );
287
312
  return tags;
288
313
  }
314
+ function manifestIcons(icons, base) {
315
+ if (icons.mode === "none") return {};
316
+ if (icons.mode === "default") {
317
+ return {
318
+ icons: [
319
+ {
320
+ src: withBase(base, withIconPath(icons.dir, ICON_FILES.manifest192)),
321
+ sizes: "192x192",
322
+ type: "image/png"
323
+ },
324
+ {
325
+ src: withBase(base, withIconPath(icons.dir, ICON_FILES.manifest512)),
326
+ sizes: "512x512",
327
+ type: "image/png",
328
+ purpose: "any maskable"
329
+ }
330
+ ]
331
+ };
332
+ }
333
+ return {
334
+ icons: icons.icons.map((icon) => ({
335
+ src: iconHref(base, icon.src),
336
+ ...icon.sizes === void 0 ? {} : { sizes: icon.sizes },
337
+ ...icon.type === void 0 ? {} : { type: icon.type },
338
+ ...icon.purpose === void 0 ? {} : { purpose: icon.purpose }
339
+ }))
340
+ };
341
+ }
289
342
  function buildManifestJson(input) {
290
343
  const {
291
344
  options,
@@ -296,7 +349,7 @@ function buildManifestJson(input) {
296
349
  display,
297
350
  schemeColor,
298
351
  backgroundColor,
299
- iconsDir,
352
+ icons,
300
353
  base
301
354
  } = input;
302
355
  const manifest = {
@@ -311,19 +364,13 @@ function buildManifestJson(input) {
311
364
  // `<meta name="theme-color">` pair. Unchanged for the default (`'dark'`) and for `'auto'`.
312
365
  theme_color: schemeColor,
313
366
  background_color: backgroundColor,
314
- icons: [
315
- {
316
- src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest192)),
317
- sizes: "192x192",
318
- type: "image/png"
319
- },
320
- {
321
- src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest512)),
322
- sizes: "512x512",
323
- type: "image/png",
324
- purpose: "any maskable"
325
- }
326
- ]
367
+ // `icons: false` reaches HERE too, not just the head links. The array was unconditional, so
368
+ // `{ manifest: true, icons: false }` shipped a manifest pointing at two PNGs the app never
369
+ // ships — an installable app with broken icons, and the reason rb went hybrid (plugin for the
370
+ // head, a hand-written manifest for the rest). A manifest with NO icons member is valid.
371
+ // The `'explicit'` arm is the other half of that fix: honest about icons it cannot see was
372
+ // only worth having once a consumer could tell it which icons it DOES have.
373
+ ...manifestIcons(icons, base)
327
374
  };
328
375
  return JSON.stringify(manifest, null, 2);
329
376
  }
@@ -331,8 +378,7 @@ function createMainPlugin(options) {
331
378
  const shortName = options.shortName ?? options.name;
332
379
  const display = options.display ?? "standalone";
333
380
  const darkreader = options.darkreader ?? "lock";
334
- const iconsEnabled = options.icons !== false;
335
- const iconsDir = options.icons === false ? void 0 : options.icons?.dir;
381
+ const icons = resolveIcons(options.icons);
336
382
  const manifestEnabled = options.manifest !== false;
337
383
  const themeColor = resolveThemeColors(options.themeColor);
338
384
  const colorScheme = options.colorScheme ?? "dark";
@@ -364,7 +410,7 @@ function createMainPlugin(options) {
364
410
  display,
365
411
  schemeColor,
366
412
  backgroundColor,
367
- iconsDir,
413
+ icons,
368
414
  base
369
415
  });
370
416
  staticTags = buildStaticTags({
@@ -372,8 +418,7 @@ function createMainPlugin(options) {
372
418
  shortName,
373
419
  themeColor,
374
420
  bootStyle,
375
- iconsEnabled,
376
- iconsDir,
421
+ icons,
377
422
  darkreader,
378
423
  manifestEnabled,
379
424
  base,
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.21.0
1
+ # basalt-ui v1.23.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.21.0
22
+ Version: 1.23.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.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",