basalt-ui 1.5.0 → 1.7.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 (68) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/agent/rules/basalt-mantine.md +80 -40
  4. package/agent/rules/basalt-tokens.md +3 -1
  5. package/agent/skills/basalt-design/SKILL.md +21 -7
  6. package/agent/templates/CLAUDE-block.md.tpl +3 -2
  7. package/agent/templates/DESIGN.md.tpl +2 -1
  8. package/configs/oxlint-plugin.js +75 -1
  9. package/configs/oxlint.json +1 -0
  10. package/dist/agent/stick-to-bottom.d.ts.map +1 -1
  11. package/dist/agent/stick-to-bottom.js +2 -1
  12. package/dist/agent-chat/composer.d.ts.map +1 -1
  13. package/dist/agent-chat/composer.js +5 -4
  14. package/dist/cli/index.d.ts +9 -0
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +2 -0
  17. package/dist/dashboard/index.d.ts +1 -1
  18. package/dist/dashboard/index.d.ts.map +1 -1
  19. package/dist/dashboard/stat-card.d.ts +6 -1
  20. package/dist/dashboard/stat-card.d.ts.map +1 -1
  21. package/dist/dashboard/stat-card.js +37 -4
  22. package/dist/guard/index.d.ts +7 -1
  23. package/dist/guard/index.d.ts.map +1 -1
  24. package/dist/guard/index.js +16 -1
  25. package/dist/guard/types.d.ts +18 -3
  26. package/dist/guard/types.d.ts.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/shell/app-sidebar.module.css +4 -6
  30. package/dist/shell/sidebar-search.module.css +4 -2
  31. package/dist/surfaces.d.ts +2 -2
  32. package/dist/surfaces.d.ts.map +1 -1
  33. package/dist/surfaces.js +8 -2
  34. package/dist/theme/controls.module.css +167 -26
  35. package/dist/theme/index.d.ts.map +1 -1
  36. package/dist/theme/index.js +36 -19
  37. package/dist/theme/nav-link.module.css +10 -10
  38. package/dist/tokens/index.d.ts +1 -0
  39. package/dist/tokens/index.d.ts.map +1 -1
  40. package/dist/tokens/index.js +4 -0
  41. package/dist/tokens/palette.d.ts +31 -0
  42. package/dist/tokens/palette.d.ts.map +1 -1
  43. package/dist/tokens/palette.js +31 -0
  44. package/dist/tokens.css +2 -0
  45. package/dist/vite.d.ts +6 -0
  46. package/dist/vite.d.ts.map +1 -1
  47. package/dist/vite.js +21 -1
  48. package/llms.txt +3 -3
  49. package/package.json +1 -1
  50. package/src/agent/stick-to-bottom.tsx +2 -1
  51. package/src/agent-chat/composer.tsx +5 -4
  52. package/src/cli/index.ts +11 -0
  53. package/src/dashboard/index.ts +1 -1
  54. package/src/dashboard/stat-card.tsx +61 -2
  55. package/src/guard/check-source.test.ts +63 -1
  56. package/src/guard/index.ts +39 -3
  57. package/src/guard/types.ts +18 -2
  58. package/src/index.ts +1 -0
  59. package/src/shell/app-sidebar.module.css +4 -6
  60. package/src/shell/sidebar-search.module.css +4 -2
  61. package/src/surfaces.ts +8 -2
  62. package/src/theme/controls.module.css +167 -26
  63. package/src/theme/index.ts +50 -19
  64. package/src/theme/nav-link.module.css +10 -10
  65. package/src/theme/shadow-surfaces.test.ts +72 -33
  66. package/src/tokens/index.ts +4 -0
  67. package/src/tokens/palette.ts +31 -0
  68. package/src/vite.ts +62 -1
@@ -147,6 +147,10 @@ const basaltVariantColorResolver = (input) => {
147
147
  const autoContrast = input.autoContrast ?? input.theme.autoContrast;
148
148
  return autoContrast ? { ...resolved, color: on } : resolved;
149
149
  }
150
+ if (input.variant === "subtle") {
151
+ const isDefaultColor = input.color === void 0 || input.color === input.theme.primaryColor;
152
+ return isDefaultColor ? { ...resolved, hover: `color-mix(in srgb, var(--vx-ink) 6%, transparent)` } : resolved;
153
+ }
150
154
  if (input.variant !== "light") return resolved;
151
155
  const ink = `var(--mantine-color-${colorName}-light-color)`;
152
156
  return {
@@ -295,17 +299,26 @@ function buildTheme(data, options = {}) {
295
299
  }
296
300
  }),
297
301
  // "Ink earns its color" — a nav selection is UI state, not the identity accent on the LABEL.
