@gottheflag/nova 0.1.0-beta.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/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Theme controller
2
+
3
+ A controller for managing the theming for applications.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ pnpm add @gottheflag/nova
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import { Controller } from "@gottheflag/nova";
15
+ import { Button } from "@gottheflag/nova/adapters";
16
+
17
+ const ctl = new Controller(document, {
18
+ // all configs are optional
19
+ storage: { key: "theme" },
20
+ attribute: "data-theme",
21
+ initial: "dark",
22
+ dark: "dark",
23
+ light: "light",
24
+ system: "system",
25
+ });
26
+
27
+ ctl.use(Button); // use adapters
28
+ ```
@@ -0,0 +1,38 @@
1
+ import { Adapter, Name } from '../core/index.js';
2
+
3
+ /**
4
+ * @example
5
+ * <button value="<prefix>light">Light</button>
6
+ * <button value="<prefix>dark">Dark</button>
7
+ */
8
+ declare const Button: Adapter & {
9
+ registry: Map<Element, Name[]>;
10
+ };
11
+
12
+ interface Options$1 {
13
+ prefix?: string;
14
+ }
15
+ declare const Radio: Adapter<Options$1> & {
16
+ registry: Map<Element, Name[]>;
17
+ prefix?: string;
18
+ };
19
+
20
+ interface Options {
21
+ prefix?: string;
22
+ }
23
+ /**
24
+ * @example
25
+ * ```html
26
+ * <select>
27
+ * <option value="theme:light">Light</option>
28
+ * <option value="theme:system">System</option>
29
+ * <option value="theme:dark">Dark</option>
30
+ * </select>
31
+ * ```
32
+ */
33
+ declare const Select: Adapter<Options> & {
34
+ registry: Map<Element, Name[]>;
35
+ prefix?: string;
36
+ };
37
+
38
+ export { Button, Radio, Select };
@@ -0,0 +1,129 @@
1
+ import { __name } from '../chunk-7QVYU63E.js';
2
+
3
+ // src/adapters/button.ts
4
+ var PREFIX = "theme:";
5
+ function parseThemeName(btn) {
6
+ const value = btn.value;
7
+ const name = value.slice(PREFIX.length);
8
+ return name ? name : value;
9
+ }
10
+ __name(parseThemeName, "parseThemeName");
11
+ var Button = {
12
+ name: "button",
13
+ registry: /* @__PURE__ */ new Map(),
14
+ discover(ctl) {
15
+ const root = ctl.root;
16
+ root.querySelectorAll(`button[value^="${PREFIX}"]`).forEach((btn) => {
17
+ const detected = [];
18
+ const theme = parseThemeName(btn);
19
+ if (theme) detected.push(theme);
20
+ this.registry.set(btn, detected);
21
+ });
22
+ },
23
+ bind(ctl) {
24
+ const root = ctl.root;
25
+ root.querySelectorAll(`button[value^="${PREFIX}"]`).forEach((btn) => {
26
+ const theme = parseThemeName(btn);
27
+ btn.addEventListener("click", () => {
28
+ if (this.registry.get(btn)?.includes(theme)) {
29
+ ctl.set(theme);
30
+ }
31
+ });
32
+ });
33
+ }
34
+ };
35
+
36
+ // src/adapters/radio.ts
37
+ var PREFIX2 = "theme:";
38
+ function parseThemeName2(btn, prefix) {
39
+ const value = btn.value;
40
+ const name = value.slice(prefix.length);
41
+ return name ? name : value;
42
+ }
43
+ __name(parseThemeName2, "parseThemeName");
44
+ var Radio = {
45
+ name: "radio",
46
+ registry: /* @__PURE__ */ new Map(),
47
+ setup(_ctl, options) {
48
+ this.prefix = options?.prefix ?? PREFIX2;
49
+ },
50
+ discover(ctl) {
51
+ const root = ctl.root;
52
+ root.querySelectorAll(`input[type="radio"][value^="${PREFIX2}"]`).forEach((radio) => {
53
+ const detected = [];
54
+ const theme = parseThemeName2(radio, this.prefix);
55
+ if (theme) detected.push(theme);
56
+ this.registry.set(radio, detected);
57
+ });
58
+ },
59
+ bind(ctl) {
60
+ const root = ctl.root;
61
+ root.querySelectorAll(`input[type="radio"][value^="${PREFIX2}"]`).forEach((radio) => {
62
+ radio.addEventListener("change", (e) => {
63
+ const target = e.currentTarget;
64
+ const theme = parseThemeName2(target, this.prefix);
65
+ if (theme) ctl.set(theme);
66
+ });
67
+ });
68
+ },
69
+ sync(ctl) {
70
+ const root = ctl.root;
71
+ const active = ctl.active();
72
+ root.querySelectorAll(`input[type="radio"][value^="${PREFIX2}"]`).forEach((radio) => {
73
+ const theme = parseThemeName2(radio, this.prefix);
74
+ radio.checked = theme === active;
75
+ });
76
+ }
77
+ };
78
+
79
+ // src/adapters/select.ts
80
+ var PREFIX3 = "theme:";
81
+ function parseThemeName3(btn, prefix) {
82
+ const value = btn.value;
83
+ const name = value.slice(prefix.length);
84
+ return name ? name : value;
85
+ }
86
+ __name(parseThemeName3, "parseThemeName");
87
+ var Select = {
88
+ name: "select",
89
+ registry: /* @__PURE__ */ new Map(),
90
+ setup(_ctl, options) {
91
+ this.prefix = options?.prefix ?? PREFIX3;
92
+ },
93
+ discover(ctl) {
94
+ const root = ctl.root;
95
+ root.querySelectorAll(`select:has(option[value^="${this.prefix}"])`).forEach((sel) => {
96
+ const detected = [];
97
+ Array.from(sel.options).forEach((option) => {
98
+ const theme = parseThemeName3(option, this.prefix);
99
+ if (theme) detected.push(theme);
100
+ });
101
+ this.registry.set(sel, detected);
102
+ });
103
+ },
104
+ bind(ctl) {
105
+ const root = ctl.root;
106
+ root.querySelectorAll(`select:has(option[value^="${this.prefix}"])`).forEach((sel) => {
107
+ sel.addEventListener("change", (e) => {
108
+ const target = e.currentTarget;
109
+ const option = target.options[target.selectedIndex];
110
+ const theme = parseThemeName3(option, this.prefix);
111
+ if (theme) ctl.set(theme);
112
+ });
113
+ });
114
+ },
115
+ sync(ctl) {
116
+ const root = ctl.root;
117
+ const active = ctl.active();
118
+ root.querySelectorAll(`select:has(option[value^="${this.prefix}"])`).forEach((sel) => {
119
+ Array.from(sel.options).forEach((option) => {
120
+ const theme = parseThemeName3(option, this.prefix);
121
+ option.selected = theme === active;
122
+ });
123
+ });
124
+ }
125
+ };
126
+
127
+ export { Button, Radio, Select };
128
+ //# sourceMappingURL=index.js.map
129
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/button.ts","../../src/adapters/radio.ts","../../src/adapters/select.ts"],"names":["PREFIX","parseThemeName","btn","value","name","slice","length","Button","registry","Map","discover","ctl","root","querySelectorAll","forEach","detected","theme","push","set","bind","addEventListener","get","includes","prefix","Radio","setup","_ctl","options","radio","e","target","currentTarget","sync","active","checked","Select","sel","Array","from","option","selectedIndex","selected"],"mappings":"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","file":"index.js","sourcesContent":["import { Controller } from \"../core/controller.js\";\r\nimport type { Adapter } from \"../core/adapter.js\";\r\nimport { State, type Name } from \"../core/types.js\";\r\n\r\nconst PREFIX = \"theme:\";\r\n\r\nfunction parseThemeName(btn: HTMLButtonElement): State {\r\n const value = btn.value;\r\n const name = value.slice(PREFIX.length) as State;\r\n\r\n return name ? name : value as State;\r\n}\r\n\r\n/**\r\n * @example\r\n * <button value=\"<prefix>light\">Light</button>\r\n * <button value=\"<prefix>dark\">Dark</button>\r\n */\r\nexport const Button: Adapter & {\r\n registry: Map<Element, Name[]>;\r\n} = {\r\n name: \"button\",\r\n\r\n registry: new Map<Element, Name[]>(),\r\n\r\n discover(ctl: Controller) {\r\n const root = ctl.root;\r\n\r\n root.querySelectorAll<HTMLButtonElement>(`button[value^=\"${PREFIX}\"]`).forEach(btn => {\r\n const detected: Name[] = [];\r\n\r\n const theme = parseThemeName(btn);\r\n if (theme) detected.push(theme);\r\n\r\n this.registry.set(btn, detected);\r\n });\r\n },\r\n\r\n bind(ctl: Controller) {\r\n const root = ctl.root;\r\n\r\n root.querySelectorAll<HTMLButtonElement>(`button[value^=\"${PREFIX}\"]`).forEach(btn => {\r\n const theme = parseThemeName(btn);\r\n\r\n btn.addEventListener(\"click\", () => {\r\n if (this.registry.get(btn)?.includes(theme)) {\r\n ctl.set(theme);\r\n }\r\n });\r\n });\r\n },\r\n};\r\n","import type { Adapter } from '../core/adapter.js';\r\nimport { Controller } from '../core/controller.js';\r\nimport { Name, State } from '../core/types.js';\r\n\r\nconst PREFIX = \"theme:\";\r\n\r\nfunction parseThemeName(btn: HTMLInputElement, prefix: string): State {\r\n const value = btn.value;\r\n const name = value.slice(prefix.length) as State;\r\n\r\n return name ? name : value as State;\r\n}\r\n\r\ninterface Options {\r\n prefix?: string;\r\n};\r\n\r\nexport const Radio: Adapter<Options> & {\r\n registry: Map<Element, Name[]>;\r\n prefix?: string;\r\n} = {\r\n name: 'radio',\r\n\r\n registry: new Map<Element, Name[]>(),\r\n\r\n setup(_ctl: Controller, options) {\r\n this.prefix = options?.prefix ?? PREFIX;\r\n },\r\n\r\n discover(ctl: Controller) {\r\n const root = ctl.root;\r\n\r\n root.querySelectorAll<HTMLInputElement>(`input[type=\"radio\"][value^=\"${PREFIX}\"]`).forEach(radio => {\r\n const detected: Name[] = [];\r\n\r\n const theme = parseThemeName(radio, this.prefix!);\r\n if (theme) detected.push(theme);\r\n\r\n this.registry.set(radio, detected);\r\n });\r\n },\r\n\r\n bind(ctl: Controller) {\r\n const root = ctl.root;\r\n\r\n root.querySelectorAll<HTMLInputElement>(`input[type=\"radio\"][value^=\"${PREFIX}\"]`).forEach(radio => { \r\n radio.addEventListener('change', e => {\r\n const target = e.currentTarget as HTMLInputElement;\r\n const theme = parseThemeName(target, this.prefix!);\r\n \r\n if (theme) ctl.set(theme);\r\n });\r\n });\r\n },\r\n\r\n sync(ctl: Controller) {\r\n const root = ctl.root;\r\n const active = ctl.active();\r\n\r\n root.querySelectorAll<HTMLInputElement>(`input[type=\"radio\"][value^=\"${PREFIX}\"]`).forEach(radio => {\r\n const theme = parseThemeName(radio, this.prefix!);\r\n radio.checked = theme === active;\r\n });\r\n }\r\n};\r\n","import type { Adapter } from \"../core/adapter.js\";\r\nimport { Controller } from \"../core/controller.js\";\r\nimport { State, type Name } from \"../core/types.js\";\r\n\r\nconst PREFIX = \"theme:\";\r\n\r\nfunction parseThemeName(btn: HTMLOptionElement, prefix: string): State {\r\n const value = btn.value;\r\n const name = value.slice(prefix.length) as State;\r\n\r\n return name ? name : value as State;\r\n}\r\n\r\ninterface Options {\r\n prefix?: string;\r\n};\r\n\r\n/**\r\n * @example\r\n * ```html\r\n * <select>\r\n * <option value=\"theme:light\">Light</option>\r\n * <option value=\"theme:system\">System</option>\r\n * <option value=\"theme:dark\">Dark</option>\r\n * </select>\r\n * ```\r\n */\r\nexport const Select: Adapter<Options> & {\r\n registry: Map<Element, Name[]>;\r\n prefix?: string;\r\n} = {\r\n name: \"select\",\r\n\r\n registry: new Map<Element, Name[]>(),\r\n\r\n setup(_ctl: Controller, options) {\r\n this.prefix = options?.prefix ?? PREFIX;\r\n },\r\n\r\n discover(ctl: Controller) {\r\n const root = ctl.root;\r\n\r\n root.querySelectorAll<HTMLSelectElement>(`select:has(option[value^=\"${this.prefix}\"])`).forEach(sel => {\r\n const detected: Name[] = [];\r\n\r\n Array.from(sel.options).forEach(option => {\r\n const theme = parseThemeName(option, this.prefix!);\r\n if (theme) detected.push(theme);\r\n });\r\n\r\n this.registry.set(sel, detected);\r\n });\r\n },\r\n\r\n bind(ctl: Controller) {\r\n const root = ctl.root;\r\n\r\n root.querySelectorAll<HTMLSelectElement>(`select:has(option[value^=\"${this.prefix}\"])`).forEach(sel => {\r\n sel.addEventListener(\"change\", e => {\r\n const target = e.currentTarget as HTMLSelectElement;\r\n const option = target.options[ target.selectedIndex ];\r\n\r\n const theme = parseThemeName(option as HTMLOptionElement, this.prefix!);\r\n if (theme) ctl.set(theme);\r\n });\r\n });\r\n },\r\n\r\n sync(ctl: Controller) {\r\n const root = ctl.root;\r\n const active = ctl.active();\r\n\r\n root.querySelectorAll<HTMLSelectElement>(`select:has(option[value^=\"${this.prefix}\"])`).forEach(sel => {\r\n Array.from(sel.options).forEach(option => {\r\n const theme = parseThemeName(option, this.prefix!);\r\n option.selected = theme === active;\r\n });\r\n });\r\n }\r\n};\r\n"]}
@@ -0,0 +1,6 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ export { __name };
5
+ //# sourceMappingURL=chunk-7QVYU63E.js.map
6
+ //# sourceMappingURL=chunk-7QVYU63E.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-7QVYU63E.js"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Effective theme name (e.g. `day`, `night`, `mint`).
3
+ */
4
+ type Name = string;
5
+ /**
6
+ * Theme state (`<dark>`, `<light>`, `<system>`).
7
+ * Different between state themes and effective themes:
8
+ * - State themes are the raw values used as identifiers (`<dark>`, `<light>`, `<system>`).
9
+ * - Effective themes are the resolved values (e.g. `<dark>`, `<light>`, `mint`).
10
+ */
11
+ type State = "system" | "light" | "dark";
12
+
13
+ /**
14
+ * Raw configuration, passed by the user.
15
+ */
16
+ interface RawConfig {
17
+ attribute?: string;
18
+ storage?: {
19
+ key: string;
20
+ };
21
+ system?: string;
22
+ light?: Name;
23
+ dark?: Name;
24
+ initial?: Name;
25
+ }
26
+ /**
27
+ * Resolved configuration by the controller.
28
+ */
29
+ interface ResolvedConfig {
30
+ attribute: string;
31
+ storage: {
32
+ key: string;
33
+ };
34
+ system: string;
35
+ light: Name;
36
+ dark: Name;
37
+ initial?: Name;
38
+ }
39
+
40
+ /**
41
+ * Theme controller logic.
42
+ */
43
+ declare class Controller {
44
+ private _root;
45
+ /**
46
+ * Resolved configuration.
47
+ */
48
+ config: ResolvedConfig;
49
+ /**
50
+ * System theme proxy.
51
+ */
52
+ private system;
53
+ private adapters;
54
+ constructor(_root?: Document | ParentNode, cfg?: RawConfig);
55
+ /**
56
+ * The root element to apply theme to.
57
+ */
58
+ get root(): HTMLElement;
59
+ /**
60
+ * @param resolved resolve state and effective themes to their values.
61
+ *
62
+ * @example
63
+ *
64
+ * system = config.system;
65
+ * light = config.light;
66
+ * dark = config.dark;
67
+ *
68
+ * resolved = true;
69
+ * [
70
+ * "system": light | dark,
71
+ * "light": light,
72
+ * "dark": dark
73
+ * ]
74
+ * resolved = false;
75
+ * [
76
+ * "system": system,
77
+ * "light": light,
78
+ * "dark": dark
79
+ * ]
80
+ */
81
+ active(resolved?: boolean): State | Name | null;
82
+ /**
83
+ * Register an adapter.
84
+ *
85
+ * @description
86
+ * Adapters are toys used to change themes.
87
+ * They define where themes lives and how they work with the controller.
88
+ *
89
+ * @param adapter Adapter instance
90
+ * @param options Adapter options
91
+ */
92
+ use<T extends Adapter<any>>(adapter: T, options?: T extends Adapter<infer O> ? O : never): this;
93
+ /**
94
+ * Set the effective theme from a theme state.
95
+ *
96
+ * @see {@link State}
97
+ * @param state Theme State
98
+ */
99
+ set(state: State): void;
100
+ /**
101
+ * Read the stored theme state from local storage.
102
+ */
103
+ get state(): State | null;
104
+ /**
105
+ * Sync the system listener.
106
+ */
107
+ private syncSystemListener;
108
+ /**
109
+ * Applies the effective theme name to the root.
110
+ *
111
+ * @param theme Effective theme name (e.g. `<dark>`, `<light>`, `mint`)
112
+ */
113
+ private apply;
114
+ /**
115
+ * Write theme state to local storage.
116
+ *
117
+ * @param theme Theme state
118
+ */
119
+ private write;
120
+ }
121
+
122
+ interface Adapter<Options = undefined> {
123
+ name: string;
124
+ /**
125
+ * Setup the adapter.
126
+ * Used to initiate any one-time setup.
127
+ *
128
+ * ---
129
+ * @remarks
130
+ * Called once, when the controller is instantiated.
131
+ *
132
+ * ---
133
+ * @param ctl Controller instance
134
+ * @param options Adapter options
135
+ */
136
+ setup?(ctl: Controller, options: Options): void;
137
+ /**
138
+ * Discover themes that are available to this adapter. \
139
+ * Used to validate, or for example do a one-time setup.
140
+ *
141
+ * ---
142
+ * @remarks
143
+ * Called once, when the controller is instantiated.
144
+ *
145
+ * ---
146
+ * @param ctl the controller instance
147
+ * @returns a list of themes that are available to this adapter
148
+ */
149
+ discover?(ctl: Controller): void;
150
+ /**
151
+ * Define the theme selection mechanism.
152
+ *
153
+ * ---
154
+ * @remarks
155
+ * Called every time the controller is instantiated.
156
+ *
157
+ * ---
158
+ * @param ctl the controller instance
159
+ */
160
+ bind?(ctl: Controller): void;
161
+ /**
162
+ * Sync theme adapter state with the controller.
163
+ *
164
+ * ---
165
+ * @remarks
166
+ * Called every time the controller is instantiated.
167
+ *
168
+ * ---
169
+ * @param ctl the controller instance
170
+ */
171
+ sync?(ctl: Controller): void;
172
+ }
173
+
174
+ export { type Adapter, Controller, type Name, type State };
@@ -0,0 +1,248 @@
1
+ import { __name } from '../chunk-7QVYU63E.js';
2
+
3
+ // src/core/system.ts
4
+ var System = class {
5
+ static {
6
+ __name(this, "System");
7
+ }
8
+ ctl;
9
+ /**
10
+ * Media query list instance.
11
+ */
12
+ mql;
13
+ /**
14
+ * Handles the media query list listener.
15
+ */
16
+ handler;
17
+ /**
18
+ * Controller instance.
19
+ */
20
+ constructor(ctl) {
21
+ this.ctl = ctl;
22
+ }
23
+ /**
24
+ * Get the effective theme by system preference.
25
+ */
26
+ get prefers() {
27
+ return matchMedia?.("(prefers-color-scheme: dark)").matches ? this.ctl.config.dark : this.ctl.config.light;
28
+ }
29
+ /**
30
+ * Check if the effective theme is dark.
31
+ */
32
+ get prefersDark() {
33
+ return this.prefers === this.ctl.config.dark;
34
+ }
35
+ /**
36
+ * Check if the effective theme is light.
37
+ */
38
+ get prefersLight() {
39
+ return this.prefers === this.ctl.config.light;
40
+ }
41
+ /**
42
+ * Start listening for system theme changes.
43
+ *
44
+ * @param onChange Callback to be called when the system theme changes.
45
+ */
46
+ start(onChange) {
47
+ if (this.handler) return;
48
+ this.mql = matchMedia("(prefers-color-scheme: dark)");
49
+ this.handler = (e) => onChange(e.matches ? "dark" : "light");
50
+ this.mql.addEventListener?.("change", this.handler);
51
+ }
52
+ /**
53
+ * Stop listening for system theme changes.
54
+ */
55
+ stop() {
56
+ if (!this.mql || !this.handler) return;
57
+ this.mql.removeEventListener?.("change", this.handler);
58
+ this.mql = this.handler = void 0;
59
+ }
60
+ };
61
+
62
+ // src/core/utils.ts
63
+ function resolveStateOf(state, system, config) {
64
+ if (state === config.system) {
65
+ return system.prefers;
66
+ }
67
+ if (state === config.light || state === config.dark) {
68
+ return state;
69
+ }
70
+ return typeof state === "string" ? state : null;
71
+ }
72
+ __name(resolveStateOf, "resolveStateOf");
73
+ function resolveRoot(root) {
74
+ if (root instanceof HTMLElement) {
75
+ return root;
76
+ } else if (root instanceof Document) {
77
+ return root.documentElement;
78
+ } else if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
79
+ return root.host;
80
+ }
81
+ return document.documentElement;
82
+ }
83
+ __name(resolveRoot, "resolveRoot");
84
+
85
+ // src/core/controller.ts
86
+ var Controller = class {
87
+ static {
88
+ __name(this, "Controller");
89
+ }
90
+ _root;
91
+ /**
92
+ * Resolved configuration.
93
+ */
94
+ config;
95
+ /**
96
+ * System theme proxy.
97
+ */
98
+ system;
99
+ adapters = [];
100
+ constructor(_root = document, cfg = {}) {
101
+ this._root = _root;
102
+ this.config = {
103
+ attribute: cfg.attribute ?? "data-theme",
104
+ storage: {
105
+ key: cfg.storage?.key ?? "theme"
106
+ },
107
+ system: cfg.system ?? "system",
108
+ light: cfg.light ?? "light",
109
+ dark: cfg.dark ?? "dark",
110
+ initial: cfg.initial
111
+ };
112
+ this.system = new System(this);
113
+ this.syncSystemListener();
114
+ }
115
+ /**
116
+ * The root element to apply theme to.
117
+ */
118
+ get root() {
119
+ return resolveRoot(this._root);
120
+ }
121
+ /**
122
+ * @param resolved resolve state and effective themes to their values.
123
+ *
124
+ * @example
125
+ *
126
+ * system = config.system;
127
+ * light = config.light;
128
+ * dark = config.dark;
129
+ *
130
+ * resolved = true;
131
+ * [
132
+ * "system": light | dark,
133
+ * "light": light,
134
+ * "dark": dark
135
+ * ]
136
+ * resolved = false;
137
+ * [
138
+ * "system": system,
139
+ * "light": light,
140
+ * "dark": dark
141
+ * ]
142
+ */
143
+ active(resolved = false) {
144
+ const state = this.state;
145
+ if (!state) {
146
+ return this.config.initial || null;
147
+ }
148
+ const theme = resolveStateOf(state, this.system, this.config);
149
+ if (!theme) return null;
150
+ return resolved ? theme : state;
151
+ }
152
+ /**
153
+ * Register an adapter.
154
+ *
155
+ * @description
156
+ * Adapters are toys used to change themes.
157
+ * They define where themes lives and how they work with the controller.
158
+ *
159
+ * @param adapter Adapter instance
160
+ * @param options Adapter options
161
+ */
162
+ use(adapter, options) {
163
+ if (this.adapters.includes(adapter)) {
164
+ console.warn(`Adapter <${adapter.name}> is already installed.`);
165
+ return this;
166
+ }
167
+ this.adapters.push(adapter);
168
+ try {
169
+ adapter.setup?.(this, options);
170
+ adapter.discover?.(this);
171
+ adapter.bind?.(this);
172
+ adapter.sync?.(this);
173
+ } catch (err) {
174
+ console.warn(`Adapter <${adapter.name}> failed to install.`, err);
175
+ }
176
+ return this;
177
+ }
178
+ /**
179
+ * Set the effective theme from a theme state.
180
+ *
181
+ * @see {@link State}
182
+ * @param state Theme State
183
+ */
184
+ set(state) {
185
+ const theme = resolveStateOf(state, this.system, this.config);
186
+ if (!theme) {
187
+ return;
188
+ }
189
+ this.write(state);
190
+ this.syncSystemListener();
191
+ this.apply(theme);
192
+ for (const a of this.adapters) {
193
+ a.sync?.(this);
194
+ }
195
+ }
196
+ /**
197
+ * Read the stored theme state from local storage.
198
+ */
199
+ get state() {
200
+ try {
201
+ return localStorage.getItem(this.config.storage.key);
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+ /**
207
+ * Sync the system listener.
208
+ */
209
+ syncSystemListener() {
210
+ const state = this.active();
211
+ if (state === this.config.system) {
212
+ this.system.start((_state) => {
213
+ const theme = resolveStateOf("system", this.system, this.config);
214
+ if (!theme) return;
215
+ this.apply(theme);
216
+ for (const a of this.adapters) {
217
+ a.sync?.(this);
218
+ }
219
+ });
220
+ } else {
221
+ this.system.stop();
222
+ }
223
+ }
224
+ /**
225
+ * Applies the effective theme name to the root.
226
+ *
227
+ * @param theme Effective theme name (e.g. `<dark>`, `<light>`, `mint`)
228
+ */
229
+ apply(theme) {
230
+ this.root.setAttribute(this.config.attribute, theme);
231
+ }
232
+ /**
233
+ * Write theme state to local storage.
234
+ *
235
+ * @param theme Theme state
236
+ */
237
+ write(theme) {
238
+ try {
239
+ localStorage.setItem(this.config.storage.key, theme);
240
+ } catch {
241
+ console.warn(`Failed to write theme state to local storage.`);
242
+ }
243
+ }
244
+ };
245
+
246
+ export { Controller };
247
+ //# sourceMappingURL=index.js.map
248
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/system.ts","../../src/core/utils.ts","../../src/core/controller.ts"],"names":["System","mql","handler","ctl","prefers","matchMedia","matches","config","dark","light","prefersDark","prefersLight","start","onChange","e","addEventListener","stop","removeEventListener","undefined","resolveStateOf","state","system","resolveRoot","root","HTMLElement","Document","documentElement","ShadowRoot","host","document","Controller","adapters","_root","cfg","attribute","storage","key","initial","syncSystemListener","active","resolved","theme","use","adapter","options","includes","console","warn","name","push","setup","discover","bind","sync","err","set","write","apply","a","localStorage","getItem","_state","setAttribute","setItem"],"mappings":"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","file":"index.js","sourcesContent":["import { Controller } from \"./controller.js\";\r\nimport { Name, State } from \"./types.js\";\r\n\r\n/**\r\n * System theme proxy.\r\n * \r\n * Used to detect system theme preference and listen for changes.\r\n */\r\nexport class System {\r\n\t/**\r\n\t * Media query list instance.\r\n\t */\r\n\tprivate mql?: MediaQueryList;\r\n\t/**\r\n\t * Handles the media query list listener.\r\n\t */\r\n\tprivate handler?: (e: MediaQueryListEvent) => void;\r\n\t/**\r\n\t * Controller instance.\r\n\t */\r\n\r\n\tconstructor(private ctl: Controller) { }\r\n\r\n\t/**\r\n\t * Get the effective theme by system preference.\r\n\t */\r\n\tget prefers(): Name {\r\n\t\treturn matchMedia?.(\"(prefers-color-scheme: dark)\").matches\r\n\t\t\t? this.ctl.config.dark\r\n\t\t\t: this.ctl.config.light;\r\n\t}\r\n\r\n\t/**\r\n\t * Check if the effective theme is dark.\r\n\t */\r\n\tget prefersDark(): boolean {\r\n\t\treturn this.prefers === this.ctl.config.dark;\r\n\t}\r\n\r\n\t/**\r\n\t * Check if the effective theme is light.\r\n\t */\r\n\tget prefersLight(): boolean {\r\n\t\treturn this.prefers === this.ctl.config.light;\r\n\t}\r\n\r\n\t/**\r\n\t * Start listening for system theme changes.\r\n\t * \r\n\t * @param onChange Callback to be called when the system theme changes.\r\n\t */\r\n\tstart(onChange: (state: Exclude<State, \"system\">) => void) {\r\n\t\tif (this.handler) return;\r\n\r\n\t\tthis.mql = matchMedia(\"(prefers-color-scheme: dark)\");\r\n\t\tthis.handler = e => onChange(e.matches ? \"dark\" : \"light\");\r\n\r\n\t\tthis.mql.addEventListener?.(\"change\", this.handler);\r\n\t}\r\n\r\n\t/**\r\n\t * Stop listening for system theme changes.\r\n\t */\r\n\tstop() {\r\n\t\tif (!this.mql || !this.handler) return;\r\n\r\n\t\tthis.mql.removeEventListener?.(\"change\", this.handler);\r\n\t\t\r\n\t\tthis.mql = this.handler = undefined;\r\n\t}\r\n}","import { ResolvedConfig } from \"./config.js\";\r\nimport { System } from \"./system.js\";\r\nimport { State, type Name } from \"./types.js\";\r\n\r\n/**\r\n * Resolve a theme state to an effective theme name.\r\n * \r\n * @see {@link State}\r\n * \r\n * @param theme Theme state\r\n * @param system System instance.\r\n * @param config Resolved set of configuration.\r\n * @returns \r\n */\r\nexport function resolveStateOf(state: State, system: System, config: ResolvedConfig): Name | null {\r\n\tif (state === config.system) {\r\n\t\treturn system.prefers;\r\n\t}\r\n\r\n\tif (state === config.light || state === config.dark) {\r\n\t\treturn state;\r\n\t}\r\n\r\n\treturn typeof state === \"string\" ? state : null;\r\n}\r\n\r\nexport function resolveRoot(root: unknown): HTMLElement {\r\n\tif (root instanceof HTMLElement) {\r\n\t\treturn root\r\n\t} else if (root instanceof Document) {\r\n\t\treturn root.documentElement\r\n\t} else if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {\r\n\t\treturn root.host\r\n\t};\r\n\r\n\treturn document.documentElement;\r\n}","import { Adapter } from \"./adapter.js\";\r\nimport { RawConfig, ResolvedConfig } from \"./config.js\";\r\nimport { System } from \"./system.js\";\r\nimport { Name, State } from \"./types.js\";\r\nimport { resolveRoot, resolveStateOf } from \"./utils.js\";\r\n\r\n/**\r\n * Theme controller logic.\r\n */\r\nexport class Controller {\r\n\t/**\r\n\t * Resolved configuration.\r\n\t */\r\n\tconfig: ResolvedConfig;\r\n\t/**\r\n\t * System theme proxy.\r\n\t */\r\n\tprivate system: System;\r\n\r\n\tprivate adapters: Adapter<any>[] = [];\r\n\r\n\tconstructor(\r\n\t\tprivate _root: Document | ParentNode = document,\r\n\t\tcfg: RawConfig = {}\r\n\t) {\r\n\t\tthis.config = {\r\n\t\t\tattribute: cfg.attribute ?? \"data-theme\",\r\n\t\t\tstorage: {\r\n\t\t\t\tkey: cfg.storage?.key ?? \"theme\",\r\n\t\t\t},\r\n\t\t\tsystem: cfg.system ?? \"system\",\r\n\t\t\tlight: cfg.light ?? \"light\",\r\n\t\t\tdark: cfg.dark ?? \"dark\",\r\n\t\t\tinitial: cfg.initial\r\n\t\t};\r\n\r\n\t\tthis.system = new System(this);\r\n\r\n\t\tthis.syncSystemListener();\r\n\t}\r\n\r\n\t/**\r\n\t * The root element to apply theme to.\r\n\t */\r\n\tget root(): HTMLElement {\r\n\t\treturn resolveRoot(this._root);\r\n\t}\r\n\r\n\t/**\r\n\t * @param resolved resolve state and effective themes to their values.\r\n\t * \r\n\t * @example\r\n\t * \r\n\t * system = config.system;\r\n\t * light = config.light;\r\n\t * dark = config.dark;\r\n\t * \r\n\t * resolved = true;\r\n\t * [\r\n\t * \t\t\"system\": light | dark,\r\n\t * \t\t\"light\": light,\r\n\t * \t\t\"dark\": dark\r\n\t * ]\r\n\t * resolved = false;\r\n\t * [\r\n\t * \t\t\"system\": system,\r\n\t * \t\t\"light\": light,\r\n\t * \t\t\"dark\": dark\r\n\t * ]\r\n\t */\r\n\tactive(resolved: boolean = false): State | Name | null {\r\n\t\tconst state = this.state;\r\n\r\n\t\tif (!state) {\r\n\t\t\treturn this.config.initial || null;\r\n\t\t}\r\n\r\n\t\tconst theme = resolveStateOf(state, this.system, this.config);\r\n\r\n\t\tif (!theme) return null;\r\n\r\n\r\n\t\treturn resolved ? theme : state;\r\n\t}\r\n\r\n\t/**\r\n\t * Register an adapter.\r\n\t * \r\n\t * @description\r\n\t * Adapters are toys used to change themes.\r\n\t * They define where themes lives and how they work with the controller.\r\n\t * \r\n\t * @param adapter Adapter instance\r\n\t * @param options Adapter options\r\n\t */\r\n\tuse<T extends Adapter<any>>(adapter: T, options?: T extends Adapter<infer O> ? O : never) {\r\n\t\tif (this.adapters.includes(adapter)) {\r\n\t\t\tconsole.warn(`Adapter <${adapter.name}> is already installed.`);\r\n\t\t\treturn this;\r\n\t\t}\r\n\r\n\t\tthis.adapters.push(adapter);\r\n\r\n\t\ttry {\r\n\t\t\tadapter.setup?.(this, options as any);\r\n\t\t\tadapter.discover?.(this);\r\n\t\t\tadapter.bind?.(this);\r\n\t\t\tadapter.sync?.(this);\r\n\t\t} catch (err) {\r\n\t\t\tconsole.warn(`Adapter <${adapter.name}> failed to install.`, err);\r\n\t\t}\r\n\r\n\t\treturn this;\r\n\t}\r\n\t\r\n\t/**\r\n\t * Set the effective theme from a theme state.\r\n\t * \r\n\t * @see {@link State}\r\n\t * @param state Theme State\r\n\t */\r\n\tset(state: State) {\r\n\t\tconst theme = resolveStateOf(state, this.system, this.config);\r\n\t\tif (!theme) {\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tthis.write(state);\r\n\r\n\t\tthis.syncSystemListener();\r\n\r\n\t\tthis.apply(theme);\r\n\r\n\t\tfor (const a of this.adapters) {\r\n\t\t\ta.sync?.(this);\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Read the stored theme state from local storage.\r\n\t */\r\n\tget state(): State | null {\r\n\t\ttry {\r\n\t\t\treturn localStorage.getItem(this.config.storage.key) as State | null;\r\n\t\t} catch { return null; }\r\n\t}\r\n\r\n\t/**\r\n\t * Sync the system listener.\r\n\t */\r\n\tprivate syncSystemListener() {\r\n\t\tconst state = this.active();\r\n\r\n\t\tif (state === this.config.system) {\r\n\t\t\tthis.system.start(_state => {\r\n\t\t\t\tconst theme = resolveStateOf(\r\n\t\t\t\t\t\"system\",\r\n\t\t\t\t\tthis.system,\r\n\t\t\t\t\tthis.config\r\n\t\t\t\t);\r\n\t\t\t\tif (!theme) return;\r\n\r\n\t\t\t\tthis.apply(theme);\r\n\r\n\t\t\t\tfor (const a of this.adapters) {\r\n\t\t\t\t\ta.sync?.(this);\r\n\t\t\t\t}\r\n\t\t\t});\r\n\t\t} else {\r\n\t\t\tthis.system.stop();\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Applies the effective theme name to the root.\r\n\t * \r\n\t * @param theme Effective theme name (e.g. `<dark>`, `<light>`, `mint`)\r\n\t */\r\n\tprivate apply(theme: Name) {\r\n\t\tthis.root.setAttribute(this.config.attribute, theme);\r\n\t}\r\n\r\n\t/**\r\n\t * Write theme state to local storage.\r\n\t * \r\n\t * @param theme Theme state\r\n\t */\r\n\tprivate write(theme: State) {\r\n\t\ttry {\r\n\t\t\tlocalStorage.setItem(this.config.storage.key, theme);\r\n\t\t} catch {\r\n\t\t\tconsole.warn(`Failed to write theme state to local storage.`);\r\n\t\t}\r\n\t}\r\n}"]}
@@ -0,0 +1 @@
1
+ !function(){"use strict";!function(l=document.documentElement,t){var e,n,u,i,a,o,r;try{let d=null!=(n=null==(e=null==t?void 0:t.storage)?void 0:e.key)?n:"theme",c=null!=(u=null==t?void 0:t.attribute)?u:"data-theme",m=null!=(i=null==t?void 0:t.system)?i:"system",s=null!=(a=null==t?void 0:t.light)?a:"light",h=null!=(o=null==t?void 0:t.dark)?o:"dark",v=null!=(r=null==t?void 0:t.initial)?r:h;if(l&&l.hasAttribute(c))return;let g=localStorage.getItem(d),f=null;g===m?f=null!=matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches?h:s:g===s||g===h?f=g:!g&&v&&(f=v),f&&l.setAttribute(c,f)}catch(l){}}()}();
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@gottheflag/nova",
3
+ "version": "0.1.0-beta.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "GTF",
8
+ "email": "dev@gottheflag.sa",
9
+ "url": "https://gottheflag.sa"
10
+ },
11
+ "description": "Theme state controller.",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/core/index.d.ts",
18
+ "import": "./dist/core/index.js"
19
+ },
20
+ "./adapters": {
21
+ "types": "./dist/adapters/index.d.ts",
22
+ "import": "./dist/adapters/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "dev": "concurrently -k -n TS,VITE -c blue,green \"tsc -p tsconfig.build.json --watch\" \"vite example\"",
27
+ "build": "tsup",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepare": "pnpm run build"
30
+ },
31
+ "keywords": [
32
+ "theme",
33
+ "light",
34
+ "dark"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/gottheflag/nova"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/gottheflag/nova/issues"
45
+ },
46
+ "homepage": "https://github.com/gottheflag/nova#readme",
47
+ "devDependencies": {
48
+ "@swc/core": "^1.15.11",
49
+ "@types/node": "^25",
50
+ "concurrently": "^9.2.1",
51
+ "terser": "^5.46.0",
52
+ "tsup": "^8.5",
53
+ "tsx": "^4.21",
54
+ "typescript": "^5.9",
55
+ "vite": "^7.3.1"
56
+ }
57
+ }