basalt-ui 1.21.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/AGENTS.md +11 -1
  2. package/CHANGELOG.md +33 -0
  3. package/MIGRATING.md +205 -9
  4. package/README.md +63 -16
  5. package/agent/rules/basalt-app.md +39 -9
  6. package/agent/rules/basalt-charts.md +41 -6
  7. package/agent/rules/basalt-router.md +1 -1
  8. package/agent/rules/basalt-state.md +4 -4
  9. package/agent/rules/basalt-tokens.md +25 -4
  10. package/agent/skills/basalt-app/SKILL.md +4 -3
  11. package/agent/skills/basalt-charts/SKILL.md +23 -8
  12. package/agent/templates/DESIGN.md.tpl +3 -1
  13. package/configs/oxlint-plugin.js +86 -9
  14. package/configs/oxlint-plugin.test.ts +173 -0
  15. package/dist/charts/hooks/useBandPlot.d.ts +138 -0
  16. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
  17. package/dist/charts/hooks/useBandPlot.js +127 -0
  18. package/dist/charts/index.d.ts +4 -0
  19. package/dist/charts/index.d.ts.map +1 -1
  20. package/dist/charts/index.js +14 -0
  21. package/dist/charts/kinds/BandStrip.d.ts +113 -0
  22. package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
  23. package/dist/charts/kinds/BandStrip.js +218 -0
  24. package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
  25. package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
  26. package/dist/charts/kinds/Bars.d.ts +5 -1
  27. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  28. package/dist/charts/kinds/Bars.js +2 -0
  29. package/dist/charts/kinds/MirroredBars.d.ts +93 -0
  30. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
  31. package/dist/charts/kinds/MirroredBars.js +315 -0
  32. package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
  33. package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
  34. package/dist/charts/kinds/MultiLine.d.ts +5 -1
  35. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  36. package/dist/charts/kinds/MultiLine.js +2 -0
  37. package/dist/charts/kinds/StackedArea.d.ts +5 -1
  38. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  39. package/dist/charts/kinds/StackedArea.js +2 -0
  40. package/dist/charts/kinds/ZonedLine.d.ts +5 -1
  41. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  42. package/dist/charts/kinds/ZonedLine.js +2 -0
  43. package/dist/charts/primitives/CartesianChart.d.ts +18 -2
  44. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  45. package/dist/charts/primitives/CartesianChart.js +5 -2
  46. package/dist/charts/primitives/HatchPattern.d.ts +31 -0
  47. package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
  48. package/dist/charts/primitives/HatchPattern.js +31 -0
  49. package/dist/cli/index.d.ts +9 -1
  50. package/dist/cli/index.d.ts.map +1 -1
  51. package/dist/cli/index.js +275 -48
  52. package/dist/cli/shipped-versions.test.d.ts +2 -0
  53. package/dist/cli/shipped-versions.test.d.ts.map +1 -0
  54. package/dist/guard/index.d.ts +67 -0
  55. package/dist/guard/index.d.ts.map +1 -1
  56. package/dist/guard/index.js +70 -11
  57. package/dist/vite.d.ts +44 -2
  58. package/dist/vite.d.ts.map +1 -1
  59. package/dist/vite.js +71 -26
  60. package/llms.txt +2 -2
  61. package/package.json +1 -1
  62. package/src/charts/hooks/useBandPlot.ts +302 -0
  63. package/src/charts/index.ts +14 -0
  64. package/src/charts/kinds/BandStrip.test.tsx +200 -0
  65. package/src/charts/kinds/BandStrip.tsx +364 -0
  66. package/src/charts/kinds/Bars.tsx +7 -1
  67. package/src/charts/kinds/DualPanel.test.tsx +10 -4
  68. package/src/charts/kinds/MirroredBars.test.tsx +193 -0
  69. package/src/charts/kinds/MirroredBars.tsx +455 -0
  70. package/src/charts/kinds/MultiLine.tsx +7 -1
  71. package/src/charts/kinds/StackedArea.tsx +7 -1
  72. package/src/charts/kinds/ZonedLine.tsx +7 -1
  73. package/src/charts/primitives/CartesianChart.test.tsx +86 -4
  74. package/src/charts/primitives/CartesianChart.tsx +28 -3
  75. package/src/charts/primitives/HatchPattern.tsx +55 -0
  76. package/src/cli/check-theme.test.ts +35 -0
  77. package/src/cli/help.test.ts +5 -1
  78. package/src/cli/index.ts +552 -79
  79. package/src/cli/shipped-versions.test.ts +83 -0
  80. package/src/cli/toolchain-wiring.test.ts +362 -6
  81. package/src/cli/waiver-audit.test.ts +88 -4
  82. package/src/guard/check-source.test.ts +293 -0
  83. package/src/guard/index.ts +209 -20
  84. package/src/vite.test.ts +112 -0
  85. package/src/vite.ts +147 -31
