basalt-ui 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/AGENTS.md +9 -5
  2. package/CHANGELOG.md +19 -0
  3. package/MIGRATING.md +147 -49
  4. package/README.md +42 -8
  5. package/agent/rules/basalt-app.md +41 -0
  6. package/agent/rules/basalt-charts.md +15 -12
  7. package/agent/rules/basalt-content.md +4 -0
  8. package/agent/rules/basalt-router.md +57 -5
  9. package/agent/rules/basalt-state.md +21 -3
  10. package/agent/rules/basalt-tokens.md +73 -16
  11. package/agent/skills/basalt-app/SKILL.md +4 -1
  12. package/agent/skills/basalt-charts/SKILL.md +5 -3
  13. package/agent/templates/CLAUDE-block.md.tpl +11 -5
  14. package/configs/lefthook.yml +48 -15
  15. package/configs/oxlint-plugin.js +181 -68
  16. package/configs/oxlint-plugin.test.ts +215 -9
  17. package/dist/charts/kinds/Bars.d.ts +1 -1
  18. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  19. package/dist/charts/kinds/Donut.d.ts +1 -1
  20. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  21. package/dist/charts/kinds/DualPanel.d.ts +1 -1
  22. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  23. package/dist/charts/kinds/Heatmap.d.ts +1 -1
  24. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  25. package/dist/charts/kinds/MultiLine.d.ts +1 -1
  26. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  27. package/dist/charts/kinds/StackedArea.d.ts +1 -1
  28. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  29. package/dist/charts/kinds/ZonedLine.d.ts +1 -1
  30. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  31. package/dist/cli/index.d.ts +48 -14
  32. package/dist/cli/index.d.ts.map +1 -1
  33. package/dist/cli/index.js +328 -66
  34. package/dist/cli/waiver-audit.test.d.ts +2 -0
  35. package/dist/cli/waiver-audit.test.d.ts.map +1 -0
  36. package/dist/guard/index.d.ts +47 -2
  37. package/dist/guard/index.d.ts.map +1 -1
  38. package/dist/guard/index.js +176 -31
  39. package/dist/router-tanstack/multi-search-param-store.d.ts +13 -0
  40. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  41. package/dist/router-tanstack/multi-search-param-store.js +4 -1
  42. package/dist/router-tanstack/search-param-store.d.ts +29 -5
  43. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  44. package/dist/router-tanstack/search-param-store.js +31 -3
  45. package/dist/router-tanstack/search-param-store.test.d.ts +2 -0
  46. package/dist/router-tanstack/search-param-store.test.d.ts.map +1 -0
  47. package/dist/shell/index.d.ts +12 -5
  48. package/dist/shell/index.d.ts.map +1 -1
  49. package/dist/shell/index.js +24 -6
  50. package/dist/tokens/index.d.ts +3 -1
  51. package/dist/tokens/index.d.ts.map +1 -1
  52. package/dist/tokens/index.js +5 -1
  53. package/dist/tokens/palette.d.ts +2 -2
  54. package/dist/tokens/palette.js +2 -2
  55. package/dist/tokens/text-scale.test.d.ts +2 -0
  56. package/dist/tokens/text-scale.test.d.ts.map +1 -0
  57. package/dist/tokens.css +4 -2
  58. package/dist/vite.d.ts +24 -2
  59. package/dist/vite.d.ts.map +1 -1
  60. package/dist/vite.js +54 -13
  61. package/llms.txt +2 -2
  62. package/package.json +1 -1
  63. package/src/charts/kinds/Bars.tsx +1 -1
  64. package/src/charts/kinds/Donut.tsx +1 -1
  65. package/src/charts/kinds/DualPanel.tsx +2 -2
  66. package/src/charts/kinds/Heatmap.tsx +1 -1
  67. package/src/charts/kinds/MultiLine.tsx +1 -1
  68. package/src/charts/kinds/StackedArea.tsx +1 -1
  69. package/src/charts/kinds/ZonedLine.tsx +1 -1
  70. package/src/cli/index.ts +607 -128
  71. package/src/cli/init-sync.test.ts +2 -0
  72. package/src/cli/template-ledger.test.ts +1 -1
  73. package/src/cli/toolchain-wiring.test.ts +162 -0
  74. package/src/cli/waiver-audit.test.ts +131 -0
  75. package/src/guard/check-source.test.ts +441 -1
  76. package/src/guard/index.ts +430 -60
  77. package/src/router-tanstack/multi-search-param-store.ts +16 -1
  78. package/src/router-tanstack/search-param-store.test.ts +135 -0
  79. package/src/router-tanstack/search-param-store.ts +90 -8
  80. package/src/shell/app-mobile-nav.tsx +1 -1
  81. package/src/shell/index.test.tsx +52 -0
  82. package/src/shell/index.tsx +64 -13
  83. package/src/tokens/index.ts +24 -1
  84. package/src/tokens/palette.ts +2 -2
  85. package/src/tokens/text-scale.test.ts +51 -0
  86. package/src/vite.test.ts +125 -6
  87. package/src/vite.ts +148 -20
@@ -54,6 +54,38 @@ const INLINE_SPACING = new RegExp(
54
54
  "g"
55
55
  );
56
56
  const MICRO_SPACING_CEILING_PX = 10;
