basalt-ui 1.22.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 (79) hide show
  1. package/AGENTS.md +9 -1
  2. package/CHANGELOG.md +19 -0
  3. package/MIGRATING.md +125 -12
  4. package/README.md +45 -11
  5. package/agent/rules/basalt-app.md +26 -2
  6. package/agent/rules/basalt-charts.md +40 -5
  7. package/agent/rules/basalt-tokens.md +17 -1
  8. package/agent/skills/basalt-charts/SKILL.md +23 -8
  9. package/agent/templates/DESIGN.md.tpl +3 -1
  10. package/configs/oxlint-plugin.js +49 -8
  11. package/configs/oxlint-plugin.test.ts +88 -31
  12. package/dist/charts/hooks/useBandPlot.d.ts +138 -0
  13. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
  14. package/dist/charts/hooks/useBandPlot.js +127 -0
  15. package/dist/charts/index.d.ts +4 -0
  16. package/dist/charts/index.d.ts.map +1 -1
  17. package/dist/charts/index.js +14 -0
  18. package/dist/charts/kinds/BandStrip.d.ts +113 -0
  19. package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
  20. package/dist/charts/kinds/BandStrip.js +218 -0
  21. package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
  22. package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
  23. package/dist/charts/kinds/Bars.d.ts +5 -1
  24. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  25. package/dist/charts/kinds/Bars.js +2 -0
  26. package/dist/charts/kinds/MirroredBars.d.ts +93 -0
  27. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
  28. package/dist/charts/kinds/MirroredBars.js +315 -0
  29. package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
  30. package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
  31. package/dist/charts/kinds/MultiLine.d.ts +5 -1
  32. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  33. package/dist/charts/kinds/MultiLine.js +2 -0
  34. package/dist/charts/kinds/StackedArea.d.ts +5 -1
  35. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  36. package/dist/charts/kinds/StackedArea.js +2 -0
  37. package/dist/charts/kinds/ZonedLine.d.ts +5 -1
  38. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  39. package/dist/charts/kinds/ZonedLine.js +2 -0
  40. package/dist/charts/primitives/CartesianChart.d.ts +18 -2
  41. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  42. package/dist/charts/primitives/CartesianChart.js +5 -2
  43. package/dist/charts/primitives/HatchPattern.d.ts +31 -0
  44. package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
  45. package/dist/charts/primitives/HatchPattern.js +31 -0
  46. package/dist/cli/index.d.ts +2 -0
  47. package/dist/cli/index.d.ts.map +1 -1
  48. package/dist/cli/index.js +47 -8
  49. package/dist/cli/shipped-versions.test.d.ts +2 -0
  50. package/dist/cli/shipped-versions.test.d.ts.map +1 -0
  51. package/dist/guard/index.d.ts.map +1 -1
  52. package/dist/guard/index.js +32 -11
  53. package/dist/vite.d.ts +42 -3
  54. package/dist/vite.d.ts.map +1 -1
  55. package/dist/vite.js +67 -30
  56. package/llms.txt +2 -2
  57. package/package.json +1 -1
  58. package/src/charts/hooks/useBandPlot.ts +302 -0
  59. package/src/charts/index.ts +14 -0
  60. package/src/charts/kinds/BandStrip.test.tsx +200 -0
  61. package/src/charts/kinds/BandStrip.tsx +364 -0
  62. package/src/charts/kinds/Bars.tsx +7 -1
  63. package/src/charts/kinds/DualPanel.test.tsx +10 -4
  64. package/src/charts/kinds/MirroredBars.test.tsx +193 -0
  65. package/src/charts/kinds/MirroredBars.tsx +455 -0
  66. package/src/charts/kinds/MultiLine.tsx +7 -1
  67. package/src/charts/kinds/StackedArea.tsx +7 -1
  68. package/src/charts/kinds/ZonedLine.tsx +7 -1
  69. package/src/charts/primitives/CartesianChart.test.tsx +86 -4
  70. package/src/charts/primitives/CartesianChart.tsx +28 -3
  71. package/src/charts/primitives/HatchPattern.tsx +55 -0
  72. package/src/cli/index.ts +125 -18
  73. package/src/cli/shipped-versions.test.ts +83 -0
  74. package/src/cli/toolchain-wiring.test.ts +171 -4
  75. package/src/cli/waiver-audit.test.ts +7 -3
  76. package/src/guard/check-source.test.ts +138 -42
  77. package/src/guard/index.ts +83 -17
  78. package/src/vite.test.ts +79 -0
  79. package/src/vite.ts +141 -39
package/src/vite.ts CHANGED
@@ -198,6 +198,30 @@ const ICON_FILES = {
198
198
  manifest512: 'web-app-manifest-512x512.png',
199
199
  } as const
