spark-html-theme 0.1.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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # ⚡ spark-html-theme
2
+
3
+ One-line dark / light / system theming for [spark-html](https://www.npmjs.com/package/spark-html).
4
+ Creates a reactive `theme` store, applies a `data-theme` attribute to `<html>`,
5
+ follows the OS preference, and remembers the choice — no boilerplate.
6
+
7
+ ```js
8
+ // main.js
9
+ import { theme } from 'spark-html-theme';
10
+ theme(); // that's it
11
+ ```
12
+
13
+ ```html
14
+ <!-- components/theme-toggle.html -->
15
+ <button onclick="{theme.toggle}">{theme.resolved}</button>
16
+
17
+ <script>
18
+ const theme = useStore('theme'); // { mode, resolved, toggle, set }
19
+ </script>
20
+ ```
21
+
22
+ Then style with the attribute:
23
+
24
+ ```css
25
+ :root { --bg: #fff; --text: #111; }
26
+ [data-theme="dark"] { --bg: #000; --text: #fff; }
27
+ body { background: var(--bg); color: var(--text); }
28
+ ```
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install spark-html-theme
34
+ ```
35
+
36
+ ## The `theme` store
37
+
38
+ | Field | Meaning |
39
+ |--------------|---------|
40
+ | `mode` | The user's choice: `'system'` \| `'light'` \| `'dark'`. |
41
+ | `resolved` | What actually applies right now: `'light'` \| `'dark'`. |
42
+ | `toggle()` | Cycle to the next mode (in `modes` order) and persist. |
43
+ | `set(mode)` | Jump to a specific mode and persist. |
44
+
45
+ Both `mode` and `resolved` are reactive — read them in any component via
46
+ `useStore('theme')`.
47
+
48
+ ## Options
49
+
50
+ ```js
51
+ theme({
52
+ key: 'theme-mode', // localStorage key
53
+ attribute: 'data-theme', // attribute written on <html>
54
+ modes: ['system', 'light', 'dark'],// toggle() cycle order
55
+ name: 'theme', // store name
56
+ });
57
+ ```
58
+
59
+ ## No flash of the wrong theme
60
+
61
+ A `type="module"` script runs after first paint, so add a tiny inline script to
62
+ `<head>` to set the theme before the page renders:
63
+
64
+ ```html
65
+ <script>
66
+ document.documentElement.dataset.theme =
67
+ (localStorage.getItem('theme-mode') === 'dark' ||
68
+ ((localStorage.getItem('theme-mode') || 'system') === 'system' &&
69
+ matchMedia('(prefers-color-scheme: dark)').matches)) ? 'dark' : 'light';
70
+ </script>
71
+ ```
72
+
73
+ (Or import `themeInitScript()` to get that string and inline it from your build.)
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "spark-html-theme",
3
+ "version": "0.1.0",
4
+ "description": "One-line dark/light/system theming for spark-html — a reactive `theme` store, data-theme attribute, system watch, and localStorage persistence.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "homepage": "https://wilkinnovo.github.io/spark",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "default": "./src/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "test": "node test/theme.js"
17
+ },
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/wilkinnovo/spark.git",
24
+ "directory": "packages/spark-html-theme"
25
+ },
26
+ "dependencies": {
27
+ "spark-html": "^0.21.0"
28
+ },
29
+ "keywords": [
30
+ "spark-html",
31
+ "theme",
32
+ "dark-mode",
33
+ "color-scheme"
34
+ ],
35
+ "license": "MIT"
36
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * spark-html-theme — one-line dark/light/system theming for spark-html.
3
+ */
4
+
5
+ /** The reactive `theme` store created by theme(). */
6
+ export interface ThemeStore {
7
+ /** The user's choice: 'system' | 'light' | 'dark' (or a custom mode). */
8
+ mode: string;
9
+ /** What actually applies right now: 'light' | 'dark'. */
10
+ resolved: string;
11
+ /** Cycle to the next mode (in `modes` order) and persist. */
12
+ toggle(): void;
13
+ /** Jump to a specific mode and persist. */
14
+ set(mode: string): void;
15
+ }
16
+
17
+ export interface ThemeOptions {
18
+ /** localStorage key for the saved mode (default 'theme-mode'). */
19
+ key?: string;
20
+ /** Attribute written on <html> with the resolved theme (default 'data-theme'). */
21
+ attribute?: string;
22
+ /** Cycle order for toggle() (default ['system','light','dark']). */
23
+ modes?: string[];
24
+ /** Store name (default 'theme'). */
25
+ name?: string;
26
+ }
27
+
28
+ /**
29
+ * Set up theming: create a reactive `theme` store, apply the resolved theme to
30
+ * `<html data-theme>`, watch the OS preference, and persist the choice. Call
31
+ * once during bootstrap. Returns the store proxy.
32
+ */
33
+ export function theme(options?: ThemeOptions): ThemeStore;
34
+
35
+ /**
36
+ * The inline no-flash snippet (a string) to drop into <head> so the correct
37
+ * theme is set before first paint.
38
+ */
39
+ export function themeInitScript(options?: { key?: string; attribute?: string }): string;
40
+
41
+ declare const _default: { theme: typeof theme; themeInitScript: typeof themeInitScript };
42
+ export default _default;
package/src/index.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * spark-html-theme — dark / light / system theming in one line.
3
+ *
4
+ * Replaces the boilerplate every site re-writes (a theme store, applying a
5
+ * `data-theme` attribute, watching the OS preference, persisting to
6
+ * localStorage, and a toggle). Call once in your bootstrap:
7
+ *
8
+ * import { theme } from 'spark-html-theme';
9
+ * theme();
10
+ *
11
+ * It creates a reactive `theme` store any component can read and drive:
12
+ *
13
+ * <span class="logo" onclick="{theme.toggle}"></span>
14
+ * <script>
15
+ * const theme = useStore('theme'); // { mode, resolved, toggle, set }
16
+ * $: label = theme.resolved; // 'light' | 'dark'
17
+ * </script>
18
+ *
19
+ * `mode` is the user's choice ('system' | 'light' | 'dark'); `resolved` is what
20
+ * actually applies ('light' | 'dark'); `toggle()` cycles through `modes`;
21
+ * `set(mode)` jumps to one. The chosen `resolved` is written to
22
+ * `document.documentElement` as `data-theme`.
23
+ *
24
+ * No-flash tip: a deferred module runs after first paint, so to avoid a flash of
25
+ * the wrong theme add this tiny inline script to <head> (it mirrors the same
26
+ * logic) — or import { themeInitScript } and inline its string:
27
+ *
28
+ * <script>document.documentElement.dataset.theme =
29
+ * (localStorage.getItem('theme-mode')||'system')==='light' ? 'light'
30
+ * : (localStorage.getItem('theme-mode')==='dark' ||
31
+ * matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
32
+ * </script>
33
+ */
34
+ import { store } from 'spark-html';
35
+
36
+ const DEFAULT_MODES = ['system', 'light', 'dark'];
37
+
38
+ /**
39
+ * Set up theming. Returns the reactive `theme` store proxy.
40
+ *
41
+ * @param {object} [options]
42
+ * @param {string} [options.key='theme-mode'] localStorage key for the mode.
43
+ * @param {string} [options.attribute='data-theme'] Attribute written on <html>.
44
+ * @param {string[]} [options.modes] Cycle order for toggle()
45
+ * (default ['system','light','dark']).
46
+ * @param {string} [options.name='theme'] Store name.
47
+ */
48
+ export function theme(options = {}) {
49
+ const key = options.key || 'theme-mode';
50
+ const attribute = options.attribute || 'data-theme';
51
+ const modes = options.modes || DEFAULT_MODES;
52
+ const name = options.name || 'theme';
53
+
54
+ const mq = typeof matchMedia !== 'undefined' ? matchMedia('(prefers-color-scheme: dark)') : null;
55
+ const read = () => { try { return localStorage.getItem(key); } catch { return null; } };
56
+ const write = (v) => { try { localStorage.setItem(key, v); } catch { /* ignore */ } };
57
+
58
+ const saved = read();
59
+ const initial = saved && modes.includes(saved) ? saved : modes[0];
60
+ const resolve = (mode) => (mode === 'system' ? (mq && mq.matches ? 'dark' : 'light') : mode);
61
+
62
+ function apply() {
63
+ s.resolved = resolve(s.mode);
64
+ const root = typeof document !== 'undefined' && document.documentElement;
65
+ if (root) root.setAttribute(attribute, s.resolved);
66
+ }
67
+ function set(mode) {
68
+ if (!modes.includes(mode)) return;
69
+ s.mode = mode;
70
+ write(mode);
71
+ apply();
72
+ }
73
+ function toggle() {
74
+ set(modes[(modes.indexOf(s.mode) + 1) % modes.length]);
75
+ }
76
+
77
+ const s = store(name, { mode: initial, resolved: resolve(initial), toggle, set });
78
+
79
+ apply();
80
+ if (mq && mq.addEventListener) mq.addEventListener('change', apply);
81
+ return s;
82
+ }
83
+
84
+ /**
85
+ * The inline no-flash snippet as a string, to drop into <head> (sets the
86
+ * `data-theme` attribute before first paint). Keep `key`/`attribute` in sync
87
+ * with theme().
88
+ */
89
+ export function themeInitScript({ key = 'theme-mode', attribute = 'data-theme' } = {}) {
90
+ return (
91
+ `(function(){try{var m=localStorage.getItem(${JSON.stringify(key)})||'system';` +
92
+ `var d=m==='dark'||(m==='system'&&matchMedia('(prefers-color-scheme: dark)').matches);` +
93
+ `document.documentElement.setAttribute(${JSON.stringify(attribute)},d?'dark':'light');}` +
94
+ `catch(e){document.documentElement.setAttribute(${JSON.stringify(attribute)},'dark');}})();`
95
+ );
96
+ }
97
+
98
+ export default { theme, themeInitScript };