basalt-ui 1.22.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 (79) hide show
  1. package/AGENTS.md +9 -1
  2. package/CHANGELOG.md +19 -0
  3. package/MIGRATING.md +125 -12
  4. package/README.md +45 -11
  5. package/agent/rules/basalt-app.md +26 -2
  6. package/agent/rules/basalt-charts.md +40 -5
  7. package/agent/rules/basalt-tokens.md +17 -1
  8. package/agent/skills/basalt-charts/SKILL.md +23 -8
  9. package/agent/templates/DESIGN.md.tpl +3 -1
  10. package/configs/oxlint-plugin.js +49 -8
  11. package/configs/oxlint-plugin.test.ts +88 -31
  12. package/dist/charts/hooks/useBandPlot.d.ts +138 -0
  13. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
  14. package/dist/charts/hooks/useBandPlot.js +127 -0
  15. package/dist/charts/index.d.ts +4 -0
  16. package/dist/charts/index.d.ts.map +1 -1
  17. package/dist/charts/index.js +14 -0
  18. package/dist/charts/kinds/BandStrip.d.ts +113 -0
  19. package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
  20. package/dist/charts/kinds/BandStrip.js +218 -0
  21. package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
  22. package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
  23. package/dist/charts/kinds/Bars.d.ts +5 -1
  24. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  25. package/dist/charts/kinds/Bars.js +2 -0
  26. package/dist/charts/kinds/MirroredBars.d.ts +93 -0
  27. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
  28. package/dist/charts/kinds/MirroredBars.js +315 -0
  29. package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
  30. package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
  31. package/dist/charts/kinds/MultiLine.d.ts +5 -1
  32. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  33. package/dist/charts/kinds/MultiLine.js +2 -0
  34. package/dist/charts/kinds/StackedArea.d.ts +5 -1
  35. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  36. package/dist/charts/kinds/StackedArea.js +2 -0
  37. package/dist/charts/kinds/ZonedLine.d.ts +5 -1
  38. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  39. package/dist/charts/kinds/ZonedLine.js +2 -0
  40. package/dist/charts/primitives/CartesianChart.d.ts +18 -2
  41. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  42. package/dist/charts/primitives/CartesianChart.js +5 -2
  43. package/dist/charts/primitives/HatchPattern.d.ts +31 -0
  44. package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
  45. package/dist/charts/primitives/HatchPattern.js +31 -0
  46. package/dist/cli/index.d.ts +2 -0
  47. package/dist/cli/index.d.ts.map +1 -1
  48. package/dist/cli/index.js +47 -8
  49. package/dist/cli/shipped-versions.test.d.ts +2 -0
  50. package/dist/cli/shipped-versions.test.d.ts.map +1 -0
  51. package/dist/guard/index.d.ts.map +1 -1
  52. package/dist/guard/index.js +32 -11
  53. package/dist/vite.d.ts +42 -3
  54. package/dist/vite.d.ts.map +1 -1
  55. package/dist/vite.js +67 -30
  56. package/llms.txt +2 -2
  57. package/package.json +1 -1
  58. package/src/charts/hooks/useBandPlot.ts +302 -0
  59. package/src/charts/index.ts +14 -0
  60. package/src/charts/kinds/BandStrip.test.tsx +200 -0
  61. package/src/charts/kinds/BandStrip.tsx +364 -0
  62. package/src/charts/kinds/Bars.tsx +7 -1
  63. package/src/charts/kinds/DualPanel.test.tsx +10 -4
  64. package/src/charts/kinds/MirroredBars.test.tsx +193 -0
  65. package/src/charts/kinds/MirroredBars.tsx +455 -0
  66. package/src/charts/kinds/MultiLine.tsx +7 -1
  67. package/src/charts/kinds/StackedArea.tsx +7 -1
  68. package/src/charts/kinds/ZonedLine.tsx +7 -1
  69. package/src/charts/primitives/CartesianChart.test.tsx +86 -4
  70. package/src/charts/primitives/CartesianChart.tsx +28 -3
  71. package/src/charts/primitives/HatchPattern.tsx +55 -0
  72. package/src/cli/index.ts +125 -18
  73. package/src/cli/shipped-versions.test.ts +83 -0
  74. package/src/cli/toolchain-wiring.test.ts +171 -4
  75. package/src/cli/waiver-audit.test.ts +7 -3
  76. package/src/guard/check-source.test.ts +138 -42
  77. package/src/guard/index.ts +83 -17
  78. package/src/vite.test.ts +79 -0
  79. package/src/vite.ts +141 -39