200
200
 
201
+ /**
202
+ * One icon a consumer names itself — a manifest `icons` member, plus optionally the head `<link>`
203
+ * that points at the same file.
204
+ *
205
+ * The field names are the manifest's own, verbatim, so what a consumer writes here is what ships in
206
+ * `site.webmanifest`. `src` is root-relative (`'/favicon.svg'` or `'favicon.svg'`, both resolve the
207
+ * same) and is prefixed with Vite's `base` exactly like basalt's built-in set.
208
+ */
209
+ export type BasaltAppIcon = {
210
+ /** Root-relative path under `public/`. Prefixed with Vite's resolved `base`. */
211
+ src: string
212
+ /** Manifest `sizes`. `'any'` for a scalable SVG; `'192x192'` for a raster. */
213
+ sizes?: string
214
+ /** MIME type, e.g. `'image/svg+xml'`. */
215
+ type?: string
216
+ /** Manifest `purpose`, e.g. `'any maskable'`. */
217
+ purpose?: string
218
+ /**
219
+ * Emit a head `<link rel="…">` for this icon too. Omitted → manifest only, which is what an app
220
+ * that links its own favicon from `index.html` wants.
221
+ */
222
+ rel?: 'icon' | 'shortcut icon' | 'apple-touch-icon' | 'mask-icon'
223
+ }
224
+
201
225
  export type BasaltAppOptions = {
202
226
  /** App display name — used verbatim as the manifest `name` and (unless overridden) OG title. */
203
227
  name: string
@@ -243,10 +267,26 @@ export type BasaltAppOptions = {
243
267
  /** Manifest `id`. Default: the resolved `startUrl` (default- or explicitly-derived, per above). */
244
268
  id?: string
245
269
  /**
246
- * Icon links + manifest icon paths. `false` skips BOTH — the head `<link>` icons and the
247
- * manifest's `icons` array — so a manifest is never emitted pointing at files that 404.
270
+ * Which icons the plugin knows about — three forms, one question each.
271
+ *
272
+ * - **omitted / `{ dir }`** — basalt's own six-filename set (the realfavicongenerator layout argo
273
+ * ships), optionally under a subdirectory of `public/`. Unchanged default.
274
+ * - **`false`** — no head `<link>` icons and no manifest `icons` member, so a manifest is never
275
+ * emitted pointing at files that 404.
276
+ * - **an array** — the icons this app ACTUALLY has, named by the consumer.
277
+ *
278
+ * The array is the form for every app that was not scaffolded from basalt's icon set, which is
279
+ * most of them. `{ dir }` can only move the six fixed filenames; it cannot rename one, so an app
280
+ * whose single icon is `favicon.svg` had no way to say so and had to keep a hand-written manifest
281
+ * beside the generated head — the last thing standing between rb and deleting a file carrying a
282
+ * permanent `theme-allow-file`. One SVG at `sizes: 'any'` is a complete, installable icon set.
283
+ *
284
+ * Every entry becomes a manifest icon; an entry becomes a head `<link>` only when it names a
285
+ * {@link BasaltAppIcon.rel}. That split is deliberate — an app that already links its favicon
286
+ * from its own `index.html` wants the manifest fixed without a duplicate tag, and an app that
287
+ * wants the plugin to own the head says so per icon. An empty array reads as `false`.
248
288
  */
249
- icons?: false | { dir?: string }
289
+ icons?: false | { dir?: string } | readonly BasaltAppIcon[]
250
290
  /** Emits `<meta name="darkreader-lock">` when `'lock'` (default). Pass `false` to omit it. */
251
291
  darkreader?: 'lock' | false
252
292
  /** Site-wide SEO metadata — only the keys provided are emitted, no per-page SEO. */
@@ -327,6 +367,39 @@ function withBase(base: string, path: string): string {
327
367
  return base + path.slice(1)
328
368
  }
329
369
 
370
+ /**
371
+ * The three `icons` forms, collapsed to what the two emitters actually need.
372
+ *
373
+ * `'none'` and `'default'` are the pre-existing behaviour verbatim — `'default'` still walks
374
+ * {@link ICON_FILES}, so an app on basalt's own icon layout emits byte-identical head tags and a
375
+ * byte-identical manifest. `'explicit'` is the new form. An EMPTY array collapses to `'none'`
376
+ * rather than to an empty `icons: []` member: a manifest declaring zero icons is a claim, and the
377
+ * honest way to say "this app has no icons" already exists.
378
+ */
379
+ type ResolvedIcons =
380
+ | { mode: 'none' }
381
+ | { mode: 'default'; dir: string | undefined }
382
+ | { mode: 'explicit'; icons: readonly BasaltAppIcon[] }
383
+
384
+ /** `Array.isArray` widens to `any[]` and does not narrow a `readonly T[]` OUT of a union. */
385
+ function isIconList(
386
+ icons: { dir?: string } | readonly BasaltAppIcon[],
387
+ ): icons is readonly BasaltAppIcon[] {
388
+ return Array.isArray(icons)
389
+ }
390
+
391
+ function resolveIcons(icons: BasaltAppOptions['icons']): ResolvedIcons {
392
+ if (icons === false) return { mode: 'none' }
393
+ if (icons === undefined) return { mode: 'default', dir: undefined }
394
+ if (!isIconList(icons)) return { mode: 'default', dir: icons.dir }
395
+ return icons.length === 0 ? { mode: 'none' } : { mode: 'explicit', icons }
396
+ }
397
+
398
+ /** Root-relative `src` (with or without its leading slash) → base-prefixed href. */
399
+ function iconHref(base: string, src: string): string {
400
+ return withBase(base, src.startsWith('/') ? src : `/${src}`)
401
+ }
402
+
330
403
  function hasViewportMeta(html: string): boolean {
331
404
  return /<meta\s+[^>]*name=["']viewport["']/i.test(html)
332
405
  }
@@ -477,8 +550,7 @@ function buildStaticTags(input: {
477
550
  shortName: string
478
551
  themeColor: { light: string; dark: string }
479
552
  bootStyle: string
480
- iconsEnabled: boolean
481
- iconsDir: string | undefined
553
+ icons: ResolvedIcons
482
554
  darkreader: 'lock' | false
483
555
  manifestEnabled: boolean
484
556
  base: string
@@ -489,8 +561,7 @@ function buildStaticTags(input: {
489
561
  shortName,
490
562
  themeColor,
491
563
  bootStyle,
492
- iconsEnabled,
493
- iconsDir,
564
+ icons,
494
565
  darkreader,
495
566
  manifestEnabled,
496
567
  base,
@@ -524,13 +595,13 @@ function buildStaticTags(input: {
524
595
  // BOOT_SCOPE so it expires the moment Mantine resolves the real scheme — see buildBootStyle.
525
596
  tags.push({ tag: 'style', children: bootStyle })
526
597
 
527
- if (iconsEnabled) {
598
+ if (icons.mode === 'default') {
528
599
  tags.push(
529
600
  {
530
601
  tag: 'link',
531
602
  attrs: {
532
603
  rel: 'shortcut icon',
533
- href: withBase(base, withIconPath(iconsDir, ICON_FILES.favicon)),
604
+ href: withBase(base, withIconPath(icons.dir, ICON_FILES.favicon)),
534
605
  },
535
606
  },
536
607
  {
@@ -538,7 +609,7 @@ function buildStaticTags(input: {
538
609
  attrs: {
539
610
  rel: 'icon',
540
611
  type: 'image/svg+xml',
541
- href: withBase(base, withIconPath(iconsDir, ICON_FILES.svg)),
612
+ href: withBase(base, withIconPath(icons.dir, ICON_FILES.svg)),
542
613
  },
543
614
  },
544
615
  {
@@ -546,7 +617,7 @@ function buildStaticTags(input: {
546
617
  attrs: {
547
618
  rel: 'icon',
548
619
  sizes: '96x96',
549
- href: withBase(base, withIconPath(iconsDir, ICON_FILES.png96)),
620
+ href: withBase(base, withIconPath(icons.dir, ICON_FILES.png96)),
550
621
  },
551
622
  },
552
623
  {
@@ -554,12 +625,29 @@ function buildStaticTags(input: {
554
625
  attrs: {
555
626
  rel: 'apple-touch-icon',
556
627
  sizes: '180x180',
557
- href: withBase(base, withIconPath(iconsDir, ICON_FILES.appleTouch)),
628
+ href: withBase(base, withIconPath(icons.dir, ICON_FILES.appleTouch)),
558
629
  },
559
630
  },
560
631
  )
561
632
  }
562
633
 
634
+ // A consumer-named icon reaches the head only when it asks to, so the plugin can fix a manifest
635
+ // without duplicating a `<link>` the app's own index.html already writes.
636
+ if (icons.mode === 'explicit') {
637
+ for (const icon of icons.icons) {
638
+ if (icon.rel === undefined) continue
639
+ tags.push({
640
+ tag: 'link',
641
+ attrs: {
642
+ rel: icon.rel,
643
+ ...(icon.type === undefined ? {} : { type: icon.type }),
644
+ ...(icon.sizes === undefined ? {} : { sizes: icon.sizes }),
645
+ href: iconHref(base, icon.src),
646
+ },
647
+ })
648
+ }
649
+ }
650
+
563
651
  tags.push(
564
652
  { tag: 'meta', attrs: { name: 'apple-mobile-web-app-title', content: shortName } },
565
653
  { tag: 'meta', attrs: { name: 'apple-mobile-web-app-capable', content: 'yes' } },
@@ -582,6 +670,39 @@ function buildStaticTags(input: {
582
670
  return tags
583
671
  }
584
672
 
673
+ /** The manifest's `icons` member, or nothing at all — never an empty array. */
674
+ function manifestIcons(
675
+ icons: ResolvedIcons,
676
+ base: string,
677
+ ): { icons?: ReadonlyArray<Record<string, string>> } {
678
+ if (icons.mode === 'none') return {}
679
+ if (icons.mode === 'default') {
680
+ return {
681
+ icons: [
682
+ {
683
+ src: withBase(base, withIconPath(icons.dir, ICON_FILES.manifest192)),
684
+ sizes: '192x192',
685
+ type: 'image/png',
686
+ },
687
+ {
688
+ src: withBase(base, withIconPath(icons.dir, ICON_FILES.manifest512)),
689
+ sizes: '512x512',
690
+ type: 'image/png',
691
+ purpose: 'any maskable',
692
+ },
693
+ ],
694
+ }
695
+ }
696
+ return {
697
+ icons: icons.icons.map((icon) => ({
698
+ src: iconHref(base, icon.src),
699
+ ...(icon.sizes === undefined ? {} : { sizes: icon.sizes }),
700
+ ...(icon.type === undefined ? {} : { type: icon.type }),
701
+ ...(icon.purpose === undefined ? {} : { purpose: icon.purpose }),
702
+ })),
703
+ }
704
+ }
705
+
585
706
  function buildManifestJson(input: {
586
707
  options: BasaltAppOptions
587
708
  shortName: string
@@ -591,8 +712,7 @@ function buildManifestJson(input: {
591
712
  display: NonNullable<BasaltAppOptions['display']>
592
713
  schemeColor: string
593
714
  backgroundColor: string
594
- iconsEnabled: boolean
595
- iconsDir: string | undefined
715
+ icons: ResolvedIcons
596
716
  base: string
597
717
  }): string {
598
718
  const {
@@ -604,8 +724,7 @@ function buildManifestJson(input: {
604
724
  display,
605
725
  schemeColor,
606
726
  backgroundColor,
607
- iconsEnabled,
608
- iconsDir,
727
+ icons,
609
728
  base,
610
729
  } = input
611
730
  const manifest = {
@@ -624,23 +743,9 @@ function buildManifestJson(input: {
624
743
  // `{ manifest: true, icons: false }` shipped a manifest pointing at two PNGs the app never
625
744
  // ships — an installable app with broken icons, and the reason rb went hybrid (plugin for the
626
745
  // head, a hand-written manifest for the rest). A manifest with NO icons member is valid.
627
- ...(iconsEnabled
628
- ? {
629
- icons: [
630
- {
631
- src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest192)),
632
- sizes: '192x192',
633
- type: 'image/png',
634
- },
635
- {
636
- src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest512)),
637
- sizes: '512x512',
638
- type: 'image/png',
639
- purpose: 'any maskable',
640
- },
641
- ],
642
- }
643
- : {}),
746
+ // The `'explicit'` arm is the other half of that fix: honest about icons it cannot see was
747
+ // only worth having once a consumer could tell it which icons it DOES have.
748
+ ...manifestIcons(icons, base),
644
749
  }
645
750
  return JSON.stringify(manifest, null, 2)
646
751
  }
@@ -649,8 +754,7 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
649
754
  const shortName = options.shortName ?? options.name
650
755
  const display = options.display ?? 'standalone'
651
756
  const darkreader = options.darkreader ?? 'lock'
652
- const iconsEnabled = options.icons !== false
653
- const iconsDir = options.icons === false ? undefined : options.icons?.dir
757
+ const icons = resolveIcons(options.icons)
654
758
  const manifestEnabled = options.manifest !== false
655
759
 
656
760
  const themeColor = resolveThemeColors(options.themeColor)
@@ -693,8 +797,7 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
693
797
  display,
694
798
  schemeColor,
695
799
  backgroundColor,
696
- iconsEnabled,
697
- iconsDir,
800
+ icons,
698
801
  base,
699
802
  })
700
803
 
@@ -703,8 +806,7 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
703
806
  shortName,
704
807
  themeColor,
705
808
  bootStyle,
706
- iconsEnabled,
707
- iconsDir,
809
+ icons,
708
810
  darkreader,
709
811
  manifestEnabled,
710
812
  base,