basalt-ui 1.26.0 → 1.27.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 (107) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/MIGRATING.md +299 -33
  4. package/agent/rules/basalt-controls.md +17 -12
  5. package/agent/rules/basalt-state.md +5 -0
  6. package/configs/oxlint-plugin.js +251 -66
  7. package/configs/oxlint-plugin.test.ts +243 -0
  8. package/configs/oxlint.json +5 -5
  9. package/dist/controls/enum-filter.d.ts +11 -2
  10. package/dist/controls/enum-filter.d.ts.map +1 -1
  11. package/dist/controls/enum-filter.js +4 -2
  12. package/dist/controls/filter-context.d.ts +7 -2
  13. package/dist/controls/filter-context.d.ts.map +1 -1
  14. package/dist/controls/index.d.ts +1 -0
  15. package/dist/controls/index.d.ts.map +1 -1
  16. package/dist/controls/index.js +2 -0
  17. package/dist/controls/multi-select-filter.js +2 -2
  18. package/dist/controls/number-filter.d.ts +26 -0
  19. package/dist/controls/number-filter.d.ts.map +1 -0
  20. package/dist/controls/number-filter.js +143 -0
  21. package/dist/controls/range-filter.d.ts +19 -5
  22. package/dist/controls/range-filter.d.ts.map +1 -1
  23. package/dist/controls/range-filter.js +22 -7
  24. package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
  25. package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
  26. package/dist/controls/search-filter.js +1 -1
  27. package/dist/controls/toggle-filter.js +1 -1
  28. package/dist/dashboard/index.d.ts +1 -1
  29. package/dist/dashboard/index.d.ts.map +1 -1
  30. package/dist/dashboard/index.js +3 -1
  31. package/dist/dashboard/stat-card.d.ts +52 -1
  32. package/dist/dashboard/stat-card.d.ts.map +1 -1
  33. package/dist/dashboard/stat-card.js +42 -15
  34. package/dist/dashboard/stat-card.module.css +71 -0
  35. package/dist/dashboard/stat-card.module.css.d.ts +4 -0
  36. package/dist/data/data-table.d.ts.map +1 -1
  37. package/dist/data/data-table.js +8 -0
  38. package/dist/guard/grace.test.d.ts.map +1 -1
  39. package/dist/guard/index.d.ts +1 -1
  40. package/dist/guard/index.d.ts.map +1 -1
  41. package/dist/guard/index.js +6 -10
  42. package/dist/index.d.ts +2 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/router-tanstack/field.d.ts +1 -1
  45. package/dist/router-tanstack/field.d.ts.map +1 -1
  46. package/dist/router-tanstack/index.d.ts +1 -1
  47. package/dist/router-tanstack/index.d.ts.map +1 -1
  48. package/dist/router-tanstack/search-store.d.ts +4 -1
  49. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  50. package/dist/router-tanstack/search-store.js +91 -7
  51. package/dist/shell/page-bar.d.ts +1 -1
  52. package/dist/state/fields.d.ts +209 -36
  53. package/dist/state/fields.d.ts.map +1 -1
  54. package/dist/state/fields.js +68 -32
  55. package/dist/state/persisted.d.ts +12 -0
  56. package/dist/state/persisted.d.ts.map +1 -1
  57. package/dist/state/persisted.js +36 -18
  58. package/dist/state/persisted.test.d.ts +2 -0
  59. package/dist/state/persisted.test.d.ts.map +1 -0
  60. package/dist/state.d.ts +1 -1
  61. package/dist/state.d.ts.map +1 -1
  62. package/dist/surfaces.d.ts +1 -1
  63. package/dist/surfaces.d.ts.map +1 -1
  64. package/dist/surfaces.js +4 -3
  65. package/dist/widget-header/widget-header.d.ts +31 -1
  66. package/dist/widget-header/widget-header.d.ts.map +1 -1
  67. package/dist/widget-header/widget-header.js +13 -1
  68. package/dist/widget-header/widget-header.module.css +15 -0
  69. package/dist/widget-header/widget-header.module.css.d.ts +1 -0
  70. package/llms.txt +3 -3
  71. package/package.json +1 -1
  72. package/src/controls/controls.router.test.tsx +416 -0
  73. package/src/controls/enum-filter.tsx +14 -3
  74. package/src/controls/filter-context.tsx +7 -2
  75. package/src/controls/index.ts +1 -0
  76. package/src/controls/multi-select-filter.tsx +2 -2
  77. package/src/controls/number-filter.tsx +291 -0
  78. package/src/controls/props-contract.test.tsx +31 -0
  79. package/src/controls/range-filter.tsx +64 -11
  80. package/src/controls/range-filter.type-guard.test.tsx +122 -0
  81. package/src/controls/search-filter.tsx +1 -1
  82. package/src/controls/toggle-filter.tsx +1 -1
  83. package/src/dashboard/index.ts +6 -1
  84. package/src/dashboard/stat-card.module.css +71 -0
  85. package/src/dashboard/stat-card.module.css.d.ts +4 -0
  86. package/src/dashboard/stat-card.test.tsx +231 -0
  87. package/src/dashboard/stat-card.tsx +81 -0
  88. package/src/data/data-table.tsx +8 -0
  89. package/src/guard/check-source.test.ts +35 -2
  90. package/src/guard/grace.test.ts +14 -5
  91. package/src/guard/index.ts +30 -17
  92. package/src/index.ts +5 -0
  93. package/src/router-tanstack/field.ts +4 -0
  94. package/src/router-tanstack/index.ts +4 -0
  95. package/src/router-tanstack/search-store.router.test.tsx +232 -11
  96. package/src/router-tanstack/search-store.test.ts +160 -0
  97. package/src/router-tanstack/search-store.ts +177 -11
  98. package/src/shell/page-bar.tsx +1 -1
  99. package/src/state/fields.test.tsx +254 -0
  100. package/src/state/fields.ts +328 -70
  101. package/src/state/persisted.test.ts +91 -0
  102. package/src/state/persisted.ts +76 -26
  103. package/src/state.ts +6 -0
  104. package/src/surfaces.ts +4 -3
  105. package/src/widget-header/widget-header.module.css +15 -0
  106. package/src/widget-header/widget-header.module.css.d.ts +1 -0
  107. package/src/widget-header/widget-header.tsx +46 -1
