@codenhub/theme 0.1.0 → 0.1.2
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/LICENSE +201 -201
- package/README.md +17 -429
- package/dist/index.d.ts +41 -15
- package/dist/index.js +1 -1
- package/docs/changelog/0.1.2.md +15 -0
- package/docs/changelog/index.md +9 -0
- package/docs/index.md +60 -0
- package/docs/reference/index.md +381 -0
- package/docs/ssr-and-pre-paint.md +46 -0
- package/docs/tokens-and-persistence.md +51 -0
- package/llms-full.txt +213 -0
- package/llms.txt +19 -0
- package/package.json +29 -17
package/README.md
CHANGED
|
@@ -1,457 +1,45 @@
|
|
|
1
1
|
# @codenhub/theme
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Zero-dependency browser theme preference, persistence, DOM, and token manager.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
pnpm add @codenhub/theme
|
|
9
|
-
npm install @codenhub/theme
|
|
10
|
-
yarn add @codenhub/theme
|
|
11
|
-
bun add @codenhub/theme
|
|
12
9
|
```
|
|
13
10
|
|
|
14
11
|
## Usage
|
|
15
12
|
|
|
16
|
-
By default, `init()` uses a valid stored preference first. If there is no valid stored preference, it maps the OS color scheme to `light` or `dark`.
|
|
17
|
-
|
|
18
13
|
```ts
|
|
19
|
-
import { createTheme } from "@codenhub/theme";
|
|
20
|
-
|
|
21
|
-
const theme = createTheme({ isTailwindCss: false, shouldApplyClass: true });
|
|
14
|
+
import { createTheme, getPrePaintScript } from "@codenhub/theme";
|
|
22
15
|
|
|
23
|
-
theme.init();
|
|
16
|
+
const theme = createTheme().init();
|
|
24
17
|
theme.set("dark");
|
|
25
|
-
theme.toggle();
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Call `destroy()` during app or test cleanup when the instance is no longer used.
|
|
29
|
-
|
|
30
|
-
### Preventing Flash of Unstyled Content (FOUC)
|
|
31
|
-
|
|
32
|
-
Because the client-side JS bundle loads asynchronously, there can be a brief flash of the default theme before the theme manager initializes. To prevent this, inject a tiny blocking script in your HTML `<head>` before any stylesheet or content:
|
|
33
|
-
|
|
34
|
-
```html
|
|
35
|
-
<script>
|
|
36
|
-
(function () {
|
|
37
|
-
try {
|
|
38
|
-
const key = "app-theme-preference";
|
|
39
|
-
const attribute = "data-theme";
|
|
40
|
-
let theme = "light";
|
|
41
|
-
try {
|
|
42
|
-
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
43
|
-
theme = "dark";
|
|
44
|
-
}
|
|
45
|
-
} catch (_) {}
|
|
46
|
-
try {
|
|
47
|
-
const stored = localStorage.getItem(key);
|
|
48
|
-
// Add all your configured theme names here to validate the stored value.
|
|
49
|
-
const allowed = ["light", "dark"];
|
|
50
|
-
if (stored && allowed.includes(stored)) {
|
|
51
|
-
theme = stored;
|
|
52
|
-
}
|
|
53
|
-
} catch (_) {}
|
|
54
|
-
document.documentElement.setAttribute(attribute, theme);
|
|
55
|
-
|
|
56
|
-
// List the names of all your dark-scheme themes here.
|
|
57
|
-
// Using an explicit set avoids false matches from substring checks (e.g. "midnight" is dark
|
|
58
|
-
// but would not match "dark"; "darkroom" would match but may not be a dark theme).
|
|
59
|
-
const darkThemes = new Set(["dark"]);
|
|
60
|
-
const isDark = darkThemes.has(theme);
|
|
61
|
-
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
|
|
62
|
-
document.documentElement.classList.add("theme-" + theme);
|
|
63
|
-
|
|
64
|
-
// If using Tailwind CSS, also toggle "dark" class:
|
|
65
|
-
// document.documentElement.classList.toggle("dark", isDark);
|
|
66
|
-
} catch (_) {}
|
|
67
|
-
})();
|
|
68
|
-
</script>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
> [!WARNING]
|
|
72
|
-
> If you customize `systemTheme`, `themes`, `storageKey`, or `attribute` in `createTheme`, make sure to update the inline FOUC script to match those values. Mismatched configurations will cause a flash of the wrong theme. Also, if you set `shouldApplyClass` to `false` or use a custom class resolver, update or remove the FOUC class application block (`classList.add`) accordingly to prevent visual shifts on initialization.
|
|
73
|
-
|
|
74
|
-
## Reference
|
|
75
|
-
|
|
76
|
-
### `@codenhub/theme`
|
|
77
|
-
|
|
78
|
-
Primary entrypoint for the theme preference API.
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
import { createTheme, DARK_THEME, LIGHT_THEME, THEME_CHANGE_EVENT } from "@codenhub/theme";
|
|
82
|
-
import type {
|
|
83
|
-
SystemThemeMap,
|
|
84
|
-
Theme,
|
|
85
|
-
ThemeChangeDetail,
|
|
86
|
-
ThemeChangeListener,
|
|
87
|
-
ThemeChangeSource,
|
|
88
|
-
ThemeClassResolver,
|
|
89
|
-
ThemeDefinition,
|
|
90
|
-
ThemeOptions,
|
|
91
|
-
} from "@codenhub/theme";
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Supported import paths:
|
|
95
|
-
|
|
96
|
-
| Path | Description |
|
|
97
|
-
| ----------------- | ----------------------------------- |
|
|
98
|
-
| `@codenhub/theme` | Main JavaScript and TypeScript API. |
|
|
99
18
|
|
|
100
|
-
|
|
19
|
+
// Get inline IIFE script string for <head> to prevent FOUC:
|
|
20
|
+
const script = theme.getPrePaintScript(); // or standalone getPrePaintScript()
|
|
101
21
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
function createTheme<TSchema extends Record<string, string> = Record<string, string>>(
|
|
106
|
-
options?: ThemeOptions<TSchema>,
|
|
107
|
-
): Theme<TSchema>;
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
#### `Theme`
|
|
111
|
-
|
|
112
|
-
Manages the active theme, storage preference, DOM attribute, `colorScheme` style, classes, system preference listener, dynamic tokens, and change notifications.
|
|
113
|
-
|
|
114
|
-
```ts
|
|
115
|
-
interface Theme<TSchema extends Record<string, string> = Record<string, string>> {
|
|
116
|
-
init(tokens?: Partial<Record<keyof TSchema, string>>): this;
|
|
117
|
-
get(): ThemeDefinition<TSchema>;
|
|
118
|
-
set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
119
|
-
toggle(tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
120
|
-
clearPreference(): ThemeDefinition<TSchema>;
|
|
121
|
-
getStored(): string | null;
|
|
122
|
-
getSystem(): ThemeDefinition<TSchema>;
|
|
123
|
-
subscribe(listener: ThemeChangeListener<TSchema>): () => void;
|
|
124
|
-
destroy(): void;
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Import from `@codenhub/theme`.
|
|
129
|
-
|
|
130
|
-
The factory throws `Error` when configured theme names are empty, duplicated, invalid for CSS class application, or referenced by `defaultTheme` or `systemTheme` without being configured.
|
|
131
|
-
|
|
132
|
-
##### `init()`
|
|
133
|
-
|
|
134
|
-
Registers the system preference listener, resolves the initial theme, applies it, and emits a change with source `"init"`. Can optionally accept initial token value overrides.
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
function init(tokens?: Partial<Record<keyof TSchema, string>>): this;
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Repeated calls do not register duplicate system preference listeners.
|
|
141
|
-
|
|
142
|
-
##### `get()`
|
|
143
|
-
|
|
144
|
-
Returns the active theme definition including any active merged tokens. If a token in `tokenSchema` is not defined in JS for the active theme, its value is dynamically resolved from the computed styles of the DOM in browser environments.
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
function get(): ThemeDefinition<TSchema>;
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
##### `set()`
|
|
151
|
-
|
|
152
|
-
Activates a configured theme by name, applies any dynamic token overrides, and stores the explicit preference when browser storage is available. Active overrides persist across subsequent theme changes unless cleared (by passing new overrides or an empty object).
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
function set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Throws `Error` when `name` is not configured.
|
|
159
|
-
|
|
160
|
-
##### `toggle()`
|
|
161
|
-
|
|
162
|
-
Toggles between the configured system light and dark themes based on the active theme's `colorScheme`, applies any dynamic token overrides, then stores the explicit preference when browser storage is available. The target name is always taken from `systemTheme.light` or `systemTheme.dark`, not by cycling the active theme name. In multi-theme setups where the active theme is not one of the system themes, `toggle()` still targets `systemTheme.light` or `systemTheme.dark`. Active overrides persist across subsequent theme changes unless cleared (by passing new overrides or an empty object).
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
function toggle(tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
##### `clearPreference()`
|
|
169
|
-
|
|
170
|
-
Removes the stored preference and activates the current system theme.
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
function clearPreference(): ThemeDefinition<TSchema>;
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
##### `getStored()`
|
|
177
|
-
|
|
178
|
-
Returns the stored theme name when it exists and is configured.
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
function getStored(): string | null;
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
Returns `null` during SSR, when storage is unavailable, when storage access throws, or when the stored name is not configured.
|
|
185
|
-
|
|
186
|
-
##### `getSystem()`
|
|
187
|
-
|
|
188
|
-
Returns the configured theme for the current `prefers-color-scheme` value.
|
|
189
|
-
|
|
190
|
-
```ts
|
|
191
|
-
function getSystem(): ThemeDefinition<TSchema>;
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Returns the default theme during SSR or when `matchMedia` is unavailable.
|
|
195
|
-
|
|
196
|
-
##### `subscribe()`
|
|
197
|
-
|
|
198
|
-
Registers an in-process listener for theme changes.
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
function subscribe(listener: ThemeChangeListener<TSchema>): () => void;
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
Returns an unsubscribe function.
|
|
205
|
-
|
|
206
|
-
##### `destroy()`
|
|
207
|
-
|
|
208
|
-
Removes the system preference listener, clears in-process subscribers, and resets active tokens and the active theme name to `defaultTheme`. Safe to call before re-initializing with `init()`.
|
|
209
|
-
|
|
210
|
-
```ts
|
|
211
|
-
function destroy(): void;
|
|
22
|
+
// Remove media-query/storage listeners and subscribers on teardown.
|
|
23
|
+
theme.destroy({ revertDom: true });
|
|
212
24
|
```
|
|
213
25
|
|
|
214
|
-
|
|
26
|
+
## Documentation
|
|
215
27
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
themes?: readonly ThemeDefinition<TSchema>[];
|
|
221
|
-
defaultTheme?: string;
|
|
222
|
-
systemTheme?: SystemThemeMap;
|
|
223
|
-
storageKey?: string;
|
|
224
|
-
attribute?: string;
|
|
225
|
-
isTailwindCss?: boolean;
|
|
226
|
-
shouldApplyClass?: boolean | ThemeClassResolver<TSchema>;
|
|
227
|
-
tokenSchema?: TSchema;
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
| Option | Type | Default | Description |
|
|
232
|
-
| ------------------ | ------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
233
|
-
| `themes` | `readonly ThemeDefinition[]` | `[LIGHT_THEME, DARK_THEME]` | Defines available themes. |
|
|
234
|
-
| `defaultTheme` | `string` | `"light"` | Theme used before init and when browser APIs are unavailable. |
|
|
235
|
-
| `systemTheme` | `SystemThemeMap` | `{ light: "light", dark: "dark" }` | Maps OS light and dark preferences to configured theme names. |
|
|
236
|
-
| `storageKey` | `string` | `"app-theme-preference"` | Key used for `localStorage`. |
|
|
237
|
-
| `attribute` | `string` | `"data-theme"` | Attribute set on `document.documentElement`. |
|
|
238
|
-
| `isTailwindCss` | `boolean` | `false` | Toggles the `dark` class when the active theme has `colorScheme: "dark"`. |
|
|
239
|
-
| `shouldApplyClass` | `boolean` or `(theme: ThemeDefinition) => string` | `true` | Adds `theme-${name}`, no class, or a resolver-provided class to `document.documentElement`. |
|
|
240
|
-
| `tokenSchema` | `TSchema` | `undefined` | Schema mapping theme token names to their corresponding CSS Custom Property names. |
|
|
241
|
-
|
|
242
|
-
When class application is enabled, each theme application removes classes for all configured themes, then adds the class for the active theme.
|
|
243
|
-
|
|
244
|
-
#### `ThemeDefinition`
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
interface ThemeDefinition<TSchema extends Record<string, string> = Record<string, string>> {
|
|
248
|
-
name: string;
|
|
249
|
-
colorScheme: "light" | "dark";
|
|
250
|
-
tokens?: Partial<Record<keyof TSchema, string>>;
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
| Field | Type | Description |
|
|
255
|
-
| ------------- | ---------------------------------------- | ---------------------------------------------------------------- |
|
|
256
|
-
| `name` | `string` | Unique theme name used for storage, attributes, and class names. |
|
|
257
|
-
| `colorScheme` | `"light" \| "dark"` | Browser color scheme applied through `style.colorScheme`. |
|
|
258
|
-
| `tokens` | `Partial<Record<keyof TSchema, string>>` | Optional theme-specific static token values. |
|
|
259
|
-
|
|
260
|
-
#### `SystemThemeMap`
|
|
261
|
-
|
|
262
|
-
```ts
|
|
263
|
-
interface SystemThemeMap {
|
|
264
|
-
light: string;
|
|
265
|
-
dark: string;
|
|
266
|
-
}
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
| Field | Type | Description |
|
|
270
|
-
| ------- | -------- | ----------------------------------------------------------- |
|
|
271
|
-
| `light` | `string` | Configured theme name used when the OS preference is light. |
|
|
272
|
-
| `dark` | `string` | Configured theme name used when the OS preference is dark. |
|
|
273
|
-
|
|
274
|
-
#### `ThemeClassResolver`
|
|
275
|
-
|
|
276
|
-
Returns the class name applied to `document.documentElement` for a theme.
|
|
277
|
-
|
|
278
|
-
```ts
|
|
279
|
-
type ThemeClassResolver<TSchema extends Record<string, string> = Record<string, string>> = (
|
|
280
|
-
theme: ThemeDefinition<TSchema>,
|
|
281
|
-
) => string;
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
The returned class name must be a single non-empty class token without whitespace.
|
|
285
|
-
|
|
286
|
-
`init()`, `set()`, `toggle()`, `clearPreference()`, or system preference changes throw `Error` if the resolver returns an empty class name or a class name containing whitespace.
|
|
287
|
-
|
|
288
|
-
#### `ThemeChangeListener`
|
|
289
|
-
|
|
290
|
-
Listener passed to `theme.subscribe()`.
|
|
291
|
-
|
|
292
|
-
```ts
|
|
293
|
-
type ThemeChangeListener<TSchema extends Record<string, string> = Record<string, string>> = (
|
|
294
|
-
detail: ThemeChangeDetail<TSchema>,
|
|
295
|
-
) => void;
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
#### `THEME_CHANGE_EVENT`
|
|
299
|
-
|
|
300
|
-
Window event name dispatched after theme changes in browser environments.
|
|
301
|
-
|
|
302
|
-
```ts
|
|
303
|
-
const THEME_CHANGE_EVENT = "themechange";
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
#### `ThemeChangeDetail`
|
|
307
|
-
|
|
308
|
-
```ts
|
|
309
|
-
interface ThemeChangeDetail<TSchema extends Record<string, string> = Record<string, string>> {
|
|
310
|
-
name: string;
|
|
311
|
-
theme: ThemeDefinition<TSchema>;
|
|
312
|
-
source: ThemeChangeSource;
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
| Field | Type | Description |
|
|
317
|
-
| -------- | ------------------- | ----------------------------------------- |
|
|
318
|
-
| `name` | `string` | Active theme name after the change. |
|
|
319
|
-
| `theme` | `ThemeDefinition` | Active theme definition after the change. |
|
|
320
|
-
| `source` | `ThemeChangeSource` | Reason the theme change was emitted. |
|
|
321
|
-
|
|
322
|
-
#### `ThemeChangeSource`
|
|
323
|
-
|
|
324
|
-
Reason a theme change was emitted.
|
|
325
|
-
|
|
326
|
-
```ts
|
|
327
|
-
type ThemeChangeSource = "init" | "set" | "toggle" | "clearPreference" | "system";
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
| Value | Emitted when |
|
|
331
|
-
| ------------------- | -------------------------------------------------- |
|
|
332
|
-
| `"init"` | `init()` resolves and applies the initial theme. |
|
|
333
|
-
| `"set"` | `set()` applies an explicit theme preference. |
|
|
334
|
-
| `"toggle"` | `toggle()` switches between system light and dark. |
|
|
335
|
-
| `"clearPreference"` | `clearPreference()` removes stored preference. |
|
|
336
|
-
| `"system"` | OS color scheme changes with no stored preference. |
|
|
337
|
-
|
|
338
|
-
#### Built-In Themes
|
|
339
|
-
|
|
340
|
-
Built-in theme definitions.
|
|
341
|
-
|
|
342
|
-
```ts
|
|
343
|
-
const LIGHT_THEME: ThemeDefinition = { name: "light", colorScheme: "light" };
|
|
344
|
-
const DARK_THEME: ThemeDefinition = { name: "dark", colorScheme: "dark" };
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
## Examples
|
|
348
|
-
|
|
349
|
-
### Dynamic CSS Tokens
|
|
350
|
-
|
|
351
|
-
```ts
|
|
352
|
-
import { createTheme } from "@codenhub/theme";
|
|
353
|
-
|
|
354
|
-
// 1. Define schema
|
|
355
|
-
const tokenSchema = {
|
|
356
|
-
primary: "--color-primary",
|
|
357
|
-
background: "--color-bg",
|
|
358
|
-
} as const;
|
|
359
|
-
|
|
360
|
-
// 2. Initialize with schema and optional theme static token values
|
|
361
|
-
const theme = createTheme({
|
|
362
|
-
tokenSchema,
|
|
363
|
-
themes: [
|
|
364
|
-
{
|
|
365
|
-
name: "light",
|
|
366
|
-
colorScheme: "light",
|
|
367
|
-
tokens: { primary: "#0070f3", background: "#ffffff" },
|
|
368
|
-
},
|
|
369
|
-
{
|
|
370
|
-
name: "dark",
|
|
371
|
-
colorScheme: "dark",
|
|
372
|
-
tokens: { primary: "#3291ff", background: "#000000" },
|
|
373
|
-
},
|
|
374
|
-
],
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
theme.init();
|
|
378
|
-
|
|
379
|
-
// 3. Switch theme and pass runtime overrides (e.g. from dynamic branding API)
|
|
380
|
-
theme.set("dark", { primary: "#ff007f" });
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Define CSS Tokens
|
|
384
|
-
|
|
385
|
-
```css
|
|
386
|
-
:root,
|
|
387
|
-
[data-theme="light"] {
|
|
388
|
-
--color-background: white;
|
|
389
|
-
--color-foreground: black;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
[data-theme="dark"] {
|
|
393
|
-
--color-background: black;
|
|
394
|
-
--color-foreground: white;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
body {
|
|
398
|
-
background: var(--color-background);
|
|
399
|
-
color: var(--color-foreground);
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### Add More Themes
|
|
404
|
-
|
|
405
|
-
```ts
|
|
406
|
-
import { createTheme, DARK_THEME, LIGHT_THEME } from "@codenhub/theme";
|
|
407
|
-
|
|
408
|
-
const theme = createTheme({
|
|
409
|
-
themes: [LIGHT_THEME, DARK_THEME, { name: "high-contrast", colorScheme: "dark" }],
|
|
410
|
-
systemTheme: { light: "light", dark: "high-contrast" },
|
|
411
|
-
shouldApplyClass: (definition) => `mode-${definition.name}`,
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
theme.init();
|
|
415
|
-
theme.set("high-contrast");
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
### Listen For Changes
|
|
419
|
-
|
|
420
|
-
```ts
|
|
421
|
-
import { createTheme, THEME_CHANGE_EVENT, type ThemeChangeDetail } from "@codenhub/theme";
|
|
422
|
-
|
|
423
|
-
const theme = createTheme().init();
|
|
424
|
-
|
|
425
|
-
const unsubscribe = theme.subscribe((detail) => {
|
|
426
|
-
console.log(detail.name, detail.theme, detail.source);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
window.addEventListener(THEME_CHANGE_EVENT, (event) => {
|
|
430
|
-
const detail = (event as CustomEvent<ThemeChangeDetail>).detail;
|
|
431
|
-
|
|
432
|
-
console.log(detail.name);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
unsubscribe();
|
|
436
|
-
theme.destroy();
|
|
437
|
-
```
|
|
28
|
+
- [Documentation overview](docs/index.md)
|
|
29
|
+
- [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md)
|
|
30
|
+
- [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md)
|
|
31
|
+
- [API reference](docs/reference/index.md)
|
|
438
32
|
|
|
439
33
|
## Requirements
|
|
440
34
|
|
|
441
|
-
- Browser integration uses `document.documentElement`, `
|
|
442
|
-
- SSR is supported
|
|
443
|
-
-
|
|
444
|
-
- `localStorage` read, write, and remove errors are logged to the console via `console.error` and treated as unavailable storage.
|
|
445
|
-
- Consumers own CSS variables, selectors, visual tokens, and persistence consent requirements.
|
|
446
|
-
- No CSS file, design tokens, framework adapter, or peer dependency is provided.
|
|
35
|
+
- Browser integration uses `document.documentElement`, `localStorage`, `matchMedia`, `storage` events, and `CustomEvent`.
|
|
36
|
+
- SSR is supported by skipping unavailable browser work and using the configured default theme.
|
|
37
|
+
- Consumers provide CSS selectors, variables, visual tokens, and any pre-paint script needed to prevent a theme flash.
|
|
447
38
|
|
|
448
39
|
## Notes
|
|
449
40
|
|
|
450
|
-
|
|
451
|
-
- Does not provide React, Vue, or other framework bindings.
|
|
452
|
-
- Does not provide server-side persistence.
|
|
453
|
-
- Does not manage user consent requirements for storage.
|
|
41
|
+
Construction validates theme names, mappings, attributes, classes, and token schemas and throws on invalid configuration. Storage failures are reported to the console and treated as unavailable storage.
|
|
454
42
|
|
|
455
43
|
## License
|
|
456
44
|
|
|
457
|
-
|
|
45
|
+
Licensed under Apache-2.0.
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ interface ThemeDefinition<TSchema extends Record<string, string> = Record<string
|
|
|
7
7
|
colorScheme: "light" | "dark";
|
|
8
8
|
/** Optional theme-specific static token values. */
|
|
9
9
|
tokens?: Partial<Record<keyof TSchema, string>>;
|
|
10
|
+
/** Optional theme name activated when calling `toggle()` from this theme. */
|
|
11
|
+
pairedTheme?: string;
|
|
10
12
|
}
|
|
11
13
|
/** Mapping from OS color-scheme preferences to configured theme names. */
|
|
12
14
|
interface SystemThemeMap {
|
|
@@ -69,15 +71,18 @@ interface Theme<TSchema extends Record<string, string> = Record<string, string>>
|
|
|
69
71
|
* Retrieves the active theme configuration including static and computed tokens.
|
|
70
72
|
*
|
|
71
73
|
* Token values are merged in this priority order (last wins):
|
|
72
|
-
* 1. CSS computed style — values read from `window.getComputedStyle` for tokens not defined in JS.
|
|
74
|
+
* 1. CSS computed style — values read from `window.getComputedStyle` for tokens not defined in JS (unless `options.skipComputed` is true).
|
|
73
75
|
* 2. Theme static tokens — values defined in `ThemeDefinition.tokens` for the active theme.
|
|
74
76
|
* 3. Runtime overrides — values passed to `init()`, `set()`, `toggle()`, or other methods.
|
|
75
77
|
*
|
|
78
|
+
* @param options - Optional getter options. Set `skipComputed: true` to avoid reading computed styles from the DOM via `window.getComputedStyle`, preventing potential layout reflows.
|
|
76
79
|
* @returns The active `ThemeDefinition` object. If `tokenSchema` is configured and a token is not
|
|
77
80
|
* explicitly defined in JS, its value is dynamically resolved from the computed style of the root DOM element in browser environments.
|
|
78
|
-
* @warning Reading computed styles from the DOM via `window.getComputedStyle` can trigger a synchronous layout reflow. Avoid calling `get()` frequently or inside high-performance loops
|
|
81
|
+
* @warning Reading computed styles from the DOM via `window.getComputedStyle` can trigger a synchronous layout reflow. Avoid calling `get()` frequently or inside high-performance loops, or pass `{ skipComputed: true }`.
|
|
79
82
|
*/
|
|
80
|
-
get(
|
|
83
|
+
get(options?: {
|
|
84
|
+
skipComputed?: boolean;
|
|
85
|
+
}): ThemeDefinition<TSchema>;
|
|
81
86
|
/**
|
|
82
87
|
* Activates a configured theme by name and updates the stored preference in `localStorage`.
|
|
83
88
|
*
|
|
@@ -89,11 +94,10 @@ interface Theme<TSchema extends Record<string, string> = Record<string, string>>
|
|
|
89
94
|
*/
|
|
90
95
|
set(name: string, tokens?: Partial<Record<keyof TSchema, string>>): ThemeDefinition<TSchema>;
|
|
91
96
|
/**
|
|
92
|
-
* Switches
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* the active theme
|
|
96
|
-
* or `systemTheme.dark`.
|
|
97
|
+
* Switches away from the active theme and persists the change. If the active theme configures a
|
|
98
|
+
* `pairedTheme`, that theme is activated. Otherwise the next theme is selected from
|
|
99
|
+
* `systemTheme.light` or `systemTheme.dark` based on the active theme's `colorScheme`, not by
|
|
100
|
+
* cycling the active theme name.
|
|
97
101
|
*
|
|
98
102
|
* @param tokens - Optional runtime override token values to apply. Active overrides persist across subsequent theme changes unless cleared (by passing new overrides or an empty object).
|
|
99
103
|
* @returns The activated `ThemeDefinition` with merged and resolved tokens.
|
|
@@ -128,13 +132,24 @@ interface Theme<TSchema extends Record<string, string> = Record<string, string>>
|
|
|
128
132
|
*/
|
|
129
133
|
subscribe(listener: ThemeChangeListener<TSchema>): () => void;
|
|
130
134
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* configured `defaultTheme` so the instance can be safely re-initialized with `init()`.
|
|
135
|
+
* Generates a synchronous inline IIFE script string to inject into document `<head>`.
|
|
136
|
+
* Prevents Flash of Unstyled Content (FOUC) by applying storage or system theme before render.
|
|
134
137
|
*
|
|
135
|
-
* @
|
|
138
|
+
* @returns Minified JavaScript script string.
|
|
136
139
|
*/
|
|
137
|
-
|
|
140
|
+
getPrePaintScript(): string;
|
|
141
|
+
/**
|
|
142
|
+
* Cleans up the theme instance by removing all in-process change listeners, the system
|
|
143
|
+
* preference media query listener, and the cross-tab storage listener. Resets active tokens
|
|
144
|
+
* and the active theme name to the configured `defaultTheme` so the instance can be safely
|
|
145
|
+
* re-initialized with `init()`.
|
|
146
|
+
*
|
|
147
|
+
* @param options - Optional cleanup options. Set `revertDom: true` to remove configured DOM attributes, classes, and CSS custom properties applied to `document.documentElement`.
|
|
148
|
+
* @sideEffect Removes event listeners from `window` and clears internal subscriber sets. If `revertDom: true`, removes theme attributes, classes, and custom properties from the DOM root.
|
|
149
|
+
*/
|
|
150
|
+
destroy(options?: {
|
|
151
|
+
revertDom?: boolean;
|
|
152
|
+
}): void;
|
|
138
153
|
}
|
|
139
154
|
//#endregion
|
|
140
155
|
//#region src/constants.d.ts
|
|
@@ -151,8 +166,19 @@ declare const DARK_THEME: ThemeDefinition;
|
|
|
151
166
|
*
|
|
152
167
|
* @param options - Configuration options for theme definitions, persistence keys, DOM attributes, custom class resolvers, and dynamic token schemas.
|
|
153
168
|
* @returns A `Theme` instance.
|
|
154
|
-
* @throws {Error} If configured theme names are empty, duplicated, invalid for CSS class application,
|
|
169
|
+
* @throws {Error} If configured theme names are empty, duplicated, invalid for CSS class application, have an
|
|
170
|
+
* invalid `colorScheme`, or if the default/system themes are not present in the configured list.
|
|
155
171
|
*/
|
|
156
172
|
declare function createTheme<TSchema extends Record<string, string> = Record<string, string>>(options?: ThemeOptions<TSchema>): Theme<TSchema>;
|
|
157
173
|
//#endregion
|
|
158
|
-
|
|
174
|
+
//#region src/pre-paint.d.ts
|
|
175
|
+
/**
|
|
176
|
+
* Generates a synchronous inline IIFE script string to inject into document `<head>`.
|
|
177
|
+
* Prevents Flash of Unstyled Content (FOUC) by applying storage or system theme before render.
|
|
178
|
+
*
|
|
179
|
+
* @param options - Configuration options used to determine storage keys, attributes, default/system themes, custom class resolvers, and token schemas.
|
|
180
|
+
* @returns Minified JavaScript script string.
|
|
181
|
+
*/
|
|
182
|
+
declare function getPrePaintScript<TSchema extends Record<string, string> = Record<string, string>>(options?: ThemeOptions<TSchema>): string;
|
|
183
|
+
//#endregion
|
|
184
|
+
export { DARK_THEME, LIGHT_THEME, type SystemThemeMap, THEME_CHANGE_EVENT, type Theme, type ThemeChangeDetail, type ThemeChangeListener, type ThemeChangeSource, type ThemeClassResolver, type ThemeDefinition, type ThemeOptions, createTheme, getPrePaintScript };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=`themechange`,t=/\s/,n=Object.freeze({name:`light`,colorScheme:`light`}),r=Object.freeze({name:`dark`,colorScheme:`dark`}),i=Object.freeze({themes:Object.freeze([n,r]),defaultTheme:n.name,systemTheme:Object.freeze({light:n.name,dark:r.name}),storageKey:`app-theme-preference`,attribute:`data-theme`,isTailwindCss:!1,shouldApplyClass:!0}),a=(e,t)=>{if(t===!1)return null;if(typeof t==`function`){let n=t(e);return o(n,`Theme class resolver returned an invalid class for theme: ${e.name}.`),n}let n=`theme-${e.name}`;return o(n,`Theme name cannot be used as a default theme class: ${e.name}.`),n},o=(e,n)=>{if(typeof e!=`string`||e.length===0||t.test(e))throw Error(n)},s=e=>{let{theme:t,options:n,activeTokens:r,resolvedClasses:i,nextClass:a}=e;if(typeof document>`u`)return;let o=document.documentElement;o.setAttribute(n.attribute,t.name),o.style.colorScheme=t.colorScheme;for(let e of i)e!==a&&o.classList.remove(e);if(a!==null&&o.classList.add(a),n.isTailwindCss&&o.classList.toggle(`dark`,t.colorScheme===`dark`),n.tokenSchema){let e=n.tokenSchema,i={...t.tokens,...r};for(let t of Object.keys(e)){let n=e[t],r=i[t];r==null?o.style.removeProperty(n):o.style.setProperty(n,r)}}},c=e=>{let{theme:t,options:n,activeTokens:r}=e,i={};if(typeof window>`u`||typeof window.getComputedStyle!=`function`||typeof document>`u`||!n.tokenSchema)return i;let a=document.documentElement;try{let e=n.tokenSchema,o={...t.tokens,...r};if(!Object.keys(e).some(e=>o[e]===void 0))return i;let s=window.getComputedStyle(a);if(!s)return i;for(let t of Object.keys(e))if(o[t]===void 0){let n=s.getPropertyValue(e[t]).trim();n&&(i[t]=n)}}catch(e){console.error(`[theme] Failed to read computed token styles:`,e)}return i},l=t=>{typeof window>`u`||typeof window.dispatchEvent!=`function`||typeof window.CustomEvent!=`function`||window.dispatchEvent(new CustomEvent(e,{detail:t}))},u=(e,t)=>{if(typeof window>`u`)return null;try{let n=window.localStorage.getItem(e);return n===null?null:t.some(e=>e.name===n)?n:null}catch(e){return console.error(`[theme] Failed to read from localStorage:`,e),null}},d=(e,t)=>{if(!(typeof window>`u`))try{window.localStorage.setItem(e,t)}catch(e){console.error(`[theme] Failed to write to localStorage:`,e)}},f=e=>{if(!(typeof window>`u`))try{window.localStorage.removeItem(e)}catch(e){console.error(`[theme] Failed to remove from localStorage:`,e)}},p=e=>{let{defaultTheme:t,systemTheme:n,themes:r}=e,i=e=>{let t=r.find(t=>t.name===e);if(t===void 0)throw Error(`Theme is not configured: ${e}.`);return t};if(typeof window>`u`||typeof window.matchMedia!=`function`)return i(t);try{return i(window.matchMedia(`(prefers-color-scheme: dark)`).matches?n.dark:n.light)}catch{return i(t)}},m=e=>{if(typeof window>`u`||typeof window.matchMedia!=`function`)return()=>{};let t=(()=>{try{return window.matchMedia(`(prefers-color-scheme: dark)`)}catch{return null}})(),n=!1;if(t)try{typeof t.addEventListener==`function`?t.addEventListener(`change`,e):typeof t.addListener==`function`&&(n=!0,t.addListener(e))}catch{}return()=>{if(t!==null)try{n&&typeof t.removeListener==`function`?t.removeListener(e):typeof t.removeEventListener==`function`&&t.removeEventListener(`change`,e)}catch{}}},h=e=>typeof window>`u`||typeof window.addEventListener!=`function`?()=>{}:(window.addEventListener(`storage`,e),()=>{window.removeEventListener(`storage`,e)}),g=e=>{if(typeof e.attribute!=`string`||e.attribute.trim().length===0)throw Error(`Theme attribute option must be a non-empty string.`);if(/[\s"'/>=]/.test(e.attribute))throw Error(`Theme attribute option must be a valid HTML attribute name.`);if(typeof e.storageKey!=`string`||e.storageKey.trim().length===0)throw Error(`Theme storageKey option must be a non-empty string.`);if(!Array.isArray(e.themes))throw Error(`Theme options.themes must be an array.`);let t=new Set;if(e.tokenSchema){for(let[t,n]of Object.entries(e.tokenSchema))if(typeof n!=`string`||!n.startsWith(`--`))throw Error(`Token schema key "${t}" must map to a CSS custom property starting with "--". Received: "${n}".`)}for(let n of e.themes){if(typeof n!=`object`||!n)throw Error(`Theme definitions must be objects.`);if(typeof n.name!=`string`||n.name.trim().length===0)throw Error(`Theme names must be non-empty strings.`);if(n.colorScheme!==`light`&&n.colorScheme!==`dark`)throw Error(`Theme "${n.name}" has an invalid colorScheme: ${n.colorScheme}. Must be "light" or "dark".`);if(t.has(n.name))throw Error(`Duplicate theme name: ${n.name}.`);if(t.add(n.name),typeof e.shouldApplyClass!=`function`&&a(n,e.shouldApplyClass),n.tokens){if(!e.tokenSchema)throw Error(`Theme "${n.name}" defines tokens but no tokenSchema is configured.`);for(let t of Object.keys(n.tokens))if(!Object.hasOwn(e.tokenSchema,t))throw Error(`Theme "${n.name}" defines token "${t}" which is not present in tokenSchema.`)}}if(!t.has(e.defaultTheme))throw Error(`Default theme is not configured: ${e.defaultTheme}.`);if(!t.has(e.systemTheme.light))throw Error(`System light theme is not configured: ${e.systemTheme.light}.`);if(!t.has(e.systemTheme.dark))throw Error(`System dark theme is not configured: ${e.systemTheme.dark}.`)},_=(e,t)=>{if(e!=null){if(typeof e!=`object`||Array.isArray(e))throw Error(`Runtime tokens must be an object.`);if(t===void 0)throw Error(`Runtime tokens provided but no tokenSchema is configured.`);for(let n of Object.keys(e))if(!Object.hasOwn(t,n))throw Error(`Runtime token override "${n}" is not present in tokenSchema.`)}};var v=class{#e;#t;#n={};#r=new Set;#i=null;#a=null;#o=!1;#s=null;#c=null;#l(){if(this.#s===null){this.#s=new Map;for(let e of this.#e.themes)this.#s.set(e.name,a(e,this.#e.shouldApplyClass))}return this.#s}#u(){if(this.#c===null){let e=this.#l();this.#c=Array.from(e.values()).filter(e=>e!==null)}return this.#c}#d=e=>{if(u(this.#e.storageKey,this.#e.themes)!==null)return;let t=e.matches?this.#e.systemTheme.dark:this.#e.systemTheme.light;this.#t!==t&&this.#p(t,{source:`system`,shouldStore:!1})};#f=e=>{if(e.key===this.#e.storageKey)if(e.newValue===null){let e=this.getSystem().name;this.#p(e,{source:`clearPreference`,shouldStore:!1})}else this.#e.themes.some(t=>t.name===e.newValue)&&this.#t!==e.newValue&&this.#p(e.newValue,{source:`set`,shouldStore:!1})};constructor(e={}){this.#e={...i,...e,systemTheme:{...i.systemTheme,...e.systemTheme}},g(this.#e),this.#t=this.#e.defaultTheme}init(e){return this.#o?this:(this.#i=m(this.#d),this.#a=h(this.#f),this.#p(u(this.#e.storageKey,this.#e.themes)??this.getSystem().name,{source:`init`,shouldStore:!1,tokens:e}),this.#o=!0,this)}get(){let e=this.#h(this.#t),t=this.#e,n=this.#n,r=null;return{...e,get tokens(){return r===null&&(r={...c({theme:e,options:t,activeTokens:n}),...e.tokens,...n}),r}}}set(e,t){return this.#p(e,{source:`set`,shouldStore:!0,tokens:t})}toggle(e){let t=this.#h(this.#t).colorScheme===`dark`?this.#e.systemTheme.light:this.#e.systemTheme.dark;return this.#p(t,{source:`toggle`,shouldStore:!0,tokens:e})}clearPreference(){return f(this.#e.storageKey),this.#p(this.getSystem().name,{source:`clearPreference`,shouldStore:!1})}getStored(){return u(this.#e.storageKey,this.#e.themes)}getSystem(){return p({defaultTheme:this.#e.defaultTheme,systemTheme:this.#e.systemTheme,themes:this.#e.themes})}subscribe(e){return this.#r.add(e),()=>{this.#r.delete(e)}}destroy(){this.#i&&=(this.#i(),null),this.#a&&=(this.#a(),null),this.#r.clear(),this.#n={},this.#t=this.#e.defaultTheme,this.#s=null,this.#c=null,this.#o=!1}#p(e,t){_(t.tokens,this.#e.tokenSchema);let n=this.#h(e);this.#t=n.name,t.tokens!==void 0&&(this.#n=t.tokens),t.shouldStore&&d(this.#e.storageKey,n.name);let r=this.#l();s({theme:n,options:this.#e,activeTokens:this.#n,resolvedClasses:this.#u(),nextClass:r.get(n.name)??null});let i=this.get();return this.#m({name:i.name,theme:i,source:t.source}),i}#m(e){for(let t of this.#r)try{t(e)}catch(e){console.error(`Error in theme change listener:`,e)}l(e)}#h(e){let t=this.#e.themes.find(t=>t.name===e);if(t===void 0)throw Error(`Theme is not configured: ${e}.`);return t}};function y(e={}){return new v(e)}export{r as DARK_THEME,n as LIGHT_THEME,e as THEME_CHANGE_EVENT,y as createTheme};
|
|
1
|
+
const e=`themechange`,t=`dark`,n=/\s/,r=Object.freeze({name:`light`,colorScheme:`light`}),i=Object.freeze({name:`dark`,colorScheme:`dark`}),a=Object.freeze({themes:Object.freeze([r,i]),defaultTheme:r.name,systemTheme:Object.freeze({light:r.name,dark:i.name}),storageKey:`app-theme-preference`,attribute:`data-theme`,isTailwindCss:!1,shouldApplyClass:!0}),o=(e,t)=>{if(t===!1)return null;if(typeof t==`function`){let n=t(e);return s(n,`Theme class resolver returned an invalid class for theme: ${e.name}.`),n}let n=`theme-${e.name}`;return s(n,`Theme name cannot be used as a default theme class: ${e.name}.`),n},s=(e,t)=>{if(typeof e!=`string`||e.length===0||n.test(e))throw Error(t)},c=e=>{let{theme:n,options:r,activeTokens:i,resolvedClasses:a,nextClass:o}=e;if(typeof document>`u`)return;let s=document.documentElement;s.setAttribute(r.attribute,n.name),s.style.colorScheme=n.colorScheme;for(let e of a)e!==o&&s.classList.remove(e);if(o!==null&&s.classList.add(o),r.isTailwindCss&&s.classList.toggle(t,n.colorScheme===`dark`),r.tokenSchema){let e=r.tokenSchema,t={...n.tokens,...i};for(let n of Object.keys(e)){let r=e[n],i=t[n];i==null?s.style.removeProperty(r):s.style.setProperty(r,i)}}},l=e=>{let{theme:t,options:n,activeTokens:r,skipComputed:i}=e,a={};if(i||typeof window>`u`||typeof window.getComputedStyle!=`function`||typeof document>`u`||!n.tokenSchema)return a;let o=document.documentElement;try{let e=n.tokenSchema,i={...t.tokens,...r};if(!Object.keys(e).some(e=>i[e]===void 0))return a;let s=window.getComputedStyle(o);if(!s)return a;for(let t of Object.keys(e))if(i[t]===void 0){let n=s.getPropertyValue(e[t]).trim();n&&(a[t]=n)}}catch(e){console.error(`[theme] Failed to read computed token styles:`,e)}return a},u=t=>{typeof window>`u`||typeof window.dispatchEvent!=`function`||typeof window.CustomEvent!=`function`||window.dispatchEvent(new CustomEvent(e,{detail:t}))},d=e=>{let{options:n,resolvedClasses:r}=e;if(typeof document>`u`)return;let i=document.documentElement;i.removeAttribute(n.attribute),i.style.removeProperty(`color-scheme`);for(let e of r)i.classList.remove(e);if(n.isTailwindCss&&i.classList.remove(t),n.tokenSchema)for(let e of Object.values(n.tokenSchema))i.style.removeProperty(e)};function f(e={}){let t={...a,...e,systemTheme:{...a.systemTheme,...e.systemTheme}},n=t.themes||a.themes,r={};for(let e of n){let n=o(e,t.shouldApplyClass),i;if(t.tokenSchema&&e.tokens)for(let[n,r]of Object.entries(t.tokenSchema)){let t=e.tokens[n];t!=null&&(i??={},i[r]=t)}r[e.name]={colorScheme:e.colorScheme,className:n,...i?{vars:i}:{}}}return`!(function(){try{var k=${JSON.stringify(t.storageKey)},a=${JSON.stringify(t.attribute)},d=${JSON.stringify(t.defaultTheme)},sl=${JSON.stringify(t.systemTheme.light)},sd=${JSON.stringify(t.systemTheme.dark)},tm=${JSON.stringify(r)},tw=${JSON.stringify(t.isTailwindCss)},s=localStorage.getItem(k),m=window.matchMedia("(prefers-color-scheme: dark)").matches,n=(s&&tm[s])?s:(m?sd:sl);if(!tm[n])n=d;var t=tm[n],r=document.documentElement;r.setAttribute(a,n);if(t&&t.colorScheme)r.style.colorScheme=t.colorScheme;if(t&&t.className)r.classList.add(t.className);if(tw)r.classList.toggle("dark",t&&t.colorScheme==="dark");if(t&&t.vars)for(var v in t.vars)r.style.setProperty(v,t.vars[v])}catch(e){}})()`}const p=(e,t)=>{if(typeof window>`u`)return null;try{let n=window.localStorage.getItem(e);return n===null?null:t.some(e=>e.name===n)?n:null}catch(e){return console.error(`[theme] Failed to read from localStorage:`,e),null}},m=(e,t)=>{if(!(typeof window>`u`))try{window.localStorage.setItem(e,t)}catch(e){console.error(`[theme] Failed to write to localStorage:`,e)}},h=e=>{if(!(typeof window>`u`))try{window.localStorage.removeItem(e)}catch(e){console.error(`[theme] Failed to remove from localStorage:`,e)}},g=e=>{let{defaultTheme:t,systemTheme:n,themes:r}=e,i=e=>{let t=r.find(t=>t.name===e);if(t===void 0)throw Error(`Theme is not configured: ${e}.`);return t};if(typeof window>`u`||typeof window.matchMedia!=`function`)return i(t);try{return i(window.matchMedia(`(prefers-color-scheme: dark)`).matches?n.dark:n.light)}catch{return i(t)}},_=e=>{if(typeof window>`u`||typeof window.matchMedia!=`function`)return()=>{};let t=(()=>{try{return window.matchMedia(`(prefers-color-scheme: dark)`)}catch{return null}})(),n=!1;if(t)try{typeof t.addEventListener==`function`?t.addEventListener(`change`,e):typeof t.addListener==`function`&&(n=!0,t.addListener(e))}catch{}return()=>{if(t!==null)try{n&&typeof t.removeListener==`function`?t.removeListener(e):typeof t.removeEventListener==`function`&&t.removeEventListener(`change`,e)}catch{}}},v=e=>typeof window>`u`||typeof window.addEventListener!=`function`?()=>{}:(window.addEventListener(`storage`,e),()=>{window.removeEventListener(`storage`,e)}),y=e=>{if(typeof e.attribute!=`string`||e.attribute.trim().length===0)throw Error(`Theme attribute option must be a non-empty string.`);if(/[\s"'/>=]/.test(e.attribute))throw Error(`Theme attribute option must be a valid HTML attribute name.`);if(typeof e.storageKey!=`string`||e.storageKey.trim().length===0)throw Error(`Theme storageKey option must be a non-empty string.`);if(!Array.isArray(e.themes))throw Error(`Theme options.themes must be an array.`);let t=new Set;if(e.tokenSchema){for(let[t,n]of Object.entries(e.tokenSchema))if(typeof n!=`string`||!n.startsWith(`--`))throw Error(`Token schema key "${t}" must map to a CSS custom property starting with "--". Received: "${n}".`)}for(let n of e.themes){if(typeof n!=`object`||!n)throw Error(`Theme definitions must be objects.`);if(typeof n.name!=`string`||n.name.trim().length===0)throw Error(`Theme names must be non-empty strings.`);if(n.colorScheme!==`light`&&n.colorScheme!==`dark`)throw Error(`Theme "${n.name}" has an invalid colorScheme: ${n.colorScheme}. Must be "light" or "dark".`);if(n.pairedTheme!==void 0&&(typeof n.pairedTheme!=`string`||n.pairedTheme.trim().length===0))throw Error(`Theme "${n.name}" pairedTheme must be a non-empty string.`);if(t.has(n.name))throw Error(`Duplicate theme name: ${n.name}.`);if(t.add(n.name),typeof e.shouldApplyClass!=`function`&&o(n,e.shouldApplyClass),n.tokens){if(!e.tokenSchema)throw Error(`Theme "${n.name}" defines tokens but no tokenSchema is configured.`);for(let t of Object.keys(n.tokens))if(!Object.hasOwn(e.tokenSchema,t))throw Error(`Theme "${n.name}" defines token "${t}" which is not present in tokenSchema.`)}}for(let n of e.themes)if(n.pairedTheme!==void 0&&!t.has(n.pairedTheme))throw Error(`Theme "${n.name}" references unconfigured pairedTheme: ${n.pairedTheme}.`);if(!t.has(e.defaultTheme))throw Error(`Default theme is not configured: ${e.defaultTheme}.`);if(!t.has(e.systemTheme.light))throw Error(`System light theme is not configured: ${e.systemTheme.light}.`);if(!t.has(e.systemTheme.dark))throw Error(`System dark theme is not configured: ${e.systemTheme.dark}.`)},b=(e,t)=>{if(e!=null){if(typeof e!=`object`||Array.isArray(e))throw Error(`Runtime tokens must be an object.`);if(t===void 0)throw Error(`Runtime tokens provided but no tokenSchema is configured.`);for(let n of Object.keys(e))if(!Object.hasOwn(t,n))throw Error(`Runtime token override "${n}" is not present in tokenSchema.`)}};var x=class{#e;#t;#n={};#r=new Set;#i=null;#a=null;#o=!1;#s=null;#c=null;#l(){if(this.#s===null){this.#s=new Map;for(let e of this.#e.themes)this.#s.set(e.name,o(e,this.#e.shouldApplyClass))}return this.#s}#u(){if(this.#c===null){let e=this.#l();this.#c=Array.from(e.values()).filter(e=>e!==null)}return this.#c}#d=e=>{if(p(this.#e.storageKey,this.#e.themes)!==null)return;let t=e.matches?this.#e.systemTheme.dark:this.#e.systemTheme.light;this.#t!==t&&this.#p(t,{source:`system`,shouldStore:!1})};#f=e=>{if(e.key===this.#e.storageKey)if(e.newValue===null){let e=this.getSystem().name;this.#p(e,{source:`clearPreference`,shouldStore:!1})}else this.#e.themes.some(t=>t.name===e.newValue)&&this.#t!==e.newValue&&this.#p(e.newValue,{source:`set`,shouldStore:!1})};constructor(e={}){this.#e={...a,...e,systemTheme:{...a.systemTheme,...e.systemTheme}},y(this.#e),this.#t=this.#e.defaultTheme}init(e){return this.#o?this:(this.#i=_(this.#d),this.#a=v(this.#f),this.#p(p(this.#e.storageKey,this.#e.themes)??this.getSystem().name,{source:`init`,shouldStore:!1,tokens:e}),this.#o=!0,this)}get(e){let t=this.#h(this.#t),n=this.#e,r=this.#n,i=e?.skipComputed,a=null;return{...t,get tokens(){return a===null&&(a={...l({theme:t,options:n,activeTokens:r,skipComputed:i}),...t.tokens,...r}),a}}}set(e,t){return this.#p(e,{source:`set`,shouldStore:!0,tokens:t})}toggle(e){let t=this.#h(this.#t),n;return n=t.pairedTheme!==void 0&&this.#e.themes.some(e=>e.name===t.pairedTheme)?t.pairedTheme:t.colorScheme===`dark`?this.#e.systemTheme.light:this.#e.systemTheme.dark,this.#p(n,{source:`toggle`,shouldStore:!0,tokens:e})}clearPreference(){return h(this.#e.storageKey),this.#p(this.getSystem().name,{source:`clearPreference`,shouldStore:!1})}getStored(){return p(this.#e.storageKey,this.#e.themes)}getSystem(){return g({defaultTheme:this.#e.defaultTheme,systemTheme:this.#e.systemTheme,themes:this.#e.themes})}subscribe(e){return this.#r.add(e),()=>{this.#r.delete(e)}}getPrePaintScript(){return f(this.#e)}destroy(e){e?.revertDom&&d({options:this.#e,resolvedClasses:this.#u()}),this.#i&&=(this.#i(),null),this.#a&&=(this.#a(),null),this.#r.clear(),this.#n={},this.#t=this.#e.defaultTheme,this.#s=null,this.#c=null,this.#o=!1}#p(e,t){b(t.tokens,this.#e.tokenSchema);let n=this.#h(e);this.#t=n.name,t.tokens!==void 0&&(this.#n=t.tokens),t.shouldStore&&m(this.#e.storageKey,n.name);let r=this.#l();c({theme:n,options:this.#e,activeTokens:this.#n,resolvedClasses:this.#u(),nextClass:r.get(n.name)??null});let i=this.get();return this.#m({name:i.name,theme:i,source:t.source}),i}#m(e){for(let t of this.#r)try{t(e)}catch(e){console.error(`Error in theme change listener:`,e)}u(e)}#h(e){let t=this.#e.themes.find(t=>t.name===e);if(t===void 0)throw Error(`Theme is not configured: ${e}.`);return t}};function S(e={}){return new x(e)}export{i as DARK_THEME,r as LIGHT_THEME,e as THEME_CHANGE_EVENT,S as createTheme,f as getPrePaintScript};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 0.1.2
|
|
3
|
+
date: 2026-09-15
|
|
4
|
+
description: A generated API reference and reorganized documentation.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.1.2
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- A generated API reference, `docs/reference/index.md`, covering every exported symbol from its TSDoc directly — regenerated on every release instead of hand-maintained separately, so it can't drift out of sync with the source.
|
|
12
|
+
|
|
13
|
+
## Changed
|
|
14
|
+
|
|
15
|
+
- Documentation split from one page, `docs/reference.md`, into three focused ones: `docs/index.md` (setup and configuration), `docs/tokens-and-persistence.md` (token resolution, `skipComputed`, persistence, cross-tab sync), and `docs/ssr-and-pre-paint.md` (`getPrePaintScript()`, SSR fallback, Tailwind's `isTailwindCss`, `shouldApplyClass`, `destroy({ revertDom: true })`), plus the generated reference above. Update any link that pointed at `docs/reference.md`.
|