@ultimat3/render 20.1.6 → 20.2.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.
- package/package.json +5 -5
- package/src/head.ts +39 -9
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/render",
|
|
3
|
-
"version": "20.1
|
|
3
|
+
"version": "20.2.1",
|
|
4
4
|
"description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"test": "bun test"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ultimat3/cache": "20.1
|
|
40
|
-
"@ultimat3/core": "20.1
|
|
41
|
-
"@ultimat3/i18n": "20.1
|
|
42
|
-
"@ultimat3/seo": "20.1
|
|
39
|
+
"@ultimat3/cache": "20.2.1",
|
|
40
|
+
"@ultimat3/core": "20.2.1",
|
|
41
|
+
"@ultimat3/i18n": "20.2.1",
|
|
42
|
+
"@ultimat3/seo": "20.2.1",
|
|
43
43
|
"sass": "1.102.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/head.ts
CHANGED
|
@@ -161,10 +161,17 @@ function carriesJson(tag: HeadTag): boolean {
|
|
|
161
161
|
return type.trim().toLowerCase().endsWith('json');
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
export type ThemeFallback = 'light' | 'dark' | 'system';
|
|
165
|
+
|
|
164
166
|
export interface ThemeScriptOptions {
|
|
165
167
|
/** Attribute the tokens key off. Never a class, never a raw colour. */
|
|
166
168
|
readonly attribute?: string;
|
|
167
169
|
readonly storageKey?: string;
|
|
170
|
+
/**
|
|
171
|
+
* What a visitor with no stored choice gets. `'system'` (the default) asks the OS; `'dark'` or
|
|
172
|
+
* `'light'` is the app's own opinion — the seam `theme.defaultMode` in `app.config.ts` reaches.
|
|
173
|
+
*/
|
|
174
|
+
readonly fallback?: ThemeFallback;
|
|
168
175
|
/** Hard cap; a theme script that grows past this is no longer "one inlined script". */
|
|
169
176
|
readonly maxBytes?: number;
|
|
170
177
|
}
|
|
@@ -172,22 +179,45 @@ export interface ThemeScriptOptions {
|
|
|
172
179
|
export const THEME_SCRIPT_MAX_BYTES = 512;
|
|
173
180
|
|
|
174
181
|
/**
|
|
175
|
-
* The
|
|
176
|
-
*
|
|
177
|
-
*
|
|
182
|
+
* The storage key `@ultimat3/ui`'s `ThemeToggle` reads (`THEME_STORAGE_KEY` there). One literal on
|
|
183
|
+
* each side, pinned equal by a test in `@ultimat3/cli`: render sits below ui in the tier table and
|
|
184
|
+
* cannot import it, and the two keys disagreeing was exactly the bug — the boot stamped one key,
|
|
185
|
+
* the toggle wrote another, and a visitor's choice never survived a reload.
|
|
178
186
|
*/
|
|
179
|
-
export
|
|
187
|
+
export const THEME_STORAGE_KEY = 'ultimate.theme';
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The script's text alone, so the CSP hash and the tag come from one string: a policy hashed from
|
|
191
|
+
* a restatement admits a script that is not the one the document carries.
|
|
192
|
+
*
|
|
193
|
+
* Only `"light"` and `"dark"` are honoured from storage — anything else (a stale value, a typo,
|
|
194
|
+
* another app's key) falls through to `fallback`, so no page can be stamped with a scheme the
|
|
195
|
+
* tokens have no block for.
|
|
196
|
+
*/
|
|
197
|
+
export function themeScriptBody(options: ThemeScriptOptions = {}): string {
|
|
180
198
|
const attribute = options.attribute ?? 'data-theme';
|
|
181
|
-
const storageKey = options.storageKey ??
|
|
199
|
+
const storageKey = options.storageKey ?? THEME_STORAGE_KEY;
|
|
200
|
+
const fallback = options.fallback ?? 'system';
|
|
201
|
+
const os = 'matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"';
|
|
202
|
+
const otherwise = fallback === 'system' ? os : JSON.stringify(fallback);
|
|
182
203
|
// `JSON.stringify`, never a value pasted between two quotes: both options land INSIDE a JS
|
|
183
204
|
// string in a `<script>` body, where one `"` ends the string and the rest is code the page runs.
|
|
184
205
|
// Author-supplied today — the same status every `emitIslandAttributes` value had before it was
|
|
185
206
|
// routed through `html.ts`. The element's own raw-text rule is applied by `renderTag` below.
|
|
186
|
-
|
|
187
|
-
`try{var
|
|
188
|
-
`
|
|
189
|
-
`document.documentElement.setAttribute(${JSON.stringify(attribute)},t)}catch(e){}
|
|
207
|
+
return (
|
|
208
|
+
`try{var s=localStorage.getItem(${JSON.stringify(storageKey)}),` +
|
|
209
|
+
`t=s==="light"||s==="dark"?s:${otherwise};` +
|
|
210
|
+
`document.documentElement.setAttribute(${JSON.stringify(attribute)},t)}catch(e){}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
190
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The injection point for the no-flash theme flip. It sets a semantic attribute and
|
|
216
|
+
* nothing else — every colour is a token, so the whole scheme swap is one attribute.
|
|
217
|
+
* Returns a `HeadTag` so it participates in dedupe like any other tag.
|
|
218
|
+
*/
|
|
219
|
+
export function themeScript(options: ThemeScriptOptions = {}): HeadTag {
|
|
220
|
+
const source = themeScriptBody(options);
|
|
191
221
|
const bytes = new TextEncoder().encode(source).byteLength;
|
|
192
222
|
// `bytes > NaN` is false for every script, so a cap that arrived non-finite does not admit a
|
|
193
223
|
// bigger script — it removes the only budget a 0kb `site/` route has.
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ export type {
|
|
|
42
42
|
HeadTagKind,
|
|
43
43
|
LdRenderer,
|
|
44
44
|
MetaRenderer,
|
|
45
|
+
ThemeFallback,
|
|
45
46
|
ThemeScriptOptions,
|
|
46
47
|
} from './head';
|
|
47
48
|
export {
|
|
@@ -50,7 +51,9 @@ export {
|
|
|
50
51
|
mergeHead,
|
|
51
52
|
renderHead,
|
|
52
53
|
THEME_SCRIPT_MAX_BYTES,
|
|
54
|
+
THEME_STORAGE_KEY,
|
|
53
55
|
themeScript,
|
|
56
|
+
themeScriptBody,
|
|
54
57
|
} from './head';
|
|
55
58
|
export { headTagKey, seoRenderers, toHeadTag } from './head-seo';
|
|
56
59
|
export type { IslandDirective } from './hydrate';
|