@@ -1,5 +1,19 @@
1
1
  import { useSyncExternalStore } from "react";
2
- import { createPersistedState, readPersistedValue } from "./persisted.js";
2
+ import { createPersistedState, readPersistedValue, writePersistedValue } from "./persisted.js";
3
+ function rangeField(o, lane) {
4
+ const { fallback } = o;
5
+ return {
6
+ kind: "range",
7
+ presets: o.presets,
8
+ // A thunk fallback stays a thunk — wrapped so the lazy value is read at the same moment every
9
+ // other lane reads one, not once here at definition.
10
+ fallback: typeof fallback === "function" ? () => ({ preset: fallback() }) : { preset: fallback },
11
+ custom: o.custom ?? false,
12
+ params: o.params ?? {},
13
+ window: o.window ?? {},
14
+ lane: resolveLane(lane)
15
+ };
16
+ }
3
17
  const field = {
4
18
  /** A closed string enum — a tab, a compare mode, a single-select filter. */
5
19
  enum(values, fallback, lane) {
@@ -9,29 +23,7 @@ const field = {
9
23
  multi(values, fallback = [], lane) {
10
24
  return { kind: "multi", values, fallback, lane: resolveLane(lane) };
11
25
  },
12
- /**
13
- * A time window: THREE URL params (preset + `from` + `to`, each renamable through `params`), so a
14
- * consumer's existing deep links and loaders keep their shape. `custom: true` additionally
15
- * allows the preset `'custom'` with two ISO dates.
16
- *
17
- * A second `field.range` in the same store — or a second range store composed into the same
18
- * route's `validateSearch` — MUST rename `from`/`to` via `params`: every range defaults to the
19
- * literal param names `'from'`/`'to'`, and two fields silently sharing them makes the later one's
20
- * `toSearch` overwrite the earlier one's dates. `createStoreCore` throws at definition for the
21
- * in-store case; the cross-store case is not detectable by either store alone.
22
- */
23
- range(o, lane) {
24
- return {
25
- kind: "range",
26
- presets: o.presets,
27
- fallback: { preset: o.fallback },
28
- // The only cast left, and it is narrow: `C` is inferred from this very literal, so the
29
- // runtime boolean IS `C` — `?? false` is what the compiler cannot see through.
30
- custom: o.custom ?? false,
31
- params: o.params ?? {},
32
- lane: resolveLane(lane)
33
- };
34
- },
26
+ range: rangeField,
35
27
  /** A number — pagination, a threshold. Out-of-range input clamps to `min`/`max`. */
36
28
  number(o, lane) {
37
29
  return {
@@ -64,6 +56,9 @@ function resolveLane(lane) {
64
56
  history: lane?.history === "push" ? "push" : "replace"
65
57
  };
66
58
  }
59
+ function resolveFallback(raw) {
60
+ return typeof raw === "function" ? raw() : raw;
61
+ }
67
62
  const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T[\d:.]+(?:Z|[+-]\d{2}:\d{2})?)?$/;
68
63
  function isIsoDate(raw) {
69
64
  return typeof raw === "string" && ISO_DATE_RE.test(raw) && !Number.isNaN(Date.parse(raw));
@@ -126,7 +121,10 @@ function resolveFieldCodec(name, f, opts) {
126
121
  lane: f.lane,
127
122
  params: [presetParam, fromParam, toParam],
128
123
  primary: presetParam,
129
- fallback: f.fallback,
124
+ get fallback() {
125
+ return resolveFallback(f.fallback);
126
+ },
127
+ windows: f.window,
130
128
  decode,
131
129
  fromSearch: (search) => decode({
132
130
  preset: search[presetParam],
@@ -160,7 +158,7 @@ function resolveFieldCodec(name, f, opts) {
160
158
  if (f.max !== void 0 && value > f.max) value = f.max;
161
159
  return value;
162
160
  };
163
- return single(name, f, decode);
161
+ return single(name, f, decode, { bounds: { min: f.min, max: f.max, int: f.int } });
164
162
  }
165
163
  case "boolean": {
166
164
  return single(name, f, decodeBoolean);
@@ -184,7 +182,10 @@ function single(name, f, decode, extra) {
184
182
  lane: f.lane,
185
183
  params: [name],
186
184
  primary: name,
187
- fallback: f.fallback,
185
+ get fallback() {
186
+ return resolveFallback(f.fallback);
187
+ },
188
+ ...extra?.bounds !== void 0 && { bounds: extra.bounds },
188
189
  decode,
189
190
  fromSearch: (search) => decodeUrl(search[name]),
190
191
  toSearch: (value) => ({ [name]: value }),
@@ -235,6 +236,10 @@ function createMemoryLane() {
235
236
  values.set(name, value);
236
237
  bump();
237
238
  },
239
+ remove: (name) => {
240
+ if (!values.delete(name)) return;
241
+ bump();
242
+ },
238
243
  clear: () => {
239
244
  if (values.size === 0) return;
240
245
  values.clear();
@@ -279,24 +284,51 @@ function createStoreCore(o) {
279
284
  const map = labels.get(entry.name);
280
285
  return entry.codec.optionValues.map((value) => ({ value, label: map?.[value] ?? value }));
281
286
  };
287
+ const clearField = (name) => {
288
+ const record = readRecord();
289
+ if (!Object.hasOwn(record, name)) return;
290
+ const next = { ...record };
291
+ delete next[name];
292
+ writePersistedValue(o.key, version, fromRecord(next));
293
+ };
282
294
  const writeField = (setRecord, name, value) => {
283
295
  setRecord({ ...readRecord(), [name]: value });
284
296
  };
285
- const handle = (entry, use) => {
297
+ const handle = (entry, use, clear) => {
286
298
  const { codec } = entry;
287
299
  const built = {
288
300
  kind: codec.kind,
289
- fallback: codec.fallback,
290
301
  use,
302
+ clear,
291
303
  isDefault: (v) => codec.equals(v, codec.fallback)
292
304
  };
293
305
  if (codec.kind === "range") {
294
- built["toWindow"] = (v) => v.preset === "custom" && v.from !== void 0 && v.to !== void 0 ? { from: v.from, to: v.to } : { window: v.preset };
306
+ built["toWindow"] = (v) => {
307
+ if (v.preset === "custom" && v.from !== void 0 && v.to !== void 0) {
308
+ return { from: v.from, to: v.to };
309
+ }
310
+ const windows = codec.windows;
311
+ if (windows !== void 0 && Object.hasOwn(windows, v.preset)) {
312
+ const resolver = windows[v.preset];
313
+ if (resolver !== void 0) return resolver(/* @__PURE__ */ new Date());
314
+ }
315
+ return { window: v.preset };
316
+ };
317
+ }
318
+ if (codec.kind === "number") {
319
+ const bounds = codec.bounds;
320
+ built["min"] = bounds?.min;
321
+ built["max"] = bounds?.max;
322
+ built["int"] = bounds?.int ?? false;
295
323
  }
296
324
  Object.defineProperty(built, "options", {
297
325
  enumerable: true,
298
326
  get: () => optionsFor(entry)
299
327
  });
328
+ Object.defineProperty(built, "fallback", {
329
+ enumerable: true,
330
+ get: () => codec.fallback
331
+ });
300
332
  return built;
301
333
  };
302
334
  return {
@@ -317,6 +349,8 @@ function createStoreCore(o) {
317
349
  readMemoryValue: (entry) => memory.resolve(entry),
318
350
  useMemoryVersion: () => useSyncExternalStore(memory.subscribe, memory.getVersion, memory.getVersion),
319
351
  resetMemory: () => memory.clear(),
352
+ clearField,
353
+ clearMemory: (name) => memory.remove(name),
320
354
  setLabels(map) {
321
355
  for (const [name, entry] of Object.entries(map)) {
322
356
  if (entry !== void 0) labels.set(name, entry);
@@ -348,8 +382,10 @@ function createLocalStore(o) {
348
382
  };
349
383
  const field2 = {};
350
384
  for (const entry of core.entries) {
351
- const use = entry.codec.lane.persist ? persistedUse(entry) : core.memoryUse(entry);
352
- field2[entry.name] = core.handle(entry, use);
385
+ const persisted = entry.codec.lane.persist;
386
+ const use = persisted ? persistedUse(entry) : core.memoryUse(entry);
387
+ const clear = persisted ? () => core.clearField(entry.name) : () => core.clearMemory(entry.name);
388
+ field2[entry.name] = core.handle(entry, use, clear);
353
389
  }
354
390
  const store = {
355
391
  field: field2,
@@ -45,4 +45,16 @@ export declare function createPersistedState<T>(opts: PersistedStateOptions<T>):
45
45
  * // stored is '1d' | '7d' | '30d' | null
46
46
  */
47
47
  export declare function readPersistedValue(key: string, version?: number): unknown | null;
48
+ /**
49
+ * Write a value under the namespaced key `basalt:<key>` in the same versioned envelope
50
+ * `createPersistedState` writes, and wake every hook on that key — the write half of
51
+ * `readPersistedValue`, and plain in the same way: no React required.
52
+ *
53
+ * The one call site is a store field's `clear()`, which has to DELETE its key from the store's
54
+ * record from an event handler, where the hook's setter is not in reach. Not exported from the
55
+ * `basalt-ui/state` barrel: the two store factories are the API, this is their seam.
56
+ *
57
+ * @internal
58
+ */
59
+ export declare function writePersistedValue(key: string, version: number, value: unknown): void;
48
60
  //# sourceMappingURL=persisted.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"persisted.d.ts","sourceRoot":"","sources":["../../src/state/persisted.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAOnD,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;IACnB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC,CAAA;IACjE;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;CAC/C,CAAA;AAqDD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC7B,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,CA8EvC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAYhF"}
1
+ {"version":3,"file":"persisted.d.ts","sourceRoot":"","sources":["../../src/state/persisted.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAOnD,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;IACnB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC,CAAA;IACjE;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;CAC/C,CAAA;AA4FD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC7B,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,CAwEvC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAYhF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAItF"}
@@ -1,6 +1,19 @@
1
1
  import { useSyncExternalStore } from "react";
2
2
  const noopSubscribe = (_cb) => () => {
3
3
  };
4
+ const channels = /* @__PURE__ */ new Map();
5
+ function notify(storageKey) {
6
+ const channel = channels.get(storageKey);
7
+ if (channel === void 0) return;
8
+ for (const listener of channel.listeners) listener();
9
+ }
10
+ function channelFor(storageKey) {
11
+ const existing = channels.get(storageKey);
12
+ if (existing !== void 0) return existing;
13
+ const created = { listeners: /* @__PURE__ */ new Set(), handler: null };
14
+ channels.set(storageKey, created);
15
+ return created;
16
+ }
4
17
  function isEnvelope(raw) {
5
18
  return typeof raw === "object" && raw !== null && "v" in raw && typeof raw["v"] === "number" && "value" in raw;
6
19
  }
@@ -24,33 +37,32 @@ function parseStorage(raw, opts) {
24
37
  return opts.initial;
25
38
  }
26
39
  }
27
- function writeStorage(opts, next) {
28
- const storageKey = `basalt:${opts.key}`;
40
+ function writeEnvelope(storageKey, version, value) {
29
41
  try {
30
- const envelope = { v: opts.version, value: next };
42
+ const envelope = { v: version, value };
31
43
  window.localStorage.setItem(storageKey, JSON.stringify(envelope));
32
44
  } catch {
33
45
  }
34
46
  }
47
+ function writeStorage(opts, next) {
48
+ writeEnvelope(`basalt:${opts.key}`, opts.version, next);
49
+ }
35
50
  function createPersistedState(opts) {
36
51
  const storageKey = `basalt:${opts.key}`;
37
- const listeners = /* @__PURE__ */ new Set();
38
- let storageHandler = null;
39
52
  const subscribe = (cb) => {
40
- listeners.add(cb);
41
- if (storageHandler === null) {
42
- storageHandler = (e) => {
43
- if (e.key === storageKey) {
44
- for (const listener of listeners) listener();
45
- }
53
+ const channel = channelFor(storageKey);
54
+ channel.listeners.add(cb);
55
+ if (channel.handler === null) {
56
+ channel.handler = (e) => {
57
+ if (e.key === storageKey) notify(storageKey);
46
58
  };
47
- window.addEventListener("storage", storageHandler);
59
+ window.addEventListener("storage", channel.handler);
48
60
  }
49
61
  return () => {
50
- listeners.delete(cb);
51
- if (listeners.size === 0 && storageHandler !== null) {
52
- window.removeEventListener("storage", storageHandler);
53
- storageHandler = null;
62
+ channel.listeners.delete(cb);
63
+ if (channel.listeners.size === 0 && channel.handler !== null) {
64
+ window.removeEventListener("storage", channel.handler);
65
+ channel.handler = null;
54
66
  }
55
67
  };
56
68
  };
@@ -73,7 +85,7 @@ function createPersistedState(opts) {
73
85
  const getServerSnapshot = () => opts.initial;
74
86
  const setState = (next) => {
75
87
  writeStorage(opts, next);
76
- for (const cb of listeners) cb();
88
+ notify(storageKey);
77
89
  };
78
90
  const isServer = typeof window === "undefined";
79
91
  return function usePersistedState() {
@@ -99,7 +111,13 @@ function readPersistedValue(key, version) {
99
111
  return null;
100
112
  }
101
113
  }
114
+ function writePersistedValue(key, version, value) {
115
+ const storageKey = `basalt:${key}`;
116
+ writeEnvelope(storageKey, version, value);
117
+ notify(storageKey);
118
+ }
102
119
  export {
103
120
  createPersistedState,
104
- readPersistedValue
121
+ readPersistedValue,
122
+ writePersistedValue
105
123
  };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=persisted.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persisted.test.d.ts","sourceRoot":"","sources":["../../src/state/persisted.test.ts"],"names":[],"mappings":""}
package/dist/state.d.ts CHANGED
@@ -13,5 +13,5 @@
13
13
  * `./state` export resolves to, and its named exports are pinned by `scripts/export-surface.json`.
14
14
  */
15
15
  export { createPersistedState, type PersistedStateOptions, readPersistedValue, } from './state/persisted.js';
16
- export { field, createLocalStore, type LocalStore, type FieldLane, type ResolvedLane, type ResolveLane, type RangeValue, type RangeParams, type RangePresets, type EnumField, type MultiField, type RangeField, type NumberField, type BooleanField, type StringField, type AnyField, type FieldValue, type FieldOption, type FieldHandle, type SearchValues, type StoredValues, type FieldCodec, type StoreEntry, type StoreCoreOptions, type StoreCore, resolveFieldCodec, createStoreCore, } from './state/fields.js';
16
+ export { field, createLocalStore, type LocalStore, type FieldLane, type ResolvedLane, type ResolveLane, type RangeValue, type RangeParams, type RangePresets, type RangeWindow, type RangeWindows, type FieldFallback, type FieldSetOptions, type EnumField, type MultiField, type RangeField, type NumberField, type BooleanField, type StringField, type AnyField, type FieldValue, type FieldOption, type FieldHandle, type SearchValues, type StoredValues, type FieldCodec, type StoreEntry, type StoreCoreOptions, type StoreCore, type FieldUse, type FieldWrite, resolveFieldCodec, createStoreCore, } from './state/fields.js';
17
17
  //# sourceMappingURL=state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,kBAAkB,GACnB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,KAAK,UAAU,EAEf,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EAEjB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,iBAAiB,EACjB,eAAe,GAChB,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,kBAAkB,GACnB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,KAAK,UAAU,EAEf,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EAEjB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,iBAAiB,EACjB,eAAe,GAChB,MAAM,gBAAgB,CAAA"}
@@ -375,7 +375,7 @@ export declare const SURFACES: {
375
375
  readonly guardKinds: readonly ["raw-selection-control"];
376
376
  readonly pluginRules: readonly ["hand-rolled-filter", "control-outside-home", "control-size-literal", "responsive-twin"];
377
377
  readonly advisoryLaws: readonly ["C1 as a cross-file law (a control placed in one file, its home declared in another)", "hand-rolled section headings (argo writes `<Text fw={600} size=\"sm\">` + children, which no AST heuristic matches without false positives)", "C11 — a table/list stating its count when it is not a BasaltDataTable", "C12 — one shape for refresh/sync (SyncButton); only the alias table sees a renamed copy"];
378
- readonly description: "The control tier (docs/CONTROLS-SPEC.md §3): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle — no value/onChange/size, law C2/C5), ViewTabs, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). Every control owns its own desktop/mobile swap in CSS (C9) and renders size=\"ctl\" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.";
378
+ readonly description: "The control tier (docs/CONTROLS-SPEC.md §3): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/NumberFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle — no value/onChange/size, law C2/C5; NumberFilter is the field.number lane, a radio list over `options` or a stepper without), ViewTabs, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). Every control owns its own desktop/mobile swap in CSS (C9) and renders size=\"ctl\" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.";
379
379
  readonly optionalPeers: readonly [];
380
380
  readonly forbiddenImports: readonly [];
381
381
  };
@@ -1 +1 @@
1
- {"version":3,"file":"surfaces.d.ts","sourceRoot":"","sources":["../src/surfaces.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surfaces.d.ts","sourceRoot":"","sources":["../src/surfaces.ts"],"names":[],"mappings":"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"}
package/dist/surfaces.js CHANGED
@@ -346,8 +346,9 @@ const SURFACES = {
346
346
  // The text lane of law C1 — the plugin's `control-outside-home` seen through a 12-line host-tag
347
347
  // window, since a regex scan has no ancestry. Same law, same wave, same promotion.
348
348
  guardKinds: ["raw-selection-control"],
349
- // The control tier itself (laws C1/C3/C5/C9). `hand-rolled-filter` ships `error` — "inside a
350
- // slot" is structural; the other three are grace entries in `PLUGIN_RULE_GRACE` until 1.27.0.
349
+ // The control tier itself (laws C1/C3/C5/C9). `hand-rolled-filter`, `control-size-literal` and
350
+ // `responsive-twin` all ship `error`; `control-outside-home` is the one grace entry left in
351
+ // `PLUGIN_RULE_GRACE`, re-dated to 1.28.0 against the wave-7 measurement (see its `why`).
351
352
  // The budget rules live on `.` with `PageBar` itself.
352
353
  pluginRules: [
353
354
  "hand-rolled-filter",
@@ -363,7 +364,7 @@ const SURFACES = {
363
364
  "C11 \u2014 a table/list stating its count when it is not a BasaltDataTable",
364
365
  "C12 \u2014 one shape for refresh/sync (SyncButton); only the alias table sees a renamed copy"
365
366
  ],
366
- description: 'The control tier (docs/CONTROLS-SPEC.md \xA73): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle \u2014 no value/onChange/size, law C2/C5), ViewTabs, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed \u2014 the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.',
367
+ description: 'The control tier (docs/CONTROLS-SPEC.md \xA73): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/NumberFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle \u2014 no value/onChange/size, law C2/C5; NumberFilter is the field.number lane, a radio list over `options` or a stepper without), ViewTabs, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed \u2014 the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.',
367
368
  optionalPeers: [],
368
369
  forbiddenImports: []
369
370
  },
@@ -20,10 +20,40 @@ export type WidgetHeaderProps = {
20
20
  /** Pre-formatted metric value (mono, hero size). Renders on its own row with `delta`, directly
21
21
  * under the title row — never inline with the title. */
22
22
  value?: string;
23
+ /**
24
+ * The value's unit, rendered immediately after it on the hero row: muted, mono, `--vx-text-sm`.
25
+ *
26
+ * It exists because `value` is a pre-formatted `string` and a KPI's unit is not part of the
27
+ * number: `412` and `TSS` are two facts at two weights, and jamming them into one string paints
28
+ * the unit at the hero's 24px — which reads as a second numeral. `subtitle` was the only place a
29
+ * unit could go before this, and a unit on its own line under the value is a different claim (the
30
+ * BASIS: `7-day rolling`), so cards using both had to pick one.
31
+ *
32
+ * Never a substitute for the number's own formatting: a thousands separator, a currency symbol or
33
+ * a `%` belong in `value`, where they are part of how the numeral reads.
34
+ */
35
+ unit?: string;
23
36
  /** Signed delta rendered via `DeltaBadge`, on the same hero-metric row as `value`. */
24
37
  delta?: number;
25
38
  /** Comparison timeframe forwarded to `DeltaBadge` (e.g. `MoM`). */
26
39
  deltaPeriod?: string;
40
+ /**
41
+ * Formats `delta` into the chip's label — forwarded to `DeltaBadge`'s `format`. Defaults to
42
+ * `${Math.abs(delta).toFixed(1)}%`.
43
+ *
44
+ * It exists because not every delta is a percentage: a pace card's trend is `0:12 /km` and a
45
+ * throughput card's is `0.3 km/h`, and the default printed both as a percent — a wrong unit on a
46
+ * KPI, which is worse than no chip. A FUNCTION rather than a label string, so the number stays the
47
+ * number: `delta` still drives the tone and the glyph, and there is exactly one place the sign is
48
+ * decided.
49
+ *
50
+ * The value arrives SIGNED, so a formatter that wants to print the sign itself (`−0:12 /km`) can —
51
+ * pair it with `deltaGlyph={false}` so the ▼ does not say the same thing twice.
52
+ */
53
+ deltaFormat?: (delta: number) => string;
54
+ /** Render `DeltaBadge`'s ▲/▼ glyph. Defaults to `true`; a zero delta never shows one. Pass
55
+ * `false` when `deltaFormat` prints the sign itself. */
56
+ deltaGlyph?: boolean;
27
57
  /** Trend visual slot, rendered below the metric row. Carries `data-placement="right"` so a
28
58
  * wave-3 composer's `bleed` layout can select it. */
29
59
  sparkline?: ReactNode;
@@ -33,5 +63,5 @@ export type WidgetHeaderProps = {
33
63
  /** Right-aligned slot (`margin-inline-start: auto`) — the C1/C5 home for header actions. */
34
64
  actions?: ReactNode;
35
65
  };
36
- export declare function WidgetHeader({ tier, title, icon, subtitle, info, value, delta, deltaPeriod, sparkline, count, actions, }: WidgetHeaderProps): import("react").JSX.Element;
66
+ export declare function WidgetHeader({ tier, title, icon, subtitle, info, value, unit, delta, deltaPeriod, deltaFormat, deltaGlyph, sparkline, count, actions, }: WidgetHeaderProps): import("react").JSX.Element;
37
67
  //# sourceMappingURL=widget-header.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"widget-header.d.ts","sourceRoot":"","sources":["../../src/widget-header/widget-header.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,iBAAiB,GAAG;IAC9B;iDAC6C;IAC7C,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAA;IAC1B,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAA;IACb,iGAAiG;IACjG,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;4DACwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;yDACqD;IACrD,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;2EACuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4FAA4F;IAC5F,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB,CAAA;AAyDD,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,EACL,WAAW,EACX,SAAS,EACT,KAAK,EACL,OAAO,GACR,EAAE,iBAAiB,+BAiCnB"}
1
+ {"version":3,"file":"widget-header.d.ts","sourceRoot":"","sources":["../../src/widget-header/widget-header.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,iBAAiB,GAAG;IAC9B;iDAC6C;IAC7C,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAA;IAC1B,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAA;IACb,iGAAiG;IACjG,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;4DACwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACvC;4DACwD;IACxD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;yDACqD;IACrD,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;2EACuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4FAA4F;IAC5F,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB,CAAA;AAyDD,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,KAAK,EACL,WAAW,EACX,WAAW,EACX,UAAU,EACV,SAAS,EACT,KAAK,EACL,OAAO,GACR,EAAE,iBAAiB,+BA6CnB"}
@@ -47,8 +47,11 @@ function WidgetHeader({
47
47
  subtitle,
48
48
  info,
49
49
  value,
50
+ unit,
50
51
  delta,
51
52
  deltaPeriod,
53
+ deltaFormat,
54
+ deltaGlyph,
52
55
  sparkline,
53
56
  count,
54
57
  actions
@@ -66,7 +69,16 @@ function WidgetHeader({
66
69
  ] }),
67
70
  (value !== void 0 || delta !== void 0) && /* @__PURE__ */ jsxs("div", { className: classes.metrics, children: [
68
71
  value !== void 0 && /* @__PURE__ */ jsx("span", { className: classes.value, children: value }),
69
- delta !== void 0 && /* @__PURE__ */ jsx(DeltaBadge, { value: delta, period: deltaPeriod })
72
+ value !== void 0 && unit !== void 0 && /* @__PURE__ */ jsx("span", { className: classes.unit, children: unit }),
73
+ delta !== void 0 && /* @__PURE__ */ jsx(
74
+ DeltaBadge,
75
+ {
76
+ value: delta,
77
+ period: deltaPeriod,
78
+ ...deltaFormat !== void 0 && { format: deltaFormat },
79
+ ...deltaGlyph !== void 0 && { withGlyph: deltaGlyph }
80
+ }
81
+ )
70
82
  ] }),
71
83
  subtitle !== void 0 && /* @__PURE__ */ jsx("span", { className: classes.subtitle, children: subtitle }),
72
84
  sparkline !== void 0 && /* @__PURE__ */ jsx("div", { className: classes.sparkline, "data-placement": "right", children: sparkline })
@@ -199,6 +199,21 @@
199
199
  color: var(--vx-ink);
200
200
  }
201
201
 
202
+ /*
203
+ * The unit beside the hero value. Mono like the number it qualifies (so `412 TSS` sits on one
204
+ * baseline grid), `--vx-text-sm` and `--vx-muted` so it never competes with the numeral — the whole
205
+ * point is that a unit is the SMALL half of the pair. `flex-shrink: 0`: the value is the row's
206
+ * shrinkable half (see `.value`), and a unit that ellipsized to `TS…` would state nothing.
207
+ */
208
+ .unit {
209
+ flex-shrink: 0;
210
+ align-self: baseline;
211
+ font-family: var(--basalt-font-mono);
212
+ font-size: var(--vx-text-sm);
213
+ font-weight: 500;
214
+ color: var(--vx-muted);
215
+ }
216
+
202
217
  /* The C1/C5 home for header actions — always pinned right, regardless of what else is present. */
203
218
  .actions {
204
219
  display: inline-flex;
@@ -11,6 +11,7 @@ declare const classes: {
11
11
  readonly count: string
12
12
  readonly metrics: string
13
13
  readonly value: string
14
+ readonly unit: string
14
15
  readonly actions: string
15
16
  readonly subtitle: string
16
17
  readonly sparkline: string
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.26.0
1
+ # basalt-ui v1.27.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.26.0
22
+ Version: 1.27.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
@@ -135,7 +135,7 @@ OptionalPeers: shiki@^4.3.0, @shikijs/langs@^4.3.1, @shikijs/themes@^4.3.1, beau
135
135
 
136
136
  ## basalt-ui/controls
137
137
  Import: import { ... } from 'basalt-ui/controls'
138
- Description: The control tier (docs/CONTROLS-SPEC.md §3): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle — no value/onChange/size, law C2/C5), ViewTabs, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.
138
+ Description: The control tier (docs/CONTROLS-SPEC.md §3): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/NumberFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle — no value/onChange/size, law C2/C5; NumberFilter is the field.number lane, a radio list over `options` or a stepper without), ViewTabs, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.
139
139
  Layer: mantine-coupled
140
140
  Rule: basalt-controls
141
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",