@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/llms-full.txt ADDED
@@ -0,0 +1,213 @@
1
+ <!-- Source: README.md -->
2
+
3
+ # @codenhub/theme
4
+
5
+ Zero-dependency browser theme preference, persistence, DOM, and token manager.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pnpm add @codenhub/theme
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createTheme, getPrePaintScript } from "@codenhub/theme";
17
+
18
+ const theme = createTheme().init();
19
+ theme.set("dark");
20
+
21
+ // Get inline IIFE script string for <head> to prevent FOUC:
22
+ const script = theme.getPrePaintScript(); // or standalone getPrePaintScript()
23
+
24
+ // Remove media-query/storage listeners and subscribers on teardown.
25
+ theme.destroy({ revertDom: true });
26
+ ```
27
+
28
+ ## Documentation
29
+
30
+ - [Documentation overview](docs/index.md)
31
+ - [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md)
32
+ - [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md)
33
+ - [API reference](docs/reference/index.md)
34
+
35
+ ## Requirements
36
+
37
+ - Browser integration uses `document.documentElement`, `localStorage`, `matchMedia`, `storage` events, and `CustomEvent`.
38
+ - SSR is supported by skipping unavailable browser work and using the configured default theme.
39
+ - Consumers provide CSS selectors, variables, visual tokens, and any pre-paint script needed to prevent a theme flash.
40
+
41
+ ## Notes
42
+
43
+ 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.
44
+
45
+ ## License
46
+
47
+ Licensed under Apache-2.0.
48
+
49
+ <!-- Source: docs/index.md -->
50
+
51
+ # Manage Browser Themes
52
+
53
+ `@codenhub/theme` owns one thing for a browser application: deciding which theme is active, keeping it in sync with `localStorage` and the OS `prefers-color-scheme` setting, and reflecting it onto the DOM as an attribute, a class, and CSS custom properties. It fits applications that want one place to own that decision instead of scattering `matchMedia` listeners and `localStorage` reads across the codebase, while keeping selectors, visual design, and CSS entirely in application code.
54
+
55
+ ## Setup
56
+
57
+ ### Installation
58
+
59
+ ```sh
60
+ pnpm add @codenhub/theme
61
+ ```
62
+
63
+ ### Quick start
64
+
65
+ ```ts
66
+ import { createTheme } from "@codenhub/theme";
67
+
68
+ const theme = createTheme().init();
69
+ theme.set("dark");
70
+ ```
71
+
72
+ `createTheme()` with no arguments configures the built-in `light` and `dark` themes. `init()` resolves the theme to apply — a stored preference if one is valid, otherwise the OS preference — and applies it to `document.documentElement`. Call `destroy()` when the manager's owner is torn down, such as in a framework component's cleanup hook; it stops listening but leaves the DOM and stored preference as they were unless told otherwise (see [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md)).
73
+
74
+ ### Configuration
75
+
76
+ Most applications configure at least `themes` and `tokenSchema` up front:
77
+
78
+ ```ts
79
+ import { createTheme } from "@codenhub/theme";
80
+
81
+ const theme = createTheme({
82
+ themes: [
83
+ { name: "light", colorScheme: "light", tokens: { primary: "#171717" } },
84
+ { name: "dark", colorScheme: "dark", tokens: { primary: "#f9fafb" } },
85
+ ],
86
+ tokenSchema: { primary: "--color-primary" },
87
+ storageKey: "app-theme-preference",
88
+ isTailwindCss: true,
89
+ }).init();
90
+ ```
91
+
92
+ `themes` replaces the built-in list entirely — include `light` and `dark` (or whatever names `defaultTheme` and `systemTheme` point at) when defining custom themes. `tokenSchema` maps the typed token names used in `ThemeDefinition.tokens` and `get()` to the CSS custom property each one writes to `document.documentElement`. See [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md) for how token values are resolved and stored, and [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md) for flash-of-unstyled-content prevention and OS-preference-only applications like `isTailwindCss`.
93
+
94
+ Construction validates every option and throws on invalid configuration: empty or duplicate theme names, a `defaultTheme` or `systemTheme` name that is not in `themes`, an invalid `colorScheme`, or a `shouldApplyClass` resolver that would produce an empty or whitespace-containing class.
95
+
96
+ ## Requirements
97
+
98
+ - Browser integration uses `document.documentElement`, `localStorage`, `matchMedia`, `storage` events, and `CustomEvent`. Every one of these is read defensively: an unavailable or throwing API is treated as absent rather than crashing the caller.
99
+ - SSR is supported by skipping unavailable browser work and using the configured `defaultTheme`. The package cannot produce server HTML attributes or prevent a flash on its own — see [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md).
100
+ - Consumers provide CSS selectors, variables, and visual tokens. The package only ever writes an attribute, optional classes, and CSS custom properties — it ships no CSS or token values beyond the built-in `light`/`dark` definitions' names.
101
+
102
+ ## Next steps
103
+
104
+ - [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md): How `get()` resolves token values, how preferences persist to `localStorage` and sync across tabs, and how to read or clear the stored preference.
105
+ - [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md): Preventing a flash of unstyled content before hydration, server-rendering considerations, and toggling Tailwind's `dark` class.
106
+ - [API reference](docs/reference/index.md): Every exported function, type, and interface member with its full signature.
107
+
108
+ <!-- Source: docs/ssr-and-pre-paint.md -->
109
+
110
+ # SSR, Pre-Paint, and Tailwind
111
+
112
+ ## Preventing a flash of unstyled content
113
+
114
+ A client-side `init()` call runs after the page has already painted once, which is visible as a flash from the default theme to the resolved one. `getPrePaintScript()` — available as a method on an existing manager, or as the standalone `getPrePaintScript(options)` export when no manager exists yet — returns a synchronous, minified inline IIFE string that duplicates the same stored-preference-then-system-preference resolution, and applies the configured attribute, classes, and any static tokens before the browser paints:
115
+
116
+ ```ts
117
+ import { getPrePaintScript } from "@codenhub/theme";
118
+
119
+ const script = getPrePaintScript({ storageKey: "app-theme-preference" });
120
+ ```
121
+
122
+ Inject the returned string into a blocking `<script>` in the document `<head>`, before any stylesheet or content that depends on the theme:
123
+
124
+ ```html
125
+ <head>
126
+ <script>
127
+ /* inline the string returned by getPrePaintScript() here */
128
+ </script>
129
+ </head>
130
+ ```
131
+
132
+ Pass the same `themes`, `tokenSchema`, `defaultTheme`, `systemTheme`, `storageKey`, `attribute`, `shouldApplyClass`, and `isTailwindCss` options used to construct the manager, so the pre-paint script and the later `init()` call resolve to the same theme. `getPrePaintScript()` only serializes **static** tokens from each theme's `ThemeDefinition.tokens` — and only when `tokenSchema` is passed to it — runtime overrides passed to `init(tokens)` are applied after hydration, during client-side execution, and cannot be part of a script that runs before any application code.
133
+
134
+ ## Server-side rendering
135
+
136
+ Without browser APIs, `Theme` methods skip storage reads/writes, DOM updates, and listener registration; `getSystem()` falls back to `defaultTheme`. Every method call remains safe to make during SSR — nothing throws for a missing `window`, `document`, or `localStorage` — but the package cannot produce server-rendered HTML attributes on its own. To avoid a flash on a server-rendered page, either:
137
+
138
+ - Render the server markup with the same attribute/class the pre-paint script would apply (requiring the server to read the same cookie or header a real theme service would use), or
139
+ - Rely on the pre-paint script above to correct the DOM before first paint, accepting that server-rendered markup itself does not carry theme state.
140
+
141
+ Keep any server-side resolution logic — stored names, system mapping, storage key, attribute, classes, color scheme — aligned with the options passed to `createTheme()` and `getPrePaintScript()`; a mismatch reintroduces the flash it exists to prevent.
142
+
143
+ ## Tailwind's `dark` class
144
+
145
+ Set `isTailwindCss: true` to toggle Tailwind CSS's `dark` class on `document.documentElement` alongside the configured attribute, for every theme whose `colorScheme` is `"dark"`. This is independent of `shouldApplyClass` (below) — a Tailwind app commonly wants both the `dark` class for Tailwind's `dark:` variant and, when `shouldApplyClass` is left at its default, a `theme-${name}` class for custom per-theme CSS.
146
+
147
+ ## Classes and cleanup
148
+
149
+ `shouldApplyClass` controls whether and how a class is applied to `document.documentElement` alongside the attribute: `true` (the default) applies `theme-${name}`, `false` applies no class, and a `ThemeClassResolver` function receives the active `ThemeDefinition` and returns one custom class token — throwing if it returns an empty or whitespace-containing string.
150
+
151
+ `destroy(options?)` removes the media-query and storage listeners and clears in-process subscribers and runtime tokens, leaving the instance safe to `init()` again. By default it does **not** touch the DOM or stored preference. Pass `{ revertDom: true }` to also remove the configured attribute, classes, and CSS custom properties from `document.documentElement` — useful when unmounting a scoped theme manager that should leave no trace, such as in a component test or a preview pane.
152
+
153
+ <!-- Source: docs/tokens-and-persistence.md -->
154
+
155
+ # Tokens, Persistence, and Cross-Tab Sync
156
+
157
+ ## Token resolution
158
+
159
+ `tokenSchema` maps typed token names to the CSS custom property each one writes, such as `{ primary: "--color-primary" }`. Once configured, `get()` returns the active `ThemeDefinition` with every schema key resolved from three sources, in this priority order (last wins):
160
+
161
+ 1. **Computed style** — for a token not defined in JS, the value is read from `window.getComputedStyle(document.documentElement)` in browser environments.
162
+ 2. **Theme static tokens** — values set on the active theme's own `ThemeDefinition.tokens`.
163
+ 3. **Runtime overrides** — values passed to `init()`, `set()`, or `toggle()`.
164
+
165
+ ```ts
166
+ const theme = createTheme({
167
+ themes: [{ name: "brand", colorScheme: "light", tokens: { accent: "#ff6600" } }],
168
+ tokenSchema: { accent: "--color-accent" },
169
+ }).init();
170
+
171
+ theme.set("brand", { accent: "#00c853" }); // runtime override wins over the static "#ff6600"
172
+ theme.get().tokens?.accent; // "#00c853"
173
+ ```
174
+
175
+ Reading computed values can force a synchronous layout reflow. Pass `{ skipComputed: true }` to `get()` to skip that source and avoid the reflow when only the JS-known values are needed.
176
+
177
+ Runtime overrides persist across subsequent theme changes until explicitly replaced — pass a new object, including `{}`, to `init()`, `set()`, or `toggle()` to clear or replace them. Passing a token key that is not in `tokenSchema` throws.
178
+
179
+ ## Persistence
180
+
181
+ An explicit preference set through `set()` or `toggle()` is written to `localStorage` under `storageKey` (default `"app-theme-preference"`). `getStored()` returns that value when it names a currently configured theme, or `null` when it is unset, invalid, or storage is unavailable (for example during SSR). `clearPreference()` removes the stored value and re-applies whichever theme `getSystem()` currently resolves to.
182
+
183
+ Storage reads, writes, and removals that throw — a full quota, a disabled storage API, a private-browsing restriction — are logged with `console.error` and treated the same as unavailable storage; they never throw out of `Theme` methods.
184
+
185
+ ## Cross-tab sync
186
+
187
+ Every tab running `createTheme().init()` against the same `storageKey` stays in sync: a `storage` event fires in every other tab when one tab writes a new preference, and each one applies the change if it names a valid configured theme. A tab with its own runtime token overrides keeps them — only the active theme name changes from cross-tab sync, not tokens passed in-process.
188
+
189
+ The OS `prefers-color-scheme` media query applies automatically only while no valid stored preference exists. Once a tab (or a previous session) has stored an explicit preference, later system-preference changes are ignored until `clearPreference()` runs.
190
+
191
+ ## Reacting to changes
192
+
193
+ `subscribe(listener)` registers a callback invoked after every applied change — from `init()`, `set()`, `toggle()`, `clearPreference()`, or an accepted `system` or cross-tab update — and returns an unsubscribe function:
194
+
195
+ ```ts
196
+ const unsubscribe = theme.subscribe(({ name, source }) => {
197
+ console.log(`theme is now "${name}" (${source})`);
198
+ });
199
+ ```
200
+
201
+ A subscriber that throws is logged and does not stop other subscribers from running. Browsers also receive a `window` `CustomEvent` named by the exported `THEME_CHANGE_EVENT` constant (`"themechange"`) carrying the same detail, for code that prefers DOM events over `subscribe()`.
202
+
203
+ <!-- Source: docs/changelog/0.1.2.md -->
204
+
205
+ # 0.1.2
206
+
207
+ ## Added
208
+
209
+ - 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.
210
+
211
+ ## Changed
212
+
213
+ - 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`.
package/llms.txt ADDED
@@ -0,0 +1,19 @@
1
+ # @codenhub/theme
2
+
3
+ > Zero-dependency browser theme preference, persistence, DOM, cross-tab, event,
4
+ > and token manager with SSR-safe fallbacks.
5
+
6
+ Call `init()` in the browser, keep pre-paint logic aligned with configuration,
7
+ and call `destroy()` to release media-query/storage listeners and subscribers.
8
+
9
+ ## Documentation
10
+
11
+ - [Documentation overview](docs/index.md): Purpose, status, and starting points.
12
+ - [Tokens, persistence, and cross-tab sync](docs/tokens-and-persistence.md): Token
13
+ resolution priority, `localStorage` persistence, and cross-tab sync.
14
+ - [SSR, pre-paint, and Tailwind](docs/ssr-and-pre-paint.md): FOUC prevention, SSR
15
+ fallbacks, and Tailwind `dark` class toggling.
16
+
17
+ ## Optional
18
+
19
+ - [API reference](docs/reference/index.md): Generated per-symbol signatures and prose.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codenhub/theme",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "Zero-dependency browser theme preference helper for TypeScript apps.",
6
6
  "homepage": "https://github.com/codenhub/codenhub/tree/main/packages/theme",
