basalt-ui 1.40.2 → 1.41.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 (55) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +22 -0
  3. package/MIGRATING.md +40 -12
  4. package/README.md +1 -1
  5. package/agent/rules/basalt-batteries.md +1 -1
  6. package/agent/skills/basalt-app/SKILL.md +1 -1
  7. package/agent/skills/basalt-design/SKILL.md +1 -1
  8. package/agent/templates/DESIGN.md.tpl +1 -1
  9. package/configs/check.yml +1 -1
  10. package/configs/oxlint-plugin.js +5 -5
  11. package/configs/oxlint-plugin.test.ts +1 -1
  12. package/dist/agent/foreign.d.ts +3 -2
  13. package/dist/agent/foreign.d.ts.map +1 -1
  14. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  15. package/dist/charts/primitives/HoverOverlay.js +2 -1
  16. package/dist/cli/doctor.d.ts +5 -2
  17. package/dist/cli/doctor.d.ts.map +1 -1
  18. package/dist/cli/doctor.js +29 -0
  19. package/dist/cli/index.d.ts +61 -4
  20. package/dist/cli/index.d.ts.map +1 -1
  21. package/dist/cli/index.js +112 -6
  22. package/dist/cli/init.d.ts.map +1 -1
  23. package/dist/cli/init.js +10 -0
  24. package/dist/cli/sync.d.ts.map +1 -1
  25. package/dist/cli/sync.js +18 -1
  26. package/dist/commands/shortcut-format.d.ts +9 -4
  27. package/dist/commands/shortcut-format.d.ts.map +1 -1
  28. package/dist/commands/shortcut-format.js +35 -5
  29. package/dist/content/copy-action.d.ts +1 -1
  30. package/dist/content/glyphs.d.ts +1 -1
  31. package/dist/data/virtual-list.d.ts +1 -1
  32. package/dist/theme/index.d.ts.map +1 -1
  33. package/dist/theme/index.js +14 -1
  34. package/dist/theme/segmented-control.module.css +8 -0
  35. package/dist/tokens/derive.d.ts.map +1 -1
  36. package/dist/tokens/derive.js +3 -1
  37. package/dist/tokens.css +1 -1
  38. package/llms.txt +2 -2
  39. package/package.json +1 -1
  40. package/src/agent/foreign.ts +3 -2
  41. package/src/charts/index.ts +2 -2
  42. package/src/charts/primitives/HoverOverlay.tsx +2 -1
  43. package/src/cli/doctor.ts +47 -4
  44. package/src/cli/index.ts +180 -11
  45. package/src/cli/init.ts +14 -0
  46. package/src/cli/sync.ts +24 -1
  47. package/src/commands/shortcut-format.ts +49 -10
  48. package/src/content/copy-action.tsx +1 -1
  49. package/src/content/glyphs.tsx +1 -1
  50. package/src/data/virtual-list.tsx +1 -1
  51. package/src/guard/index.ts +1 -1
  52. package/src/theme/index.ts +17 -1
  53. package/src/theme/segmented-control.module.css +8 -0
  54. package/src/tokens/derive.ts +3 -1
  55. /package/agent/templates/{CLAUDE-block.md.tpl → AGENTS-block.md.tpl} +0 -0
package/dist/cli/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  readFileSync,
8
8
  rmdirSync,
9
9
  statSync,
10
+ realpathSync,
10
11
  unlinkSync,
11
12
  writeFileSync
12
13
  } from "node:fs";
