@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 +21 -0
- package/README.md +184 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
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// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|