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.
- package/AGENTS.md +11 -1
- package/CHANGELOG.md +33 -0
- package/MIGRATING.md +205 -9
- package/README.md +63 -16
- package/agent/rules/basalt-app.md +39 -9
- package/agent/rules/basalt-charts.md +41 -6
- package/agent/rules/basalt-router.md +1 -1
- package/agent/rules/basalt-state.md +4 -4
- package/agent/rules/basalt-tokens.md +25 -4
- package/agent/skills/basalt-app/SKILL.md +4 -3
- package/agent/skills/basalt-charts/SKILL.md +23 -8
- package/agent/templates/DESIGN.md.tpl +3 -1
- package/configs/oxlint-plugin.js +86 -9
- package/configs/oxlint-plugin.test.ts +173 -0
- 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 +9 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +275 -48
- 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 +67 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +70 -11
- package/dist/vite.d.ts +44 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +71 -26
- 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/check-theme.test.ts +35 -0
- package/src/cli/help.test.ts +5 -1
- package/src/cli/index.ts +552 -79
- package/src/cli/shipped-versions.test.ts +83 -0
- package/src/cli/toolchain-wiring.test.ts +362 -6
- package/src/cli/waiver-audit.test.ts +88 -4
- package/src/guard/check-source.test.ts +293 -0
- package/src/guard/index.ts +209 -20
- package/src/vite.test.ts +112 -0
- package/src/vite.ts +147 -31
package/src/vite.test.ts
CHANGED
|
@@ -201,6 +201,118 @@ describe('basaltAppPlugin — base handling', () => {
|
|
|
201
201
|
false,
|
|
202
202
|
)
|
|
203
203
|
})
|
|
204
|
+
|
|
205
|
+
// The head half of `icons: false` shipped; the manifest half did not. `{ manifest: true, icons:
|
|
206
|
+
// false }` emitted a site.webmanifest naming two PNGs the app never ships — an installable app
|
|
207
|
+
// with two 404s, which is why rb went hybrid rather than use the plugin's manifest at all.
|
|
208
|
+
test('icons: false reaches the MANIFEST, not just the head links', () => {
|
|
209
|
+
const plugin = getPlugin({ name: 'Test App', themeColor: THEME_COLOR, icons: false })
|
|
210
|
+
const middleware = getDevMiddleware(plugin, '/myapp/')
|
|
211
|
+
const manifest = JSON.parse(runMiddleware(middleware, '/myapp/site.webmanifest') ?? '{}')
|
|
212
|
+
|
|
213
|
+
expect(manifest.icons).toBeUndefined()
|
|
214
|
+
expect(JSON.stringify(manifest)).not.toContain('.png')
|
|
215
|
+
// …and the rest of the manifest is unaffected.
|
|
216
|
+
expect(manifest.start_url).toBe('/myapp/')
|
|
217
|
+
expect(manifest.name).toBe('Test App')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('the default (icons on) still carries both manifest icon entries', () => {
|
|
221
|
+
const plugin = getPlugin({ name: 'Test App', themeColor: THEME_COLOR })
|
|
222
|
+
const middleware = getDevMiddleware(plugin, '/')
|
|
223
|
+
const manifest = JSON.parse(runMiddleware(middleware, '/site.webmanifest') ?? '{}')
|
|
224
|
+
expect(manifest.icons).toHaveLength(2)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('icons.dir still redirects the manifest icon srcs', () => {
|
|
228
|
+
const plugin = getPlugin({
|
|
229
|
+
name: 'Test App',
|
|
230
|
+
themeColor: THEME_COLOR,
|
|
231
|
+
icons: { dir: '/brand' },
|
|
232
|
+
})
|
|
233
|
+
const middleware = getDevMiddleware(plugin, '/')
|
|
234
|
+
const manifest = JSON.parse(runMiddleware(middleware, '/site.webmanifest') ?? '{}')
|
|
235
|
+
expect(manifest.icons[0].src).toBe('/brand/web-app-manifest-192x192.png')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// ── the consumer-named array form ─────────────────────────────────────────
|
|
239
|
+
//
|
|
240
|
+
// `{ dir }` can move basalt's six filenames but not rename one, so an app whose only icon is
|
|
241
|
+
// `favicon.svg` could pick between a manifest naming two PNGs it does not ship and a manifest
|
|
242
|
+
// with no icons at all. Both are wrong for an installable app, and the second is why rb still
|
|
243
|
+
// kept a hand-written manifest one release after `icons: false` was fixed.
|
|
244
|
+
|
|
245
|
+
// rb's REAL icon set, copied from apps/web/public/manifest.webmanifest — the manifest this
|
|
246
|
+
// option exists to let the plugin generate.
|
|
247
|
+
const RB_ICONS = [
|
|
248
|
+
{ src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' },
|
|
249
|
+
] as const
|
|
250
|
+
|
|
251
|
+
test("generates rb's hand-written manifest icons member verbatim", () => {
|
|
252
|
+
const plugin = getPlugin({ name: 'rb', themeColor: THEME_COLOR, icons: RB_ICONS })
|
|
253
|
+
const middleware = getDevMiddleware(plugin, '/')
|
|
254
|
+
const manifest = JSON.parse(runMiddleware(middleware, '/site.webmanifest') ?? '{}')
|
|
255
|
+
|
|
256
|
+
expect(manifest.icons).toEqual([
|
|
257
|
+
{ src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' },
|
|
258
|
+
])
|
|
259
|
+
expect(JSON.stringify(manifest)).not.toContain('.png')
|
|
260
|
+
expect(manifest.name).toBe('rb')
|
|
261
|
+
expect(manifest.display).toBe('standalone')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('a named icon reaches the head only when it asks for a rel', () => {
|
|
265
|
+
const iconLinks = (icons: NonNullable<BasaltAppOptions['icons']>): HtmlTagDescriptor[] => {
|
|
266
|
+
const plugin = getPlugin({ name: 'rb', themeColor: THEME_COLOR, icons })
|
|
267
|
+
resolveConfig(plugin, '/')
|
|
268
|
+
return transformHtml(plugin, HTML_NO_VIEWPORT).filter(
|
|
269
|
+
(tag) => tag.tag === 'link' && String(tag.attrs?.['rel']).includes('icon'),
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// rb's index.html already links favicon.svg — the manifest gets fixed, the head is left alone.
|
|
274
|
+
expect(iconLinks(RB_ICONS)).toHaveLength(0)
|
|
275
|
+
|
|
276
|
+
const withRel = iconLinks([{ src: 'favicon.svg', type: 'image/svg+xml', rel: 'icon' }])
|
|
277
|
+
expect(withRel).toHaveLength(1)
|
|
278
|
+
expect(withRel[0]?.attrs).toEqual({
|
|
279
|
+
rel: 'icon',
|
|
280
|
+
type: 'image/svg+xml',
|
|
281
|
+
href: '/favicon.svg',
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('a named icon src is base-prefixed, with or without its leading slash', () => {
|
|
286
|
+
const plugin = getPlugin({
|
|
287
|
+
name: 'rb',
|
|
288
|
+
themeColor: THEME_COLOR,
|
|
289
|
+
icons: [
|
|
290
|
+
{ src: 'favicon.svg', sizes: 'any' },
|
|
291
|
+
{ src: '/icon-512.png', sizes: '512x512' },
|
|
292
|
+
],
|
|
293
|
+
})
|
|
294
|
+
const middleware = getDevMiddleware(plugin, '/myapp/')
|
|
295
|
+
const manifest = JSON.parse(runMiddleware(middleware, '/myapp/site.webmanifest') ?? '{}')
|
|
296
|
+
|
|
297
|
+
expect(manifest.icons.map((icon: { src: string }) => icon.src)).toEqual([
|
|
298
|
+
'/myapp/favicon.svg',
|
|
299
|
+
'/myapp/icon-512.png',
|
|
300
|
+
])
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('an empty array reads as icons: false — no member, not an empty one', () => {
|
|
304
|
+
const plugin = getPlugin({ name: 'rb', themeColor: THEME_COLOR, icons: [] })
|
|
305
|
+
const middleware = getDevMiddleware(plugin, '/')
|
|
306
|
+
const manifest = JSON.parse(runMiddleware(middleware, '/site.webmanifest') ?? '{}')
|
|
307
|
+
expect(manifest.icons).toBeUndefined()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('the array form omits keys the consumer did not write', () => {
|
|
311
|
+
const plugin = getPlugin({ name: 'rb', themeColor: THEME_COLOR, icons: [{ src: '/a.svg' }] })
|
|
312
|
+
const middleware = getDevMiddleware(plugin, '/')
|
|
313
|
+
const manifest = JSON.parse(runMiddleware(middleware, '/site.webmanifest') ?? '{}')
|
|
314
|
+
expect(manifest.icons).toEqual([{ src: '/a.svg' }])
|
|
315
|
+
})
|
|
204
316
|
})
|
|
205
317
|
|
|
206
318
|
describe('basaltAppPlugin — the anti-FOUC boot rule (V1)', () => {
|
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
|
|
@@ -242,8 +266,27 @@ export type BasaltAppOptions = {
|
|
|
242
266
|
scope?: string
|
|
243
267
|
/** Manifest `id`. Default: the resolved `startUrl` (default- or explicitly-derived, per above). */
|
|
244
268
|
id?: string
|
|
245
|
-
/**
|
|
246
|
-
|
|
269
|
+
/**
|
|
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`.
|
|
288
|
+
*/
|
|
289
|
+
icons?: false | { dir?: string } | readonly BasaltAppIcon[]
|
|
247
290
|
/** Emits `<meta name="darkreader-lock">` when `'lock'` (default). Pass `false` to omit it. */
|
|
248
291
|
darkreader?: 'lock' | false
|
|
249
292
|
/** Site-wide SEO metadata — only the keys provided are emitted, no per-page SEO. */
|
|
@@ -324,6 +367,39 @@ function withBase(base: string, path: string): string {
|
|
|
324
367
|
return base + path.slice(1)
|
|
325
368
|
}
|
|
326
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
|
+
|
|
327
403
|
function hasViewportMeta(html: string): boolean {
|
|
328
404
|
return /<meta\s+[^>]*name=["']viewport["']/i.test(html)
|
|
329
405
|
}
|
|
@@ -474,8 +550,7 @@ function buildStaticTags(input: {
|
|
|
474
550
|
shortName: string
|
|
475
551
|
themeColor: { light: string; dark: string }
|
|
476
552
|
bootStyle: string
|
|
477
|
-
|
|
478
|
-
iconsDir: string | undefined
|
|
553
|
+
icons: ResolvedIcons
|
|
479
554
|
darkreader: 'lock' | false
|
|
480
555
|
manifestEnabled: boolean
|
|
481
556
|
base: string
|
|
@@ -486,8 +561,7 @@ function buildStaticTags(input: {
|
|
|
486
561
|
shortName,
|
|
487
562
|
themeColor,
|
|
488
563
|
bootStyle,
|
|
489
|
-
|
|
490
|
-
iconsDir,
|
|
564
|
+
icons,
|
|
491
565
|
darkreader,
|
|
492
566
|
manifestEnabled,
|
|
493
567
|
base,
|
|
@@ -521,13 +595,13 @@ function buildStaticTags(input: {
|
|
|
521
595
|
// BOOT_SCOPE so it expires the moment Mantine resolves the real scheme — see buildBootStyle.
|
|
522
596
|
tags.push({ tag: 'style', children: bootStyle })
|
|
523
597
|
|
|
524
|
-
if (
|
|
598
|
+
if (icons.mode === 'default') {
|
|
525
599
|
tags.push(
|
|
526
600
|
{
|
|
527
601
|
tag: 'link',
|
|
528
602
|
attrs: {
|
|
529
603
|
rel: 'shortcut icon',
|
|
530
|
-
href: withBase(base, withIconPath(
|
|
604
|
+
href: withBase(base, withIconPath(icons.dir, ICON_FILES.favicon)),
|
|
531
605
|
},
|
|
532
606
|
},
|
|
533
607
|
{
|
|
@@ -535,7 +609,7 @@ function buildStaticTags(input: {
|
|
|
535
609
|
attrs: {
|
|
536
610
|
rel: 'icon',
|
|
537
611
|
type: 'image/svg+xml',
|
|
538
|
-
href: withBase(base, withIconPath(
|
|
612
|
+
href: withBase(base, withIconPath(icons.dir, ICON_FILES.svg)),
|
|
539
613
|
},
|
|
540
614
|
},
|
|
541
615
|
{
|
|
@@ -543,7 +617,7 @@ function buildStaticTags(input: {
|
|
|
543
617
|
attrs: {
|
|
544
618
|
rel: 'icon',
|
|
545
619
|
sizes: '96x96',
|
|
546
|
-
href: withBase(base, withIconPath(
|
|
620
|
+
href: withBase(base, withIconPath(icons.dir, ICON_FILES.png96)),
|
|
547
621
|
},
|
|
548
622
|
},
|
|
549
623
|
{
|
|
@@ -551,12 +625,29 @@ function buildStaticTags(input: {
|
|
|
551
625
|
attrs: {
|
|
552
626
|
rel: 'apple-touch-icon',
|
|
553
627
|
sizes: '180x180',
|
|
554
|
-
href: withBase(base, withIconPath(
|
|
628
|
+
href: withBase(base, withIconPath(icons.dir, ICON_FILES.appleTouch)),
|
|
555
629
|
},
|
|
556
630
|
},
|
|
557
631
|
)
|
|
558
632
|
}
|
|
559
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
|
+
|
|
560
651
|
tags.push(
|
|
561
652
|
{ tag: 'meta', attrs: { name: 'apple-mobile-web-app-title', content: shortName } },
|
|
562
653
|
{ tag: 'meta', attrs: { name: 'apple-mobile-web-app-capable', content: 'yes' } },
|
|
@@ -579,6 +670,39 @@ function buildStaticTags(input: {
|
|
|
579
670
|
return tags
|
|
580
671
|
}
|
|
581
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
|
+
|
|
582
706
|
function buildManifestJson(input: {
|
|
583
707
|
options: BasaltAppOptions
|
|
584
708
|
shortName: string
|
|
@@ -588,7 +712,7 @@ function buildManifestJson(input: {
|
|
|
588
712
|
display: NonNullable<BasaltAppOptions['display']>
|
|
589
713
|
schemeColor: string
|
|
590
714
|
backgroundColor: string
|
|
591
|
-
|
|
715
|
+
icons: ResolvedIcons
|
|
592
716
|
base: string
|
|
593
717
|
}): string {
|
|
594
718
|
const {
|
|
@@ -600,7 +724,7 @@ function buildManifestJson(input: {
|
|
|
600
724
|
display,
|
|
601
725
|
schemeColor,
|
|
602
726
|
backgroundColor,
|
|
603
|
-
|
|
727
|
+
icons,
|
|
604
728
|
base,
|
|
605
729
|
} = input
|
|
606
730
|
const manifest = {
|
|
@@ -615,19 +739,13 @@ function buildManifestJson(input: {
|
|
|
615
739
|
// `<meta name="theme-color">` pair. Unchanged for the default (`'dark'`) and for `'auto'`.
|
|
616
740
|
theme_color: schemeColor,
|
|
617
741
|
background_color: backgroundColor,
|
|
618
|
-
icons:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest512)),
|
|
626
|
-
sizes: '512x512',
|
|
627
|
-
type: 'image/png',
|
|
628
|
-
purpose: 'any maskable',
|
|
629
|
-
},
|
|
630
|
-
],
|
|
742
|
+
// `icons: false` reaches HERE too, not just the head links. The array was unconditional, so
|
|
743
|
+
// `{ manifest: true, icons: false }` shipped a manifest pointing at two PNGs the app never
|
|
744
|
+
// ships — an installable app with broken icons, and the reason rb went hybrid (plugin for the
|
|
745
|
+
// head, a hand-written manifest for the rest). A manifest with NO icons member is valid.
|
|
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),
|
|
631
749
|
}
|
|
632
750
|
return JSON.stringify(manifest, null, 2)
|
|
633
751
|
}
|
|
@@ -636,8 +754,7 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
|
|
|
636
754
|
const shortName = options.shortName ?? options.name
|
|
637
755
|
const display = options.display ?? 'standalone'
|
|
638
756
|
const darkreader = options.darkreader ?? 'lock'
|
|
639
|
-
const
|
|
640
|
-
const iconsDir = options.icons === false ? undefined : options.icons?.dir
|
|
757
|
+
const icons = resolveIcons(options.icons)
|
|
641
758
|
const manifestEnabled = options.manifest !== false
|
|
642
759
|
|
|
643
760
|
const themeColor = resolveThemeColors(options.themeColor)
|
|
@@ -680,7 +797,7 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
|
|
|
680
797
|
display,
|
|
681
798
|
schemeColor,
|
|
682
799
|
backgroundColor,
|
|
683
|
-
|
|
800
|
+
icons,
|
|
684
801
|
base,
|
|
685
802
|
})
|
|
686
803
|
|
|
@@ -689,8 +806,7 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
|
|
|
689
806
|
shortName,
|
|
690
807
|
themeColor,
|
|
691
808
|
bootStyle,
|
|
692
|
-
|
|
693
|
-
iconsDir,
|
|
809
|
+
icons,
|
|
694
810
|
darkreader,
|
|
695
811
|
manifestEnabled,
|
|
696
812
|
base,
|