@@ -233,6 +234,8 @@ function waiverHintFor(relPath, profile) {
233
234
  const BLOCK_BEGIN_PREFIX = "<!-- basalt:begin";
234
235
  const BLOCK_END = "<!-- basalt:end -->";
235
236
  const MANIFEST_PATH = ".basalt/manifest.json";
237
+ const AGENTS_MD = "AGENTS.md";
238
+ const LEGACY_BLOCK_HOST = "CLAUDE.md";
236
239
  function sha256(text) {
237
240
  return createHash("sha256").update(text, "utf8").digest("hex");
238
241
  }
@@ -308,6 +311,91 @@ function applyBlock(host, block) {
308
311
  ${block.trim()}
309
312
  `;
310
313
  }
314
+ function sameFile(a, b) {
315
+ try {
316
+ return realpathSync(a) === realpathSync(b);
317
+ } catch (error) {
318
+ if (error.code === "ENOENT") return false;
319
+ throw error;
320
+ }
321
+ }
322
+ function hasManagedBlock(host) {
323
+ return host !== null && host.includes(BLOCK_BEGIN_PREFIX);
324
+ }
325
+ function scanBlock(host) {
326
+ const begin = hasManagedBlock(host);
327
+ if (!begin && !host.includes(BLOCK_END)) return { region: null, problem: null };
328
+ const bad = (problem) => ({ region: null, problem });
329
+ if (!begin) return bad(`\`${BLOCK_END}\` with no \`${BLOCK_BEGIN_PREFIX}\``);
330
+ let region;
331
+ try {
332
+ region = findBlockRegion(host);
333
+ } catch (error) {
334
+ return bad(error.message);
335
+ }
336
+ if (region === null) return bad(`\`${BLOCK_BEGIN_PREFIX}\` has no matching \`${BLOCK_END}\``);
337
+ if (host.indexOf(BLOCK_END) < region.start || host.indexOf(BLOCK_END, region.end) !== -1) {
338
+ return bad(`an extra or misplaced \`${BLOCK_END}\` outside the basalt block`);
339
+ }
340
+ return { region, problem: null };
341
+ }
342
+ function isAgentsImportLine(line) {
343
+ return line.trim() === `@${AGENTS_MD}`;
344
+ }
345
+ function importsAgentsMd(text) {
346
+ return text.split(/\r?\n/).some(isAgentsImportLine);
347
+ }
348
+ function migrateBlockToAgentsMd(cwd, manifest, opts = {}) {
349
+ const legacyHash = manifest.files[LEGACY_BLOCK_HOST];
350
+ if (legacyHash !== void 0) {
351
+ manifest.files[AGENTS_MD] ??= legacyHash;
352
+ delete manifest.files[LEGACY_BLOCK_HOST];
353
+ }
354
+ const none = { pending: false, deletedHost: false, problem: null };
355
+ const legacyAbs = resolve(cwd, LEGACY_BLOCK_HOST);
356
+ const agentsAbs = resolve(cwd, AGENTS_MD);
357
+ const legacy = readIfExists(legacyAbs);
358
+ if (legacy === null) return none;
359
+ try {
360
+ if (sameFile(legacyAbs, agentsAbs)) return none;
361
+ } catch (error) {
362
+ const why = error.code ?? error.message;
363
+ return { ...none, problem: `cannot tell whether ${LEGACY_BLOCK_HOST} is ${AGENTS_MD} (${why})` };
364
+ }
365
+ const legacyScan = scanBlock(legacy);
366
+ const { region } = legacyScan;
367
+ if (legacyScan.problem !== null) {
368
+ return { ...none, problem: `${LEGACY_BLOCK_HOST}: ${legacyScan.problem}` };
369
+ }
370
+ if (region === null) return none;
371
+ const agents = readIfExists(agentsAbs) ?? "";
372
+ const agentsScan = scanBlock(agents);
373
+ if (agentsScan.problem !== null)
374
+ return { ...none, problem: `${AGENTS_MD}: ${agentsScan.problem}` };
375
+ const legacyBlock = legacy.slice(region.start, region.end);
376
+ if (agentsScan.region !== null && normalizeForLedger(agents.slice(agentsScan.region.start, agentsScan.region.end)) !== normalizeForLedger(legacyBlock)) {
377
+ return {
378
+ ...none,
379
+ problem: `${LEGACY_BLOCK_HOST} and ${AGENTS_MD} each carry a basalt block and they differ \u2014 delete the stale block from ${LEGACY_BLOCK_HOST} (or the one in ${AGENTS_MD}, if ${LEGACY_BLOCK_HOST}'s is the one you want kept), then re-run.`
380
+ };
381
+ }
382
+ const before = legacy.slice(0, region.start).trimEnd();
383
+ const after = legacy.slice(region.end).trimStart();
384
+ const remainder = before === "" || after === "" ? `${before}${after}` : `${before}
385
+
386
+ ${after}`;
387
+ const deletedHost = remainder.split(/\r?\n/).every((l) => l.trim() === "" || isAgentsImportLine(l));
388
+ if (opts.dryRun === true) return { pending: true, deletedHost, problem: null };
389
+ if (agentsScan.region === null) writeFileEnsuringDir(agentsAbs, applyBlock(agents, legacyBlock));
390
+ if (deletedHost) unlinkSync(legacyAbs);
391
+ else writeFileSync(legacyAbs, `${remainder.trimEnd()}
392
+ `);
393
+ return { pending: true, deletedHost, problem: null };
394
+ }
395
+ function blockMigrationProblem(cwd) {
396
+ const { problem } = migrateBlockToAgentsMd(cwd, { version: 1, files: {} }, { dryRun: true });
397
+ return problem === null ? null : `${problem.replace(/\.$/, "")}. Nothing else was reconciled \u2014 resolve this by hand, then re-run.`;
398
+ }
311
399
  function hasDependency(cwd, pkgName) {
312
400
  try {
313
401
  const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf8"));
@@ -397,15 +485,15 @@ function managedFiles(peers, placement = ROOT_PLACEMENT) {
397
485
  source: `agent/skills/${name}/SKILL.md`,
398
486
  render: (ctx) => readSource(ctx.pkgRoot, `agent/skills/${name}/SKILL.md`)
399
487
  }));
