@askrjs/themes 0.0.17 → 0.0.20
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/README.md +5 -2
- package/THEMING.md +8 -1
- package/dist/components/_internal/block-layout.js +1 -1
- package/dist/components/_internal/block-layout.js.map +1 -1
- package/dist/components/_internal/style.js +13 -22
- package/dist/components/_internal/style.js.map +1 -1
- package/dist/components/grid/grid.js +5 -42
- package/dist/components/grid/grid.js.map +1 -1
- package/dist/components/jsx-types.d.ts +0 -3
- package/dist/components/theme/theme.js +37 -15
- package/dist/components/theme/theme.js.map +1 -1
- package/dist/ssr.js +23 -11
- package/dist/ssr.js.map +1 -1
- package/dist/themes/default/index.css +8 -2
- package/package.json +6 -6
- package/src/components/_internal/block-layout.ts +2 -2
- package/src/components/_internal/style.ts +18 -29
- package/src/components/grid/grid.tsx +6 -57
- package/src/components/jsx-types.ts +0 -5
- package/src/components/theme/theme.tsx +64 -24
- package/src/ssr.ts +45 -20
- package/src/themes/default/styles/display/coverage.css +3 -1
- package/src/themes/default/styles/layout/block.css +4 -0
- package/src/themes/default/tokens.css +1 -1
- package/templates/theme/tokens.css +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSX } from "@askrjs/askr/jsx-runtime";
|
|
2
2
|
import { defineScope, getSignal, readScope, state } from "@askrjs/askr";
|
|
3
|
-
import type
|
|
3
|
+
import { cloneElement, isElement, type JSXElement } from "@askrjs/askr/foundations/structures";
|
|
4
4
|
import { Button } from "@askrjs/ui";
|
|
5
5
|
import type { ButtonNativeProps, PressEvent } from "@askrjs/ui";
|
|
6
6
|
|
|
@@ -67,7 +67,6 @@ export const CAT_THEME_OPTIONS: readonly ThemeOption[] = [
|
|
|
67
67
|
|
|
68
68
|
const DEFAULT_STORAGE_KEY = "askr-theme";
|
|
69
69
|
const STATIC_CHILDREN = Symbol.for("askr.static-children");
|
|
70
|
-
const STATIC_CHILD_SLOTS_CACHE = Symbol.for("__askrStaticChildSlots");
|
|
71
70
|
const documentThemeCoordinators = new WeakMap<Document, ThemeCoordinator>();
|
|
72
71
|
type ThemeCoordinator = ReturnType<typeof createThemeCoordinator>;
|
|
73
72
|
type InternalThemeScopeValue = ThemeScopeValue & {
|
|
@@ -110,7 +109,9 @@ export function ThemeScope(props: ThemeScopeProps): JSX.Element {
|
|
|
110
109
|
const ownedCoordinator = state<ThemeCoordinator>(getDefaultThemeCoordinator())();
|
|
111
110
|
const coordinator = parentScope.coordinator ?? ownedCoordinator;
|
|
112
111
|
const scopeDepth = parentScope.depth + 1;
|
|
113
|
-
coordinator.register(scopeId, scopeDepth, currentTheme, scopeSignal)
|
|
112
|
+
coordinator.register(scopeId, scopeDepth, currentTheme, scopeSignal, (nextTheme) => {
|
|
113
|
+
if (themeState() !== nextTheme) themeState.set(nextTheme);
|
|
114
|
+
});
|
|
114
115
|
|
|
115
116
|
const setTheme = (nextTheme: ThemeName) => {
|
|
116
117
|
themeState.set(nextTheme);
|
|
@@ -143,6 +144,29 @@ export function ThemeScope(props: ThemeScopeProps): JSX.Element {
|
|
|
143
144
|
(nextTheme) => localResolvedSystemTheme.set(nextTheme),
|
|
144
145
|
scopeSignal,
|
|
145
146
|
);
|
|
147
|
+
if (element && typeof window !== "undefined") {
|
|
148
|
+
const onStorage = (event: StorageEvent) => {
|
|
149
|
+
let storageMatches = event.storageArea == null;
|
|
150
|
+
try {
|
|
151
|
+
storageMatches ||= event.storageArea === window.localStorage;
|
|
152
|
+
} catch {
|
|
153
|
+
// Locked-down/private contexts may deny access to localStorage.
|
|
154
|
+
}
|
|
155
|
+
if (!storageMatches || event.key !== storageKey) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const nextTheme = event.newValue as ThemeName | null;
|
|
159
|
+
if (!nextTheme) return;
|
|
160
|
+
themeState.set(nextTheme);
|
|
161
|
+
coordinator.activate(scopeId, nextTheme);
|
|
162
|
+
};
|
|
163
|
+
window.addEventListener("storage", onStorage);
|
|
164
|
+
scopeSignal.addEventListener(
|
|
165
|
+
"abort",
|
|
166
|
+
() => window.removeEventListener("storage", onStorage),
|
|
167
|
+
{ once: true },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
146
170
|
if (!element || persistenceAdoption.complete) return;
|
|
147
171
|
persistenceAdoption.complete = true;
|
|
148
172
|
const storedTheme = readStoredTheme(storageKey);
|
|
@@ -173,6 +197,20 @@ function getDefaultThemeCoordinator(): ThemeCoordinator {
|
|
|
173
197
|
return coordinator;
|
|
174
198
|
}
|
|
175
199
|
|
|
200
|
+
function removeEmptyGeneratedStyleRegistries(root: Node | null): void {
|
|
201
|
+
const ownerDocument =
|
|
202
|
+
root?.nodeType === 9
|
|
203
|
+
? (root as Document)
|
|
204
|
+
: (root?.ownerDocument ?? (typeof document === "undefined" ? null : document));
|
|
205
|
+
if (!ownerDocument) return;
|
|
206
|
+
|
|
207
|
+
for (const registry of ownerDocument.querySelectorAll<HTMLStyleElement>(
|
|
208
|
+
"style[data-askr-style-registry]",
|
|
209
|
+
)) {
|
|
210
|
+
if (!registry.textContent?.trim()) registry.remove();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
176
214
|
function createThemeCoordinator() {
|
|
177
215
|
const scopes = new Map<
|
|
178
216
|
symbol,
|
|
@@ -181,6 +219,7 @@ function createThemeCoordinator() {
|
|
|
181
219
|
sequence: number;
|
|
182
220
|
theme: ThemeName;
|
|
183
221
|
signal: AbortSignal;
|
|
222
|
+
onThemeChange: (themeName: ThemeName) => void;
|
|
184
223
|
}
|
|
185
224
|
>();
|
|
186
225
|
let nextSequence = 0;
|
|
@@ -213,6 +252,7 @@ function createThemeCoordinator() {
|
|
|
213
252
|
setTimeout(() => {
|
|
214
253
|
scheduled = false;
|
|
215
254
|
syncActive();
|
|
255
|
+
if (scopes.size === 0) removeEmptyGeneratedStyleRegistries(root);
|
|
216
256
|
}, 0);
|
|
217
257
|
};
|
|
218
258
|
|
|
@@ -234,13 +274,20 @@ function createThemeCoordinator() {
|
|
|
234
274
|
}
|
|
235
275
|
schedule();
|
|
236
276
|
},
|
|
237
|
-
register(
|
|
277
|
+
register(
|
|
278
|
+
id: symbol,
|
|
279
|
+
depth: number,
|
|
280
|
+
themeName: ThemeName,
|
|
281
|
+
signal: AbortSignal,
|
|
282
|
+
onThemeChange: (themeName: ThemeName) => void,
|
|
283
|
+
) {
|
|
238
284
|
const existing = scopes.get(id);
|
|
239
285
|
scopes.set(id, {
|
|
240
286
|
depth,
|
|
241
287
|
sequence: existing?.sequence ?? nextSequence++,
|
|
242
288
|
theme: themeName,
|
|
243
289
|
signal,
|
|
290
|
+
onThemeChange,
|
|
244
291
|
});
|
|
245
292
|
if (!existing) {
|
|
246
293
|
signal.addEventListener(
|
|
@@ -259,6 +306,13 @@ function createThemeCoordinator() {
|
|
|
259
306
|
const scope = scopes.get(id);
|
|
260
307
|
if (scope) scope.theme = themeName;
|
|
261
308
|
explicitOwner = id;
|
|
309
|
+
const ownerDepth = scope?.depth;
|
|
310
|
+
for (const [scopeId, registered] of scopes) {
|
|
311
|
+
if (scopeId !== id && registered.depth === ownerDepth) {
|
|
312
|
+
registered.theme = themeName;
|
|
313
|
+
registered.onThemeChange(themeName);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
262
316
|
syncThemeTarget(target(), themeName);
|
|
263
317
|
},
|
|
264
318
|
});
|
|
@@ -422,16 +476,6 @@ function renderThemeToggleIconSlots(
|
|
|
422
476
|
));
|
|
423
477
|
}
|
|
424
478
|
|
|
425
|
-
function isJSXElement(value: unknown): value is JSXElement {
|
|
426
|
-
return (
|
|
427
|
-
typeof value === "object" &&
|
|
428
|
-
value !== null &&
|
|
429
|
-
"$$typeof" in value &&
|
|
430
|
-
"type" in value &&
|
|
431
|
-
"props" in value
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
479
|
function cloneThemeToggleIcon(icon: unknown, key?: string): unknown {
|
|
436
480
|
if (Array.isArray(icon)) {
|
|
437
481
|
const clonedChildren = icon.map((child) => cloneThemeToggleIcon(child));
|
|
@@ -444,7 +488,7 @@ function cloneThemeToggleIcon(icon: unknown, key?: string): unknown {
|
|
|
444
488
|
return clonedChildren;
|
|
445
489
|
}
|
|
446
490
|
|
|
447
|
-
if (!
|
|
491
|
+
if (!isElement(icon)) return icon;
|
|
448
492
|
|
|
449
493
|
const props = icon.props as Record<string, unknown> | undefined;
|
|
450
494
|
const clonedProps = props ? { ...props } : {};
|
|
@@ -453,15 +497,11 @@ function cloneThemeToggleIcon(icon: unknown, key?: string): unknown {
|
|
|
453
497
|
clonedProps.children = cloneThemeToggleIcon(clonedProps.children);
|
|
454
498
|
}
|
|
455
499
|
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
...
|
|
459
|
-
key:
|
|
460
|
-
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
delete (clonedIcon as Record<symbol, unknown>)[STATIC_CHILD_SLOTS_CACHE];
|
|
464
|
-
return clonedIcon;
|
|
500
|
+
const clonedIcon = cloneElement(icon, clonedProps);
|
|
501
|
+
return {
|
|
502
|
+
...clonedIcon,
|
|
503
|
+
key: icon.key ?? key ?? null,
|
|
504
|
+
} satisfies JSXElement;
|
|
465
505
|
}
|
|
466
506
|
|
|
467
507
|
function syncThemeTarget(
|
package/src/ssr.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { styleRulesForHtml } from "./components/_internal/style";
|
|
2
|
-
|
|
3
1
|
const STYLE_REGISTRY_ATTR = "data-askr-style-registry";
|
|
2
|
+
const STYLE_CLASS_PREFIX = "ak-style-";
|
|
3
|
+
const CLASS_ATTRIBUTE_PATTERN = /\sclass=(?:"([^"]*)"|'([^']*)')/g;
|
|
4
|
+
const MAX_STYLE_RULES = 512;
|
|
4
5
|
|
|
5
6
|
type DocumentRenderArgsLike = {
|
|
6
7
|
appHtml: string;
|
|
@@ -22,6 +23,16 @@ function escapeStyleRawText(value: string): string {
|
|
|
22
23
|
return value.replace(/<\/style/gi, "<\\/style");
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function generatedStyleClasses(html: string): Set<string> {
|
|
27
|
+
const classes = new Set<string>();
|
|
28
|
+
for (const attribute of html.matchAll(CLASS_ATTRIBUTE_PATTERN)) {
|
|
29
|
+
for (const className of (attribute[1] ?? attribute[2] ?? "").split(/\s+/)) {
|
|
30
|
+
if (className.startsWith(STYLE_CLASS_PREFIX)) classes.add(className);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return classes;
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
function findHeadEnd(html: string): number {
|
|
26
37
|
let index = 0;
|
|
27
38
|
while (index < html.length) {
|
|
@@ -72,24 +83,38 @@ export function withThemeStyles<TArgs extends DocumentRenderArgsLike>(
|
|
|
72
83
|
return (args) => {
|
|
73
84
|
const documentHtml = documentRenderer(args);
|
|
74
85
|
const registeredStyles = args.context.styles;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
const generatedClasses = generatedStyleClasses(documentHtml);
|
|
87
|
+
if (registeredStyles === undefined) {
|
|
88
|
+
if (generatedClasses.size > 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Generated theme classes require request-local SSR style registrations from @askrjs/askr.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return documentHtml;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const styles = new Map<string, string>();
|
|
97
|
+
for (const style of registeredStyles) {
|
|
98
|
+
if (!style.id || !style.cssText) {
|
|
99
|
+
throw new TypeError("Invalid SSR theme style registration.");
|
|
100
|
+
}
|
|
101
|
+
const existing = styles.get(style.id);
|
|
102
|
+
if (existing !== undefined && existing !== style.cssText) {
|
|
103
|
+
throw new RangeError(`SSR style registration collision for ${JSON.stringify(style.id)}.`);
|
|
104
|
+
}
|
|
105
|
+
styles.set(style.id, style.cssText);
|
|
106
|
+
if (styles.size > MAX_STYLE_RULES) {
|
|
107
|
+
throw new RangeError("Theme style registry capacity exceeded.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const className of generatedClasses) {
|
|
111
|
+
if (!styles.has(className)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Missing request-local SSR style registration for ${JSON.stringify(className)}.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const rules = Array.from(styles.values());
|
|
93
118
|
if (rules.length === 0) return documentHtml;
|
|
94
119
|
|
|
95
120
|
const nonce =
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
:where([data-slot="theme-picker"]) {
|
|
9
|
+
appearance: none;
|
|
9
10
|
display: inline-flex;
|
|
10
|
-
|
|
11
|
+
block-size: var(--ak-density-control-height-md, 2.25rem);
|
|
12
|
+
min-block-size: var(--ak-density-control-height-md, 2.25rem);
|
|
11
13
|
min-inline-size: min(8rem, 100%);
|
|
12
14
|
max-inline-size: 100%;
|
|
13
15
|
padding-inline: var(--ak-density-control-padding-x-md) 2.1rem;
|
|
@@ -24,17 +24,21 @@
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
:where([data-slot="nav-group-label"]) {
|
|
27
|
+
max-inline-size: 100%;
|
|
27
28
|
margin: 0;
|
|
28
29
|
color: var(--ak-color-text-muted);
|
|
29
30
|
font-size: var(--ak-font-size-xs);
|
|
30
31
|
font-weight: var(--ak-font-weight-medium);
|
|
31
32
|
line-height: var(--ak-line-height-tight);
|
|
33
|
+
overflow-wrap: anywhere;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
:where([data-slot="nav-item"]) {
|
|
37
|
+
max-inline-size: 100%;
|
|
35
38
|
min-block-size: 2rem;
|
|
36
39
|
color: var(--ak-color-text-muted);
|
|
37
40
|
text-decoration: none;
|
|
41
|
+
white-space: normal;
|
|
38
42
|
transition:
|
|
39
43
|
background var(--ak-duration-fast) var(--ak-ease-standard),
|
|
40
44
|
color var(--ak-duration-fast) var(--ak-ease-standard);
|