basalt-ui 1.22.0 → 1.23.1

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 +29 -12
  2. package/CHANGELOG.md +28 -0
  3. package/MIGRATING.md +207 -12
  4. package/README.md +74 -21
  5. package/agent/rules/basalt-app.md +33 -3
  6. package/agent/rules/basalt-charts.md +78 -7
  7. package/agent/rules/basalt-content.md +3 -3
  8. package/agent/rules/basalt-mantine.md +4 -4
  9. package/agent/rules/basalt-tokens.md +35 -6
  10. package/agent/skills/basalt-app/SKILL.md +25 -12
  11. package/agent/skills/basalt-charts/SKILL.md +27 -10
  12. package/agent/templates/CLAUDE-block.md.tpl +3 -2
  13. package/agent/templates/DESIGN.md.tpl +3 -1
  14. package/configs/oxlint-plugin.js +49 -8
  15. package/configs/oxlint-plugin.test.ts +88 -31
  16. package/dist/charts/hooks/useBandPlot.d.ts +138 -0
  17. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
  18. package/dist/charts/hooks/useBandPlot.js +127 -0
  19. package/dist/charts/index.d.ts +4 -0
  20. package/dist/charts/index.d.ts.map +1 -1
  21. package/dist/charts/index.js +14 -0
  22. package/dist/charts/kinds/BandStrip.d.ts +127 -0
  23. package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
  24. package/dist/charts/kinds/BandStrip.js +271 -0
  25. package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
  26. package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
  27. package/dist/charts/kinds/Bars.d.ts +5 -1
  28. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  29. package/dist/charts/kinds/Bars.js +2 -0
  30. package/dist/charts/kinds/MirroredBars.d.ts +95 -0
  31. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
  32. package/dist/charts/kinds/MirroredBars.js +328 -0
  33. package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
  34. package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
  35. package/dist/charts/kinds/MultiLine.d.ts +5 -1
  36. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  37. package/dist/charts/kinds/MultiLine.js +2 -0
  38. package/dist/charts/kinds/StackedArea.d.ts +5 -1
  39. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  40. package/dist/charts/kinds/StackedArea.js +2 -0
  41. package/dist/charts/kinds/ZonedLine.d.ts +5 -1
  42. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  43. package/dist/charts/kinds/ZonedLine.js +2 -0
  44. package/dist/charts/primitives/CartesianChart.d.ts +18 -2
  45. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  46. package/dist/charts/primitives/CartesianChart.js +5 -2
  47. package/dist/charts/primitives/HatchPattern.d.ts +31 -0
  48. package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
  49. package/dist/charts/primitives/HatchPattern.js +31 -0
  50. package/dist/cli/index.d.ts +2 -0
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +228 -31
  53. package/dist/cli/shipped-versions.test.d.ts +2 -0
  54. package/dist/cli/shipped-versions.test.d.ts.map +1 -0
  55. package/dist/guard/index.d.ts.map +1 -1
  56. package/dist/guard/index.js +60 -13
  57. package/dist/vite.d.ts +42 -3
  58. package/dist/vite.d.ts.map +1 -1
  59. package/dist/vite.js +67 -30
  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 +282 -0
  65. package/src/charts/kinds/BandStrip.tsx +457 -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 +232 -0
  69. package/src/charts/kinds/MirroredBars.tsx +491 -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/help.test.ts +74 -1
  77. package/src/cli/index.ts +436 -52
  78. package/src/cli/shipped-versions.test.ts +83 -0
  79. package/src/cli/tokens-css.test.ts +62 -0
  80. package/src/cli/toolchain-wiring.test.ts +171 -4
  81. package/src/cli/waiver-audit.test.ts +7 -3
  82. package/src/guard/check-source.test.ts +211 -42
  83. package/src/guard/index.ts +149 -23
  84. package/src/vite.test.ts +79 -0
  85. package/src/vite.ts +141 -39
@@ -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
+ }
@@ -4,7 +4,15 @@
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, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
7
+ import {
8
+ existsSync,
9
+ mkdirSync,
10
+ mkdtempSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ rmSync,
14
+ writeFileSync,
15
+ } from 'node:fs'
8
16
  import { tmpdir } from 'node:os'
9
17
  import { resolve } from 'node:path'
10
18
 
@@ -95,3 +103,68 @@ describe('run() --help short-circuits before dispatch', () => {
95
103
  expect(existsSync(resolve(dir, MANIFEST_PATH))).toBe(true)
96
104
  })
97
105
  })
106
+
107
+ /**
108
+ * `--version` and the unknown-flag gate — one failure mode, two halves.
109
+ *
110
+ * Six consumer repos in one upgrade round reached for `basalt-ui --version` to prove WHICH CLI ran
111
+ * (a `bunx` cache does not re-resolve, so a pinned version and the version that gates you can
112
+ * differ) and got a usage dump with no version in it. The other half is the same shape: an
113
+ * unrecognized FLAG was silently ignored and the command exited 0, so a mistyped gate read as a
114
+ * passing gate.
115
+ */
116
+ describe('run() --version and the unknown-flag gate', () => {
117
+ const version = (
118
+ JSON.parse(readFileSync(resolve(import.meta.dir, '../../package.json'), 'utf8')) as {
119
+ version: string
120
+ }
121
+ ).version
122
+
123
+ it('`--version` prints the bare resolved version and exits 0', () => {
124
+ const { code, log } = capture(() => run(['--version'], dir))
125
+ expect(code).toBe(0)
126
+ expect(log.trim()).toBe(version)
127
+ })
128
+
129
+ it('`-v` and `version` are the same thing', () => {
130
+ expect(capture(() => run(['-v'], dir)).log.trim()).toBe(version)
131
+ expect(capture(() => run(['version'], dir)).log.trim()).toBe(version)
132
+ })
133
+
134
+ it('the printed line is greppable on its own — no usage block around it', () => {
135
+ const { log } = capture(() => run(['--version'], dir))
136
+ expect(log).not.toContain('Usage: basalt')
137
+ })
138
+
139
+ it('an unrecognized flag exits 1 and NAMES it, on a command that would otherwise pass', () => {
140
+ const { code, log } = capture(() => run(['doctor', '--json'], dir))
141
+ expect(code).toBe(1)
142
+ expect(log).toContain("unrecognized flag '--json'")
143
+ expect(log).toContain('--tokens-only')
144
+ })
145
+
146
+ it('a near-miss flag does not slip through as a silent no-op', () => {
147
+ const { code, log } = capture(() => run(['check-theme', '--audit-allow'], dir))
148
+ expect(code).toBe(1)
149
+ expect(log).toContain("unrecognized flag '--audit-allow'")
150
+ })
151
+
152
+ it('a value-taking flag does not make its VALUE look like an unknown flag', () => {
153
+ const out = resolve(dir, 'tokens.css')
154
+ const { code } = capture(() => run(['tokens:css', '--out', out, '--only', 'core'], dir))
155
+ expect(code).toBe(0)
156
+ })
157
+
158
+ it('an unknown COMMAND names what it did not understand, above the usage block', () => {
159
+ const { code, log } = capture(() => run(['check-theme --audit-allows'], dir))
160
+ expect(code).toBe(1)
161
+ expect(log).toContain("unknown command 'check-theme --audit-allows'")
162
+ expect(log).toContain('Usage: basalt')
163
+ })
164
+
165
+ it('`--help` still wins over the flag gate, so a reader can always find out what is accepted', () => {
166
+ const { code, log } = capture(() => run(['doctor', '--json', '--help'], dir))
167
+ expect(code).toBe(0)
168
+ expect(log).toContain('Usage: basalt')
169
+ })
170
+ })