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.
- package/AGENTS.md +9 -1
- package/CHANGELOG.md +19 -0
- package/MIGRATING.md +125 -12
- package/README.md +45 -11
- package/agent/rules/basalt-app.md +26 -2
- package/agent/rules/basalt-charts.md +40 -5
- package/agent/rules/basalt-tokens.md +17 -1
- package/agent/skills/basalt-charts/SKILL.md +23 -8
- package/agent/templates/DESIGN.md.tpl +3 -1
- package/configs/oxlint-plugin.js +49 -8
- package/configs/oxlint-plugin.test.ts +88 -31
- package/dist/charts/hooks/useBandPlot.d.ts +138 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
- package/dist/charts/hooks/useBandPlot.js +127 -0
- package/dist/charts/index.d.ts +4 -0
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +14 -0
- package/dist/charts/kinds/BandStrip.d.ts +113 -0
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
- package/dist/charts/kinds/BandStrip.js +218 -0
- package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
- package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
- package/dist/charts/kinds/Bars.d.ts +5 -1
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +2 -0
- package/dist/charts/kinds/MirroredBars.d.ts +93 -0
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
- package/dist/charts/kinds/MirroredBars.js +315 -0
- package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
- package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
- package/dist/charts/kinds/MultiLine.d.ts +5 -1
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +2 -0
- package/dist/charts/kinds/StackedArea.d.ts +5 -1
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +2 -0
- package/dist/charts/kinds/ZonedLine.d.ts +5 -1
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +2 -0
- package/dist/charts/primitives/CartesianChart.d.ts +18 -2
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +5 -2
- package/dist/charts/primitives/HatchPattern.d.ts +31 -0
- package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
- package/dist/charts/primitives/HatchPattern.js +31 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +47 -8
- package/dist/cli/shipped-versions.test.d.ts +2 -0
- package/dist/cli/shipped-versions.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +32 -11
- package/dist/vite.d.ts +42 -3
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +67 -30
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/hooks/useBandPlot.ts +302 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/kinds/BandStrip.test.tsx +200 -0
- package/src/charts/kinds/BandStrip.tsx +364 -0
- package/src/charts/kinds/Bars.tsx +7 -1
- package/src/charts/kinds/DualPanel.test.tsx +10 -4
- package/src/charts/kinds/MirroredBars.test.tsx +193 -0
- package/src/charts/kinds/MirroredBars.tsx +455 -0
- package/src/charts/kinds/MultiLine.tsx +7 -1
- package/src/charts/kinds/StackedArea.tsx +7 -1
- package/src/charts/kinds/ZonedLine.tsx +7 -1
- package/src/charts/primitives/CartesianChart.test.tsx +86 -4
- package/src/charts/primitives/CartesianChart.tsx +28 -3
- package/src/charts/primitives/HatchPattern.tsx +55 -0
- package/src/cli/index.ts +125 -18
- package/src/cli/shipped-versions.test.ts +83 -0
- package/src/cli/toolchain-wiring.test.ts +171 -4
- package/src/cli/waiver-audit.test.ts +7 -3
- package/src/guard/check-source.test.ts +138 -42
- package/src/guard/index.ts +83 -17
- package/src/vite.test.ts +79 -0
- 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
|
-
*
|
|
247
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
707
|
-
iconsDir,
|
|
809
|
+
icons,
|
|
708
810
|
darkreader,
|
|
709
811
|
manifestEnabled,
|
|
710
812
|
base,
|