basalt-ui 1.20.0 → 1.21.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 -5
- package/CHANGELOG.md +19 -0
- package/MIGRATING.md +147 -49
- package/README.md +42 -8
- package/agent/rules/basalt-app.md +41 -0
- package/agent/rules/basalt-charts.md +15 -12
- package/agent/rules/basalt-content.md +4 -0
- package/agent/rules/basalt-router.md +57 -5
- package/agent/rules/basalt-state.md +21 -3
- package/agent/rules/basalt-tokens.md +73 -16
- package/agent/skills/basalt-app/SKILL.md +4 -1
- package/agent/skills/basalt-charts/SKILL.md +5 -3
- package/agent/templates/CLAUDE-block.md.tpl +11 -5
- package/configs/lefthook.yml +48 -15
- package/configs/oxlint-plugin.js +181 -68
- package/configs/oxlint-plugin.test.ts +215 -9
- package/dist/charts/kinds/Bars.d.ts +1 -1
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.d.ts +1 -1
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.d.ts +1 -1
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.d.ts +1 -1
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.d.ts +1 -1
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.d.ts +1 -1
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/cli/index.d.ts +48 -14
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +328 -66
- package/dist/cli/waiver-audit.test.d.ts +2 -0
- package/dist/cli/waiver-audit.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +47 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +176 -31
- package/dist/router-tanstack/multi-search-param-store.d.ts +13 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +4 -1
- package/dist/router-tanstack/search-param-store.d.ts +29 -5
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +31 -3
- package/dist/router-tanstack/search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-param-store.test.d.ts.map +1 -0
- package/dist/shell/index.d.ts +12 -5
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +24 -6
- package/dist/tokens/index.d.ts +3 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +5 -1
- package/dist/tokens/palette.d.ts +2 -2
- package/dist/tokens/palette.js +2 -2
- package/dist/tokens/text-scale.test.d.ts +2 -0
- package/dist/tokens/text-scale.test.d.ts.map +1 -0
- package/dist/tokens.css +4 -2
- package/dist/vite.d.ts +24 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +54 -13
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/kinds/Bars.tsx +1 -1
- package/src/charts/kinds/Donut.tsx +1 -1
- package/src/charts/kinds/DualPanel.tsx +2 -2
- package/src/charts/kinds/Heatmap.tsx +1 -1
- package/src/charts/kinds/MultiLine.tsx +1 -1
- package/src/charts/kinds/StackedArea.tsx +1 -1
- package/src/charts/kinds/ZonedLine.tsx +1 -1
- package/src/cli/index.ts +607 -128
- package/src/cli/init-sync.test.ts +2 -0
- package/src/cli/template-ledger.test.ts +1 -1
- package/src/cli/toolchain-wiring.test.ts +162 -0
- package/src/cli/waiver-audit.test.ts +131 -0
- package/src/guard/check-source.test.ts +441 -1
- package/src/guard/index.ts +430 -60
- package/src/router-tanstack/multi-search-param-store.ts +16 -1
- package/src/router-tanstack/search-param-store.test.ts +135 -0
- package/src/router-tanstack/search-param-store.ts +90 -8
- package/src/shell/app-mobile-nav.tsx +1 -1
- package/src/shell/index.test.tsx +52 -0
- package/src/shell/index.tsx +64 -13
- package/src/tokens/index.ts +24 -1
- package/src/tokens/palette.ts +2 -2
- package/src/tokens/text-scale.test.ts +51 -0
- package/src/vite.test.ts +125 -6
- package/src/vite.ts +148 -20
package/src/vite.test.ts
CHANGED
|
@@ -17,13 +17,20 @@ function resolveConfig(plugin: Plugin, base: string): void {
|
|
|
17
17
|
configResolved({ base } as unknown as ResolvedConfig)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
type HtmlTransformResult = { html: string; tags: HtmlTagDescriptor[] }
|
|
21
|
+
|
|
22
|
+
/** Invokes the plugin's `transformIndexHtml` hook (object form, `order: 'pre'`). */
|
|
23
|
+
function transform(plugin: Plugin, html: string): HtmlTransformResult {
|
|
24
|
+
const hook = plugin.transformIndexHtml as unknown as {
|
|
25
|
+
order: 'pre'
|
|
26
|
+
handler: (html: string) => HtmlTransformResult
|
|
27
|
+
}
|
|
28
|
+
return hook.handler(html)
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
/** Invokes the plugin's `transformIndexHtml` hook and returns the emitted tags. */
|
|
21
32
|
function transformHtml(plugin: Plugin, html: string): HtmlTagDescriptor[] {
|
|
22
|
-
|
|
23
|
-
html: string
|
|
24
|
-
tags: HtmlTagDescriptor[]
|
|
25
|
-
}
|
|
26
|
-
return transformIndexHtml(html).tags
|
|
33
|
+
return transform(plugin, html).tags
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
type FakeReq = { url: string }
|
|
@@ -63,6 +70,7 @@ describe('basaltAppPlugin — base handling', () => {
|
|
|
63
70
|
const tags = transformHtml(plugin, HTML_NO_VIEWPORT)
|
|
64
71
|
|
|
65
72
|
expect(tags).toEqual([
|
|
73
|
+
{ tag: 'meta', attrs: { charset: 'UTF-8' } },
|
|
66
74
|
{
|
|
67
75
|
tag: 'meta',
|
|
68
76
|
attrs: {
|
|
@@ -78,7 +86,11 @@ describe('basaltAppPlugin — base handling', () => {
|
|
|
78
86
|
tag: 'meta',
|
|
79
87
|
attrs: { name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#111111' },
|
|
80
88
|
},
|
|
81
|
-
{
|
|
89
|
+
{
|
|
90
|
+
tag: 'style',
|
|
91
|
+
children:
|
|
92
|
+
'html:not([data-mantine-color-scheme]){background-color:#111111;color-scheme:dark}',
|
|
93
|
+
},
|
|
82
94
|
{ tag: 'link', attrs: { rel: 'shortcut icon', href: '/favicon.ico' } },
|
|
83
95
|
{ tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' } },
|
|
84
96
|
{ tag: 'link', attrs: { rel: 'icon', sizes: '96x96', href: '/favicon-96x96.png' } },
|
|
@@ -190,3 +202,110 @@ describe('basaltAppPlugin — base handling', () => {
|
|
|
190
202
|
)
|
|
191
203
|
})
|
|
192
204
|
})
|
|
205
|
+
|
|
206
|
+
describe('basaltAppPlugin — the anti-FOUC boot rule (V1)', () => {
|
|
207
|
+
const bootStyleOf = (options: Partial<BasaltAppOptions>): string => {
|
|
208
|
+
const plugin = getPlugin({ name: 'Test App', themeColor: THEME_COLOR, ...options })
|
|
209
|
+
resolveConfig(plugin, '/')
|
|
210
|
+
const style = transformHtml(plugin, HTML_NO_VIEWPORT).find((tag) => tag.tag === 'style')
|
|
211
|
+
return String(style?.children ?? '')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
test('every mode scopes the rule so Mantine can take it back', () => {
|
|
215
|
+
// The whole defect: an UNLAYERED `html{color-scheme:dark}` outranks Mantine's own layered
|
|
216
|
+
// `:root{color-scheme:var(--mantine-color-scheme)}`, so light mode kept dark native controls
|
|
217
|
+
// forever. Scoping to the absence of the attribute Mantine writes is what expires it.
|
|
218
|
+
for (const colorScheme of ['dark', 'light', 'auto', false] as const) {
|
|
219
|
+
const css = bootStyleOf({ colorScheme })
|
|
220
|
+
expect(css).toContain('html:not([data-mantine-color-scheme])')
|
|
221
|
+
expect(css).not.toMatch(/(^|[};])html\{/)
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('default is dark — byte-identical to 1.20.0 apart from the scope', () => {
|
|
226
|
+
expect(bootStyleOf({})).toBe(
|
|
227
|
+
'html:not([data-mantine-color-scheme]){background-color:#111111;color-scheme:dark}',
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('light paints the light surface and declares color-scheme: light', () => {
|
|
232
|
+
expect(bootStyleOf({ colorScheme: 'light' })).toBe(
|
|
233
|
+
'html:not([data-mantine-color-scheme]){background-color:#ffffff;color-scheme:light}',
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('auto declares `light dark` and follows prefers-color-scheme for the paint', () => {
|
|
238
|
+
expect(bootStyleOf({ colorScheme: 'auto' })).toBe(
|
|
239
|
+
'html:not([data-mantine-color-scheme]){background-color:#ffffff;color-scheme:light dark}' +
|
|
240
|
+
'@media(prefers-color-scheme:dark){html:not([data-mantine-color-scheme])' +
|
|
241
|
+
'{background-color:#111111}}',
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('an explicit backgroundColor wins over the auto media pair', () => {
|
|
246
|
+
expect(bootStyleOf({ colorScheme: 'auto', backgroundColor: '#abcdef' })).toBe(
|
|
247
|
+
'html:not([data-mantine-color-scheme]){background-color:#abcdef;color-scheme:light dark}',
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('colorScheme: false is the full opt-out — paint, no color-scheme at all', () => {
|
|
252
|
+
expect(bootStyleOf({ colorScheme: false })).toBe(
|
|
253
|
+
'html:not([data-mantine-color-scheme]){background-color:#111111}',
|
|
254
|
+
)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('the manifest theme_color follows the boot scheme', () => {
|
|
258
|
+
const read = (options: Partial<BasaltAppOptions>): Record<string, unknown> => {
|
|
259
|
+
const plugin = getPlugin({ name: 'Test App', themeColor: THEME_COLOR, ...options })
|
|
260
|
+
const body = runMiddleware(getDevMiddleware(plugin, '/'), '/site.webmanifest')
|
|
261
|
+
return JSON.parse(body ?? '{}') as Record<string, unknown>
|
|
262
|
+
}
|
|
263
|
+
expect(read({})['theme_color']).toBe('#111111')
|
|
264
|
+
expect(read({ colorScheme: 'auto' })['theme_color']).toBe('#111111')
|
|
265
|
+
expect(read({ colorScheme: 'light' })['theme_color']).toBe('#ffffff')
|
|
266
|
+
expect(read({ colorScheme: 'light' })['background_color']).toBe('#ffffff')
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('basaltAppPlugin — the encoding declaration (V2)', () => {
|
|
271
|
+
const SHELL =
|
|
272
|
+
'<!doctype html>\n<html lang="en">\n <head>\n <meta charset="utf-8" />\n' +
|
|
273
|
+
' <title>App</title>\n </head>\n <body></body>\n</html>'
|
|
274
|
+
|
|
275
|
+
test("the consumer's charset is hoisted out of the html and re-emitted first", () => {
|
|
276
|
+
const plugin = getPlugin({ name: 'Test App', themeColor: THEME_COLOR })
|
|
277
|
+
resolveConfig(plugin, '/')
|
|
278
|
+
const result = transform(plugin, SHELL)
|
|
279
|
+
|
|
280
|
+
expect(result.html).not.toContain('charset')
|
|
281
|
+
expect(result.tags[0]).toEqual({ tag: 'meta', attrs: { charset: 'utf-8' } })
|
|
282
|
+
// The hoist takes the whole line — no orphaned indentation left behind.
|
|
283
|
+
expect(result.html).toContain('<head>\n <title>App</title>')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('a shell that declares no encoding gets one', () => {
|
|
287
|
+
const plugin = getPlugin({ name: 'Test App', themeColor: THEME_COLOR })
|
|
288
|
+
resolveConfig(plugin, '/')
|
|
289
|
+
expect(transform(plugin, HTML_NO_VIEWPORT).tags[0]).toEqual({
|
|
290
|
+
tag: 'meta',
|
|
291
|
+
attrs: { charset: 'UTF-8' },
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('the legacy http-equiv form is left completely alone', () => {
|
|
296
|
+
const html =
|
|
297
|
+
'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8">' +
|
|
298
|
+
'</head><body></body></html>'
|
|
299
|
+
const plugin = getPlugin({ name: 'Test App', themeColor: THEME_COLOR })
|
|
300
|
+
resolveConfig(plugin, '/')
|
|
301
|
+
const result = transform(plugin, html)
|
|
302
|
+
|
|
303
|
+
expect(result.html).toBe(html)
|
|
304
|
+
expect(result.tags.some((tag) => tag.attrs?.['charset'] !== undefined)).toBe(false)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test("the hook runs `pre` so no other plugin's tags can land ahead of the charset", () => {
|
|
308
|
+
const plugin = getPlugin({ name: 'Test App', themeColor: THEME_COLOR })
|
|
309
|
+
expect((plugin.transformIndexHtml as { order?: string }).order).toBe('pre')
|
|
310
|
+
})
|
|
311
|
+
})
|
package/src/vite.ts
CHANGED
|
@@ -211,7 +211,21 @@ export type BasaltAppOptions = {
|
|
|
211
211
|
* plugin exists. Pass an explicit `{ light, dark }` pair of already-flat CSS colors to override.
|
|
212
212
|
*/
|
|
213
213
|
themeColor?: 'auto' | { light: string; dark: string }
|
|
214
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Which scheme the anti-FOUC `<style>` paints before the app boots. Default `'dark'` —
|
|
216
|
+
* `BasaltProvider`'s own default. `'light'` paints the light surface, `'auto'` declares
|
|
217
|
+
* `color-scheme: light dark` and picks the background off `prefers-color-scheme`, and `false`
|
|
218
|
+
* paints the background but declares no `color-scheme` at all (leave native controls to Mantine).
|
|
219
|
+
*
|
|
220
|
+
* Set this to whatever the app passes as `defaultColorScheme` — a mismatch is a visible flash of
|
|
221
|
+
* the wrong surface, not a correctness bug: the boot rule stops applying the moment Mantine
|
|
222
|
+
* writes `data-mantine-color-scheme` on `<html>` (see `BOOT_SCOPE`).
|
|
223
|
+
*/
|
|
224
|
+
colorScheme?: 'dark' | 'light' | 'auto' | false
|
|
225
|
+
/**
|
|
226
|
+
* Manifest `background_color` + anti-FOUC paint color. Default: the theme color matching
|
|
227
|
+
* `colorScheme` (the dark one for `'dark'` / `'auto'` / `false`, the light one for `'light'`).
|
|
228
|
+
*/
|
|
215
229
|
backgroundColor?: string
|
|
216
230
|
/** Manifest `display` mode. Default: `'standalone'`. */
|
|
217
231
|
display?: 'standalone' | 'minimal-ui' | 'fullscreen' | 'browser'
|
|
@@ -319,6 +333,91 @@ const VIEWPORT_TAG: HtmlTagDescriptor = {
|
|
|
319
333
|
attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1.0, viewport-fit=cover' },
|
|
320
334
|
}
|
|
321
335
|
|
|
336
|
+
// ── The anti-FOUC boot rule ───────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The selector every anti-FOUC declaration is scoped to.
|
|
340
|
+
*
|
|
341
|
+
* The rule paints the page before any stylesheet lands, so it is UNLAYERED by construction — and an
|
|
342
|
+
* unlayered declaration outranks every layered one regardless of specificity. Through 1.20.0 that
|
|
343
|
+
* meant a flat `html{color-scheme:dark}` permanently beat Mantine's own
|
|
344
|
+
* `@layer mantine{:root{color-scheme:var(--mantine-color-scheme)}}`: a light-scheme consumer got
|
|
345
|
+
* dark scrollbars, dark `<select>` popups and dark date pickers forever, with no opt-out (found
|
|
346
|
+
* independently by two consumers).
|
|
347
|
+
*
|
|
348
|
+
* `MantineProvider` writes `data-mantine-color-scheme` onto `<html>` as soon as it mounts, so
|
|
349
|
+
* scoping the boot rule to its ABSENCE makes it expire exactly when the real theme exists — which
|
|
350
|
+
* is the anti-FOUC contract stated honestly. A cascade layer would also work in principle and is
|
|
351
|
+
* deliberately NOT used: a layer's position is fixed by where its name is first encountered, and in
|
|
352
|
+
* dev Mantine's CSS is injected by JS after this `<style>` is already in the document, so the layer
|
|
353
|
+
* order would depend on bundler mode. Selector scoping depends on nothing.
|
|
354
|
+
*/
|
|
355
|
+
const BOOT_SCOPE = 'html:not([data-mantine-color-scheme])'
|
|
356
|
+
|
|
357
|
+
/** The `color-scheme` value each boot mode declares. `false` declares none. */
|
|
358
|
+
const BOOT_COLOR_SCHEME = { dark: 'dark', light: 'light', auto: 'light dark' } as const
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Builds the anti-FOUC stylesheet: paint the surface (and, unless opted out, tell the UA which
|
|
362
|
+
* native-control palette to use) for the frames between first paint and Mantine's stylesheet.
|
|
363
|
+
*
|
|
364
|
+
* `'auto'` with no explicit `backgroundColor` emits a `prefers-color-scheme` pair so the pre-boot
|
|
365
|
+
* paint follows the OS the same way `color-scheme: light dark` does; an explicit `backgroundColor`
|
|
366
|
+
* is used verbatim for both, since the consumer has named one color on purpose.
|
|
367
|
+
*/
|
|
368
|
+
function buildBootStyle(input: {
|
|
369
|
+
colorScheme: 'dark' | 'light' | 'auto' | false
|
|
370
|
+
themeColor: { light: string; dark: string }
|
|
371
|
+
backgroundColor: string
|
|
372
|
+
explicitBackground: boolean
|
|
373
|
+
}): string {
|
|
374
|
+
const { colorScheme, themeColor, backgroundColor, explicitBackground } = input
|
|
375
|
+
|
|
376
|
+
if (colorScheme === false) return `${BOOT_SCOPE}{background-color:${backgroundColor}}`
|
|
377
|
+
|
|
378
|
+
const scheme = BOOT_COLOR_SCHEME[colorScheme]
|
|
379
|
+
if (colorScheme !== 'auto' || explicitBackground) {
|
|
380
|
+
return `${BOOT_SCOPE}{background-color:${backgroundColor};color-scheme:${scheme}}`
|
|
381
|
+
}
|
|
382
|
+
return (
|
|
383
|
+
`${BOOT_SCOPE}{background-color:${themeColor.light};color-scheme:${scheme}}` +
|
|
384
|
+
`@media(prefers-color-scheme:dark){${BOOT_SCOPE}{background-color:${themeColor.dark}}}`
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── The encoding declaration ──────────────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
/** `<meta charset="…">`, with the value in group 1/2/3 depending on the quoting style. */
|
|
391
|
+
const CHARSET_META_RE =
|
|
392
|
+
/[ \t]*<meta\s+charset\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>=]+))\s*\/?>[ \t]*\r?\n?/i
|
|
393
|
+
/** The legacy `<meta http-equiv="Content-Type" charset=…>` form — left verbatim, never rewritten. */
|
|
394
|
+
const HTTP_EQUIV_CHARSET_RE = /<meta\s[^>]*http-equiv\s*=\s*["']?content-type["']?[^>]*>/i
|
|
395
|
+
const DEFAULT_CHARSET = 'UTF-8'
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Hoists the shell's encoding declaration ahead of everything this plugin injects.
|
|
399
|
+
*
|
|
400
|
+
* Every tag here defaults to `injectTo: 'head-prepend'`, which lands it immediately after `<head>`
|
|
401
|
+
* and therefore AHEAD of the consumer's own `<meta charset>`. On a realistic shell that pushed the
|
|
402
|
+
* declaration to byte 1653 — past the 1024-byte window the HTML spec gives it — and there is no
|
|
403
|
+
* consumer-side fix, because the plugin owns the injection point. So the plugin owns the hoist too:
|
|
404
|
+
* the consumer's tag is removed from the html and re-emitted as this plugin's FIRST head-prepend
|
|
405
|
+
* tag. A shell that declares no encoding at all gets `UTF-8`.
|
|
406
|
+
*
|
|
407
|
+
* The legacy `http-equiv` form is detected and left completely alone — rewriting a consumer's
|
|
408
|
+
* markup into a different (if equivalent) tag is not this plugin's business.
|
|
409
|
+
*/
|
|
410
|
+
function hoistCharset(html: string): { html: string; tag: HtmlTagDescriptor | undefined } {
|
|
411
|
+
if (HTTP_EQUIV_CHARSET_RE.test(html)) return { html, tag: undefined }
|
|
412
|
+
|
|
413
|
+
const match = CHARSET_META_RE.exec(html)
|
|
414
|
+
const charset = match?.[1] ?? match?.[2] ?? match?.[3] ?? DEFAULT_CHARSET
|
|
415
|
+
return {
|
|
416
|
+
html: match ? html.replace(CHARSET_META_RE, '') : html,
|
|
417
|
+
tag: { tag: 'meta', attrs: { charset } },
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
322
421
|
function buildSeoTags(input: {
|
|
323
422
|
name: string
|
|
324
423
|
description: string | undefined
|
|
@@ -374,7 +473,7 @@ function buildStaticTags(input: {
|
|
|
374
473
|
options: BasaltAppOptions
|
|
375
474
|
shortName: string
|
|
376
475
|
themeColor: { light: string; dark: string }
|
|
377
|
-
|
|
476
|
+
bootStyle: string
|
|
378
477
|
iconsEnabled: boolean
|
|
379
478
|
iconsDir: string | undefined
|
|
380
479
|
darkreader: 'lock' | false
|
|
@@ -386,7 +485,7 @@ function buildStaticTags(input: {
|
|
|
386
485
|
options,
|
|
387
486
|
shortName,
|
|
388
487
|
themeColor,
|
|
389
|
-
|
|
488
|
+
bootStyle,
|
|
390
489
|
iconsEnabled,
|
|
391
490
|
iconsDir,
|
|
392
491
|
darkreader,
|
|
@@ -416,13 +515,11 @@ function buildStaticTags(input: {
|
|
|
416
515
|
},
|
|
417
516
|
)
|
|
418
517
|
|
|
419
|
-
// Anti-FOUC —
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
children: `html{background-color:${backgroundColor};color-scheme:dark}`,
|
|
425
|
-
})
|
|
518
|
+
// Anti-FOUC — paints the boot surface before any stylesheet loads. injectTo defaults to
|
|
519
|
+
// 'head-prepend', so this (and every tag below) lands immediately after <head> and the hoisted
|
|
520
|
+
// <meta charset>, ahead of the consumer's own <link rel="stylesheet"> tags. Scoped to
|
|
521
|
+
// BOOT_SCOPE so it expires the moment Mantine resolves the real scheme — see buildBootStyle.
|
|
522
|
+
tags.push({ tag: 'style', children: bootStyle })
|
|
426
523
|
|
|
427
524
|
if (iconsEnabled) {
|
|
428
525
|
tags.push(
|
|
@@ -489,7 +586,7 @@ function buildManifestJson(input: {
|
|
|
489
586
|
scope: string
|
|
490
587
|
id: string
|
|
491
588
|
display: NonNullable<BasaltAppOptions['display']>
|
|
492
|
-
|
|
589
|
+
schemeColor: string
|
|
493
590
|
backgroundColor: string
|
|
494
591
|
iconsDir: string | undefined
|
|
495
592
|
base: string
|
|
@@ -501,7 +598,7 @@ function buildManifestJson(input: {
|
|
|
501
598
|
scope,
|
|
502
599
|
id,
|
|
503
600
|
display,
|
|
504
|
-
|
|
601
|
+
schemeColor,
|
|
505
602
|
backgroundColor,
|
|
506
603
|
iconsDir,
|
|
507
604
|
base,
|
|
@@ -514,7 +611,9 @@ function buildManifestJson(input: {
|
|
|
514
611
|
start_url: startUrl,
|
|
515
612
|
scope,
|
|
516
613
|
display,
|
|
517
|
-
|
|
614
|
+
// A manifest carries ONE theme color, so it follows the boot scheme rather than the dual
|
|
615
|
+
// `<meta name="theme-color">` pair. Unchanged for the default (`'dark'`) and for `'auto'`.
|
|
616
|
+
theme_color: schemeColor,
|
|
518
617
|
background_color: backgroundColor,
|
|
519
618
|
icons: [
|
|
520
619
|
{
|
|
@@ -542,7 +641,17 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
|
|
|
542
641
|
const manifestEnabled = options.manifest !== false
|
|
543
642
|
|
|
544
643
|
const themeColor = resolveThemeColors(options.themeColor)
|
|
545
|
-
|
|
644
|
+
// The boot scheme picks which half of the pair is "the" single color (manifest theme_color, the
|
|
645
|
+
// default paint). `'auto'` and `false` keep the historical dark default.
|
|
646
|
+
const colorScheme = options.colorScheme ?? 'dark'
|
|
647
|
+
const schemeColor = colorScheme === 'light' ? themeColor.light : themeColor.dark
|
|
648
|
+
const backgroundColor = options.backgroundColor ?? schemeColor
|
|
649
|
+
const bootStyle = buildBootStyle({
|
|
650
|
+
colorScheme,
|
|
651
|
+
themeColor,
|
|
652
|
+
backgroundColor,
|
|
653
|
+
explicitBackground: options.backgroundColor !== undefined,
|
|
654
|
+
})
|
|
546
655
|
|
|
547
656
|
// Icon hrefs, the manifest link, and the manifest's start_url/scope/icon src all depend on
|
|
548
657
|
// Vite's resolved `base`, which is only known once `configResolved` fires — so manifestJson and
|
|
@@ -569,7 +678,7 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
|
|
|
569
678
|
scope,
|
|
570
679
|
id,
|
|
571
680
|
display,
|
|
572
|
-
|
|
681
|
+
schemeColor,
|
|
573
682
|
backgroundColor,
|
|
574
683
|
iconsDir,
|
|
575
684
|
base,
|
|
@@ -579,7 +688,7 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
|
|
|
579
688
|
options,
|
|
580
689
|
shortName,
|
|
581
690
|
themeColor,
|
|
582
|
-
|
|
691
|
+
bootStyle,
|
|
583
692
|
iconsEnabled,
|
|
584
693
|
iconsDir,
|
|
585
694
|
darkreader,
|
|
@@ -588,9 +697,20 @@ function createMainPlugin(options: BasaltAppOptions): Plugin {
|
|
|
588
697
|
manifestHref,
|
|
589
698
|
})
|
|
590
699
|
},
|
|
591
|
-
transformIndexHtml
|
|
592
|
-
|
|
593
|
-
|
|
700
|
+
transformIndexHtml: {
|
|
701
|
+
// `pre` so this plugin's tags are the FIRST head-prepend contributions Vite collects — that
|
|
702
|
+
// ordering is what puts the hoisted <meta charset> at the very top of <head> rather than
|
|
703
|
+
// behind another plugin's tags. Vite injects all collected tags after every hook has run.
|
|
704
|
+
order: 'pre',
|
|
705
|
+
handler(html) {
|
|
706
|
+
const hoisted = hoistCharset(html)
|
|
707
|
+
const tags = [
|
|
708
|
+
...(hoisted.tag ? [hoisted.tag] : []),
|
|
709
|
+
...(hasViewportMeta(html) ? [] : [VIEWPORT_TAG]),
|
|
710
|
+
...staticTags,
|
|
711
|
+
]
|
|
712
|
+
return { html: hoisted.html, tags }
|
|
713
|
+
},
|
|
594
714
|
},
|
|
595
715
|
generateBundle() {
|
|
596
716
|
if (!manifestEnabled) return
|
|
@@ -666,13 +786,21 @@ function createServiceWorkerPlugin(
|
|
|
666
786
|
* manifest `theme_color` both need a FLAT color, so the flattening step is the contract this
|
|
667
787
|
* plugin owes its output regardless of which shape the palette hands it.
|
|
668
788
|
*
|
|
789
|
+
* Two behaviours worth knowing before adopting it, both of which the plugin owns because the
|
|
790
|
+
* consumer cannot reach them:
|
|
791
|
+
* • It HOISTS the shell's `<meta charset>` ahead of everything it injects, so the encoding
|
|
792
|
+
* declaration stays inside the spec's first 1024 bytes (see `hoistCharset`).
|
|
793
|
+
* • The anti-FOUC `<style>` expires the moment Mantine resolves the real scheme, and `colorScheme`
|
|
794
|
+
* selects which surface it paints until then (see `BOOT_SCOPE` / `buildBootStyle`).
|
|
795
|
+
*
|
|
669
796
|
* @example
|
|
670
797
|
* // vite.config.ts
|
|
671
798
|
* import { basaltAppPlugin, basaltViteConfig } from 'basalt-ui/vite'
|
|
672
799
|
*
|
|
673
800
|
* export default {
|
|
674
801
|
* ...basaltViteConfig({ port: 5173 }),
|
|
675
|
-
*
|
|
802
|
+
* // colorScheme mirrors <BasaltProvider defaultColorScheme> — omit it for the dark default.
|
|
803
|
+
* plugins: [react(), ...basaltAppPlugin({ name: 'Argo', colorScheme: 'auto' })],
|
|
676
804
|
* }
|
|
677
805
|
*/
|
|
678
806
|
export function basaltAppPlugin(options: BasaltAppOptions): PluginOption[] {
|