@cruxext/theme 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Maysara Elshewehy (https://github.com/maysara-elshewehy)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ <!-- ╔══════════════════════════════ BEG ══════════════════════════════╗ -->
2
+
3
+ <br>
4
+ <div align="center">
5
+ <p>
6
+ <img src="./assets/img/logo.png" alt="logo" style="" height="60" />
7
+ </p>
8
+ </div>
9
+
10
+ <div align="center">
11
+ <img src="https://img.shields.io/badge/v-0.0.1-black"/>
12
+ <a href="https://github.com/cruxext-org"><img src="https://img.shields.io/badge/🔥-@cruxext-black"/></a>
13
+ <br>
14
+ <img src="https://img.shields.io/badge/coverage-0%25-brightgreen" alt="Test Coverage" />
15
+ <img src="https://img.shields.io/github/issues/cruxext-orgz/theme?style=flat" alt="Github Repo Issues" />
16
+ <img src="https://img.shields.io/github/stars/cruxext-orgz/theme?style=social" alt="GitHub Repo stars" />
17
+ </div>
18
+ <br>
19
+
20
+ <!-- ╚═════════════════════════════════════════════════════════════════╝ -->
21
+
22
+
23
+
24
+ <!-- ╔══════════════════════════════ DOC ══════════════════════════════╗ -->
25
+
26
+ - ## Overview 👀
27
+
28
+ - #### Why ?
29
+ > A lightweight, reactive theme management solution for dark/light mode switching with persistent storage and system preference detection, built for the CruxJS ecosystem.
30
+
31
+ - #### When ?
32
+ > Use this extension when you need to:
33
+ > - Implement dark/light mode switching in your application
34
+ > - Respect user's system color scheme preferences
35
+ > - Persist theme preferences across sessions
36
+ > - Build reactive UI components that respond to theme changes
37
+ > - Integrate theme management into CruxJS-based applications
38
+
39
+ > When using [@cruxjs/app](https://github.com/cruxjs-org/app) and [@cruxjs/client](https://github.com/cruxjs-org/client).
40
+
41
+ <br>
42
+ <br>
43
+
44
+ - ## Quick Start 🔥
45
+
46
+ > install [`hmm`](https://github.com/minejs-org/hmm) first.
47
+
48
+ ```bash
49
+ # in your terminal
50
+ hmm i @cruxext/theme
51
+ ```
52
+
53
+ ```ts
54
+ // in your ts files
55
+ import { ... } from `@cruxext/theme`;
56
+ ```
57
+
58
+ <div align="center"> <img src="./assets/img/line.png" alt="line" style="display: block; margin-top:20px;margin-bottom:20px;width:500px;"/> </div>
59
+ <br>
60
+
61
+ - ### Example
62
+
63
+ ```typescript
64
+ import { type JSXElement } from '@minejs/jsx';
65
+ import { effect, signal } from '@minejs/signals';
66
+ import { Button } from '@cruxkit/core';
67
+ import { toggleTheme, getCurrentTheme } from '@cruxext/theme';
68
+
69
+ export function MyComponent(): JSXElement {
70
+ const isDark = signal(getCurrentTheme() == 'dark');
71
+
72
+ effect(() => {
73
+ console.log('isDark:', isDark());
74
+ const btn = document.querySelector('#theme_button');
75
+ if(!btn) return;
76
+ toggleTheme();
77
+
78
+ btn.textContent = isDark() ? '☀️ Light Mode' : '🌙 Dark Mode';
79
+ });
80
+
81
+ return (
82
+ <Button
83
+ id="theme_button"
84
+ variant="solid"
85
+ color="brand"
86
+ onClick={() => isDark.set(!isDark())}
87
+ >
88
+ {isDark() ? '☀️ Light Mode' : '🌙 Dark Mode'}
89
+ </Button>
90
+ );
91
+ }
92
+ ```
93
+
94
+ <br>
95
+ <br>
96
+
97
+ - ## Documentation 📑
98
+
99
+
100
+ - ### API ⛓️
101
+
102
+ - #### `createThemeExtension(config?: ThemeManagerConfig): ClientExtension`
103
+ > Initializes the theme extension for your CruxJS application. Call this during your app bootstrap.
104
+
105
+ ```typescript
106
+ import { createThemeExtension } from '@cruxext/theme';
107
+
108
+ const themeExt = createThemeExtension({
109
+ default: 'light',
110
+ available: ['light', 'dark', 'auto']
111
+ });
112
+ ```
113
+
114
+ - #### `setTheme(themeName: string): void`
115
+
116
+ > Sets the active theme to the specified name. Must be one of the available themes defined in config.
117
+
118
+ ```typescript
119
+ import { setTheme } from '@cruxext/theme';
120
+
121
+ setTheme('dark');
122
+ ```
123
+
124
+ - #### `toggleTheme(): void`
125
+
126
+ > Toggles between available themes. Cycles through the first non-current available theme.
127
+
128
+ ```typescript
129
+ import { toggleTheme } from '@cruxext/theme';
130
+
131
+ toggleTheme();
132
+ ```
133
+
134
+ - #### `getCurrentTheme(): string`
135
+
136
+ > Returns the name of the currently active theme.
137
+
138
+ ```typescript
139
+ import { getCurrentTheme } from '@cruxext/theme';
140
+
141
+ const current = getCurrentTheme(); // 'light' | 'dark' | etc.
142
+ ```
143
+
144
+ - #### `getThemeManager(): ThemeManager`
145
+
146
+ > Returns the ThemeManager instance for advanced usage and direct signal access.
147
+
148
+ ```typescript
149
+ import { getThemeManager, signal } from '@cruxext/theme';
150
+
151
+ const manager = getThemeManager();
152
+ const themeSignal = manager.signal; // reactive signal
153
+ ```
154
+
155
+ <div align="center"> <img src="./assets/img/line.png" alt="line" style="display: block; margin-top:20px;margin-bottom:20px;width:500px;"/> </div>
156
+ <br>
157
+
158
+ - ### Related 🔗
159
+
160
+ - ##### [@minejs/signals](https://github.com/minejs-org/signals)
161
+ > Reactive signals library used for theme state management
162
+
163
+ - ##### [@minejs/store](https://github.com/minejs-org/store)
164
+ > Persistent storage solution for maintaining theme preferences
165
+
166
+ - ##### [@cruxkit/core](https://github.com/cruxkit/core)
167
+ > Core UI component library that works seamlessly with theming
168
+
169
+ <!-- ╚═════════════════════════════════════════════════════════════════╝ -->
170
+
171
+
172
+
173
+ <!-- ╔══════════════════════════════ END ══════════════════════════════╗ -->
174
+
175
+ <br>
176
+ <br>
177
+
178
+ ---
179
+
180
+ <div align="center">
181
+ <a href="https://github.com/maysara-elshewehy"><img src="https://img.shields.io/badge/by-Maysara-black"/></a>
182
+ </div>
183
+
184
+ <!-- ╚═════════════════════════════════════════════════════════════════╝ -->
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var store=require('@minejs/store'),signals=require('@minejs/signals'),client=require('@cruxjs/client');var i=class{constructor(e){this.config=e;let t=new store.Storage({type:"local"});this.store=store.createStore({state:{theme:e.default},persist:true,storage:t,storageKey:"app:theme"}),this.signal=this.store.state.theme;let s=t.get("app:theme:theme")||e.default;this.signal.set(s),this.applyTheme(s),signals.effect(()=>{let n=this.signal?.();n&&this.applyTheme(n),console.log(`[ThemeManager] Reactive effect applied theme: ${n}`);}),signals.effect(()=>{let n=window.matchMedia("(prefers-color-scheme: dark)"),h=l=>{t.get("app:theme:theme")||this.signal.set(l.matches?"dark":"light");};return n.addEventListener("change",h),()=>n.removeEventListener("change",h)});}getTheme(){return this.signal?.()??this.config.default??"light"}setTheme(e){if(!this.config.available.includes(e)){console.warn(`[ThemeManager] Unsupported theme: ${e}`);return}this.signal&&(this.signal.set(e),console.log(`[ThemeManager] Theme set to: ${e}`));}toggleTheme(){if(!this.signal)return;let e=this.signal(),t=this.config.available.find(r=>r!==e);t&&(this.signal.set(t),console.log(`[ThemeManager] Theme toggled to: ${t}`));}applyTheme(e){(document.documentElement||document.rootElement).setAttribute("data-theme",e),console.log(`[ThemeManager] Applied theme: ${e}`);}};var m,a=()=>m,C=o=>a().setTheme(o),y=()=>a().toggleTheme(),b=()=>a().getTheme();function w(o){return {name:"ThemeExtension",onBoot:e=>{o&&(e.cconfig.theme={...e.cconfig.theme,...o}),e.cconfig.theme||(e.cconfig.theme={default:"dark",available:["dark","light"]}),m=new i({default:e.cconfig.theme.default,available:e.cconfig.theme.available});}}}Object.defineProperty(exports,"ThemeManagerConfig",{enumerable:true,get:function(){return client.ThemeConfig}});exports.ThemeManager=i;exports.createThemeExtension=w;exports.getCurrentTheme=b;exports.getThemeManager=a;exports.setTheme=C;exports.toggleTheme=y;//# sourceMappingURL=index.cjs.map
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mod/theme_manager.ts","../src/index.ts"],"names":["ThemeManager","config","storage","Storage","createStore","initialTheme","effect","currentTheme","mediaQuery","handleChange","e","themeName","nextTheme","t","themeManager","getThemeManager","setTheme","toggleTheme","getCurrentTheme","createThemeExtension","ctx"],"mappings":"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","file":"index.cjs","sourcesContent":["// src/mod/theme_manager.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import { ThemeManagerConfig } from \"../types\";\r\n import { createStore, Storage } from '@minejs/store';\r\n import { signal, effect } from '@minejs/signals';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n export class ThemeManager {\r\n\r\n // ┌──────────────────────────────── INIT ──────────────────────────────┐\r\n\r\n public store: ReturnType<typeof createStore>;\r\n public signal: ReturnType<typeof signal<string>>;\r\n\r\n constructor(public config: ThemeManagerConfig) {\r\n\r\n const storage = new Storage({ type: 'local' });\r\n\r\n this.store = createStore({\r\n state: {\r\n theme: config.default\r\n },\r\n persist: true,\r\n storage,\r\n storageKey: 'app:theme'\r\n });\r\n\r\n this.signal = this.store.state.theme;\r\n\r\n // Set initial theme on body\r\n const storedTheme = storage.get('app:theme:theme') as string | null;\r\n const initialTheme = storedTheme || config.default;\r\n this.signal.set(initialTheme);\r\n this.applyTheme(initialTheme);\r\n\r\n // Setup reactive effect: apply theme whenever signal changes\r\n effect(() => {\r\n const currentTheme = this.signal?.();\r\n if (currentTheme) {\r\n this.applyTheme(currentTheme);\r\n }\r\n\r\n console.log(`[ThemeManager] Reactive effect applied theme: ${currentTheme}`);\r\n });\r\n\r\n // Listen for system theme changes\r\n effect(() => {\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n\r\n const handleChange = (e: MediaQueryListEvent) => {\r\n if (!storage.get('app:theme:theme')) this.signal.set(e.matches ? 'dark' : 'light');\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n return () => mediaQuery.removeEventListener('change', handleChange);\r\n });\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌──────────────────────────────── MAIN ──────────────────────────────┐\r\n\r\n getTheme(): string {\r\n return this.signal?.() ?? this.config.default ?? 'light';\r\n }\r\n\r\n setTheme(themeName: string): void {\r\n if (!this.config.available.includes(themeName)) {\r\n console.warn(`[ThemeManager] Unsupported theme: ${themeName}`);\r\n return;\r\n }\r\n\r\n // Update signal directly - effect will handle DOM updates\r\n if (this.signal) {\r\n this.signal.set(themeName);\r\n console.log(`[ThemeManager] Theme set to: ${themeName}`);\r\n }\r\n }\r\n\r\n toggleTheme(): void {\r\n if (!this.signal) return;\r\n const currentTheme = this.signal();\r\n const nextTheme = this.config.available.find(t => t !== currentTheme);\r\n if (nextTheme) {\r\n this.signal.set(nextTheme);\r\n console.log(`[ThemeManager] Theme toggled to: ${nextTheme}`);\r\n }\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌──────────────────────────────── HELP ──────────────────────────────┐\r\n\r\n private applyTheme(themeName: string): void {\r\n const htmlEl = document.documentElement || document.rootElement;\r\n htmlEl.setAttribute('data-theme', themeName);\r\n console.log(`[ThemeManager] Applied theme: ${themeName}`);\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n };\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import { ClientExtension, ExtensionContext } from \"@cruxjs/client\";\r\n import { ThemeManagerConfig } from \"./types\";\r\n import { ThemeManager } from \"./mod/theme_manager\";\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ MAIN ════════════════════════════════════════╗\r\n\r\n // Theme manager instance\r\n let themeManager : ThemeManager;\r\n\r\n // Exported theme functions\r\n export const getThemeManager = () => themeManager;\r\n export const setTheme = (themeName: string) => getThemeManager().setTheme(themeName);\r\n export const toggleTheme = () => getThemeManager().toggleTheme();\r\n export const getCurrentTheme = () => getThemeManager().getTheme();\r\n\r\n // Theme extension\r\n export function createThemeExtension(config?: ThemeManagerConfig) : ClientExtension {\r\n return {\r\n name : 'ThemeExtension',\r\n\r\n onBoot: (ctx: ExtensionContext) => {\r\n // if config provided, merge into client config\r\n if (config) {\r\n ctx.cconfig.theme = {\r\n ...ctx.cconfig.theme,\r\n ...config\r\n };\r\n }\r\n\r\n // if no theme config provided, set default\r\n if (!ctx.cconfig.theme) {\r\n ctx.cconfig.theme = {\r\n default : 'dark',\r\n available : ['dark', 'light']\r\n };\r\n }\r\n\r\n // create theme manager instance\r\n themeManager = new ThemeManager({\r\n default : ctx.cconfig.theme!.default,\r\n available : ctx.cconfig.theme!.available\r\n });\r\n }\r\n };\r\n };\r\n\r\n // export\r\n export * from \"./types\";\r\n export { ThemeManager };\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
@@ -0,0 +1,23 @@
1
+ import { ThemeConfig, ClientExtension } from '@cruxjs/client';
2
+ export { ThemeConfig as ThemeManagerConfig } from '@cruxjs/client';
3
+ import { createStore } from '@minejs/store';
4
+ import { signal } from '@minejs/signals';
5
+
6
+ declare class ThemeManager {
7
+ config: ThemeConfig;
8
+ store: ReturnType<typeof createStore>;
9
+ signal: ReturnType<typeof signal<string>>;
10
+ constructor(config: ThemeConfig);
11
+ getTheme(): string;
12
+ setTheme(themeName: string): void;
13
+ toggleTheme(): void;
14
+ private applyTheme;
15
+ }
16
+
17
+ declare const getThemeManager: () => ThemeManager;
18
+ declare const setTheme: (themeName: string) => void;
19
+ declare const toggleTheme: () => void;
20
+ declare const getCurrentTheme: () => string;
21
+ declare function createThemeExtension(config?: ThemeConfig): ClientExtension;
22
+
23
+ export { ThemeManager, createThemeExtension, getCurrentTheme, getThemeManager, setTheme, toggleTheme };
@@ -0,0 +1,23 @@
1
+ import { ThemeConfig, ClientExtension } from '@cruxjs/client';
2
+ export { ThemeConfig as ThemeManagerConfig } from '@cruxjs/client';
3
+ import { createStore } from '@minejs/store';
4
+ import { signal } from '@minejs/signals';
5
+
6
+ declare class ThemeManager {
7
+ config: ThemeConfig;
8
+ store: ReturnType<typeof createStore>;
9
+ signal: ReturnType<typeof signal<string>>;
10
+ constructor(config: ThemeConfig);
11
+ getTheme(): string;
12
+ setTheme(themeName: string): void;
13
+ toggleTheme(): void;
14
+ private applyTheme;
15
+ }
16
+
17
+ declare const getThemeManager: () => ThemeManager;
18
+ declare const setTheme: (themeName: string) => void;
19
+ declare const toggleTheme: () => void;
20
+ declare const getCurrentTheme: () => string;
21
+ declare function createThemeExtension(config?: ThemeConfig): ClientExtension;
22
+
23
+ export { ThemeManager, createThemeExtension, getCurrentTheme, getThemeManager, setTheme, toggleTheme };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import {Storage,createStore}from'@minejs/store';import {effect}from'@minejs/signals';export{ThemeConfig as ThemeManagerConfig}from'@cruxjs/client';var i=class{constructor(e){this.config=e;let t=new Storage({type:"local"});this.store=createStore({state:{theme:e.default},persist:true,storage:t,storageKey:"app:theme"}),this.signal=this.store.state.theme;let s=t.get("app:theme:theme")||e.default;this.signal.set(s),this.applyTheme(s),effect(()=>{let n=this.signal?.();n&&this.applyTheme(n),console.log(`[ThemeManager] Reactive effect applied theme: ${n}`);}),effect(()=>{let n=window.matchMedia("(prefers-color-scheme: dark)"),h=l=>{t.get("app:theme:theme")||this.signal.set(l.matches?"dark":"light");};return n.addEventListener("change",h),()=>n.removeEventListener("change",h)});}getTheme(){return this.signal?.()??this.config.default??"light"}setTheme(e){if(!this.config.available.includes(e)){console.warn(`[ThemeManager] Unsupported theme: ${e}`);return}this.signal&&(this.signal.set(e),console.log(`[ThemeManager] Theme set to: ${e}`));}toggleTheme(){if(!this.signal)return;let e=this.signal(),t=this.config.available.find(r=>r!==e);t&&(this.signal.set(t),console.log(`[ThemeManager] Theme toggled to: ${t}`));}applyTheme(e){(document.documentElement||document.rootElement).setAttribute("data-theme",e),console.log(`[ThemeManager] Applied theme: ${e}`);}};var m,a=()=>m,C=o=>a().setTheme(o),y=()=>a().toggleTheme(),b=()=>a().getTheme();function w(o){return {name:"ThemeExtension",onBoot:e=>{o&&(e.cconfig.theme={...e.cconfig.theme,...o}),e.cconfig.theme||(e.cconfig.theme={default:"dark",available:["dark","light"]}),m=new i({default:e.cconfig.theme.default,available:e.cconfig.theme.available});}}}export{i as ThemeManager,w as createThemeExtension,b as getCurrentTheme,a as getThemeManager,C as setTheme,y as toggleTheme};//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mod/theme_manager.ts","../src/index.ts"],"names":["ThemeManager","config","storage","Storage","createStore","initialTheme","effect","currentTheme","mediaQuery","handleChange","e","themeName","nextTheme","t","themeManager","getThemeManager","setTheme","toggleTheme","getCurrentTheme","createThemeExtension","ctx"],"mappings":"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","file":"index.js","sourcesContent":["// src/mod/theme_manager.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import { ThemeManagerConfig } from \"../types\";\r\n import { createStore, Storage } from '@minejs/store';\r\n import { signal, effect } from '@minejs/signals';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n export class ThemeManager {\r\n\r\n // ┌──────────────────────────────── INIT ──────────────────────────────┐\r\n\r\n public store: ReturnType<typeof createStore>;\r\n public signal: ReturnType<typeof signal<string>>;\r\n\r\n constructor(public config: ThemeManagerConfig) {\r\n\r\n const storage = new Storage({ type: 'local' });\r\n\r\n this.store = createStore({\r\n state: {\r\n theme: config.default\r\n },\r\n persist: true,\r\n storage,\r\n storageKey: 'app:theme'\r\n });\r\n\r\n this.signal = this.store.state.theme;\r\n\r\n // Set initial theme on body\r\n const storedTheme = storage.get('app:theme:theme') as string | null;\r\n const initialTheme = storedTheme || config.default;\r\n this.signal.set(initialTheme);\r\n this.applyTheme(initialTheme);\r\n\r\n // Setup reactive effect: apply theme whenever signal changes\r\n effect(() => {\r\n const currentTheme = this.signal?.();\r\n if (currentTheme) {\r\n this.applyTheme(currentTheme);\r\n }\r\n\r\n console.log(`[ThemeManager] Reactive effect applied theme: ${currentTheme}`);\r\n });\r\n\r\n // Listen for system theme changes\r\n effect(() => {\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n\r\n const handleChange = (e: MediaQueryListEvent) => {\r\n if (!storage.get('app:theme:theme')) this.signal.set(e.matches ? 'dark' : 'light');\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n return () => mediaQuery.removeEventListener('change', handleChange);\r\n });\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌──────────────────────────────── MAIN ──────────────────────────────┐\r\n\r\n getTheme(): string {\r\n return this.signal?.() ?? this.config.default ?? 'light';\r\n }\r\n\r\n setTheme(themeName: string): void {\r\n if (!this.config.available.includes(themeName)) {\r\n console.warn(`[ThemeManager] Unsupported theme: ${themeName}`);\r\n return;\r\n }\r\n\r\n // Update signal directly - effect will handle DOM updates\r\n if (this.signal) {\r\n this.signal.set(themeName);\r\n console.log(`[ThemeManager] Theme set to: ${themeName}`);\r\n }\r\n }\r\n\r\n toggleTheme(): void {\r\n if (!this.signal) return;\r\n const currentTheme = this.signal();\r\n const nextTheme = this.config.available.find(t => t !== currentTheme);\r\n if (nextTheme) {\r\n this.signal.set(nextTheme);\r\n console.log(`[ThemeManager] Theme toggled to: ${nextTheme}`);\r\n }\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌──────────────────────────────── HELP ──────────────────────────────┐\r\n\r\n private applyTheme(themeName: string): void {\r\n const htmlEl = document.documentElement || document.rootElement;\r\n htmlEl.setAttribute('data-theme', themeName);\r\n console.log(`[ThemeManager] Applied theme: ${themeName}`);\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n };\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import { ClientExtension, ExtensionContext } from \"@cruxjs/client\";\r\n import { ThemeManagerConfig } from \"./types\";\r\n import { ThemeManager } from \"./mod/theme_manager\";\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ MAIN ════════════════════════════════════════╗\r\n\r\n // Theme manager instance\r\n let themeManager : ThemeManager;\r\n\r\n // Exported theme functions\r\n export const getThemeManager = () => themeManager;\r\n export const setTheme = (themeName: string) => getThemeManager().setTheme(themeName);\r\n export const toggleTheme = () => getThemeManager().toggleTheme();\r\n export const getCurrentTheme = () => getThemeManager().getTheme();\r\n\r\n // Theme extension\r\n export function createThemeExtension(config?: ThemeManagerConfig) : ClientExtension {\r\n return {\r\n name : 'ThemeExtension',\r\n\r\n onBoot: (ctx: ExtensionContext) => {\r\n // if config provided, merge into client config\r\n if (config) {\r\n ctx.cconfig.theme = {\r\n ...ctx.cconfig.theme,\r\n ...config\r\n };\r\n }\r\n\r\n // if no theme config provided, set default\r\n if (!ctx.cconfig.theme) {\r\n ctx.cconfig.theme = {\r\n default : 'dark',\r\n available : ['dark', 'light']\r\n };\r\n }\r\n\r\n // create theme manager instance\r\n themeManager = new ThemeManager({\r\n default : ctx.cconfig.theme!.default,\r\n available : ctx.cconfig.theme!.available\r\n });\r\n }\r\n };\r\n };\r\n\r\n // export\r\n export * from \"./types\";\r\n export { ThemeManager };\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@cruxext/theme",
3
+ "version": "0.0.1",
4
+ "description": "Lightweight reactive theme manager for CruxJS with dark/light mode support and persistent storage.",
5
+ "keywords": ["cruxjs", "extension", "theme"],
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/cruxext-org/theme#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/cruxext-org/theme/issues"
10
+ },
11
+ "author": {
12
+ "name": "Maysara",
13
+ "email": "maysara.elshewehy@gmail.com",
14
+ "url": "https://github.com/maysara-elshewehy"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/cruxext-org/theme.git"
19
+ },
20
+ "type": "module",
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "files": ["dist"],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "require": "./dist/index.js"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "lint": "eslint src --ext .ts",
34
+ "test": "bun test"
35
+ },
36
+ "engines": {
37
+ "bun": ">=1.3.3"
38
+ },
39
+ "peerDependencies": {
40
+ "bun": "^1.3.3"
41
+ },
42
+ "dependencies": {
43
+ "@cruxjs/client": "0.1.3",
44
+ "@minejs/signals": "^0.0.6",
45
+ "@minejs/store": "^0.0.3"
46
+ },
47
+ "devDependencies": {
48
+ "@eslint/js": "^9.39.2",
49
+ "@stylistic/eslint-plugin": "^5.6.1",
50
+ "@types/bun": "^1.3.5",
51
+ "@types/node": "^20.19.27",
52
+ "bun-plugin-dts": "^0.3.0",
53
+ "bun-types": "^1.3.5",
54
+ "ts-node": "^10.9.2",
55
+ "tsup": "^8.5.1",
56
+ "typescript": "^5.9.3",
57
+ "typescript-eslint": "^8.51.0"
58
+ }
59
+ }