@@ -11,7 +11,10 @@
11
11
  "directory": "packages/theme"
12
12
  },
13
13
  "files": [
14
- "dist"
14
+ "dist",
15
+ "docs",
16
+ "llms.txt",
17
+ "llms-full.txt"
15
18
  ],
16
19
  "type": "module",
17
20
  "main": "./dist/index.js",
@@ -26,26 +29,35 @@
26
29
  "publishConfig": {
27
30
  "access": "public"
28
31
  },
29
- "devDependencies": {
30
- "@playwright/test": "^1.61.1",
31
- "jsdom": "^29.1.1",
32
- "tsdown": "^0.22.3",
33
- "typescript": "^6.0.3",
34
- "vitest": "^4.1.10",
35
- "@codenhub/styles": "0.0.4",
36
- "@codenhub/vite-plugin-icons": "0.0.1"
37
- },
38
32
  "scripts": {
39
33
  "dev": "pnpm --filter=@codenhub/theme-dev dev",
40
34
  "debug": "pnpm build && pnpm --filter=@codenhub/theme-debug dev",
41
35
  "build": "tsdown src/index.ts --format esm --dts --clean --no-fixed-extension --minify",
36
+ "prepublishOnly": "pnpm build && pnpm typecheck",
42
37
  "status:npm": "npm view @codenhub/theme version dist-tags time --json && npm dist-tag ls @codenhub/theme && npm access get status @codenhub/theme",
43
- "status:pack": "npm pack --dry-run",
44
- "test": "vitest run && pnpm build && pnpm test:visual",
38
+ "status:pack": "npm pack --dry-run --ignore-scripts",
39
+ "test": "vitest run",
40
+ "test:browser": "playwright test",
41
+ "test:browser:watch": "playwright test --ui",
45
42
  "test:coverage": "vitest run --coverage",
46
- "test:visual": "playwright test",
47
43
  "test:watch": "vitest",
48
- "test:visual:watch": "playwright test --ui",
49
- "typecheck": "tsc --noEmit"
44
+ "typecheck": "tsc -b"
45
+ },
46
+ "devDependencies": {
47
+ "@playwright/test": "catalog:",
48
+ "@vitest/coverage-v8": "catalog:",
49
+ "jsdom": "catalog:",
50
+ "tsdown": "catalog:",
51
+ "typescript": "catalog:",
52
+ "vitest": "catalog:"
53
+ },
54
+ "codenhub": {
55
+ "docs": {
56
+ "label": "ThemeSystem",
57
+ "status": "active",
58
+ "reference": {
59
+ "prose": true
60
+ }
61
+ }
50
62
  }
51
- }
63
+ }