package/dist/cli/index.js CHANGED
@@ -160,6 +160,16 @@ function hasBasaltProject(dir) {
160
160
  return false;
161
161
  }
162
162
  }
163
+ function descendantProjects(cwd, depth = 2) {
164
+ if (depth === 0) return [];
165
+ const out = [];
166
+ for (const name of subdirNames(cwd, { skipDotDirs: true })) {
167
+ const dir = resolve(cwd, name);
168
+ if (hasBasaltProject(dir)) out.push(dir);
169
+ else out.push(...descendantProjects(dir, depth - 1));
170
+ }
171
+ return out;
172
+ }
163
173
  function resolveProjectDir(cwd) {
164
174
  const override = process.env["BASALT_CWD"];
165
175
  if (override !== void 0 && override.length > 0) {
@@ -167,7 +177,8 @@ function resolveProjectDir(cwd) {
167
177
  return { dir, relocatedFrom: dir === cwd ? null : cwd, ambiguous: null };
168
178
  }
169
179
  if (hasBasaltProject(cwd)) return { dir: cwd, relocatedFrom: null, ambiguous: null };
170
- const candidates = collectWorkspacePackages(cwd).packages.filter((pkg) => pkg.dir !== cwd && hasBasaltProject(pkg.dir)).map((pkg) => pkg.dir);
180
+ const declared = collectWorkspacePackages(cwd).packages.filter((pkg) => pkg.dir !== cwd && hasBasaltProject(pkg.dir)).map((pkg) => pkg.dir);
181
+ const candidates = declared.length > 0 ? declared : descendantProjects(cwd);
171
182
  if (candidates.length === 1) {
172
183
  return { dir: candidates[0], relocatedFrom: cwd, ambiguous: null };
173
184
  }
@@ -239,7 +250,7 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
239
250
  const project = resolveProjectDir(invocationCwd);
240
251
  if (project.ambiguous !== null) {
241
252
  console.error(
242
- `\u2716 basalt-ui check-theme: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} workspace packages carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
253
+ `\u2716 basalt-ui check-theme: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} packages below it carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
243
254
  );
244
255
  return 1;
245
256
  }
@@ -565,8 +576,9 @@ basalt.exemptRules entries (${exemptCount}):`);
565
576
  `
566
577
  ${live} live, ${dead} dead, ${outOfReach} unjudgeable, ${unaccountable} unaccountable (reported as theme-allow-unscoped by a normal run).`
567
578
  );
579
+ const includes = cfg.include ?? [];
568
580
  lines.push(
569
- `Scope: the ${scanned.length} file(s) check-theme scans under basalt.roots (${resolveRoots(cfg).join(", ")}). A waiver outside them is not audited \u2014 widen roots to cover it, or accept that nothing polices that file.`
581
+ `Scope: the ${scanned.length} file(s) check-theme scans \u2014 everything under basalt.roots (${resolveRoots(cfg).join(", ")}), plus each root's sibling index.html and public/ tree, ` + (includes.length === 0 ? "plus anything named in basalt.include (none). " : `plus basalt.include (${includes.join(", ")}). `) + "A waiver in a file none of those reach is not audited \u2014 widen roots, or name it in basalt.include, or accept that nothing polices that file."
570
582
  );