400
- const claudeBlock = {
401
- dest: "CLAUDE.md",
488
+ const agentsBlock = {
489
+ dest: AGENTS_MD,
402
490
  mode: "managed",
403
491
  markers: true,
404
- source: "agent/templates/CLAUDE-block.md.tpl",
492
+ source: "agent/templates/AGENTS-block.md.tpl",
405
493
  // With markers, render() returns the fully-rendered region INCLUDING its begin/end markers.
406
- // The writer splices it into the consumer's host CLAUDE.md at apply time.
494
+ // The writer splices it into the consumer's host AGENTS.md at apply time.
407
495
  render: (ctx) => {
408
- const tpl = readSource(ctx.pkgRoot, "agent/templates/CLAUDE-block.md.tpl");
496
+ const tpl = readSource(ctx.pkgRoot, "agent/templates/AGENTS-block.md.tpl");
409
497
  if (tpl === null) return null;
410
498
  return fillTemplate(tpl, ctx.vars).trim();
411
499
  }
@@ -498,7 +586,7 @@ pre-commit:
498
586
  if (peers.hasQuery && placement.relocatedQueryClient === null) scaffolds.push(queryClient);
499
587
  if (peers.hasRouter && peers.hasQuery) scaffolds.push(rootRoute);
500
588
  const rootTooling = placement.isPackageRepoRoot ? [lefthook, ci] : [];
501
- return [...rules, ...skills, claudeBlock, design, oxfmt, ...rootTooling, oxlintrc, ...scaffolds];
589
+ return [...rules, ...skills, agentsBlock, design, oxfmt, ...rootTooling, oxlintrc, ...scaffolds];
502
590
  }
503
591
  function placementNotices(cwd, placement) {
504
592
  const out = [];
@@ -558,6 +646,14 @@ Retired but locally edited, so left in place \u2014 your agent still loads them.
558
646
  ` + pruned.drifted.map((dest) => ` \xB7 ${dest}`).join("\n")
559
647
  );
560
648
  }
649
+ function reportBlockMigration(migration, command) {
650
+ if (!migration.pending) return;
651
+ const tail = migration.deletedHost ? `${LEGACY_BLOCK_HOST} held nothing else and was deleted.` : `the rest of ${LEGACY_BLOCK_HOST} is yours; while it exists Claude Code loads it INSTEAD of ${AGENTS_MD} unless it imports @${AGENTS_MD} \u2014 move your content over and delete it.`;
652
+ console.log(
653
+ `
654
+ basalt-ui ${command}: moved the basalt block from ${LEGACY_BLOCK_HOST} to ${AGENTS_MD} \u2014 ${tail}`
655
+ );
656
+ }
561
657
  function pruneRetiredManagedFiles(cwd, manifest, files, opts = {}) {
562
658
  const result = { removed: [], drifted: [] };
563
659
  for (const dest of retiredManagedDests(manifest, files)) {
@@ -944,6 +1040,8 @@ function run(argv, cwd = process.cwd()) {
944
1040
  }
945
1041
  }
946
1042
  export {
1043
+ AGENTS_MD,
1044
+ LEGACY_BLOCK_HOST,
947
1045
  MANIFEST_PATH,
948
1046
  OXLINT_FILES,
949
1047
  OXLINT_NO_BIN,
@@ -954,6 +1052,7 @@ export {
954
1052
  SKIP,
955
1053
  basaltBinCommand,
956
1054
  basaltPresetEntry,
1055
+ blockMigrationProblem,
957
1056
  checkTheme,
958
1057
  classify,
959
1058
  conflictingProfileFlags,
@@ -966,11 +1065,15 @@ export {
966
1065
  fontsCss,
967
1066
  guardHook,
968
1067
  hasBasaltKey,
1068
+ hasManagedBlock,
1069
+ importsAgentsMd,
969
1070
  init,
970
1071
  inspectLefthookGate,
1072
+ isAgentsImportLine,
971
1073
  isStringArray,
972
1074
  managedFiles,
973
1075
  mergeOxlintExtends,
1076
+ migrateBlockToAgentsMd,
974
1077
  namesShipped,
975
1078
  normalizeColorFunctions,
976
1079
  normalizeForLedger,
@@ -989,6 +1092,7 @@ export {
989
1092
  reconcileRoots,
990
1093
  relativePosix,
991
1094
  renderContext,
1095
+ reportBlockMigration,
992
1096
  reportPrune,
993
1097
  resolveGuardConfig,
994
1098
  resolvePeerFlags,
@@ -997,6 +1101,8 @@ export {
997
1101
  retiredManagedDests,
998
1102
  rootsNotices,
999
1103
  run,
1104
+ sameFile,
1105
+ scanBlock,
1000
1106
  scannableFiles,
1001
1107
  sha256,
1002
1108
  shippedAssetPath,
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,SAAS,CAAA;AA2C7D,yGAAyG;AACzG,wBAAgB,IAAI,CAAC,GAAG,GAAE,MAAsB,EAAE,aAAa,GAAE,aAAkB,GAAG,MAAM,CA4K3F"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,SAAS,CAAA;AA8C7D,yGAAyG;AACzG,wBAAgB,IAAI,CAAC,GAAG,GAAE,MAAsB,EAAE,aAAa,GAAE,aAAkB,GAAG,MAAM,CAuL3F"}
package/dist/cli/init.js CHANGED
@@ -3,9 +3,11 @@ import { resolve } from "node:path";
3
3
  import { deriveSpacing } from "../tokens/index.js";
4
4
  import {
5
5
  MANIFEST_PATH,
6
+ blockMigrationProblem,
6
7
  findBasaltInstall,
7
8
  managedFiles,
8
9
  mergeOxlintExtends,
10
+ migrateBlockToAgentsMd,
9
11
  normalizeForLedger,
10
12
  patchPackageJson,
11
13
  placementNotices,
@@ -13,6 +15,7 @@ import {
13
15
  readManifest,
14
16
  reconcileRoots,
15
17
  renderContext,
18
+ reportBlockMigration,
16
19
  reportPrune,
17
20
  resolvePeerFlags,
18
21
  resolvePlacement,
@@ -31,6 +34,11 @@ const KEPT_FILE_COST = {
31
34
  "DESIGN.md": "the design brief Claude reads is yours, not the seeded one."
32
35
  };
33
36
  function init(cwd = process.cwd(), scaffoldFlags = {}) {
37
+ const blockProblem = blockMigrationProblem(cwd);
38
+ if (blockProblem !== null) {
39
+ console.error(`\u2716 basalt-ui init: ${blockProblem}`);
40
+ return 1;
41
+ }
34
42
  const install = findBasaltInstall(cwd);
35
43
  if (install.dir === null) {
36
44
  console.error(
@@ -50,6 +58,7 @@ function init(cwd = process.cwd(), scaffoldFlags = {}) {
50
58
  const peers = resolvePeerFlags(cwd, scaffoldFlags);
51
59
  const placement = resolvePlacement(cwd);
52
60
  const files = managedFiles(peers, placement);
61
+ const migration = migrateBlockToAgentsMd(cwd, manifest);
53
62
  const writtenFiles = [];
54
63
  const keptFiles = [];
55
64
  const missingSources = [];
@@ -79,6 +88,7 @@ function init(cwd = process.cwd(), scaffoldFlags = {}) {
79
88
  `basalt-ui init: ${writtenFiles.length} written, ${keptFiles.length} kept, manifest at ${MANIFEST_PATH}`
80
89
  );
81
90
  reportPrune(pruned);
91
+ reportBlockMigration(migration, "init");
82
92
  if (missingSources.length > 0) {
83
93
  console.log(
84
94
  `basalt-ui init: ${missingSources.length} shipped asset(s) not present, skipped: ${missingSources.join(", ")}`
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/cli/sync.ts"],"names":[],"mappings":"AA8CA,gGAAgG;AAChG,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAGjE;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,mGAAmG;IACnG,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC1B,CAAA;AAED;;;;;;;;;GASG;AACH;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrF;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,IAAI,CAAC,IAAI,GAAE,WAAgB,EAAE,aAAa,GAAE,MAAsB,GAAG,MAAM,CAoM1F"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/cli/sync.ts"],"names":[],"mappings":"AAmDA,gGAAgG;AAChG,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAGjE;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,mGAAmG;IACnG,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC1B,CAAA;AAED;;;;;;;;;GASG;AACH;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrF;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,IAAI,CAAC,IAAI,GAAE,WAAgB,EAAE,aAAa,GAAE,MAAsB,GAAG,MAAM,CAsN1F"}
package/dist/cli/sync.js CHANGED
@@ -10,8 +10,12 @@ import {
10
10
  diffSummary,
11
11
  findBasaltInstall,
12
12
  inspectLefthookGate,
13
+ AGENTS_MD,
14
+ blockMigrationProblem,
13
15
  isStringArray,
16
+ LEGACY_BLOCK_HOST,
14
17
  managedFiles,
18
+ migrateBlockToAgentsMd,
15
19
  normalizeForLedger,
16
20
  noteRelocation,
17
21
  OXLINT_FILES,
@@ -25,6 +29,7 @@ import {
25
29
  reconcileRoots,
26
30
  relativePosix,
27
31
  renderContext,
32
+ reportBlockMigration,
28
33
  reportPrune,
29
34
  resolvePeerFlags,
30
35
  resolvePlacement,
@@ -72,12 +77,18 @@ function sync(opts = {}, invocationCwd = process.cwd()) {
72
77
  );
73
78
  return 1;
74
79
  }
80
+ const blockProblem = blockMigrationProblem(cwd);
81
+ if (blockProblem !== null) {
82
+ console.error(`\u2716 basalt-ui sync: ${blockProblem}`);
83
+ return 1;
84
+ }
75
85
  const rootsState = reconcileRoots(cwd, { write: opts.check !== true });
76
86
  const ctx = renderContext(cwd);
77
87
  const manifest = readManifest(cwd);
78
88
  const peers = resolvePeerFlags(cwd, {});
79
89
  const placement = resolvePlacement(cwd);
80
90
  const files = managedFiles(peers, placement);
91
+ const migration = migrateBlockToAgentsMd(cwd, manifest, { dryRun: opts.check === true });
81
92
  let updated = 0;
82
93
  let created = 0;
83
94
  let recreated = 0;
@@ -155,10 +166,15 @@ function sync(opts = {}, invocationCwd = process.cwd()) {
155
166
  `basalt-ui sync --check: ${retired.length} retired rule/skill file(s) still present: ${retired.join(", ")} \u2014 run \`basalt-ui sync\`.`
156
167
  );
157
168
  }