57
+ const STYLE_ATTRIBUTE_OPENER = /\b(?:style|styles|sx|css)\s*=\s*\{?\s*$/;
58
+ const OBJECT_DECLARATION = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::([^=]*))?=\s*$/;
59
+ const STYLE_OBJECT_NAME = /(?:Style|Styles|Css)$|^(?:style|styles|css|sx)$/;
60
+ const STYLE_OBJECT_TYPE = /\bCSS(?:Properties|Object)\b|\bStyle(?:s|Prop)?\b/;
61
+ const STYLE_BOUND_IDENTIFIER = /\b(?:style|styles|sx|css)=\{\s*([A-Za-z_$][\w$]*)\s*\}/g;
62
+ const NESTED_OBJECT_KEY = /(?:[\w$]+|'[^']*'|"[^"]*"|\[[^\]]*\])\s*:\s*$/;
63
+ const MAX_STYLE_CONTEXT_SCAN = 4e3;
64
+ function isStyleObjectContext(codeText, at, styleIdentifiers) {
65
+ let depth = 0;
66
+ const stop = Math.max(0, at - MAX_STYLE_CONTEXT_SCAN);
67
+ for (let i = at - 1; i >= stop; i--) {
68
+ const ch = codeText[i];
69
+ if (ch === "}") depth++;
70
+ else if (ch === "{") {
71
+ if (depth > 0) {
72
+ depth--;
73
+ continue;
74
+ }
75
+ const before = codeText.slice(Math.max(0, i - 120), i);
76
+ if (STYLE_ATTRIBUTE_OPENER.test(before)) return true;
77
+ const declaration = OBJECT_DECLARATION.exec(before);
78
+ if (declaration !== null) {
79
+ const [, name = "", annotation = ""] = declaration;
80
+ return STYLE_OBJECT_NAME.test(name) || STYLE_OBJECT_TYPE.test(annotation) || styleIdentifiers.has(name);
81
+ }
82
+ if (before.endsWith("{")) continue;
83
+ if (NESTED_OBJECT_KEY.test(before)) continue;
84
+ return false;
85
+ }
86
+ }
87
+ return false;
88
+ }
57
89
  const ROOT_FONT_SIZE_PX = 16;