571
583
  if (dead > 0) {
572
584
  lines.push(
@@ -860,6 +872,16 @@ function migrateLegacyOxfmt(cwd, pkgRoot, manifest) {
860
872
  const shipped = readSource(pkgRoot, "configs/oxfmt.json");
861
873
  if (shipped !== null && onDisk === shipped) unlinkSync(legacyAbs);
862
874
  }
875
+ const DESIGN_VERSION_LINE = /^(> Managed by basalt-ui) \(\d+\.\d+\.\d+\)\./m;
876
+ function migrateDesignVersionLine(cwd) {
877
+ const dest = resolve(cwd, "DESIGN.md");
878
+ const onDisk = readIfExists(dest);
879
+ if (onDisk === null || !DESIGN_VERSION_LINE.test(onDisk)) return;
880
+ writeFileEnsuringDir(
881
+ dest,
882
+ onDisk.replace(DESIGN_VERSION_LINE, "$1 \u2014 run `basalt-ui doctor` for the version.")
883
+ );
884
+ }
863
885
  function writeFileEnsuringDir(abs, content) {
864
886
  mkdirSync(dirname(abs), { recursive: true });
865
887
  writeFileSync(abs, content);
@@ -1287,10 +1309,15 @@ function findManifestAbove(dir) {
1287
1309
  }
1288
1310
  }
1289
1311
  function sync(opts = {}, invocationCwd = process.cwd()) {
1312
+ const syncFlags = opts.flags ?? [];
1313
+ if (conflictingProfileFlags(syncFlags)) {
1314
+ console.error("basalt-ui sync: --tokens-only and --framework are alternatives \u2014 pass one.");
1315
+ return 1;
1316
+ }
1290
1317
  const project = resolveProjectDir(invocationCwd);
1291
1318
  if (project.ambiguous !== null) {
1292
1319
  console.error(
1293
- `\u2716 basalt-ui sync: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} workspace packages carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
1320
+ `\u2716 basalt-ui sync: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} packages below it carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
1294
1321
  );
1295
1322
  return 1;
1296
1323
  }
@@ -1300,6 +1327,12 @@ function sync(opts = {}, invocationCwd = process.cwd()) {
1300
1327
  `basalt-ui sync: no basalt config at ${project.relocatedFrom} \u2014 running in ${relativePosix(project.relocatedFrom, cwd)}, where it lives.`
1301
1328
  );
1302
1329
  }
1330
+ if (declaredProfile(readBasaltConfig(cwd), syncFlags) === "tokens-only") {
1331
+ console.log(
1332
+ `basalt-ui sync: ${MANIFEST_PATH}: n/a \u2014 a tokens-only consumer has no scaffold to reconcile. The token layer is refreshed by re-running \`basalt-ui tokens:css\`, and \`tokens:css --check\` is its drift gate. Pass --framework to force the full profile.`
1333
+ );
1334
+ return 0;
1335
+ }
1303
1336
  if (!existsSync(resolve(cwd, MANIFEST_PATH))) {
1304
1337
  const elsewhere = findManifestAbove(cwd);
1305
1338
  const wouldWrite = managedFiles(resolvePeerFlags(cwd, {}), resolvePlacement(cwd)).length;
@@ -1311,7 +1344,10 @@ function sync(opts = {}, invocationCwd = process.cwd()) {
1311
1344
  const rootsState = reconcileRoots(cwd, { write: opts.check !== true });
1312
1345
  const ctx = renderContext(cwd);
1313
1346
  const manifest = readManifest(cwd);
1314
- if (!opts.check) migrateLegacyOxfmt(cwd, ctx.pkgRoot, manifest);
1347
+ if (!opts.check) {
1348
+ migrateLegacyOxfmt(cwd, ctx.pkgRoot, manifest);
1349
+ migrateDesignVersionLine(cwd);
1350
+ }
1315
1351
  const peers = resolvePeerFlags(cwd, {});
1316
1352
  const placement = resolvePlacement(cwd);
1317
1353
  const files = managedFiles(peers, placement);
@@ -1634,7 +1670,7 @@ function doctor(invocationCwd = process.cwd(), flags = []) {
1634
1670
  `
1635
1671
  basalt-ui doctor \u2014 ${invocationCwd}
1636
1672
 
1637
- \u2716 no basalt config at ${invocationCwd}, and ${project.ambiguous.length} workspace packages carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run doctor from one of them, or set BASALT_CWD to pick.
1673
+ \u2716 no basalt config at ${invocationCwd}, and ${project.ambiguous.length} packages below it carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run doctor from one of them, or set BASALT_CWD to pick.
1638
1674
 
1639
1675
  1 hard failure(s), 0 warning(s).
1640
1676
  `
@@ -2205,7 +2241,7 @@ async function guardHook(cwd = process.cwd()) {
2205
2241
  }
2206
2242
  return 0;
2207
2243
  }
2208
- const USAGE = "Usage: basalt-ui <init [--with-router] [--with-query] [--merge-lint] | sync [--force] [--check] |\n check-theme [--audit-allows] | check-coverage | info [--json] |\n doctor [--tokens-only|--framework] | guard-hook | tokens:css | fonts:css | help>\n\ncheck-theme [--tokens-only|--framework] [--audit-allows]\n --audit-allows reports instead of scanning: every `theme-allow` annotation and every\n `basalt.exemptRules` entry, with what each one still suppresses \u2014 proved by re-running the\n guard with that one waiver neutralized, not by reading its text. Exits 1 on a waiver that\n suppresses nothing (dead), which is what makes it usable as a CI gate.\n\ntokens:css [--out <path>] [--check] [--selector-attribute <attr> | --selector-class <class>]\n [--light-class <class>] [--dark-value <v>] [--light-value <v>]\n [--default-scheme <dark|light|none>] [--media-fallback] [--only <core|all>]\n [--no-legacy-aliases]\n Emit the --vx-* stylesheet (stdout unless --out). Defaults reproduce basalt-ui/tokens.css.\n --selector-class emits `:root.dark` instead of an attribute selector (the Tailwind convention).\n --check writes nothing and exits 1 when --out differs from what would be emitted (a CI gate).\n --no-legacy-aliases drops the deprecated camelCase spellings (--vx-accentFill and friends),\n which are emitted by default as aliases of the canonical kebab-case names.\n\nfonts:css [--out <path>] [--check]\n Emit the shipped --basalt-font-* stacks as plain CSS \u2014 the typeface half of the token layer,\n otherwise reachable only by importing styles.css.\n\ndoctor [--tokens-only|--framework]\n Auto-detects a tokens-only consumer (no manifest + no @mantine/core) and checks only what\n applies. A check that cannot RUN is reported as SKIPPED and exits non-zero.\n\ncheck-theme / doctor honour BASALT_CWD, and relocate to the single workspace package carrying a\nbasalt config when invoked from a repo root that has none \u2014 several carrying one is ambiguous\nand exits 1 in BOTH. --tokens-only and --framework are alternatives; passing both is an error.\n\nEvery subcommand accepts --help / -h to print this message and exit without running.";
2244
+ const USAGE = "Usage: basalt-ui <init [--with-router] [--with-query] [--merge-lint] |\n sync [--force] [--check] [--tokens-only|--framework] |\n check-theme [--audit-allows] | check-coverage | info [--json] |\n doctor [--tokens-only|--framework] | guard-hook | tokens:css | fonts:css | help>\n\ncheck-theme [--tokens-only|--framework] [--audit-allows]\n --audit-allows reports instead of scanning: every `theme-allow` annotation and every\n `basalt.exemptRules` entry, with what each one still suppresses \u2014 proved by re-running the\n guard with that one waiver neutralized, not by reading its text. Exits 1 on a waiver that\n suppresses nothing (dead), which is what makes it usable as a CI gate.\n\ntokens:css [--out <path>] [--check] [--selector-attribute <attr> | --selector-class <class>]\n [--light-class <class>] [--dark-value <v>] [--light-value <v>]\n [--default-scheme <dark|light|none>] [--media-fallback] [--only <core|all>]\n [--no-legacy-aliases]\n Emit the --vx-* stylesheet (stdout unless --out). Defaults reproduce basalt-ui/tokens.css.\n --selector-class emits `:root.dark` instead of an attribute selector (the Tailwind convention).\n --check writes nothing and exits 1 when --out differs from what would be emitted (a CI gate).\n --no-legacy-aliases drops the deprecated camelCase spellings (--vx-accentFill and friends),\n which are emitted by default as aliases of the canonical kebab-case names.\n\nfonts:css [--out <path>] [--check]\n Emit the shipped --basalt-font-* stacks as plain CSS \u2014 the typeface half of the token layer,\n otherwise reachable only by importing styles.css.\n\ndoctor [--tokens-only|--framework]\n Auto-detects a tokens-only consumer (no manifest + no @mantine/core) and checks only what\n applies. A check that cannot RUN is reported as SKIPPED and exits non-zero.\n\nsync [--force] [--check] [--tokens-only|--framework]\n Refreshes an EXISTING install; it never creates one (that is init). A tokens-only consumer\n has no scaffold to reconcile, so sync reports n/a and exits 0 there \u2014 as doctor does.\n\ncheck-theme / doctor / sync all resolve their project the same way: they honour BASALT_CWD, and\nrelocate to the single package below the cwd carrying a basalt config when invoked from a repo\nroot that has none \u2014 several carrying one is ambiguous and exits 1 in ALL THREE.\n--tokens-only and --framework are alternatives; passing both is an error.\n\nEvery subcommand accepts --help / -h to print this message and exit without running.";
2209
2245
  function run(argv, cwd = process.cwd()) {
2210
2246
  const [cmd, ...flags] = argv;
2211
2247
  if (cmd === "help" || cmd === "--help" || cmd === "-h" || flags.includes("--help") || flags.includes("-h")) {
@@ -2220,7 +2256,10 @@ function run(argv, cwd = process.cwd()) {
2220
2256
  mergeLint: flags.includes("--merge-lint")
2221
2257
  });
2222
2258
  case "sync":
2223
- return sync({ force: flags.includes("--force"), check: flags.includes("--check") }, cwd);
2259
+ return sync(
2260
+ { force: flags.includes("--force"), check: flags.includes("--check"), flags },
2261
+ cwd
2262
+ );
2224
2263
  case "check-theme":
2225
2264
  return checkTheme(cwd, flags);
2226
2265
  case "check-coverage":
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=shipped-versions.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shipped-versions.test.d.ts","sourceRoot":"","sources":["../../src/cli/shipped-versions.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"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"}
@@ -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,23 @@ 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
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
+ }
257
270
  const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*\*?|<!--|^\s*\*)\s*$/;
258
271
  function escapeRegExp(value) {
259
272
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -273,7 +286,7 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
273
286
  list.push(annotation);
274
287
  byLine.set(index, list);
275
288
  };
276
- const record = (index, annotation, trailing) => {
289
+ const record = (index, annotation, trailing, tokenAt) => {
277
290
  declared.push({ line: index + 1, annotation });
278
291
  if (annotation.scope === "file") {
279
292
  fileScoped.push(annotation);
@@ -281,11 +294,14 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
281
294
  }
282
295
  add(index, annotation);
283
296
  if (!trailing) {
284
- for (let ahead = 1; ahead <= MAX_COMMENT_BLOCK_LINES; ahead++) {
285
- const raw = lines[index + ahead] ?? "";
286
- add(index + ahead, annotation);
287
- 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] ?? "";
288
303
  if (raw.trim() === "") break;
304
+ if (!isCommentOnly(raw, codeLines[at] ?? "")) break;
289
305
  }
290
306
  return;
291
307
  }
@@ -301,11 +317,15 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
301
317
  for (let i = 0; i < lines.length; i++) {
302
318
  const line = lines[i] ?? "";
303
319
  const stripped = codeLines[i] ?? "";
304
- const code = stripped.trim();
305
- const isCommentOnlyLine = code === "" || COMMENT_ONLY_LINE.test(line) || code === "}" && JSX_COMMENT_CLOSE.test(line);
320
+ const isCommentOnlyLine = isCommentOnly(line, stripped);
306
321
  if (syntax === "markup") {
307
322
  for (const m of line.matchAll(jsonKey)) {
308
- 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
+ );
309
329
  }
310
330
  }
311
331
  for (const m of line.matchAll(token)) {
@@ -317,7 +337,8 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
317
337
  record(
318
338
  i,
319
339
  parseAllowAnnotation(rest, m[1] === void 0 ? "line" : "file"),
320
- !isCommentOnlyLine
340
+ !isCommentOnlyLine,
341
+ at
321
342
  );
322
343
  }
323
344
  }
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;
@@ -81,12 +104,28 @@ export type BasaltAppOptions = {
81
104
  /** Manifest `id`. Default: the resolved `startUrl` (default- or explicitly-derived, per above). */
82
105
  id?: string;
83
106
  /**
84
- * Icon links + manifest icon paths. `false` skips BOTH — the head `<link>` icons and the
85
- * manifest's `icons` array — so a manifest is never emitted pointing at files that 404.
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`.
86
125
  */
87
126
  icons?: false | {
88
127
  dir?: string;
89
- };
128
+ } | readonly BasaltAppIcon[];
90
129
  /** Emits `<meta name="darkreader-lock">` when `'lock'` (default). Pass `false` to omit it. */
91
130
  darkreader?: 'lock' | false;
92
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;;;OAGG;IACH,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;AA4gBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,8 +349,7 @@ function buildManifestJson(input) {
296
349
  display,
297
350
  schemeColor,
298
351
  backgroundColor,
299
- iconsEnabled,
300
- iconsDir,
352
+ icons,
301
353
  base
302
354
  } = input;
303
355
  const manifest = {
@@ -316,21 +368,9 @@ function buildManifestJson(input) {
316
368
  // `{ manifest: true, icons: false }` shipped a manifest pointing at two PNGs the app never
317
369
  // ships — an installable app with broken icons, and the reason rb went hybrid (plugin for the
318
370
  // head, a hand-written manifest for the rest). A manifest with NO icons member is valid.
319
- ...iconsEnabled ? {
320
- icons: [
321
- {
322
- src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest192)),
323
- sizes: "192x192",
324
- type: "image/png"
325
- },
326
- {
327
- src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest512)),
328
- sizes: "512x512",
329
- type: "image/png",
330
- purpose: "any maskable"
331
- }
332
- ]
333
- } : {}
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)
334
374
  };