@@ -151,8 +151,24 @@ export type CartesianChartProps<T> = {
151
151
  /** Passing `y2` is what makes the chart dual-axis: it draws the right axis and widens the
152
152
  * right margin by measurement. Series opt in with `axis: 'right'`. */
153
153
  y2?: AxisConfig<T>
154
- /** Exact number of x ticks. Default: as many as fit (`smartTicks`). */
154
+ /** Exact number of x ticks. Default: as many as fit (`smartTicks`). Ignored when
155
+ * `xTickValues` is set. */
155
156
  xTicks?: number
157
+ /**
158
+ * Which domain keys get a tick, chosen from the full key list and the resolved plot width.
159
+ * Takes precedence over `xTicks`; omit both for `smartTicks`.
160
+ *
161
+ * A COUNT is not always enough. `smartTicks`/`smartTicksEvery` append the final key
162
+ * unconditionally, so when the step does not land on the last index that appended tick sits a
163
+ * partial step from its neighbour — measured on a 24h window, two `DD.MM HH:MM` labels printed
164
+ * on top of each other at the right edge, at every tick count. The only fix a count can express
165
+ * is thinning the whole axis to avoid one crowded pair. A consumer whose labels are richer than
166
+ * the default `DD.MM` needs to pick the VALUES, which until now meant measuring the container
167
+ * itself with `useChartSize` to derive a count — the chart already knows its own width.
168
+ *
169
+ * `BandStrip`/`MirroredBars` take the identical prop, so the seam does not fork by kind.
170
+ */
171
+ xTickValues?: (keys: readonly string[], xMax: number) => readonly string[]
156
172
  /** X tick label formatter. Default `fmtAxisDate` (DD.MM). */
157
173
  formatX?: (key: string) => string
158
174
  /** Value-range bands on the left scale, drawn behind the marks. */
@@ -267,6 +283,7 @@ export function CartesianChart<T>({
267
283
  y,
268
284
  y2,
269
285
  xTicks,
286
+ xTickValues,
270
287
  formatX = fmtAxisDate,
271
288
  zones,
272
289
  xZones,
@@ -305,6 +322,7 @@ export function CartesianChart<T>({
305
322
  {...(y !== undefined && { y })}
306
323
  {...(y2 !== undefined && { y2 })}
307
324
  {...(xTicks !== undefined && { xTicks })}
325
+ {...(xTickValues !== undefined && { xTickValues })}
308
326
  formatX={formatX}
309
327
  {...(zones !== undefined && { zones })}
310
328
  {...(xZones !== undefined && { xZones })}
@@ -343,6 +361,7 @@ function CartesianPlot<T>({
343
361
  y,
344
362
  y2,
345
363
  xTicks,
364
+ xTickValues,
346
365
  formatX = fmtAxisDate,
347
366
  zones,
348
367
  xZones,
@@ -459,9 +478,15 @@ function CartesianPlot<T>({
459
478
  [rightDomain, yMax, y2?.nice],
460
479
  )
461
480
 
481
+ // Resolution order: explicit VALUES win, then an explicit COUNT, then as many as fit.
462
482
  const tickValues = useMemo(
463
- () => (xTicks === undefined ? smartTicks(keys, xMax) : smartTicksEvery(keys, xTicks)),
464
- [keys, xMax, xTicks],
483
+ () =>
484
+ xTickValues !== undefined
485
+ ? [...xTickValues(keys, xMax)]
486
+ : xTicks === undefined
487
+ ? smartTicks(keys, xMax)
488
+ : smartTicksEvery(keys, xTicks),
489
+ [keys, xMax, xTicks, xTickValues],
465
490
  )
466
491
 
467
492
  const cursor = useChartCursor<T>({
@@ -0,0 +1,55 @@
1
+ import type { ReactNode } from 'react'
2
+ import { alpha } from '../../tokens'
3
+
4
+ /**
5
+ * A diagonal-hatch `<pattern>` — the one fill that reads as **absence** rather than as a weak
6
+ * measurement.
7
+ *
8
+ * A faint solid tint cannot do this job: it sits on the same perceptual axis as every other
9
+ * intensity of the same hue, so "nothing was measured here" and "everything measured here was
10
+ * fine" collapse into neighbouring steps of one ramp. Hatching is off that axis entirely and
11
+ * survives being placed beside a colour ramp of the same hue.
12
+ *
13
+ * `id` must be unique per document — `BandStrip` / `MirroredBars` namespace theirs with `chartId`.
14
+ * `opacity` is applied through `alpha()` on the stroke rather than as an `opacity` attribute, so
15
+ * the hue keeps resolving per colour scheme.
16
+ */
17
+ export function HatchPattern({
18
+ id,
19
+ color,
20
+ opacity = 0.55,
21
+ size = 6,
22
+ }: {
23
+ id: string
24
+ color: string
25
+ opacity?: number
26
+ size?: number
27
+ }): ReactNode {
28
+ return (
29
+ <pattern
30
+ id={id}
31
+ width={size}
32
+ height={size}
33
+ patternUnits="userSpaceOnUse"
34
+ patternTransform="rotate(45)"
35
+ >
36
+ <line x1={0} y1={0} x2={0} y2={size} stroke={alpha(color, opacity)} strokeWidth={size / 3} />
37
+ </pattern>
38
+ )
39
+ }
40
+
41
+ /** `fill` value referencing a {@link HatchPattern} by id. */
42
+ export function hatchFill(id: string): string {
43
+ return `url(#${id})`
44
+ }
45
+
46
+ /**
47
+ * The pattern's own repeat, shrunk to fit the band it textures.
48
+ *
49
+ * Left at a fixed 6px a sub-pixel band draws less than one diagonal rule, so a fill/hatch split
50
+ * inside one band antialiases into a single smudge no reader can tell from a fully-measured band.
51
+ * Floored at 2 — below that the stroke has nothing to render on.
52
+ */
53
+ export function hatchSizeFor(bandWidth: number): number {
54
+ return Math.max(2, Math.min(6, Math.round(bandWidth)))
55
+ }
@@ -742,3 +742,38 @@ describe('severity — warnings report, errors fail', () => {
742
742
  expect(err).toContain('✖ Theme guard')
743
743
  })
744
744
  })
745
+
746
+ // ── the manifest waiver hint, gated on the profile ────────────────────────────────────────────
747
+ //
748
+ // rollhook. The hint leads with `basaltAppPlugin`, which is right for a Mantine app — a hex in a
749
+ // manifest can never be right, and the plugin removes the hand-copy entirely. A `tokens-only`
750
+ // consumer has definitionally opted out of that layer: rollhook's Astro site has no index.html to
751
+ // transform, imports no basalt JavaScript at all, and owns maskable icons the plugin does not emit.
752
+ // Leading with it there is advice the consumer cannot take.
753
+
754
+ describe('check-theme — the manifest waiver hint follows the profile', () => {
755
+ function manifestFixture(basalt: Record<string, unknown>): void {
756
+ writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ name: 'fixture', basalt }))
757
+ mkdirSync(resolve(dir, 'src'), { recursive: true })
758
+ writeFileSync(resolve(dir, 'src', 'app.tsx'), 'export const App = () => null\n')
759
+ mkdirSync(resolve(dir, 'public'), { recursive: true })
760
+ writeFileSync(
761
+ resolve(dir, 'public', 'site.webmanifest'),
762
+ JSON.stringify({ name: 'x', theme_color: '#18181b' }, null, 2),
763
+ )
764
+ }
765
+
766
+ it('offers basaltAppPlugin first to a framework consumer', () => {
767
+ manifestFixture({ roots: ['src'] })
768
+ const { err } = run()
769
+ expect(err).toContain('let basaltAppPlugin emit it')
770
+ })
771
+
772
+ it('leads with the member for a tokens-only consumer, and says which remedy it withheld', () => {
773
+ manifestFixture({ roots: ['src'], profile: 'tokens-only' })
774
+ const { err } = run()
775
+ expect(err).toContain('JSON has no comments — declare a deliberate exception')
776
+ expect(err).not.toContain('let basaltAppPlugin emit it')
777
+ expect(err).toContain('a tokens-only consumer has opted out of')
778
+ })
779
+ })
@@ -4,7 +4,7 @@
4
4
  * matched `cmd === 'sync'` first and `--help` was just another item in `flags`, ignored).
5
5
  */
6
6
  import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
7
- import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
7
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
8
8
  import { tmpdir } from 'node:os'
9
9
  import { resolve } from 'node:path'
10
10
 
@@ -86,6 +86,10 @@ describe('run() --help short-circuits before dispatch', () => {
86
86
  })
87
87
 
88
88
  it('`sync` without --help still runs for real and writes the manifest (control case)', () => {
89
+ // `sync` refuses to scaffold where nothing was ever installed, so the control case has to be a
90
+ // repo that HAS an install — which is what `sync` is for.
91
+ mkdirSync(resolve(dir, '.basalt'), { recursive: true })
92
+ writeFileSync(resolve(dir, MANIFEST_PATH), JSON.stringify({ version: 1, files: {} }))
89
93
  const { code } = capture(() => run(['sync'], dir))
90
94
  expect(code).toBe(0)
91
95
  expect(existsSync(resolve(dir, MANIFEST_PATH))).toBe(true)