298
- // The active item is panel bg + `shadow-card` (forced here at the THEME level via NavLink's
299
- // `--nl-*` vars, so it holds for every render path — including a consumer's router `<Link>`
300
- // via `renderNavLink`, which never sees the shell CSS module); the active ICON is
301
- // accent-colored via `nav-link.module.css` (targets the `[data-position='left']` leftSection,
302
- // which the flat `vars`/`styles` API can't express conditionally on `[data-active]`).
302
+ // The active item is a SELECTED ROW, not a raised control: the same ink-tint idiom as the
303
+ // hover state (one step stronger, ink-9%), no panel fill and no `shadow-card` — a raised,
304
+ // panel-filled row read as a button sitting in the nav rather than as the current location.
305
+ // Selection is carried by the tint + ink text + weight 600 + the accent icon instead. Forced
306
+ // here at the THEME level via NavLink's `--nl-*` vars so it holds for every render path —
307
+ // including a consumer's router `<Link>` via `renderNavLink`, which never sees the shell CSS
308
+ // module; the active ICON is accent-colored via `nav-link.module.css` (targets the
309
+ // `[data-position='left']` leftSection, which the flat `vars`/`styles` API can't express
310
+ // conditionally on `[data-active]`).
311
+ //
312
+ // `--nl-hover` (ink-13%) only ever reaches an ACTIVE row in practice — the inactive hover is
313
+ // set at ink-6% by higher-specificity `:not([data-active])` rules in `nav-link.module.css` /
314
+ // `app-sidebar.module.css` — so it exists to keep the active row responding to the pointer
315
+ // now that its rest state sits in the same tint family as an inactive hover.
303
316
  NavLink: NavLink.extend({
304
317
  vars: () => ({
305
318
  root: {
306
- "--nl-bg": "var(--vx-surface-panel)",
319
+ "--nl-bg": "color-mix(in srgb, var(--vx-ink) 9%, transparent)",
307
320
  "--nl-color": "var(--vx-ink)",
308
- "--nl-hover": "color-mix(in srgb, var(--vx-ink) 6%, transparent)"
321
+ "--nl-hover": "color-mix(in srgb, var(--vx-ink) 13%, transparent)"
309
322
  },
310
323
  children: {}
311
324
  }),
@@ -339,7 +352,7 @@ function buildTheme(data, options = {}) {
339
352
  label: { fontSize: VX.text.md, lineHeight: "var(--vx-space-row-line-height)" }
340
353
  }
341
354
  }),
342
- // Field idiom (docs/DESIGN-SPEC.md §5): field surface + `shadow-card` depth + faint placeholder,
355
+ // Field idiom (docs/DESIGN-SPEC.md §5): field surface + `shadow-raised` depth + faint placeholder,
343
356
  // accent border + subtle accent ring on focus — see controls.module.css. The two slots carry
344
357
  // different halves of it: the `wrapper` is where Mantine declares the `--input-*` vars, the
345
358
  // `input` is the box that actually paints the surface (bg + radius) and therefore the only