335
375
  return JSON.stringify(manifest, null, 2);
336
376
  }
@@ -338,8 +378,7 @@ function createMainPlugin(options) {
338
378
  const shortName = options.shortName ?? options.name;
339
379
  const display = options.display ?? "standalone";
340
380
  const darkreader = options.darkreader ?? "lock";
341
- const iconsEnabled = options.icons !== false;
342
- const iconsDir = options.icons === false ? void 0 : options.icons?.dir;
381
+ const icons = resolveIcons(options.icons);
343
382
  const manifestEnabled = options.manifest !== false;
344
383
  const themeColor = resolveThemeColors(options.themeColor);
345
384
  const colorScheme = options.colorScheme ?? "dark";
@@ -371,8 +410,7 @@ function createMainPlugin(options) {
371
410
  display,
372
411
  schemeColor,
373
412
  backgroundColor,
374
- iconsEnabled,
375
- iconsDir,
413
+ icons,
376
414
  base
377
415
  });
378
416
  staticTags = buildStaticTags({
@@ -380,8 +418,7 @@ function createMainPlugin(options) {
380
418
  shortName,
381
419
  themeColor,
382
420
  bootStyle,
383
- iconsEnabled,
384
- iconsDir,
421
+ icons,
385
422
  darkreader,
386
423
  manifestEnabled,
387
424
  base,
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.22.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.22.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.22.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",