58
90
  function isSubScaleCssSpacing(line, matchIndex) {
59
91
  const colon = line.indexOf(":", matchIndex);
@@ -129,13 +161,52 @@ const EXEMPT_RULE_ALIASES = {
129
161
  "surface-shadow-override": "raw-surface",
130
162
  "css-raw-surface": "raw-surface"
131
163
  };
164
+ function normalizeExemptPattern(pattern) {
165
+ return pattern.trim().replace(/^\.\//, "").replace(/\/+$/, "");
166
+ }
167
+ function exemptGlobToRegExp(pattern) {
168
+ const body = pattern.split(/(\*\*\/|\*\*|\*|\?)/).map((part) => {
169
+ if (part === "**/") return "(?:.*/)?";
170
+ if (part === "**") return ".*";
171
+ if (part === "*") return "[^/]*";
172
+ if (part === "?") return "[^/]";
173
+ return part.replace(/[.+^${}()|[\]\\]/g, "\\$&");
174
+ }).join("");
175
+ return new RegExp(`^${body}$`);
176
+ }
177
+ function exemptPatternMatches(pattern, relPath) {
178
+ const p = normalizeExemptPattern(pattern);
179
+ if (p === "") return false;
180
+ if (relPath === p || relPath.startsWith(`${p}/`)) return true;
181
+ if (/[*?]/.test(p)) {
182
+ const re = exemptGlobToRegExp(p);
183
+ if (re.test(relPath)) return true;
184
+ if (!p.includes("/") && re.test(relPath.split("/").pop() ?? "")) return true;
185
+ return false;
186
+ }
187
+ return !p.includes("/") && relPath.split("/").includes(p);
188
+ }
132
189
  function isRuleExempt(kind, relPath, exemptRules) {
133
190
  const alias = EXEMPT_RULE_ALIASES[kind];
134
191
  if (alias !== void 0 && isRuleExempt(alias, relPath, exemptRules)) return true;
135
192
  const patterns = exemptRules?.[kind];
136
193
  if (patterns === void 0 || patterns.length === 0) return false;
137
- const segments = relPath.split("/");
138
- return patterns.some((pattern) => segments.includes(pattern.replace(/\/$/, "")));
194
+ return patterns.some((pattern) => exemptPatternMatches(pattern, relPath));
195
+ }
196
+ function unmatchedExemptPatterns(cfg, scannedRelPaths) {
197
+ const out = [];
198
+ for (const [kind, patterns] of Object.entries(cfg.exemptRules ?? {})) {
199
+ const known = Object.hasOwn(GUARD_RULES, kind);
200
+ for (const pattern of patterns ?? []) {
201
+ if (!known) {
202
+ out.push({ kind, pattern, reason: "unknown-kind" });
203
+ continue;
204
+ }
205
+ if (scannedRelPaths.some((relPath) => exemptPatternMatches(pattern, relPath))) continue;
206
+ out.push({ kind, pattern, reason: "no-match" });
207
+ }
208
+ }
209
+ return out;
139
210
  }
140
211
  const PLUGIN_RULE_IDS = /* @__PURE__ */ new Set([
141
212
  "no-raw-font-size",
@@ -157,7 +228,7 @@ const PLUGIN_RULE_IDS = /* @__PURE__ */ new Set([
157
228
  "ai-sdk-major"
158
229
  ]);
159
230
  const MIN_ALLOW_REASON_LENGTH = 4;
160
- function parseAllowAnnotation(rest) {
231
+ function parseAllowAnnotation(rest, scope) {
161
232
  const rules = [];
162
233
  const unknownRules = [];
163
234
  let remainder = rest.replace(/^[\s,]+/, "");
@@ -176,51 +247,87 @@ function parseAllowAnnotation(rest) {
176
247
  inIdSlot = /^\s*,/.test(after);
177
248
  }
178
249
  const reason = remainder.replace(/^(?:—|–|-{1,2}|:)\s*/, "").trim();
179
- return { rules, unknownRules, hasReason: reason.length >= MIN_ALLOW_REASON_LENGTH };
250
+ return { rules, unknownRules, hasReason: reason.length >= MIN_ALLOW_REASON_LENGTH, scope };
180
251
  }
181
252
  const MAX_CSS_CONTINUATION_LINES = 8;
253
+ const MAX_COMMENT_BLOCK_LINES = 8;
182
254
  const COMMENT_OPENER_BEFORE = /(?:\/\/|\/\*|<!--)\s*$/;
183
255
  const COMMENT_ONLY_LINE = /^\s*(?:\{\s*\/\*|\/\/|\/\*|\*|<!--)/;
184
256
  const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*|<!--|^\s*\*)\s*$/;
257
+ function escapeRegExp(value) {
258
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
259
+ }
260
+ function allowTokenRe(allowComment) {
261
+ return new RegExp(`${escapeRegExp(allowComment)}(-file)?(?![\\w-])`, "g");
262
+ }
263
+ function jsonAllowKeyRe(allowComment) {
264
+ return new RegExp(`"basalt:${escapeRegExp(allowComment)}(-file)?"\\s*:\\s*"([^"]*)"`, "g");
265
+ }
185
266
  function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
186
267
  const byLine = /* @__PURE__ */ new Map();
268
+ const fileScoped = [];
187
269
  const declared = [];
188
270
  const add = (index, annotation) => {
189
271
  const list = byLine.get(index) ?? [];
190
272
  list.push(annotation);
191
273
  byLine.set(index, list);
192
274
  };
275
+ const record = (index, annotation, trailing) => {
276
+ declared.push({ line: index + 1, annotation });
277
+ if (annotation.scope === "file") {
278
+ fileScoped.push(annotation);
279
+ return;
280
+ }
281
+ add(index, annotation);
282
+ 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;
287
+ if (raw.trim() === "") break;
288
+ }
289
+ return;
290
+ }
291
+ if (syntax !== "css") return;
292
+ for (let back = 1; back <= MAX_CSS_CONTINUATION_LINES; back++) {
293
+ const previous = (codeLines[index - back] ?? "").trim();
294
+ if (previous === "" || /[;{}]$/.test(previous)) break;
295
+ add(index - back, annotation);
296
+ }
297
+ };
298
+ const token = allowTokenRe(allowComment);
299
+ const jsonKey = jsonAllowKeyRe(allowComment);
193
300
  for (let i = 0; i < lines.length; i++) {
194
301
  const line = lines[i] ?? "";
195
302
  const stripped = codeLines[i] ?? "";
196
303
  const isCommentOnlyLine = stripped.trim() === "" || COMMENT_ONLY_LINE.test(line);
197
- let from = 0;
198
- for (; ; ) {
199
- const at = line.indexOf(allowComment, from);
200
- if (at === -1) break;
201
- from = at + allowComment.length;
202
- if (stripped[at] !== " " && !COMMENT_OPENER_BEFORE.test(line.slice(0, at))) continue;
203
- const annotation = parseAllowAnnotation(line.slice(from));
204
- if (ANNOTATION_PREFIX.test(line.slice(0, at))) declared.push({ line: i + 1, annotation });
205
- add(i, annotation);
206
- if (isCommentOnlyLine) {
207
- add(i + 1, annotation);
208
- continue;
209
- }
210
- if (syntax !== "css") continue;
211
- for (let back = 1; back <= MAX_CSS_CONTINUATION_LINES; back++) {
212
- const previous = (codeLines[i - back] ?? "").trim();
213
- if (previous === "" || /[;{}]$/.test(previous)) break;
214
- add(i - back, annotation);
304
+ if (syntax === "markup") {
305
+ for (const m of line.matchAll(jsonKey)) {
306
+ record(i, parseAllowAnnotation(m[2] ?? "", m[1] === void 0 ? "line" : "file"), false);
215
307
  }
216
308
  }
309
+ for (const m of line.matchAll(token)) {
310
+ const at = m.index;
311
+ const before = line.slice(0, at);
312
+ if (stripped[at] !== " " && !COMMENT_OPENER_BEFORE.test(before)) continue;
313
+ if (!ANNOTATION_PREFIX.test(before)) continue;
314
+ const rest = line.slice(at + m[0].length);
315
+ record(
316
+ i,
317
+ parseAllowAnnotation(rest, m[1] === void 0 ? "line" : "file"),
318
+ !isCommentOnlyLine
319
+ );
320
+ }
217
321
  }
218
- return { waivers: byLine, declared };
322
+ return { waivers: byLine, fileScoped, declared };
323
+ }
324
+ function annotationCovers(annotation, kind) {
325
+ if (annotation.rules.includes(kind)) return true;
326
+ if (annotation.scope === "file") return false;
327
+ return annotation.rules.length === 0 && annotation.unknownRules.length === 0;
219
328
  }
220
329
  function annotationsCover(annotations, kind) {
221
- return (annotations ?? []).some(
222
- (a) => a.rules.length === 0 && a.unknownRules.length === 0 || a.rules.includes(kind)
223
- );
330
+ return (annotations ?? []).some((a) => annotationCovers(a, kind));
224
331
  }
225
332
  function guardSyntaxFor(relPath) {
226
333
  if (relPath.endsWith(".css")) return "css";
@@ -606,14 +713,14 @@ const GUARD_RULES = {
606
713
  kind: "theme-allow-unscoped",
607
714
  pattern: /theme-allow/,
608
715
  // handled inline (annotation pass); entry keeps the registry complete
609
- message: "theme-allow without a usable rule id and a reason. Write `theme-allow <rule-id> \u2014 <why>`: the id scopes the exception to that one kind (a bare comment waives EVERY kind on the line, including ones added later), and the reason is what makes it reviewable in a diff. An id that names no rule is a typo, not a blanket waiver \u2014 it suppresses nothing."
716
+ message: "theme-allow without a usable rule id and a reason. Write `theme-allow <rule-id> \u2014 <why>`: the id scopes the exception to that one kind (a bare comment waives EVERY kind on the line, including ones added later), and the reason is what makes it reviewable in a diff. An id that names no rule is a typo, not a blanket waiver \u2014 it suppresses nothing. `theme-allow-file <rule-id> \u2014 <why>` declares the whole file, and must name its ids: a bare one waives nothing."
610
717
  },
611
718
  "surface-shadow-override": {
612
719
  kind: "surface-shadow-override",
613
720
  pattern: SHADOW_DECL,
614
721
  // handled inline (value is judged, not just matched)
615
722
  enabled: (cfg) => cfg.rawSurface,
616
- message: "Token-composed boxShadow that REPLACES card depth \u2014 a card whose shadow drops --vx-shadow-card is the one card in the app with no depth. Compose with it (`${VX.shadowCard}, <your ring>`) rather than instead of it."
723
+ message: "Token-composed boxShadow that REPLACES surface depth \u2014 a card whose shadow drops --vx-shadow-card is the one card in the app with no depth. Compose with the depth token for its tier (`${VX.shadowCard}, <your ring>`; a fixed/floating surface wants --vx-shadow-overlay) rather than instead of it."
617
724
  },
618
725
  "css-raw-surface": {
619
726
  kind: "css-raw-surface",
@@ -638,6 +745,19 @@ const GUARD_RULES = {
638
745
  message: "Raw font-size literal \u2014 route through VX.text.* (numbers) / --vx-text-* (CSS), or the Mantine fz token. Same rule the oxlint plugin enforces as basalt/no-raw-font-size; this is the half a check-theme-only CI can see."
639
746
  }
640
747
  };
748
+ function guardKindRemedy(kind) {
749
+ return `${kind}: ${GUARD_RULES[kind].message}`;
750
+ }
751
+ const JSON_FAMILY_FILE = /\.(?:webmanifest|json)$/;
752
+ function guardWaiverHint(relPath) {
753
+ if (relPath.endsWith(".webmanifest")) {
754
+ return 'A manifest cannot reference a CSS variable, so a hex here is a hand-copy that drifts: let basaltAppPlugin emit it (`manifest: { \u2026 }` in vite.config.ts) instead of hand-writing it. For a manifest basalt does not generate, declare the exception with a `"basalt:theme-allow-file": "raw-hex \u2014 <why>"` member.';
755
+ }
756
+ if (JSON_FAMILY_FILE.test(relPath)) {
757
+ return 'JSON has no comments \u2014 declare a deliberate exception with a `"basalt:theme-allow-file": "<rule-id> \u2014 <why>"` member.';
758
+ }
759
+ return "Add a `theme-allow <rule-id> \u2014 <why>` comment for a deliberate exception (`theme-allow-file` declares the whole file).";
760
+ }
641
761
  function ruleApplies(kind, relPath) {
642
762
  const appliesTo = GUARD_RULES[kind].appliesTo;
643
763
  return appliesTo === void 0 || appliesTo(relPath);
@@ -655,8 +775,19 @@ function checkSource(text, relPath, cfg) {
655
775
  const syntax = guardSyntaxFor(relPath);
656
776
  const codeText = syntax === "markup" ? stripMarkupComments(text) : stripComments(text, syntax);
657
777
  const codeLines = codeText.split("\n");
658
- const { waivers, declared } = collectAllowAnnotations(lines, codeLines, cfg.allowComment, syntax);
659
- const isAllowed = (line, kind) => annotationsCover(waivers.get(line - 1), kind);
778
+ const { waivers, fileScoped, declared } = collectAllowAnnotations(
779
+ lines,
780
+ codeLines,
781
+ cfg.allowComment,
782
+ syntax
783
+ );
784
+ const fileWaived = /* @__PURE__ */ new Set();
785
+ for (const annotation of fileScoped) {
786
+ for (const rule of annotation.rules) {
787
+ if (Object.hasOwn(GUARD_RULES, rule)) fileWaived.add(rule);
788
+ }
789
+ }
790
+ const isAllowed = (line, kind) => fileWaived.has(kind) || annotationsCover(waivers.get(line - 1), kind);
660
791
  const isAllowedInRange = (start, end, kind) => {
661
792
  for (let n = start; n <= end; n++) if (isAllowed(n, kind)) return true;
662
793
  return false;
@@ -678,6 +809,14 @@ function checkSource(text, relPath, cfg) {
678
809
  });
679
810
  }
680
811
  const rawHtmlLayoutLines = /* @__PURE__ */ new Set();
812
+ const lineStarts = [];
813
+ for (let n = 0, i = 0; i < codeLines.length; i++) {
814
+ lineStarts.push(n);
815
+ n += (codeLines[i] ?? "").length + 1;
816
+ }
817
+ const styleIdentifiers = new Set(
818
+ [...codeText.matchAll(STYLE_BOUND_IDENTIFIER)].map((m) => m[1] ?? "")
819
+ );
681
820
  for (let i = 0; i < lines.length; i++) {
682
821
  const line = codeLines[i] ?? "";
683
822
  for (const m of line.matchAll(GUARD_RULES["raw-hex"].pattern)) {
@@ -751,6 +890,9 @@ function checkSource(text, relPath, cfg) {
751
890
  if (GUARD_RULES["inline-spacing"].enabled(cfg)) {
752
891
  for (const m of line.matchAll(GUARD_RULES["inline-spacing"].pattern)) {
753
892
  if (syntax === "css" && isSubScaleCssSpacing(line, m.index)) continue;
893
+ const hasUnit = /^[a-z%]/.test(line.slice(m.index + m[0].length));
894
+ if (syntax === "ts" && !hasUnit && !isStyleObjectContext(codeText, lineStarts[i] + m.index, styleIdentifiers))
895
+ continue;
754
896
  push("inline-spacing", i + 1, m[0]);
755
897
  }
756
898
  }
@@ -860,5 +1002,8 @@ export {
860
1002
  GENERATED_HEADER_LINE,
861
1003
  GUARD_RULES,
862
1004
  TOKENS_ONLY_DISABLED_KINDS,
863
- checkSource
1005
+ checkSource,
1006
+ guardKindRemedy,
1007
+ guardWaiverHint,
1008
+ unmatchedExemptPatterns
864
1009
  };
@@ -8,6 +8,7 @@
8
8
  * 2. `useStore` — React hook: `const [values, persist] = store.useStore()`.
9
9
  * Call `persist(next)` alongside `navigate` so the selection survives navigation.
10
10
  * 3. `readStored` — plain function for non-React contexts (tests, guards).
11
+ * 4. `linkSearch` — the click-time `search:` thunk for a nav link, ready-made.
11
12
  *
12
13
  * Headless — no Mantine, no JSX. Same tier as `createSearchParamStore`.
13
14
  *
@@ -73,6 +74,18 @@ export type MultiSearchParamStore<T extends string, P extends string = string> =
73
74
  useStore: () => readonly [readonly T[], (next: readonly T[]) => void];
74
75
  /** Plain read — for use outside React (tests, guards, fallback reads). */
75
76
  readStored: () => readonly T[] | null;
77
+ /**
78
+ * The click-time `search:` thunk for a nav link — `() => ({ [param]: readStored() ?? fallback })`.
79
+ * Pass it BY REFERENCE (`search: store.linkSearch`), never call it; see `createSearchParamStore`
80
+ * for why a module-scope literal silently defeats the store.
81
+ *
82
+ * No dev warning rides on this one, unlike the single-value store: an empty array in the URL is
83
+ * indistinguishable from an absent param here (`validateSearch` falls back to storage for both),
84
+ * so a link declaring `{ tags: [] }` restores correctly and there is no broken state to detect.
85
+ */
86
+ linkSearch: () => {
87
+ [K in P]: readonly T[];
88
+ };
76
89
  };
77
90
  export declare function createMultiSearchParamStore<const T extends string, const P extends string>(opts: MultiSearchParamStoreOptions<T, P>): MultiSearchParamStore<T, P>;
78
91
  //# sourceMappingURL=multi-search-param-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"multi-search-param-store.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/multi-search-param-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAMH,MAAM,MAAM,4BAA4B,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACtF,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAA;IACX,6DAA6D;IAC7D,KAAK,EAAE,CAAC,CAAA;IACR,0FAA0F;IAC1F,MAAM,EAAE,SAAS,CAAC,EAAE,CAAA;IACpB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,SAAS,CAAC,EAAE,CAAA;IACvB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC/E;;;OAGG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK;SAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,EAAE;KAAE,CAAA;IAC/E;;;OAGG;IACH,QAAQ,EAAE,MAAM,SAAS,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC,CAAA;IACrE,0EAA0E;IAC1E,UAAU,EAAE,MAAM,SAAS,CAAC,EAAE,GAAG,IAAI,CAAA;CACtC,CAAA;AAID,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,CAAC,CAAC,SAAS,MAAM,EACxF,IAAI,EAAE,4BAA4B,CAAC,CAAC,EAAE,CAAC,CAAC,GACvC,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC,CAoC7B"}
1
+ {"version":3,"file":"multi-search-param-store.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/multi-search-param-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAMH,MAAM,MAAM,4BAA4B,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACtF,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAA;IACX,6DAA6D;IAC7D,KAAK,EAAE,CAAC,CAAA;IACR,0FAA0F;IAC1F,MAAM,EAAE,SAAS,CAAC,EAAE,CAAA;IACpB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,SAAS,CAAC,EAAE,CAAA;IACvB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC/E;;;OAGG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK;SAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,EAAE;KAAE,CAAA;IAC/E;;;OAGG;IACH,QAAQ,EAAE,MAAM,SAAS,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC,CAAA;IACrE,0EAA0E;IAC1E,UAAU,EAAE,MAAM,SAAS,CAAC,EAAE,GAAG,IAAI,CAAA;IACrC;;;;;;;;OAQG;IACH,UAAU,EAAE,MAAM;SAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,EAAE;KAAE,CAAA;CAC7C,CAAA;AAID,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,CAAC,CAAC,SAAS,MAAM,EACxF,IAAI,EAAE,4BAA4B,CAAC,CAAC,EAAE,CAAC,CAAC,GACvC,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC,CAwC7B"}
@@ -16,6 +16,9 @@ function createMultiSearchParamStore(opts) {
16
16
  if (!Array.isArray(raw)) return null;
17
17
  return normalize(raw);
18
18
  };
19
+ const linkSearch = () => {
20
+ return { [opts.param]: readStored() ?? fallback };
21
+ };
19
22
  const validateSearch = (search) => {
20
23
  const fromUrl = normalize(search[opts.param]);
21
24
  if (fromUrl.length > 0) {
@@ -28,7 +31,7 @@ function createMultiSearchParamStore(opts) {
28
31
  version,
29
32
  initial: fallback
30
33
  });
31
- return { validateSearch, useStore, readStored };
34
+ return { validateSearch, useStore, readStored, linkSearch };
32
35
  }
33
36
  export {
34
37
  createMultiSearchParamStore
@@ -9,6 +9,19 @@
9
9
  * Call `persist(next)` alongside `navigate` so the selection survives
10
10
  * navigation away from and back to the route.
11
11
  * 3. `readStored` — plain function for non-React contexts (tests, guards).
12
+ * 4. `linkSearch` — the click-time `search:` thunk a nav link needs, ready-made.
13
+ *
14
+ * ## The half that is easy to skip
15
+ *
16
+ * `validateSearch` restores the value when you navigate TO the route with no param. It cannot do
17
+ * anything about a nav link that declares the param itself — and a link written as
18
+ * `search: { range: '30d' }` at module scope pins the fallback on every click, which silently
19
+ * defeats the whole store. The reference consumer shipped exactly that: three features adopted the
20
+ * persistence, the reader had ZERO call sites, and "remember my window" had never once worked.
21
+ *
22
+ * `linkSearch` exists so that failure is unreachable from the API rather than only warned about in
23
+ * this comment — the mistake is made while typing `search:`, so the remedy has to be something
24
+ * autocomplete offers at that keystroke. The JSDoc was already here when it happened.
12
25
  *
13
26
  * Headless — no Mantine, no JSX. Same tier as `useBasaltNav` and
14
27
  * `useRouterBreadcrumbs`.
@@ -51,17 +64,18 @@
51
64
  *
52
65
  * **5. Nav links** — carry the param across sub-pages, set per destination in
53
66
  * the nav definition so only the dashboard sub-tree inherits it and every
54
- * other link stays clean. Use a CLICK-TIME THUNK over `readStored`:
55
- *
56
- * const dashSearch = () => ({ range: dashboardRange.readStored() ?? '30d' })
67
+ * other link stays clean. Pass `linkSearch` BY REFERENCE — it is the click-time thunk:
57
68
  *
58
69
  * navGroup({ id: 'dash', label: 'Dashboard' }, [
59
70
  * { id: 'dash-overview', label: 'Overview', icon: <IconHome />,
60
- * link: linkOptions({ to: '/dashboard', search: dashSearch }) },
71
+ * link: linkOptions({ to: '/dashboard', search: dashboardRange.linkSearch }) },
61
72
  * { id: 'dash-activity', label: 'Activity', icon: <IconPulse />,
62
- * link: linkOptions({ to: '/dashboard/activity', search: dashSearch }) },
73
+ * link: linkOptions({ to: '/dashboard/activity', search: dashboardRange.linkSearch }) },
63
74
  * ])
64
75
  *
76
+ * `linkSearch` IS that thunk (`() => ({ [param]: readStored() ?? fallback })`) — hand-rolling it
77
+ * over `readStored` still works and is what you need when the link also carries other params.
78
+ *
65
79
  * NOT `search: true`. That flag means "keep the current search" and TanStack
66
80
  * only offers it where the target's search is OPTIONAL — a route wired to a
67
81
  * store's `validateSearch` always returns the param, so the router requires a
@@ -97,6 +111,16 @@ export type SearchParamStore<T extends string, P extends string = string> = {
97
111
  useStore: () => readonly [T, (next: T) => void];
98
112
  /** Plain read — for use outside React (tests, guards, fallback reads). */
99
113
  readStored: () => T | null;
114
+ /**
115
+ * The click-time `search:` thunk for a nav link — `() => ({ [param]: readStored() ?? fallback })`.
116
+ *
117
+ * Pass it BY REFERENCE (`search: store.linkSearch`), never call it: `<Link>` re-evaluates the
118
+ * thunk on every click, so arriving from outside the sub-tree restores the last selection,
119
+ * whereas a value computed once at module scope goes stale immediately and pins the fallback.
120
+ */
121
+ linkSearch: () => {
122
+ [K in P]: T;
123
+ };
100
124
  };
101
125
  export declare function createSearchParamStore<const T extends string, const P extends string>(opts: SearchParamStoreOptions<T, P>): SearchParamStore<T, P>;
102
126
  //# sourceMappingURL=search-param-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-param-store.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/search-param-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAMH,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACjF,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAA;IACX,mEAAmE;IACnE,KAAK,EAAE,CAAC,CAAA;IACR,2DAA2D;IAC3D,MAAM,EAAE,SAAS,CAAC,EAAE,CAAA;IACpB,kEAAkE;IAClE,QAAQ,EAAE,CAAC,CAAA;IACX,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC1E;;;OAGG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK;SAAG,CAAC,IAAI,CAAC,GAAG,CAAC;KAAE,CAAA;IACpE;;;OAGG;IACH,QAAQ,EAAE,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAC/C,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;CAC3B,CAAA;AAID,wBAAgB,sBAAsB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,CAAC,CAAC,SAAS,MAAM,EACnF,IAAI,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,GAClC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAyBxB"}
1
+ {"version":3,"file":"search-param-store.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/search-param-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAMH,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACjF,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAA;IACX,mEAAmE;IACnE,KAAK,EAAE,CAAC,CAAA;IACR,2DAA2D;IAC3D,MAAM,EAAE,SAAS,CAAC,EAAE,CAAA;IACpB,kEAAkE;IAClE,QAAQ,EAAE,CAAC,CAAA;IACX,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC1E;;;OAGG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK;SAAG,CAAC,IAAI,CAAC,GAAG,CAAC;KAAE,CAAA;IACpE;;;OAGG;IACH,QAAQ,EAAE,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAC/C,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;IAC1B;;;;;;OAMG;IACH,UAAU,EAAE,MAAM;SAAG,CAAC,IAAI,CAAC,GAAG,CAAC;KAAE,CAAA;CAClC,CAAA;AAgCD,wBAAgB,sBAAsB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,CAAC,CAAC,SAAS,MAAM,EACnF,IAAI,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,GAClC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAyDxB"}
@@ -1,25 +1,53 @@
1
1
  import { createPersistedState, readPersistedValue } from "../state.js";
2
+ function warnLinkPinsFallback(input) {
3
+ const { key, param, urlValue, storedValue } = input;
4
+ console.warn(
5
+ `[basalt-ui] createSearchParamStore('${key}'): the URL pinned \`${param}=${urlValue}\` (the fallback) while '${storedValue}' was persisted, and nothing has read this store back. A nav link is almost certainly declaring \`search: { ${param}: '${urlValue}' }\` at module scope, which overrides the stored value on every click \u2014 pass the store's own reader instead: \`search: <store>.linkSearch\`. (dev only)`
6
+ );
7
+ }
2
8
  function createSearchParamStore(opts) {
3
9
  const version = opts.version ?? 1;
4
- const readStored = () => {
10
+ let readerWired = false;
11
+ let warned = false;
12
+ const read = () => {
5
13
  const raw = readPersistedValue(opts.key, version);
6
14
  if (typeof raw !== "string") return null;
7
15
  if (opts.values.includes(raw)) return raw;
8
16
  return null;
9
17
  };
18
+ const readStored = () => {
19
+ readerWired = true;
20
+ return read();
21
+ };
22
+ const linkSearch = () => {
23
+ readerWired = true;
24
+ return { [opts.param]: read() ?? opts.fallback };
25
+ };
10
26
  const validateSearch = (search) => {
11
27
  const raw = search[opts.param];
12
28
  if (typeof raw === "string" && opts.values.includes(raw)) {
29
+ if (!warned && !readerWired && raw === opts.fallback && process.env["NODE_ENV"] !== "production") {
30
+ const stored = read();
31
+ if (stored !== null && stored !== raw) {
32
+ warned = true;
33
+ warnLinkPinsFallback({
34
+ key: opts.key,
35
+ param: opts.param,
36
+ urlValue: raw,
37
+ storedValue: stored
38
+ });
39
+ }
40
+ }
13
41
  return { [opts.param]: raw };
14
42
  }
15
- return { [opts.param]: readStored() ?? opts.fallback };
43
+ return { [opts.param]: read() ?? opts.fallback };
16
44
  };
17
45
  const useStore = createPersistedState({
18
46
  key: opts.key,
19
47
  version,
20
48
  initial: opts.fallback
21
49
  });
22
- return { validateSearch, useStore, readStored };
50
+ return { validateSearch, useStore, readStored, linkSearch };
23
51
  }
24
52
  export {
25
53
  createSearchParamStore
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search-param-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-param-store.test.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/search-param-store.test.ts"],"names":[],"mappings":""}
@@ -67,13 +67,20 @@ export type BasaltShellProps = {
67
67
  * `onOpen`, e.g. `() => openSpotlight()` from basalt-ui/commands.
68
68
  */
69
69
  search?: SidebarSearchConfig;
70
- /** localStorage key for the persisted sidebar-collapsed flag. Ignored when `collapsed` is set. */
70
+ /**
71
+ * localStorage key for the persisted sidebar-collapsed flag. Ignored when `collapsed` is set.
72
+ *
73
+ * Persisted through basalt's own `createPersistedState`, so the real storage key is
74
+ * `basalt:<storageKey>` and the value is the versioned `{ v, value }` envelope. A consumer
75
+ * mirroring this state reads it with `readPersistedValue(storageKey, 1)` from `basalt-ui/state`
76
+ * — never with a bare `localStorage.getItem`, which is what the pre-1.20.1 shell forced.
77
+ */
71
78
  storageKey?: string;
72
79
  /**
73
- * Controlled desktop-collapse value. When provided, the shell no longer owns the
74
- * `useLocalStorage`-persisted collapse state — the consumer does (e.g. to drive it from its own
75
- * `Cmd+B` hotkey). Pair with `onCollapsedChange` to receive toggle events; omitting both
76
- * reproduces today's internal, persisted collapse behavior unchanged.
80
+ * Controlled desktop-collapse value. When provided, the shell no longer owns the persisted
81
+ * collapse state — the consumer does (e.g. to drive it from its own `Cmd+B` hotkey), and should
82
+ * own it through `createPersistedState` too. Pair with `onCollapsedChange` to receive toggle
83
+ * events; omitting both reproduces today's internal, persisted collapse behavior unchanged.
77
84
  */
78
85
  collapsed?: boolean;
79
86
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell/index.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMlD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,KAAK,EAAE,eAAe,EAA0B,cAAc,EAAE,MAAM,cAAc,CAAA;AAM3F,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,uBAAuB,EACvB,eAAe,GAChB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAElF;;;;GAIG;AACH,YAAY,EACV,SAAS,EACT,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,cAAc,CAAA;AAErB,gEAAgE;AAChE,MAAM,MAAM,WAAW,GAAG;IACxB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,6DAA6D;AAC7D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6CAA6C;IAC7C,KAAK,EAAE,WAAW,CAAA;IAClB,mGAAmG;IACnG,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,4EAA4E;IAC5E,aAAa,CAAC,EAAE,SAAS,CAAA;IACzB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,SAAS,CAAA;IAC3B,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,kBAAkB,CAAA;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAA;IAChD,oBAAoB;IACpB,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,CAAA;AAgGD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,SAAS,EACT,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,UAAuC,EACvC,SAAS,EAAE,aAAa,EACxB,iBAAiB,EACjB,OAAO,EACP,MAAM,EACN,QAAQ,GACT,EAAE,gBAAgB,+BA4FlB"}
@@ -1,6 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { AppShell } from "@mantine/core";
3
- import { useLocalStorage } from "@mantine/hooks";
4
3
  import { useMemo } from "react";
5
4
  import { AppSidebar } from "./app-sidebar.js";
6
5
  import { MobileNav, accountRowCount } from "./app-mobile-nav.js";
@@ -8,6 +7,7 @@ import { projectMobileNav } from "./mobile-nav-model.js";
8
7
  import { AppBreadcrumbs } from "./app-breadcrumbs.js";
9
8
  import { PageActionsOutlet, PageHeaderProvider } from "./page-header.js";
10
9
  import { useBasaltSpacing } from "../theme/index.js";
10
+ import { createPersistedState } from "../state.js";
11
11
  import headerClasses from "./app-header.module.css";
12
12
  import mobileNavClasses from "./app-mobile-nav.module.css";
13
13
  import { AppSidebar as AppSidebar2 } from "./app-sidebar.js";
@@ -47,6 +47,28 @@ function findActiveWithParent(items, parentLabel, parentAnchor, parentHref) {
47
47
  }
48
48
  return void 0;
49
49
  }
50
+ const COLLAPSE_VERSION = 1;
51
+ const collapseStores = /* @__PURE__ */ new Map();
52
+ function migrateLegacyCollapse(key) {
53
+ try {
54
+ if (window.localStorage.getItem(`basalt:${key}`) !== null) return;
55
+ const legacy = window.localStorage.getItem(key);
56
+ if (legacy !== "true" && legacy !== "false") return;
57
+ window.localStorage.setItem(
58
+ `basalt:${key}`,
59
+ JSON.stringify({ v: COLLAPSE_VERSION, value: legacy === "true" })
60
+ );
61
+ } catch {
62
+ }
63
+ }
64
+ function collapseStore(key) {
65
+ const cached = collapseStores.get(key);
66
+ if (cached) return cached;
67
+ if (typeof window !== "undefined") migrateLegacyCollapse(key);
68
+ const store = createPersistedState({ key, version: COLLAPSE_VERSION, initial: false });
69
+ collapseStores.set(key, store);
70
+ return store;
71
+ }
50
72
  function BasaltShell({
51
73
  brand,
52
74
  sections,
@@ -62,11 +84,7 @@ function BasaltShell({
62
84
  children
63
85
  }) {
64
86
  const { step } = useBasaltSpacing();
65
- const [storedCollapsed, setStoredCollapsed] = useLocalStorage({
66
- key: storageKey,
67
- defaultValue: false,
68
- getInitialValueInEffect: false
69
- });
87
+ const [storedCollapsed, setStoredCollapsed] = collapseStore(storageKey)();
70
88
  const isCollapseControlled = collapsedProp !== void 0;
71
89
  const collapsed = isCollapseControlled ? collapsedProp : storedCollapsed;
72
90
  const toggleCollapse = () => {
@@ -40,6 +40,7 @@ export declare const VX: {
40
40
  readonly legendGap: number;
41
41
  readonly legendFontSize: 13.5;
42
42
  readonly text: {
43
+ readonly nano: 10;
43
44
  readonly micro: 11;
44
45
  readonly xs: 12.5;
45
46
  readonly sm: 13.5;
@@ -49,6 +50,7 @@ export declare const VX: {
49
50
  readonly h2: 21;
50
51
  readonly kpi: 24;
51
52
  readonly h1: 26;
53
+ readonly display: 30;
52
54
  };
53
55
  readonly radiusCard: "var(--vx-radius-card)";
54
56
  readonly radiusCtrl: "var(--vx-radius-ctrl)";
@@ -240,7 +242,7 @@ export type BuildPaletteOpts = {
240
242
  * `'core'` keeps the 9 generic anchors — the stack rhythm, the two control heights, the row
241
243
  * insets — and drops the 99 named for a specific basalt component
242
244
  * (`--vx-space-agent-transcript-inset`, `--vx-space-toc-sub-indent`, …), taking the emitted set
243
- * from 202 variables to 103. Nothing outside basalt's own React components reads those, so a
245
+ * from 204 variables to 105. Nothing outside basalt's own React components reads those, so a
244
246
  * framework-free consumer is carrying them for nothing.
245
247
  *
246
248
  * This is a SPACING filter only: color, radius, shadow, type and status variables are identical
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tokens/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tokens/index.ts"],"names":[],"mappings":"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"}