@@ -391,16 +404,19 @@ function buildTheme(data, options = {}) {
391
404
  PillsInput: PillsInput.extend({
392
405
  classNames: { wrapper: controlsClasses.inputWrapper, input: controlsClasses.input }
393
406
  }),
394
- // Button / ActionIcon (`default` variant): same `shadow-card` depth as every other control
395
- // surface (docs/DESIGN-SPEC.md §5: "search trigger … panel + shadow-card", "icon button …
396
- // panel + shadow-card"; doctrine inversion #1). Background already resolves through the shared
407
+ // Button / ActionIcon: depth for EVERY variant that owns a box (docs/DESIGN-SPEC.md §5's
408
+ // Button idiom; doctrine inversion #1) — `default`/`filled`/`light` on the shared
409
+ // `shadow-raised`, `outline` on ring-free `shadow-ctrl`, `subtle` flat at rest. Emphasis stays
410
+ // in fill weight,
411
+ // never in z-height. Background for `default` already resolves through the shared
397
412
  // `--mantine-color-default` var (→ `--vx-surface-panel`, see cssVariablesResolver below) and
398
- // the border through `--mantine-color-default-border` (→ transparent, same resolver) — neither
413
+ // its border through `--mantine-color-default-border` (→ transparent, same resolver) — neither
399
414
  // component had a `.extend()` block at all before this, which is why they never picked up
400
- // either. Only the shadow needs adding here, in controls.module.css: Mantine's own
401
- // `[data-variant='default']` rule never declares a `box-shadow`, so no specificity fight is
402
- // needed to win one. `filled`/`subtle`/`light` variants are untouched — scoped by attribute
403
- // selector in the CSS, not here.
415
+ // either. Everything else — the per-variant depth and the disabled/reduced-motion grounding —
416
+ // lives in controls.module.css, where the variant scoping
417
+ // is an attribute selector: Mantine declares no `box-shadow` on either component at any
418
+ // specificity, so none of it needs a specificity fight. Nothing about depth is expressible in
419
+ // the flat `styles` objects here, which is the whole reason that file exists.
404
420
  //
405
421
  // Density coupling: mirrors Input's `--input-height` override above — Mantine's OWN
406
422
  // `--button-height-md`/`--ai-size` (via `--ai-size-md`) are global, non-retunable statics
@@ -439,7 +455,8 @@ function buildTheme(data, options = {}) {
439
455
  // real attribute selectors ((0,2,0)) — Mantine's own rule wraps the same attribute in `:where()`
440
456
  // (0 specificity, landing at (0,1,0) total), so no `html[…]` prefix fight is needed here either.
441
457
  // Neither component sets a background of its own, so panel bg is added alongside the shadow —
442
- // the same panel-bg + shadow-card + no-border triad as Card/Paper.
458
+ // the same panel-bg + depth + no-border triad as Card/Paper — on `shadow-raised`, the shared
459
+ // CONTROL depth, not Card/Paper's `shadow-card`: a selection card is something you click.
443
460
  CheckboxCard: CheckboxCard.extend({
444
461
  classNames: { card: controlsClasses.checkboxCardRoot }
445
462
  }),
@@ -780,8 +797,8 @@ function buildCssVariablesResolver(data) {
780
797
  "--mantine-color-default": `var(--vx-surface-panel, ${SURFACE.panel.light})`,
781
798
  // cards / default controls
782
799
  "--mantine-color-default-hover": `var(--vx-surface-elevated, ${SURFACE.elevated.light})`,
783
- // THE central lever for doctrine inversion #1 on CONTROLS (docs/DESIGN-SPEC.md §8): depth is
784
- // `shadow-card`, never a hairline, for every `variant="default"` control — not just Card/Paper.
800
+ // THE central lever for doctrine inversion #1 on CONTROLS (docs/DESIGN-SPEC.md §8): depth is a
801
+ // shadow, never a hairline, for every `variant="default"` control — not just Card/Paper.
785
802
  // `defaultVariantColorsResolver`'s `variant === 'default'` branch reads this ONE var for every
786
803
  // component that uses it (Button, ActionIcon, Chip, CheckboxCard, RadioCard, …), so pointing it
787
804
  // at `transparent` kills the stock 1px hairline everywhere at once. `transparent`, not `border:
@@ -1,19 +1,19 @@
1
1
  /*
2
- * Active nav row = heavier weight + card depth, applied at the THEME level via `classNames` so it
3
- * reaches EVERY NavLink render path — including a consumer's router `<Link>` mounted through
4
- * `renderNavLink`, which a shell-scoped CSS module never sees. Mantine's `styles` prop only takes
5
- * flat inline properties (a nested `&[data-active]` there throws a React warning), so the state
6
- * selectors live here. React Router's NavLink marks the active item via `aria-current` rather than
7
- * the `active` prop's `data-active`, so both states are matched.
2
+ * Active nav row = heavier weight over the ink tint `--nl-bg` carries (set in theme/index.ts,
3
+ * applied unconditionally by Mantine's own `[data-active]` CSS). NO depth: an active row is a
4
+ * SELECTED ROW, not a raised control — it deliberately reads as a stronger version of the hover
5
+ * tint rather than as a button, so the row never competes with the real filled controls around it.
8
6
  *
9
- * The row's background comes from `--nl-bg` (set in theme/index.ts, applies unconditionally via
10
- * Mantine's own `[data-active]` CSS) — `shadow-card` is added here because there's no `--nl-*` var
11
- * for depth.
7
+ * Applied at the THEME level via `classNames` so it reaches EVERY NavLink render path — including a
8
+ * consumer's router `<Link>` mounted through `renderNavLink`, which a shell-scoped CSS module never
9
+ * sees. Mantine's `styles` prop only takes flat inline properties (a nested `&[data-active]` there
10
+ * throws a React warning), so the state selectors live here. React Router's NavLink marks the
11
+ * active item via `aria-current` rather than the `active` prop's `data-active`, so both states are
12
+ * matched.
12
13
  */
13
14
  .root[data-active],
14
15
  .root[aria-current='page'] {
15
16
  font-weight: 600;
16
- box-shadow: var(--vx-shadow-card);
17
17
  }
18
18
 
19
19
  /*
@@ -86,6 +86,7 @@ export declare const VX: {
86
86
  readonly divider: "var(--vx-divider)";
87
87
  readonly shadowCard: "var(--vx-shadow-card)";
88
88
  readonly shadowCtrl: "var(--vx-shadow-ctrl)";
89
+ readonly shadowRaised: "var(--vx-shadow-raised)";
89
90
  readonly shadowOverlay: "var(--vx-shadow-overlay)";
90
91
  readonly surface: {
91
92
  readonly bg: "var(--vx-surface-bg)";
@@ -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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"}
@@ -119,6 +119,9 @@ const VX = {
119
119
  // Depth shadows — a whisper shadow + a 1px ring baked into the value (never a `border` prop).
120
120
  shadowCard: "var(--vx-shadow-card)",
121
121
  shadowCtrl: "var(--vx-shadow-ctrl)",
122
+ // Raised interactive controls — drop + a 1px INSET edge, the one depth every raised Button/
123
+ // ActionIcon variant shares (works over a fill, a tint or a panel alike; see SHADOW.raised).
124
+ shadowRaised: "var(--vx-shadow-raised)",
122
125
  // Floating-layer elevation (menus, popovers, tooltips, modals, drawers) — a real drop shadow.
123
126
  shadowOverlay: "var(--vx-shadow-overlay)",
124
127
  // App surfaces (cards, borders) — shared with the Mantine chrome
@@ -218,6 +221,7 @@ function frameworkPrimitives(side, data, legacyAliases) {
218
221
  decl("divider", su.divider[side]),
219
222
  decl("shadow-card", SHADOW.card[side]),
220
223
  decl("shadow-ctrl", SHADOW.ctrl[side]),
224
+ decl("shadow-raised", SHADOW.raised[side]),
221
225
  decl("shadow-overlay", SHADOW.overlay[side]),
222
226
  decl("surface-bg", su.bg[side]),
223
227
  decl("surface-panel", su.panel[side]),
@@ -84,6 +84,37 @@ export declare const SHADOW: {
84
84
  readonly light: "0 1px 2px rgba(28,25,23,0.12)";
85
85
  readonly dark: "0 1px 2px rgba(0,0,0,0.35)";
86
86
  };
87
+ /**
88
+ * Interactive-control depth — the ONE value every raised Button/ActionIcon/Input variant shares
89
+ * (`docs/DESIGN-SPEC.md` §5's Button idiom). Exists rather than reusing `card` because `card`'s
90
+ * ring is OUTSET, and an outset ring paints in the color of whatever it is drawn over: fine
91
+ * between neutral panels, but on light scheme it resolves to `--vx-surface-hairline` and draws a
92
+ * pale grey outline around a saturated `filled` button, which reads as a rendering defect.
93
+ *
94
+ * THE TWO SCHEMES ARE SHAPED DIFFERENTLY, and the asymmetry is the whole design — do NOT "tidy"
95
+ * them into one shape. Both were arrived at by elimination, on screen:
96
+ *
97
+ * - DARK = drop + a full inset rim. Its edge is a LIGHTENING, so it cannot collide with the
98
+ * dark drop below the box; a uniform lightening on a dark surface reads as rim light wrapping
99
+ * a raised object, which is exactly right.
100
+ *
101
+ * - LIGHT = drop ONLY, no edge at all. A light-mode edge would have to be a DARKENING (there is
102
+ * nothing lighter than a near-white control to lighten with), and every position for one
103
+ * fails: a uniform dark ring does not read as depth, it reads as a BORDER — it made `default`
104
+ * and `light` look like outlined buttons and every form field look boxed. Moving it to the
105
+ * bottom edge alone then put a dark inner line directly above the dark drop, i.e. a visible
106
+ * DOUBLE BOTTOM BORDER. There is no third position, because a light-mode raised object's only
107
+ * honest cue is the shadow it casts. So light casts one and draws nothing.
108
+ *
109
+ * Consequence to know, on light only: a `default` control nested in a Card shares
110
+ * `--vx-surface-panel` with it and has no boundary but the drop. That is accepted — a wrong-
111
+ * looking border on every control in the app is worse than a soft edge in one nesting case. If it
112
+ * ever needs more, raise the DROP. Do not reintroduce an edge; both shapes have been tried.
113
+ */
114
+ readonly raised: {
115
+ readonly light: "0 1px 2px rgba(28,25,23,0.10)";
116
+ readonly dark: "0 1px 2px rgba(0,0,0,0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent)";
117
+ };
87
118
  readonly overlay: {
88
119
  readonly light: "0 8px 24px rgba(28,25,23,0.10)";
89
120
  readonly dark: "0 8px 24px rgba(0,0,0,0.5)";
@@ -1 +1 @@
1
- {"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../../src/tokens/palette.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../../src/tokens/palette.ts"],"names":[],"mappings":"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"}
@@ -44,6 +44,37 @@ const SHADOW = {
44
44
  light: "0 1px 2px rgba(28,25,23,0.12)",
45
45
  dark: "0 1px 2px rgba(0,0,0,0.35)"
46
46
  },
47
+ /**
48
+ * Interactive-control depth — the ONE value every raised Button/ActionIcon/Input variant shares
49
+ * (`docs/DESIGN-SPEC.md` §5's Button idiom). Exists rather than reusing `card` because `card`'s
50
+ * ring is OUTSET, and an outset ring paints in the color of whatever it is drawn over: fine
51
+ * between neutral panels, but on light scheme it resolves to `--vx-surface-hairline` and draws a
52
+ * pale grey outline around a saturated `filled` button, which reads as a rendering defect.
53
+ *
54
+ * THE TWO SCHEMES ARE SHAPED DIFFERENTLY, and the asymmetry is the whole design — do NOT "tidy"
55
+ * them into one shape. Both were arrived at by elimination, on screen:
56
+ *
57
+ * - DARK = drop + a full inset rim. Its edge is a LIGHTENING, so it cannot collide with the
58
+ * dark drop below the box; a uniform lightening on a dark surface reads as rim light wrapping
59
+ * a raised object, which is exactly right.
60
+ *
61
+ * - LIGHT = drop ONLY, no edge at all. A light-mode edge would have to be a DARKENING (there is
62
+ * nothing lighter than a near-white control to lighten with), and every position for one
63
+ * fails: a uniform dark ring does not read as depth, it reads as a BORDER — it made `default`
64
+ * and `light` look like outlined buttons and every form field look boxed. Moving it to the
65
+ * bottom edge alone then put a dark inner line directly above the dark drop, i.e. a visible
66
+ * DOUBLE BOTTOM BORDER. There is no third position, because a light-mode raised object's only
67
+ * honest cue is the shadow it casts. So light casts one and draws nothing.
68
+ *
69
+ * Consequence to know, on light only: a `default` control nested in a Card shares
70
+ * `--vx-surface-panel` with it and has no boundary but the drop. That is accepted — a wrong-
71
+ * looking border on every control in the app is worse than a soft edge in one nesting case. If it
72
+ * ever needs more, raise the DROP. Do not reintroduce an edge; both shapes have been tried.
73
+ */
74
+ raised: {
75
+ light: "0 1px 2px rgba(28,25,23,0.10)",
76
+ dark: "0 1px 2px rgba(0,0,0,0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent)"
77
+ },
47
78
  // Floating-layer elevation (menus, popovers, tooltips, modals, drawers) — a REAL drop shadow, a
48
79
  // step above `card`, so detached surfaces read as lifted off the page (`docs/DESIGN-SPEC.md` §5).
49
80
  // Unlike `card`, no embedded ring: floating surfaces carry a real 1px `--vx-surface-border` so
package/dist/tokens.css CHANGED
@@ -206,6 +206,7 @@ html[data-mantine-color-scheme='dark'] {
206
206
  --vx-divider: color-mix(in srgb, #ffffff 6%, transparent);
207
207
  --vx-shadow-card: 0 1px 3px rgba(0,0,0,0.4), inset 0 0 0 1px color-mix(in srgb, #ffffff 4%, transparent);
208
208
  --vx-shadow-ctrl: 0 1px 2px rgba(0,0,0,0.35);
209
+ --vx-shadow-raised: 0 1px 2px rgba(0,0,0,0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent);
209
210
  --vx-shadow-overlay: 0 8px 24px rgba(0,0,0,0.5);
210
211
  --vx-surface-bg: #27272a;
211
212
  --vx-surface-panel: #2b2b2e;
@@ -269,6 +270,7 @@ html[data-mantine-color-scheme='light'] {
269
270
  --vx-divider: color-mix(in srgb, #e5e5e5 65%, transparent);
270
271
  --vx-shadow-card: 0 1px 2px rgba(28,25,23,0.05), 0 0 0 1px var(--vx-surface-hairline);
271
272
  --vx-shadow-ctrl: 0 1px 2px rgba(28,25,23,0.12);
273
+ --vx-shadow-raised: 0 1px 2px rgba(28,25,23,0.10);
272
274
  --vx-shadow-overlay: 0 8px 24px rgba(28,25,23,0.10);
273
275
  --vx-surface-bg: #f2f2f5;
274
276
  --vx-surface-panel: #fafafd;
package/dist/vite.d.ts CHANGED
@@ -25,6 +25,12 @@ export type BasaltViteOptions = {
25
25
  basaltSrc?: string;
26
26
  /** App version surfaced via the `__APP_VERSION__` define. Defaults to `0.0.0`. */
27
27
  version?: string;
28
+ /**
29
+ * Print a one-time notice when `basalt-ui init` has never been run in this project (no
30
+ * `.basalt/manifest.json` found at or above the cwd) — see `warnIfUnenforced`. Default `true`.
31
+ * Set `false` to adopt the components without the toolchain and stop being told about it.
32
+ */
33
+ enforcementNotice?: boolean;
28
34
  };
29
35
  export declare function basaltViteConfig(opts: BasaltViteOptions): UserConfig;
30
36
  export type BasaltAppOptions = {
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAA6B,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAI/E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,UAAU,CAyEpE;AAgBD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrD,mGAAmG;IACnG,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wDAAwD;IACxD,OAAO,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAA;IAChE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mGAAmG;IACnG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,wFAAwF;IACxF,KAAK,CAAC,EAAE,KAAK,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC3B,oFAAoF;IACpF,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,SAAS,GAAG,qBAAqB,CAAA;KAAE,CAAA;IACvF,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,KAAK,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClD,CAAA;AAuZD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAMzE"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAA6B,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAI/E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AA6CD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,UAAU,CAgFpE;AAgBD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrD,mGAAmG;IACnG,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wDAAwD;IACxD,OAAO,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAA;IAChE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mGAAmG;IACnG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,wFAAwF;IACxF,KAAK,CAAC,EAAE,KAAK,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC3B,oFAAoF;IACpF,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,SAAS,GAAG,qBAAqB,CAAA;KAAE,CAAA;IACvF,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,KAAK,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClD,CAAA;AAuZD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAMzE"}
package/dist/vite.js CHANGED
@@ -1,10 +1,30 @@
1
- import { resolve } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
2
3
  import { searchForWorkspaceRoot } from "vite";
3
4
  import { SURFACE } from "./tokens/palette.js";
4
5
  import { resolveColorMix } from "./vite-color-mix.js";
6
+ const UNENFORCED_SEARCH_DEPTH = 3;
7
+ let unenforcedNoticePrinted = false;
8
+ function warnIfUnenforced(cwd) {
9
+ if (unenforcedNoticePrinted) return;
10
+ let dir = cwd;
11
+ for (let i = 0; i <= UNENFORCED_SEARCH_DEPTH; i++) {
12
+ if (existsSync(resolve(dir, ".basalt/manifest.json"))) return;
13
+ const parent = dirname(dir);
14
+ if (parent === dir) break;
15
+ dir = parent;
16
+ }
17
+ unenforcedNoticePrinted = true;
18
+ console.warn(
19
+ "\n[basalt-ui] Installed, but `basalt-ui init` has never run here \u2014 no oxlint preset, no theme\n guard, no agent rules. You have the components and none of the enforcement.\n Fix: `bunx basalt-ui init`, then add `oxlint . && basalt-ui check-theme` as your\n lint script. Silence: pass `enforcementNotice: false` to basaltViteConfig().\n"
20
+ );
21
+ }
5
22
  function basaltViteConfig(opts) {
6
23
  const { port, host, allowedHosts, apiTarget, version = "0.0.0" } = opts;
7
24
  const basaltSrc = opts.basaltSrc ?? process.env["BASALT_LOCAL"];
25
+ if (opts.enforcementNotice !== false && basaltSrc === void 0) {
26
+ warnIfUnenforced(process.cwd());
27
+ }
8
28
  const config = {
9
29
  define: {
10
30
  __APP_VERSION__: JSON.stringify(process.env["BUILD_VERSION"] ?? version),
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.5.0
1
+ # basalt-ui v1.7.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,11 +19,11 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.5.0
22
+ Version: 1.7.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
26
- Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard, EmptyState, SettingsSection/SettingsRow/DangerZone)
26
+ Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, SettingsSection/SettingsRow/DangerZone)
27
27
  Layer: mantine-coupled
28
28
  Rule: basalt-mantine
29
29
  OptionalPeers: react-markdown@^10.1.0, remark-gfm@^4.0.1, shiki@^4.3.0, beautiful-mermaid@^1.1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -81,7 +81,8 @@ const LazyStickToBottom = lazy(() =>
81
81
  border: 'none',
82
82
  cursor: 'pointer',
83
83
  backgroundColor: 'var(--vx-surface-panel)',
84
- boxShadow: 'var(--vx-shadow-card)',
84
+ // Control depth, not panel depth — this is a real <button> (DESIGN-SPEC §2).
85
+ boxShadow: 'var(--vx-shadow-raised)',
85
86
  color: 'var(--vx-faint)',
86
87
  fontFamily: 'var(--basalt-font-mono)',
87
88
  fontSize: VX.text.md,
@@ -76,13 +76,14 @@ export function Composer({
76
76
  disabled,
77
77
  onChange: (event) => setValue(event.currentTarget.value),
78
78
  onKeyDown: handleKeyDown,
79
- // Composer input surface (docs/DESIGN-SPEC.md §5): panel + shadow-card, control radius (6px,
80
- // VX.radiusCtrl) — the ring
81
- // lives in the shadow, so the input carries no separate border.
79
+ // Composer input surface (docs/DESIGN-SPEC.md §5): panel + control radius (6px, VX.radiusCtrl);
80
+ // the ring lives in the shadow, so the input carries no separate border. Depth is deliberately
81
+ // NOT set here — it arrives from the themed `.input` class (theme/controls.module.css), which
82
+ // also grounds it when disabled. A `boxShadow` in this `styles` object renders as an INLINE
83
+ // style and would beat that class unconditionally, leaving a disabled composer raised.
82
84
  styles: {
83
85
  input: {
84
86
  backgroundColor: 'var(--vx-surface-panel)',
85
- boxShadow: 'var(--vx-shadow-card)',
86
87
  borderRadius: VX.radiusCtrl,
87
88
  border: 'none',
88
89
  },
package/src/cli/index.ts CHANGED
@@ -80,6 +80,15 @@ export type BasaltConfig = {
80
80
  * surface tokens. Default: `true` (ON). Set `false` to disable the `off-system-surface-var` check.
81
81
  */
82
82
  offSystemSurfaceVar?: boolean
83
+ /**
84
+ * Flag a SHADE-PINNED Mantine color — `c="yellow.7"`, `bg="blue.4"`,
85
+ * `var(--mantine-color-red-6)`. A pinned step is one fixed swatch in BOTH color schemes, so it
86
+ * cannot stay legible in either; route a verdict color through `VX.status.*` / `--vx-status-*`,
87
+ * or drop the index (`c="red"`) and let the theme resolve the shade per scheme. `gray-*`/`dark-*`
88
+ * in `var()` form belong to `off-system-surface-var` instead. Default: `true` (ON). Set `false`
89
+ * to disable the `mantine-shade-index` check.
90
+ */
91
+ mantineShadeIndex?: boolean
83
92
  /**
84
93
  * Flag raw lowercase JSX layout/surface elements (`div`/`span`/`section`/…) carrying an inline
85
94
  * `style={{}}` with a layout/surface property — steer to a Mantine layout primitive
@@ -348,6 +357,7 @@ export function checkTheme(cwd: string = process.cwd()): number {
348
357
  spacingSteps: cfg.spacingSteps ?? DEFAULT_GUARD_CONFIG.spacingSteps,
349
358
  rawRadius: cfg.rawRadius ?? DEFAULT_GUARD_CONFIG.rawRadius,
350
359
  forbiddenAccents: cfg.forbiddenAccents ?? DEFAULT_GUARD_CONFIG.forbiddenAccents,
360
+ mantineShadeIndex: cfg.mantineShadeIndex ?? DEFAULT_GUARD_CONFIG.mantineShadeIndex,
351
361
  rawSurface: cfg.rawSurface ?? DEFAULT_GUARD_CONFIG.rawSurface,
352
362
  cardWithBorder: cfg.cardWithBorder ?? DEFAULT_GUARD_CONFIG.cardWithBorder,
353
363
  offSystemSurfaceVar: cfg.offSystemSurfaceVar ?? DEFAULT_GUARD_CONFIG.offSystemSurfaceVar,
@@ -1827,6 +1837,7 @@ export async function guardHook(cwd: string = process.cwd()): Promise<number> {
1827
1837
  spacingSteps: cfg.spacingSteps ?? DEFAULT_GUARD_CONFIG.spacingSteps,
1828
1838
  rawRadius: cfg.rawRadius ?? DEFAULT_GUARD_CONFIG.rawRadius,
1829
1839
  forbiddenAccents: cfg.forbiddenAccents ?? DEFAULT_GUARD_CONFIG.forbiddenAccents,
1840
+ mantineShadeIndex: cfg.mantineShadeIndex ?? DEFAULT_GUARD_CONFIG.mantineShadeIndex,
1830
1841
  rawSurface: cfg.rawSurface ?? DEFAULT_GUARD_CONFIG.rawSurface,
1831
1842
  cardWithBorder: cfg.cardWithBorder ?? DEFAULT_GUARD_CONFIG.cardWithBorder,
1832
1843
  offSystemSurfaceVar: cfg.offSystemSurfaceVar ?? DEFAULT_GUARD_CONFIG.offSystemSurfaceVar,
@@ -1,5 +1,5 @@
1
1
  export { DeltaBadge, type DeltaBadgeProps } from './delta-badge'
2
- export { StatCard, type StatCardProps } from './stat-card'
2
+ export { StatCard, type StatCardProps, type StatCardTone } from './stat-card'
3
3
  export { EmptyState, type EmptyStateProps } from './empty-state'
4
4
  export {
5
5
  SettingsSection,
@@ -8,6 +8,15 @@
8
8
  * `src/dashboard` stays @visx-free — `sparkline` is a plain `ReactNode` slot, never a chart import
9
9
  * here. Pass a `LineSparkline`/`BarSparkline` from `basalt-ui/charts` at the call site.
10
10
  *
11
+ * `tone` marks the card as past a threshold with an accent rail down its leading edge. It exists
12
+ * because `value` is typed `string`: without it, a card whose number has crossed a threshold reads
13
+ * exactly as calm as one that hasn't, and the only way out from the consumer side is to wrap the
14
+ * card in a hand-rolled positioned `<Box>` — which is a second card idiom in the app, drawn by code
15
+ * the guard has no way to recognize as one. The rail is the largest signal available without
16
+ * touching the value's own type: full card height, overlaying the card's edge rather than adding to
17
+ * it, so layout is unchanged. `undefined` renders nothing at all — absence of a reading is neither a
18
+ * good one nor a bad one, and must never be tinted.
19
+ *
11
20
  * @example
12
21
  * import { StatCard } from 'basalt-ui'
13
22
  * import { LineSparkline } from 'basalt-ui/charts'
@@ -18,12 +27,28 @@
18
27
  * delta={4.2}
19
28
  * sparkline={<LineSparkline data={history} width={160} height={32} />}
20
29
  * />
30
+ *
31
+ * @example
32
+ * // Past a threshold — an accent rail down the leading edge, value untouched.
33
+ * <StatCard label="Downtime" value="29 min" tone="bad" />
21
34
  */
22
- import { Card, Group } from '@mantine/core'
35
+ import { Box, Card, Group, VisuallyHidden } from '@mantine/core'
23
36
  import type { ReactNode } from 'react'
24
37
  import { DeltaBadge } from './delta-badge'
25
38
  import { VX } from '../tokens'
26
39
 
40
+ /** A threshold verdict on the card's value. `undefined` is never tinted — see the module docblock. */
41
+ export type StatCardTone = 'warn' | 'bad'
42
+
43
+ /** The rail's width, in px. Wide enough to read as a deliberate mark at a glance, narrow enough
44
+ * that it cannot be mistaken for the card's own edge. */
45
+ const TONE_RAIL_WIDTH = 3
46
+
47
+ const TONE_LABEL: Record<StatCardTone, string> = {
48
+ warn: 'Past the warning threshold',
49
+ bad: 'Past the severe threshold',
50
+ }
51
+
27
52
  export type StatCardProps = {
28
53
  /** Mono uppercase micro-label (docs/DESIGN-SPEC.md §3: 12.5px, tracking 0.06em, faint). */
29
54
  label: string
@@ -38,9 +63,20 @@ export type StatCardProps = {
38
63
  sparkline?: ReactNode
39
64
  /** Optional header-right slot (e.g. a ghost "..." menu trigger) — consumer-owned. */
40
65
  menu?: ReactNode
66
+ /** Threshold verdict — draws an accent rail down the card's leading edge and announces itself to
67
+ * assistive tech. Omit it for a reading that is fine, and for one that is absent. */
68
+ tone?: StatCardTone
41
69
  }
42
70
 
43
- export function StatCard({ label, value, delta, deltaPeriod, sparkline, menu }: StatCardProps) {
71
+ export function StatCard({
72
+ label,
73
+ value,
74
+ delta,
75
+ deltaPeriod,
76
+ sparkline,
77
+ menu,
78
+ tone,
79
+ }: StatCardProps) {
44
80
  return (
45
81
  <Card
46
82
  style={{
@@ -56,8 +92,31 @@ export function StatCard({ label, value, delta, deltaPeriod, sparkline, menu }:
56
92
  flexDirection: 'column',
57
93
  gap: 'var(--vx-space-stat-card-gap)',
58
94
  overflow: 'hidden',
95
+ // Anchors the tone rail. Set unconditionally so a card's stacking context does not change
96
+ // depending on whether it happens to have crossed a threshold this render.
97
+ position: 'relative',
59
98
  }}
99
+ data-tone={tone}
60
100
  >
101
+ {tone !== undefined && (
102
+ <>
103
+ {/* The rail is decoration; the verdict it encodes is real information, so it is also
104
+ stated in text for anyone who cannot see a 3px colour bar. Colour alone is never the
105
+ only carrier of a threshold. */}
106
+ <VisuallyHidden>{TONE_LABEL[tone]}</VisuallyHidden>
107
+ <Box
108
+ aria-hidden="true"
109
+ style={{
110
+ position: 'absolute',
111
+ insetBlock: 0,
112
+ insetInlineStart: 0,
113
+ width: TONE_RAIL_WIDTH,
114
+ background: tone === 'bad' ? VX.status.bad : VX.status.warn,
115
+ }}
116
+ />
117
+ </>
118
+ )}
119
+
61
120
  <Group justify="space-between" align="flex-start" wrap="nowrap" gap="xs">
62
121
  <span
63
122
  style={{