169
+ if (migration.pending) {
170
+ console.error(
171
+ `basalt-ui sync --check: the basalt block is still in ${LEGACY_BLOCK_HOST} \u2014 it now lives in ${AGENTS_MD}. Run \`basalt-ui sync\` to move it.`
172
+ );
173
+ }
158
174
  if (staleForCheck > 0) {
159
175
  console.error(`basalt-ui sync --check: ${staleForCheck} managed file(s) out of date.`);
160
- return 1;
161
176
  }
177
+ if (staleForCheck > 0 || migration.pending) return 1;
162
178
  const n = placementSkips.length;
163
179
  const skips = n === 0 ? "" : ` (${n} permanent placement skip(s) \u2014 --verbose for detail)`;
164
180
  console.log(`\u2713 basalt-ui sync --check: all managed files current.${skips}`);
@@ -172,6 +188,7 @@ function sync(opts = {}, invocationCwd = process.cwd()) {
172
188
  `basalt-ui sync: ${updated} updated, ${created} created, ${recreated} recreated, ${skippedDrift} skipped (drift), ${pruned.removed.length} retired.`
173
189
  );
174
190
  reportPrune(pruned);
191
+ reportBlockMigration(migration, "sync");
175
192
  if (driftLines.length > 0) {
176
193
  console.log("Locally-edited files were skipped (run with --force to overwrite):");
177
194
  for (const l of driftLines) console.log(l);
@@ -9,7 +9,7 @@
9
9
  * parseShortcut('Mod+S', false) // ['Ctrl', 'S']
10
10
  *
11
11
  * // Single display string for Spotlight description:
12
- * formatShortcutDisplay('Mod+S', true) // '⌘ S' on mac
12
+ * formatShortcutDisplay('Mod+S', true) // '⌘S' on mac
13
13
  * formatShortcutDisplay('Mod+S', false) // 'Ctrl+S' on windows
14
14
  */
15
15
  /**
@@ -22,16 +22,20 @@ export declare function detectMac(): boolean;
22
22
  * Convert a shortcut string to a list of Kbd-friendly tokens.
23
23
  * 'Mod+S' → ['⌘', 'S'] (mac) or ['Ctrl', 'S'] (other).
24
24
  * 'Shift+Mod+P' → ['⇧', '⌘', 'P'] (mac) or ['Shift', 'Ctrl', 'P'] (other).
25
+ * Named keys read as glyphs on mac and Title-case words elsewhere:
26
+ * 'Mod+Backspace' → ['⌘', '⌫'] (mac) or ['Ctrl', 'Backspace'] (other).
25
27
  * isMac is the post-mount platform flag; defaults false on SSR / first render.
26
28
  *
27
29
  * @example
28
30
  * parseShortcut('Mod+S', true) // ['⌘', 'S']
29
31
  * parseShortcut('Shift+Mod+P', false) // ['Shift', 'Ctrl', 'P']
32
+ * parseShortcut('ArrowLeft', true) // ['←']
30
33
  */
31
34
  export declare function parseShortcut(shortcut: string, isMac: boolean): string[];
32
35
  /**
33
36
  * Format a shortcut string for inline display (e.g. Spotlight action description).
34
- * Returns a compact string like '⌘ S' (mac) or 'Ctrl+S' (windows).
37
+ * Returns a compact string like '⌘S' / '⌘⌫' (mac, glyphs with no separator) or 'Ctrl+S' /
38
+ * 'Ctrl+Backspace' (elsewhere, Title-case words joined with '+').
35
39
  *
36
40
  * isMac is the post-mount platform flag. Pass false on SSR / first client render.
37
41
  *
@@ -40,9 +44,10 @@ export declare function parseShortcut(shortcut: string, isMac: boolean): string[
40
44
  * always-on and produces the same tokens via parseShortcut.
41
45
  *
42
46
  * @example
43
- * formatShortcutDisplay('Mod+S', true) // '⌘ S'
47
+ * formatShortcutDisplay('Mod+S', true) // '⌘S'
48
+ * formatShortcutDisplay('Mod+Backspace', true) // '⌘⌫'
44
49
  * formatShortcutDisplay('Mod+S', false) // 'Ctrl+S'
45
- * formatShortcutDisplay('Shift+Mod+P', true) // '⇧ ⌘ P'
50
+ * formatShortcutDisplay('Shift+Mod+P', true) // '⇧⌘P'
46
51
  */
47
52
  export declare function formatShortcutDisplay(shortcut: string, isMac: boolean): string;
48
53
  //# sourceMappingURL=shortcut-format.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shortcut-format.d.ts","sourceRoot":"","sources":["../../src/commands/shortcut-format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAMnC;AAID;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAiBxE;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAI9E"}
1
+ {"version":3,"file":"shortcut-format.d.ts","sourceRoot":"","sources":["../../src/commands/shortcut-format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAMnC;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAqBxE;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAG9E"}
@@ -4,9 +4,36 @@ function detectMac() {
4
4
  if (uadPlatform !== void 0) return /mac|iphone|ipad/i.test(uadPlatform);
5
5
  return /Mac|iPhone|iPad/.test(navigator.userAgent);
6
6
  }
7
+ const NAMED_KEYS = {
8
+ backspace: ["\u232B", "Backspace"],
9
+ delete: ["\u2326", "Delete"],
10
+ del: ["\u2326", "Delete"],
11
+ arrowleft: ["\u2190", "Left"],
12
+ arrowright: ["\u2192", "Right"],
13
+ arrowup: ["\u2191", "Up"],
14
+ arrowdown: ["\u2193", "Down"],
15
+ left: ["\u2190", "Left"],
16
+ right: ["\u2192", "Right"],
17
+ up: ["\u2191", "Up"],
18
+ down: ["\u2193", "Down"],
19
+ escape: ["\u238B", "Esc"],
20
+ esc: ["\u238B", "Esc"],
21
+ enter: ["\u21A9", "Enter"],
22
+ return: ["\u21A9", "Enter"],
23
+ tab: ["\u21E5", "Tab"],
24
+ space: ["\u2423", "Space"],
25
+ home: ["\u2196", "Home"],
26
+ end: ["\u2198", "End"],
27
+ pageup: ["\u21DE", "PgUp"],
28
+ pagedown: ["\u21DF", "PgDn"]
29
+ };
30
+ function plainKey(key) {
31
+ return key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1);
32
+ }
7
33
  function parseShortcut(shortcut, isMac) {
8
34
  return shortcut.split("+").map((key) => {
9
- switch (key.toLowerCase()) {
35
+ const lower = key.toLowerCase();
36
+ switch (lower) {
10
37
  case "mod":
11
38
  return isMac ? "\u2318" : "Ctrl";
12
39
  case "shift":
@@ -16,15 +43,18 @@ function parseShortcut(shortcut, isMac) {
16
43
  case "meta":
17
44
  return isMac ? "\u2318" : "Meta";
18
45
  case "ctrl":
19
- return isMac ? "^" : "Ctrl";
20
- default:
21
- return key.toUpperCase();
46
+ return isMac ? "\u2303" : "Ctrl";
47
+ default: {
48
+ const named = NAMED_KEYS[lower];
49
+ if (named !== void 0) return isMac ? named[0] : named[1];
50
+ return plainKey(key);
51
+ }
22
52
  }
23
53
  });
24
54
  }
25
55
  function formatShortcutDisplay(shortcut, isMac) {
26
56
  const tokens = parseShortcut(shortcut, isMac);
27
- return isMac ? tokens.join(" ") : tokens.join("+");
57
+ return tokens.join(isMac ? "" : "+");
28
58
  }
29
59
  export {
30
60
  detectMac,
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * The check + tooltip are the entire success signal — no notification (docs/CONTENT-SPEC.md §7).
8
8
  * `color="teal"` resolves through the bridged Mantine family, so it stays on the token layer in
9
- * both schemes (see the package CLAUDE.md on `cssVariablesResolver`).
9
+ * both schemes (see the package MAINTAINING.md on `cssVariablesResolver`).
10
10
  *
11
11
  * Not part of the public surface — reach it through `CodeBlock` / `HeadingAnchor`.
12
12
  */
@@ -4,7 +4,7 @@
4
4
  * hand-drawn ones that drift apart.
5
5
  *
6
6
  * Inline SVG rather than an icon dep: the framework takes icons as `ReactNode` from the consumer
7
- * (see the package CLAUDE.md) and ships none, but these are internal chrome of a shipped component,
7
+ * (see the package MAINTAINING.md) and ships none, but these are internal chrome of a shipped component,
8
8
  * not a consumer-facing slot. `currentColor` + `1em`-agnostic sizing keeps them steerable from CSS
9
9
  * — the heading anchor sizes its glyph as an optical ratio of whatever step the heading lands on
10
10
  * (docs/DESIGN-SPEC.md §3), while the code-block chrome pins a fixed 14px.
@@ -103,7 +103,7 @@ export type BasaltVirtualListHandle = {
103
103
  /**
104
104
  * The underlying `@tanstack/react-virtual` instance — the full escape hatch. Returns the RAW
105
105
  * `@tanstack/react-virtual` `Virtualizer`, so a future major of that library reaching this shape
106
- * ships here as a plain `feat:` under the no-majors doctrine (`../../CLAUDE.md` "Commit type
106
+ * ships here as a plain `feat:` under the no-majors doctrine (root `AGENTS.md` "Commit type
107
107
  * discipline"), not a silent break.
108
108
  */
109
109
  getVirtualizer: () => Virtualizer<HTMLDivElement, Element>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"AAiEA,OAAO,KAAK,EACV,oBAAoB,EAGpB,oBAAoB,EAErB,MAAM,eAAe,CAAA;AAStB,OAAO,KAAK,EAAe,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAGpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAQlD;;;;;GAKG;AACH,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAIlD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAMvD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AAgBnE,OAAO,QAAQ,eAAe,CAAC;IAC7B,UAAU,iBAAiB;QACzB,YAAY,CAAC,EAAE,YAAY,CAAA;QAC3B,WAAW,CAAC,EAAE,iBAAiB,CAAA;QAC/B,YAAY,CAAC,EAAE,YAAY,CAAA;QAC3B,aAAa,CAAC,EAAE,WAAW,CAAA;KAC5B;CACF;AAgiCD;+EAC+E;AAC/E,eAAO,MAAM,SAAS,EAAE,oBAAuD,CAAA;AAyT/E;;8FAE8F;AAC9F,eAAO,MAAM,oBAAoB,EAAE,oBACc,CAAA;AAEjD,kEAAkE;AAClE,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;IAE9B;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAA;IAEzB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC7B,CAAA;AA0BD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,CAAC,EAAE,IAAI,CAAC,oBAAoB,EAAE,aAAa,CAAC,EACrD,OAAO,CAAC,EAAE,wBAAwB,GACjC,oBAAoB,CAmCtB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"AAiEA,OAAO,KAAK,EACV,oBAAoB,EAGpB,oBAAoB,EAErB,MAAM,eAAe,CAAA;AAStB,OAAO,KAAK,EAAe,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAGpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAQlD;;;;;GAKG;AACH,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAIlD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAMvD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AAgBnE,OAAO,QAAQ,eAAe,CAAC;IAC7B,UAAU,iBAAiB;QACzB,YAAY,CAAC,EAAE,YAAY,CAAA;QAC3B,WAAW,CAAC,EAAE,iBAAiB,CAAA;QAC/B,YAAY,CAAC,EAAE,YAAY,CAAA;QAC3B,aAAa,CAAC,EAAE,WAAW,CAAA;KAC5B;CACF;AAuiCD;+EAC+E;AAC/E,eAAO,MAAM,SAAS,EAAE,oBAAuD,CAAA;AAkU/E;;8FAE8F;AAC9F,eAAO,MAAM,oBAAoB,EAAE,oBACc,CAAA;AAEjD,kEAAkE;AAClE,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;IAE9B;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAA;IAEzB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC7B,CAAA;AA0BD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,CAAC,EAAE,IAAI,CAAC,oBAAoB,EAAE,aAAa,CAAC,EACrD,OAAO,CAAC,EAAE,wBAAwB,GACjC,oBAAoB,CAmCtB"}
@@ -156,13 +156,17 @@ const basaltVariantColorResolver = (input) => {
156
156
  const isDefaultColor = input.color === void 0 || input.color === input.theme.primaryColor;
157
157
  return isDefaultColor ? { ...resolved, hover: `color-mix(in srgb, var(--vx-ink) 6%, transparent)` } : resolved;
158
158
  }
159
+ if (input.variant === "outline") {
160
+ return { ...resolved, color: `color-mix(in srgb, ${resolved.color} 70%, var(--vx-ink))` };
161
+ }
159
162
  if (input.variant !== "light") return resolved;
160
163
  const ink = `var(--mantine-color-${colorName}-light-color)`;
161
164
  return {
162
165
  ...resolved,
163
166
  background: `color-mix(in srgb, ${ink} 13%, transparent)`,
164
167
  hover: `color-mix(in srgb, ${ink} 20%, transparent)`,
165
- color: ink,
168
+ // Same pull toward ink as `outline`: the saturated tone text sat at 3.2–4.4:1 on its own tint.
169
+ color: `color-mix(in srgb, ${ink} 70%, var(--vx-ink))`,
166
170
  border: "none"
167
171
  };
168
172
  };
@@ -1003,6 +1007,12 @@ function buildCssVariablesResolver(data) {
1003
1007
  // secondary / muted text
1004
1008
  "--mantine-color-text": `var(--vx-ink, ${INK.ink.light})`,
1005
1009
  // primary body/heading text
1010
+ // Field errors: Mantine's own error red measures 1.98:1 (dark) / 4.37:1 (light). The solid status red
1011
+ // pulled toward ink clears AA on every text surface in both schemes.
1012
+ "--mantine-color-error": "color-mix(in srgb, var(--vx-bad-solid) 85%, var(--vx-ink))",
1013
+ // Links: Mantine reads the accent FILL shade (4.38:1 on the light page, under WCAG AA); the hover
1014
+ // accent is the next step darker and clears 4.5:1 on every light text surface.
1015
+ "--mantine-color-anchor": "var(--vx-accent-hover)",
1006
1016
  // `--app-shell-border-color` is set by `components.AppShell.extend({ vars })` above — this
1007
1017
  // entry was dead (an element-level declaration always beats an inherited `:root` value).
1008
1018
  // THE strict-surface lever. Mantine components do NOT chain border/surface colors through
@@ -1031,6 +1041,9 @@ function buildCssVariablesResolver(data) {
1031
1041
  "--mantine-color-default-border": "transparent",
1032
1042
  "--mantine-color-dimmed": `var(--vx-neutral, ${NEUTRAL.neutral.dark})`,
1033
1043
  "--mantine-color-text": `var(--vx-ink, ${INK.ink.dark})`,
1044
+ // Field errors: Mantine's own error red measures 1.98:1 (dark) / 4.37:1 (light). The solid status red
1045
+ // pulled toward ink clears AA on every text surface in both schemes.
1046
+ "--mantine-color-error": "color-mix(in srgb, var(--vx-bad-solid) 85%, var(--vx-ink))",
1034
1047
  // See the light block above — `--app-shell-border-color` is set by `AppShell.extend` now.
1035
1048
  // Dark components read `--mantine-color-dark-4` (border) raw. `buildDarkTuple` already sets
1036
1049
  // dark-4, but to a slightly lighter step than the hairline token, so layout borders read
@@ -13,6 +13,14 @@
13
13
  font-weight: 600;
14
14
  }
15
15
 
16
+ /*
17
+ * Inactive labels read ink2, not Mantine's gray-7 (4.0:1 on the light track): the active segment is
18
+ * told apart by the raised pill and weight 600, so the inactive text can stay at full AA contrast.
19
+ */
20
+ .label:not([data-active], [data-disabled]) {
21
+ color: var(--vx-ink2);
22
+ }
23
+
16
24
  /*
17
25
  * Mantine's own `.control` ships `flex: 1` with no `min-width` override, so its initial value
18
26
  * (`auto`) floors every option at its min-content width — a `fullWidth` track therefore still
@@ -1 +1 @@
1
- {"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../../src/tokens/derive.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;;;OAQG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;IAC/C;kGAC8F;IAC9F,UAAU,EAAE,MAAM,CAAA;IAClB;mDAC+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB,EAAE,YAOnC,CAAA;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAEjF;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAEnE;AAuHD,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,QAAQ,GACR,QAAQ,CAAA;AA6DZ,MAAM,MAAM,UAAU,GAClB,IAAI,GACJ,OAAO,GACP,YAAY,GACZ,UAAU,GACV,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,UAAU,CAAA;AA2Bd,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAQvD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAS/C;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAA;IACvB,6EAA6E;IAC7E,iBAAiB,EAAE,MAAM,CAAA;IACzB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED;8BAC8B;AAC9B,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,SAAS,CAAA;IACjB,WAAW,EAAE,SAAS,CAAA;IACtB,UAAU,EAAE,SAAS,CAAA;IACrB,eAAe,EAAE,SAAS,CAAA;IAC1B,QAAQ,EAAE,SAAS,CAAA;IACnB,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;IACnC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;IACtC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;CACrC,CAAA;AAED;;8DAE8D;AAC9D,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,CAQjE;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAC7B,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,SAAS,CAAC,EAChD,MAAM,EAAE,YAAY,GACnB,cAAc,CA+GhB"}
1
+ {"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../../src/tokens/derive.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;;;OAQG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;IAC/C;kGAC8F;IAC9F,UAAU,EAAE,MAAM,CAAA;IAClB;mDAC+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB,EAAE,YAOnC,CAAA;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAEjF;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAEnE;AAuHD,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,QAAQ,GACR,QAAQ,CAAA;AA6DZ,MAAM,MAAM,UAAU,GAClB,IAAI,GACJ,OAAO,GACP,YAAY,GACZ,UAAU,GACV,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,UAAU,CAAA;AA2Bd,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAUvD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAS/C;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAA;IACvB,6EAA6E;IAC7E,iBAAiB,EAAE,MAAM,CAAA;IACzB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED;8BAC8B;AAC9B,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,SAAS,CAAA;IACjB,WAAW,EAAE,SAAS,CAAA;IACtB,UAAU,EAAE,SAAS,CAAA;IACrB,eAAe,EAAE,SAAS,CAAA;IAC1B,QAAQ,EAAE,SAAS,CAAA;IACnB,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;IACnC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;IACtC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;CACrC,CAAA;AAED;;8DAE8D;AAC9D,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,CAQjE;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAC7B,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,SAAS,CAAC,EAChD,MAAM,EAAE,YAAY,GACnB,cAAc,CA+GhB"}
@@ -127,7 +127,9 @@ const SURFACE_TONE = {
127
127
  }
128
128
  };
129
129
  const INK_TONE = {
130
- light: { ink: 15.16, ink2: 27.09, muted: 34.88, faint: 48.44 },
130
+ // light `faint` is tone 44.5 so tertiary text clears WCAG AA (4.5:1) on every text surface
131
+ // (bg / panel / subtle / field); 48.44 landed at 4.23–4.31:1 on bg and subtle.
132
+ light: { ink: 15.16, ink2: 27.09, muted: 34.88, faint: 44.5 },
131
133
  dark: { ink: 90.94, ink2: 88.12, muted: 84.91, faint: 66.24 }
132
134
  };
133
135
  const STATUS_HUE = { good: 151.975, warn: 76.64, bad: 29.905 };
package/dist/tokens.css CHANGED
@@ -291,7 +291,7 @@ html[data-mantine-color-scheme='light'] {
291
291
  --vx-ink: #262629;
292
292
  --vx-ink2: #404043;
293
293
  --vx-muted: #525255;
294
- --vx-faint: #737376;
294
+ --vx-faint: #69696c;
295
295
  --vx-accent: #4374a6;
296
296
  --vx-accent-hover: #3c6895;
297
297
  --vx-accent-fill: #4374a6;
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.40.2
1
+ # basalt-ui v1.41.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.40.2
22
+ Version: 1.41.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.40.2",
3
+ "version": "1.41.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -97,8 +97,9 @@ export type PartRenderContext<TPart = ForeignPart> = {
97
97
  export type PartRenderer<TPart = ForeignPart> = (ctx: PartRenderContext<TPart>) => ReactNode
98
98
 
99
99
  /**
100
- * Canonical const-generic factory (`packages/basalt-ui/CLAUDE.md`, "Canonical token-factory
101
- * contract") — identity passthrough, exact-keyed return, no builder, no config bag. Mirrors
100
+ * Canonical const-generic factory (`packages/basalt-ui/MAINTAINING.md` § "Token system — the
101
+ * derived palette", "Series color is consumer data") — identity passthrough, exact-keyed return,
102
+ * no builder, no config bag. Mirrors
102
103
  * `defineSeries` (`tokens/index.ts:648-650`) and `defineNotifications`
103
104
  * (`notifications/define-notifications.ts:112-115`).
104
105
  *
@@ -166,8 +166,8 @@ export {
166
166
  } from './sparklines'
167
167
 
168
168
  // ── Re-exported visx primitives ──────────────────────────────────────────
169
- // Bespoke charts (genuinely unique compositions per CLAUDE.md) need raw
170
- // visx primitives. Re-exporting them keeps the dependency declared in one
169
+ // Bespoke charts (genuinely unique compositions, per `agent/rules/basalt-charts.md` "Adding a
170
+ // chart") need raw visx primitives. Re-exporting them keeps the dependency declared in one
171
171
  // place and preserves the rule that consumers only import from `basalt-ui/charts`.
172
172
  export { Group } from '@visx/group'
173
173
  export { GridRows, GridColumns } from '@visx/grid'
@@ -66,7 +66,8 @@ export function HoverOverlay({
66
66
  'aria-orientation': 'horizontal' as const,
67
67
  'aria-valuemin': 0,
68
68
  ...(valueMax !== undefined && { 'aria-valuemax': valueMax }),
69
- ...(valueNow !== undefined && { 'aria-valuenow': valueNow }),
69
+ // `aria-valuenow` is required on a slider: 0 (the first point) until the cursor lands on one.
70
+ 'aria-valuenow': valueNow ?? 0,
70
71
  ...(valueText !== undefined && { 'aria-valuetext': valueText }),
71
72
  onKeyDown,
72
73
  ...(onBlur !== undefined && { onBlur }),