resonare 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.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Si Phuoc
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,332 @@
1
+ # Resonare [![Version](https://img.shields.io/npm/v/resonare.svg?labelColor=black&color=blue)](https://www.npmjs.com/package/resonare)
2
+
3
+ A configuration-based theme store for building deeply personal user interface.
4
+
5
+ ## Features
6
+
7
+ - Define and manage multi-dimensional themes, eg:
8
+ - Color scheme: system, light, dark
9
+ - Contrast preference: standard, high
10
+ - Spacing: compact, comfortable, spacious
11
+ - etc
12
+ - Framework-agnostic
13
+ - Prevent theme flicker on page load
14
+ - Honor system preferences
15
+ - Create sections with independent theming
16
+ - Sync theme selection across tabs and windows
17
+ - Flexible persistence options, defaulting to localStorage
18
+ - SSR-friendly
19
+
20
+ ## Demo
21
+
22
+ - [Demo](https://resonare.universse.workers.dev)
23
+
24
+ ## Installation
25
+
26
+ To install:
27
+
28
+ ```bash
29
+ npm i resonare
30
+ # or
31
+ yarn add resonare
32
+ # or
33
+ pnpm add resonare
34
+ ```
35
+
36
+ ## Basic Usage
37
+
38
+ It's recommended to initialize Resonare in a synchronous script to avoid theme flicker on page load. If your project's bundler supports importing static asset as string, you can inline the minified version of Resonare to reduce the number of HTTP requests. Check out the demo for example of this pattern with Vite.
39
+
40
+ ```html
41
+ <script src="https://unpkg.com/resonare"></script>
42
+ <!-- or -->
43
+ <script src="https://cdn.jsdelivr.net/npm/resonare"></script>
44
+
45
+ <script>
46
+ ;(async () => {
47
+ const themeStore = window.resonare.createThemeStore({
48
+ config: {
49
+ colorScheme: {
50
+ options: [
51
+ {
52
+ value: 'system',
53
+ media: ['(prefers-color-scheme: dark)', 'dark', 'light'],
54
+ },
55
+ 'light',
56
+ 'dark',
57
+ ]
58
+ },
59
+ },
60
+ })
61
+
62
+ themeStore.subscribe(({ resolvedThemes }) => {
63
+ for (const [theme, optionKey] of Object.entries(resolvedThemes)) {
64
+ document.documentElement.dataset[theme] = optionKey
65
+ }
66
+ })
67
+
68
+ await themeStore.restore()
69
+ themeStore.sync()
70
+ })()
71
+ </script>
72
+ ```
73
+
74
+ If you are using TypeScript, add `node_modules/resonare/global.d.ts` to `include` in `tsconfig.json`.
75
+
76
+ ```json
77
+ {
78
+ "include": [
79
+ "node_modules/resonare/global.d.ts",
80
+ // ...
81
+ ]
82
+ }
83
+ ```
84
+
85
+ ## API
86
+
87
+ ### `createThemeStore`
88
+
89
+ ```ts
90
+ import { createThemeStore, type ThemeConfig, type ThemeStore } from 'resonare'
91
+
92
+ const config = {
93
+ colorScheme: {
94
+ options: [
95
+ {
96
+ value: 'system',
97
+ media: ['(prefers-color-scheme: dark)', 'dark', 'light'],
98
+ },
99
+ 'light',
100
+ 'dark',
101
+ ]
102
+ },
103
+ contrast: {
104
+ options: [
105
+ {
106
+ value: 'system',
107
+ media: [
108
+ '(prefers-contrast: more) and (forced-colors: none)',
109
+ 'high',
110
+ 'standard',
111
+ ],
112
+ },
113
+ 'standard',
114
+ 'high',
115
+ ],
116
+ defaultOption: 'standard',
117
+ }
118
+ } as const satisfies ThemeConfig
119
+
120
+ declare module 'resonare' {
121
+ interface ThemeStoreRegistry {
122
+ resonare: ThemeStore<typeof config>
123
+ }
124
+ }
125
+
126
+
127
+ const themeStore = createThemeStore({
128
+ // optional, default 'resonare'
129
+ // should be unique, also used as client storage key
130
+ key: 'resonare',
131
+
132
+ // required, specify theme and options
133
+ config,
134
+
135
+ // optional, useful for SSR
136
+ initialState: {
137
+ themes: {
138
+ colorScheme: 'dark',
139
+ contrast: 'high',
140
+ },
141
+ },
142
+
143
+ // optional, specify your own client storage
144
+ // localStorage is used by default
145
+ storage: ({ abortController }) => ({
146
+ getItem: (key: string) => {
147
+ return JSON.parse(localStorage.getItem(key) || 'null')
148
+ },
149
+
150
+ setItem: (key: string, value: object) => {
151
+ localStorage.setItem(key, JSON.stringify(value))
152
+ },
153
+
154
+ watch: (cb: (key: string, value: unknown) => void) => {
155
+ const controller = new AbortController()
156
+
157
+ window.addEventListener(
158
+ 'storage',
159
+ (e) => {
160
+ if (e.storageArea !== localStorage) return
161
+
162
+ cb(e.key, JSON.parse(e.newValue!))
163
+ },
164
+ {
165
+ signal: AbortSignal.any([
166
+ abortController.signal,
167
+ controller.signal,
168
+ ]),
169
+ },
170
+ )
171
+
172
+ return () => {
173
+ controller.abort()
174
+ }
175
+ },
176
+ })
177
+ })
178
+ ```
179
+
180
+ ### `getThemeStore`
181
+
182
+ ```ts
183
+ import { getThemeStore } from 'resonare'
184
+
185
+ // Get an existing theme store by key
186
+ const themeStore = getThemeStore('resonare')
187
+ ```
188
+
189
+ ### `destroyThemeStore`
190
+
191
+ ```ts
192
+ import { destroyThemeStore } from 'resonare'
193
+
194
+ // Get an existing theme store by key
195
+ const themeStore = destroyThemeStore('resonare')
196
+ ```
197
+
198
+ ### ThemeStore Methods
199
+
200
+ ```ts
201
+ interface ThemeStore<T> {
202
+ // get current theme selection
203
+ getThemes(): Record<string, string>
204
+
205
+ // get resolved theme selection (after media queries)
206
+ getResolvedThemes(): Record<string, string>
207
+
208
+ // update theme
209
+ setThemes(themes: Partial<Record<string, string>>): Promise<void>
210
+
211
+ // get state to persist, useful for server-side persistence
212
+ // to restore, pass the returned object to initialState
213
+ getStateToPersist(): object
214
+
215
+ // restore persisted theme selection from client storage
216
+ restore(): Promise<void>
217
+
218
+ // sync theme selection across tabs/windows
219
+ sync(): () => void
220
+
221
+ // subscribe to theme changes
222
+ subscribe(
223
+ callback: ({
224
+ themes,
225
+ resolvedThemes,
226
+ }: {
227
+ themes: Record<string, string>
228
+ resolvedThemes: Record<string, string>
229
+ }) => void,
230
+ options?: { immediate?: boolean }
231
+ ): () => void
232
+ }
233
+ ```
234
+
235
+ ### React Integration
236
+
237
+ Ensure that you have initialized Resonare as per instructions under [Basic Usage](#basic-usage).
238
+
239
+ ```tsx
240
+ import * as React from 'react'
241
+ import { getThemesAndOptions } from 'resonare'
242
+ import { useResonare } from 'resonare/react'
243
+
244
+ function ThemeSelect() {
245
+ const { themes, setThemes } = useResonare(() =>
246
+ window.resonare.getThemeStore(),
247
+ )
248
+
249
+ return getThemesAndOptions(config).map(([theme, options]) => (
250
+ <div key={theme}>
251
+ <label htmlFor={theme}>{theme}</label>
252
+ <select
253
+ id={theme}
254
+ name={theme}
255
+ onChange={(e) => {
256
+ setThemes({ [theme]: e.target.value })
257
+ }}
258
+ value={themes[theme]}
259
+ >
260
+ {options.map((option) => (
261
+ <option key={option} value={option}>
262
+ {option}
263
+ </option>
264
+ ))}
265
+ </select>
266
+ </div>
267
+ ))
268
+ }
269
+ ```
270
+
271
+ ### Server-side persistence
272
+
273
+ If you are storing theme selection on the server, you can choose to use `memoryStorageAdapter` to avoid storing any data client-side. There's no need to initialize Resonare in a synchronous script. Ensure you pass the persisted theme selection when initializing Resonare as `initialState`.
274
+
275
+ ```tsx
276
+ import {
277
+ createThemeStore,
278
+ getThemesAndOptions,
279
+ memoryStorageAdapter,
280
+ type ThemeConfig,
281
+ type Themes,
282
+ } from 'resonare'
283
+ import { useResonare } from 'resonare/react'
284
+ import * as React from 'react'
285
+
286
+ const config = {
287
+ colorScheme: {
288
+ options: ['light', 'dark'],
289
+ },
290
+ contrast: {
291
+ options: ['standard', 'high'],
292
+ },
293
+ } as const satisfies ThemeConfig
294
+
295
+ export function ThemeSelect({
296
+ persistedServerThemes,
297
+ }: { persistedServerThemes: Themes<typeof config> }) {
298
+ const [themeStore] = React.useState(() =>
299
+ createThemeStore({
300
+ config,
301
+ initialState: {
302
+ themes: persistedServerThemes,
303
+ },
304
+ storage: memoryStorageAdapter(),
305
+ }),
306
+ )
307
+
308
+ const { themes, setThemes } = useResonare(() => themeStore, {
309
+ initOnMount: true,
310
+ })
311
+
312
+ return getThemesAndOptions(config).map(([theme, options]) => (
313
+ <div key={theme}>
314
+ <label htmlFor={theme}>{theme}</label>
315
+ <select
316
+ id={theme}
317
+ name={theme}
318
+ onChange={(e) => {
319
+ setThemes({ [theme]: e.target.value })
320
+ }}
321
+ value={themes[theme]}
322
+ >
323
+ {options.map((option) => (
324
+ <option key={option} value={option}>
325
+ {option}
326
+ </option>
327
+ ))}
328
+ </select>
329
+ </div>
330
+ ))
331
+ }
332
+ ```
@@ -0,0 +1,65 @@
1
+ import { n as StorageAdapterCreate } from "./storage-BP1DGXF0.js";
2
+
3
+ //#region src/index.d.ts
4
+ type ThemeOption = {
5
+ value: string;
6
+ media?: [string, string, string];
7
+ };
8
+ type ThemeConfig = Record<string, {
9
+ options: Array<string | ThemeOption>;
10
+ defaultOption?: string;
11
+ }>;
12
+ type Themes<T extends ThemeConfig> = { [K in keyof T]: T[K]['options'] extends Array<infer U> ? U extends string ? U : U extends ThemeOption ? U['value'] : never : never };
13
+ type Listener<T extends ThemeConfig> = (value: {
14
+ themes: Themes<T>;
15
+ resolvedThemes: Themes<T>;
16
+ }) => void;
17
+ type ThemeKeysWithSystemOption<T extends ThemeConfig> = { [K in keyof T]: T[K]['options'] extends Array<infer U> ? U extends {
18
+ media: [string, string, string];
19
+ } ? K : never : never }[keyof T];
20
+ type NonSystemOptionValues<T extends ThemeConfig, K$1 extends keyof T> = T[K$1]['options'] extends Array<infer U> ? U extends string ? U : U extends ThemeOption ? U extends {
21
+ media: [string, string, string];
22
+ } ? never : U['value'] : never : never;
23
+ type SystemOptions<T extends ThemeConfig> = { [K in ThemeKeysWithSystemOption<T>]?: [NonSystemOptionValues<T, K>, NonSystemOptionValues<T, K>] };
24
+ type PersistedState<T extends ThemeConfig> = {
25
+ version: 1;
26
+ themes: Themes<T>;
27
+ systemOptions: SystemOptions<T>;
28
+ };
29
+ type ThemeStoreOptions<T extends ThemeConfig> = {
30
+ key?: string;
31
+ config: T;
32
+ initialState?: Partial<PersistedState<T>>;
33
+ storage?: StorageAdapterCreate;
34
+ };
35
+ type ThemeAndOptions<T extends ThemeConfig> = Array<{ [K in keyof T]: [K, Array<T[K]['options'] extends Array<infer U> ? U extends string ? U : U extends ThemeOption ? U['value'] : never : never>] }[keyof T]>;
36
+ declare function getThemesAndOptions<T extends ThemeConfig>(config: T): ThemeAndOptions<T>;
37
+ declare function getDefaultThemes<T extends ThemeConfig>(config: T): Themes<T>;
38
+ declare class ThemeStore<T extends ThemeConfig> {
39
+ #private;
40
+ constructor({
41
+ key,
42
+ config,
43
+ initialState,
44
+ storage
45
+ }: ThemeStoreOptions<T>);
46
+ getThemes: () => Themes<T>;
47
+ getResolvedThemes: () => Themes<T>;
48
+ setThemes: (themes: Partial<Themes<T>> | ((currentThemes: Themes<T>) => Partial<Themes<T>>)) => Promise<void>;
49
+ updateSystemOption: <K$1 extends ThemeKeysWithSystemOption<T>>(themeKey: K$1, [ifMatch, ifNotMatch]: [NonSystemOptionValues<T, K$1>, NonSystemOptionValues<T, K$1>]) => void;
50
+ getStateToPersist: () => PersistedState<T>;
51
+ restore: () => Promise<void>;
52
+ subscribe: (callback: Listener<T>, {
53
+ immediate
54
+ }?: {
55
+ immediate?: boolean;
56
+ }) => (() => void);
57
+ sync: () => (() => void) | undefined;
58
+ ___destroy: () => void;
59
+ }
60
+ declare const createThemeStore: <T extends ThemeConfig>(options: ThemeStoreOptions<T>) => ThemeStore<T>;
61
+ declare const getThemeStore: <T extends keyof ThemeStoreRegistry>(key?: T | undefined) => ThemeStoreRegistry[T];
62
+ declare const destroyThemeStore: <T extends keyof ThemeStoreRegistry>(key?: T | undefined) => void;
63
+ interface ThemeStoreRegistry {}
64
+ //#endregion
65
+ export { ThemeStoreRegistry as a, destroyThemeStore as c, getThemesAndOptions as d, ThemeStoreOptions as i, getDefaultThemes as l, ThemeConfig as n, Themes as o, ThemeStore as r, createThemeStore as s, ThemeAndOptions as t, getThemeStore as u };
@@ -0,0 +1,2 @@
1
+ import { a as ThemeStoreRegistry, c as destroyThemeStore, d as getThemesAndOptions, i as ThemeStoreOptions, l as getDefaultThemes, n as ThemeConfig, o as Themes, r as ThemeStore, s as createThemeStore, t as ThemeAndOptions, u as getThemeStore } from "./index-C1bWhjq0.js";
2
+ export { ThemeAndOptions, ThemeConfig, ThemeStore, ThemeStoreOptions, ThemeStoreRegistry, Themes, createThemeStore, destroyThemeStore, getDefaultThemes, getThemeStore, getThemesAndOptions };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { a as getThemeStore, i as getDefaultThemes, n as createThemeStore, o as getThemesAndOptions, r as destroyThemeStore, t as ThemeStore } from "./src-BsTQDM3B.js";
2
+
3
+ export { ThemeStore, createThemeStore, destroyThemeStore, getDefaultThemes, getThemeStore, getThemesAndOptions };
@@ -0,0 +1,54 @@
1
+ import { n as ThemeConfig, o as Themes, r as ThemeStore } from "./index-C1bWhjq0.js";
2
+
3
+ //#region src/react.d.ts
4
+ declare function useResonare<T extends ThemeConfig>(getStore: () => ThemeStore<T>, {
5
+ initOnMount
6
+ }?: {
7
+ initOnMount?: boolean | undefined;
8
+ }): {
9
+ themes: Themes<T>;
10
+ resolvedThemes: Themes<T>;
11
+ setThemes: (themes: Partial<Themes<T>> | ((currentThemes: Themes<T>) => Partial<Themes<T>>)) => Promise<void>;
12
+ updateSystemOption: <K extends { [K_1 in keyof T]: T[K_1]["options"] extends (infer U)[] ? U extends {
13
+ media: [string, string, string];
14
+ } ? K_1 : never : never }[keyof T]>(themeKey: K, [ifMatch, ifNotMatch]: [T[K]["options"] extends (infer U)[] ? U extends string ? U : U extends {
15
+ value: string;
16
+ media?: [string, string, string];
17
+ } ? U extends {
18
+ media: [string, string, string];
19
+ } ? never : U["value"] : never : never, T[K]["options"] extends (infer U)[] ? U extends string ? U : U extends {
20
+ value: string;
21
+ media?: [string, string, string];
22
+ } ? U extends {
23
+ media: [string, string, string];
24
+ } ? never : U["value"] : never : never]) => void;
25
+ getStateToPersist: () => {
26
+ version: 1;
27
+ themes: Themes<T>;
28
+ systemOptions: { [K_1 in { [K in keyof T]: T[K]["options"] extends (infer U)[] ? U extends {
29
+ media: [string, string, string];
30
+ } ? K : never : never }[keyof T]]?: [T[K_1]["options"] extends (infer U)[] ? U extends string ? U : U extends {
31
+ value: string;
32
+ media?: [string, string, string];
33
+ } ? U extends {
34
+ media: [string, string, string];
35
+ } ? never : U["value"] : never : never, T[K_1]["options"] extends (infer U)[] ? U extends string ? U : U extends {
36
+ value: string;
37
+ media?: [string, string, string];
38
+ } ? U extends {
39
+ media: [string, string, string];
40
+ } ? never : U["value"] : never : never] };
41
+ };
42
+ restore: () => Promise<void>;
43
+ sync: () => (() => void) | undefined;
44
+ subscribe: (callback: (value: {
45
+ themes: Themes<T>;
46
+ resolvedThemes: Themes<T>;
47
+ }) => void, {
48
+ immediate
49
+ }?: {
50
+ immediate?: boolean;
51
+ }) => (() => void);
52
+ };
53
+ //#endregion
54
+ export { useResonare };
package/dist/react.js ADDED
@@ -0,0 +1,71 @@
1
+ import { c } from "react-compiler-runtime";
2
+ import * as React from "react";
3
+
4
+ //#region src/react.ts
5
+ function noop() {}
6
+ const emptyObject = {};
7
+ const emptyStore = {
8
+ getThemes: () => emptyObject,
9
+ getResolvedThemes: () => emptyObject,
10
+ setThemes: noop,
11
+ updateSystemOption: noop,
12
+ getStateToPersist: noop,
13
+ restore: noop,
14
+ sync: noop,
15
+ subscribe: () => noop
16
+ };
17
+ function useResonare(getStore, t0) {
18
+ const $ = c(13);
19
+ let t1;
20
+ if ($[0] !== t0) {
21
+ t1 = t0 === void 0 ? {} : t0;
22
+ $[0] = t0;
23
+ $[1] = t1;
24
+ } else t1 = $[1];
25
+ const { initOnMount: t2 } = t1;
26
+ const initOnMount = t2 === void 0 ? false : t2;
27
+ const [isMounted, setIsMounted] = React.useState(initOnMount);
28
+ let t3;
29
+ let t4;
30
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
31
+ t3 = () => {
32
+ setIsMounted(true);
33
+ };
34
+ t4 = [];
35
+ $[2] = t3;
36
+ $[3] = t4;
37
+ } else {
38
+ t3 = $[2];
39
+ t4 = $[3];
40
+ }
41
+ React.useEffect(t3, t4);
42
+ const { getThemes, getResolvedThemes, setThemes, updateSystemOption, getStateToPersist, restore, sync, subscribe } = isMounted ? getStore() : emptyStore;
43
+ const themes = React.useSyncExternalStore(subscribe, getThemes, getThemes);
44
+ const t5 = getResolvedThemes();
45
+ let t6;
46
+ if ($[4] !== getStateToPersist || $[5] !== restore || $[6] !== setThemes || $[7] !== subscribe || $[8] !== sync || $[9] !== t5 || $[10] !== themes || $[11] !== updateSystemOption) {
47
+ t6 = {
48
+ themes,
49
+ resolvedThemes: t5,
50
+ setThemes,
51
+ updateSystemOption,
52
+ getStateToPersist,
53
+ restore,
54
+ sync,
55
+ subscribe
56
+ };
57
+ $[4] = getStateToPersist;
58
+ $[5] = restore;
59
+ $[6] = setThemes;
60
+ $[7] = subscribe;
61
+ $[8] = sync;
62
+ $[9] = t5;
63
+ $[10] = themes;
64
+ $[11] = updateSystemOption;
65
+ $[12] = t6;
66
+ } else t6 = $[12];
67
+ return t6;
68
+ }
69
+
70
+ //#endregion
71
+ export { useResonare };
@@ -0,0 +1 @@
1
+ var resonare=(function(e){var t=`resonare`;let n=({storageType:e=`localStorage`}={})=>({abortController:t})=>({getItem:t=>JSON.parse(window[e].getItem(t)||`null`),setItem:(t,n)=>{window[e].setItem(t,JSON.stringify(n))},watch:n=>{let r=new AbortController;return window.addEventListener(`storage`,t=>{t.storageArea===window[e]&&n(t.key,JSON.parse(t.newValue))},{signal:AbortSignal.any([t.signal,r.signal])}),()=>{r.abort()}}}),r=()=>({abortController:e})=>{let n=new Map,r=new BroadcastChannel(t);return{getItem:e=>n.get(e)||null,setItem:(e,t)=>{n.set(e,t)},broadcast:(e,t)=>{r.postMessage({key:e,value:t})},watch:t=>{let n=new AbortController;return r.addEventListener(`message`,e=>{t(e.data.key,e.data.value)},{signal:AbortSignal.any([e.signal,n.signal])}),()=>{n.abort()}}}},i=`resonare`;function a(e){return Object.entries(e).map(([e,{options:t}])=>[e,t.map(e=>typeof e==`string`?e:e.value)])}function o(e){return Object.fromEntries(Object.entries(e).map(([e,{options:t,defaultOption:n}])=>[e,n??(typeof t[0]==`string`?t[0]:t[0].value)]))}let s=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;var c=class{#e;#t;#n;#r;#i;#a=new Set;#o;#s=new AbortController;constructor({key:e=i,config:t,initialState:r={},storage:a=n()}){let s={},c={...r.systemOptions};Object.entries(t).forEach(([e,{options:t}])=>{s[e]=s[e]||{},t.forEach(t=>{typeof t==`string`?s[e][t]={value:t}:(t.media&&!Object.hasOwn(c,e)&&(c[e]=[t.media[1],t.media[2]]),s[e][t.value]=t)})}),this.#n={key:e,config:s,storage:a},this.#r=c,this.#e=o(t),this.#t={...this.#e,...r.themes},this.#i=this.#n.storage({abortController:this.#s}),this.#o={}}getThemes=()=>this.#t;getResolvedThemes=()=>this.#l();setThemes=async e=>{let t=typeof e==`function`?e(this.#t):e;this.#c({...this.#t,...t});let n=this.getStateToPersist();await this.#i.setItem(this.#n.key,n),this.#i.broadcast?.(this.#n.key,n)};updateSystemOption=(e,[t,n])=>{this.#r[e]=[t,n],this.setThemes({...this.#t})};getStateToPersist=()=>({version:1,themes:this.#t,systemOptions:this.#r});restore=async()=>{let e=await this.#i.getItem(this.#n.key);if(!e){this.#c({...this.#e});return}Object.hasOwn(e,`version`)||(e={version:1,themes:e,systemOptions:this.#r}),this.#r={...this.#r,...e.systemOptions},this.#c({...this.#e,...e.themes})};subscribe=(e,{immediate:t=!1}={})=>(t&&e({themes:this.#t,resolvedThemes:this.#l()}),this.#a.add(e),()=>{this.#a.delete(e)});sync=()=>{if(!this.#i.watch){console.warn(`[${i}] No watch method was provided for storage.`);return}return this.#i.watch((e,t)=>{e===this.#n.key&&(this.#r=t.systemOptions,this.#c(t.themes))})};___destroy=()=>{this.#a.clear(),this.#s.abort()};#c=e=>{this.#t=e,this.#d()};#l=()=>Object.fromEntries(Object.entries(this.#t).map(([e,t])=>{let n=this.#n.config[e][t];return[e,this.#u({themeKey:e,option:n})]}));#u=({themeKey:e,option:t})=>{if(!t.media)return t.value;if(!s)return console.warn(`[${i}] Option with key "media" cannot be resolved in server environment.`),t.value;let[n]=t.media;if(!this.#o[n]){let r=window.matchMedia(n);this.#o[n]=r,r.addEventListener(`change`,()=>{this.#t[e]===t.value&&this.#c({...this.#t})},{signal:this.#s.signal})}let[r,a]=this.#r[e];return this.#o[n].matches?r:a};#d=()=>{for(let e of this.#a)e({themes:this.#t,resolvedThemes:this.#l()})}};let l=new class{#e=new Map;create=e=>{let t=e.key||i;this.#e.has(t)&&this.destroy(t);let n=new c(e);return this.#e.set(t,n),n};get=e=>{let t=e||i;if(!this.#e.has(t))throw Error(`[${i}] Theme store with key '${t}' could not be found. Please run \`createThemeStore\` with key '${t}' first.`);return this.#e.get(t)};destroy=e=>{let t=e||i;if(!this.#e.has(t))throw Error(`[${i}] Theme store with key '${t}' could not be found. Please run \`createThemeStore\` with key '${t}' first.`);this.#e.get(t).___destroy(),this.#e.delete(t)}},u=l.create,d=l.get,f=l.destroy;return e.createThemeStore=u,e.destroyThemeStore=f,e.getThemeStore=d,e.getThemesAndOptions=a,e.localStorageAdapter=n,e.memoryStorageAdapter=r,e})({});
@@ -0,0 +1,188 @@
1
+ import { t as localStorageAdapter } from "./storage-ByzkGDod.js";
2
+
3
+ //#region src/index.ts
4
+ const PACKAGE_NAME = "resonare";
5
+ function getThemesAndOptions(config) {
6
+ return Object.entries(config).map(([themeKey, { options }]) => {
7
+ return [themeKey, options.map((option) => typeof option === "string" ? option : option.value)];
8
+ });
9
+ }
10
+ function getDefaultThemes(config) {
11
+ return Object.fromEntries(Object.entries(config).map(([themeKey, { options, defaultOption }]) => {
12
+ return [themeKey, defaultOption ?? (typeof options[0] === "string" ? options[0] : options[0].value)];
13
+ }));
14
+ }
15
+ const isClient = !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined");
16
+ var ThemeStore = class {
17
+ #defaultThemes;
18
+ #currentThemes;
19
+ #options;
20
+ #systemOptions;
21
+ #storage;
22
+ #listeners = /* @__PURE__ */ new Set();
23
+ #mediaQueryCache;
24
+ #abortController = new AbortController();
25
+ constructor({ key = PACKAGE_NAME, config, initialState = {}, storage = localStorageAdapter() }) {
26
+ const keyedConfig = {};
27
+ const systemOptions = { ...initialState.systemOptions };
28
+ Object.entries(config).forEach(([themeKey, { options }]) => {
29
+ keyedConfig[themeKey] = keyedConfig[themeKey] || {};
30
+ options.forEach((option) => {
31
+ if (typeof option === "string") keyedConfig[themeKey][option] = { value: option };
32
+ else {
33
+ if (option.media && !Object.hasOwn(systemOptions, themeKey)) systemOptions[themeKey] = [option.media[1], option.media[2]];
34
+ keyedConfig[themeKey][option.value] = option;
35
+ }
36
+ });
37
+ });
38
+ this.#options = {
39
+ key,
40
+ config: keyedConfig,
41
+ storage
42
+ };
43
+ this.#systemOptions = systemOptions;
44
+ this.#defaultThemes = getDefaultThemes(config);
45
+ this.#currentThemes = {
46
+ ...this.#defaultThemes,
47
+ ...initialState.themes
48
+ };
49
+ this.#storage = this.#options.storage({ abortController: this.#abortController });
50
+ this.#mediaQueryCache = {};
51
+ }
52
+ getThemes = () => {
53
+ return this.#currentThemes;
54
+ };
55
+ getResolvedThemes = () => {
56
+ return this.#resolveThemes();
57
+ };
58
+ setThemes = async (themes) => {
59
+ const updatedThemes = typeof themes === "function" ? themes(this.#currentThemes) : themes;
60
+ this.#setThemesAndNotify({
61
+ ...this.#currentThemes,
62
+ ...updatedThemes
63
+ });
64
+ const stateToPersist = this.getStateToPersist();
65
+ await this.#storage.setItem(this.#options.key, stateToPersist);
66
+ this.#storage.broadcast?.(this.#options.key, stateToPersist);
67
+ };
68
+ updateSystemOption = (themeKey, [ifMatch, ifNotMatch]) => {
69
+ this.#systemOptions[themeKey] = [ifMatch, ifNotMatch];
70
+ this.setThemes({ ...this.#currentThemes });
71
+ };
72
+ getStateToPersist = () => {
73
+ return {
74
+ version: 1,
75
+ themes: this.#currentThemes,
76
+ systemOptions: this.#systemOptions
77
+ };
78
+ };
79
+ restore = async () => {
80
+ let persistedState = await this.#storage.getItem(this.#options.key);
81
+ if (!persistedState) {
82
+ this.#setThemesAndNotify({ ...this.#defaultThemes });
83
+ return;
84
+ }
85
+ if (!Object.hasOwn(persistedState, "version")) persistedState = {
86
+ version: 1,
87
+ themes: persistedState,
88
+ systemOptions: this.#systemOptions
89
+ };
90
+ this.#systemOptions = {
91
+ ...this.#systemOptions,
92
+ ...persistedState.systemOptions
93
+ };
94
+ this.#setThemesAndNotify({
95
+ ...this.#defaultThemes,
96
+ ...persistedState.themes
97
+ });
98
+ };
99
+ subscribe = (callback, { immediate = false } = {}) => {
100
+ if (immediate) callback({
101
+ themes: this.#currentThemes,
102
+ resolvedThemes: this.#resolveThemes()
103
+ });
104
+ this.#listeners.add(callback);
105
+ return () => {
106
+ this.#listeners.delete(callback);
107
+ };
108
+ };
109
+ sync = () => {
110
+ if (!this.#storage.watch) {
111
+ console.warn(`[${PACKAGE_NAME}] No watch method was provided for storage.`);
112
+ return;
113
+ }
114
+ return this.#storage.watch((key, persistedState) => {
115
+ if (key !== this.#options.key) return;
116
+ this.#systemOptions = persistedState.systemOptions;
117
+ this.#setThemesAndNotify(persistedState.themes);
118
+ });
119
+ };
120
+ ___destroy = () => {
121
+ this.#listeners.clear();
122
+ this.#abortController.abort();
123
+ };
124
+ #setThemesAndNotify = (themes) => {
125
+ this.#currentThemes = themes;
126
+ this.#notify();
127
+ };
128
+ #resolveThemes = () => {
129
+ return Object.fromEntries(Object.entries(this.#currentThemes).map(([themeKey, optionKey]) => {
130
+ const option = this.#options.config[themeKey][optionKey];
131
+ return [themeKey, this.#resolveThemeOption({
132
+ themeKey,
133
+ option
134
+ })];
135
+ }));
136
+ };
137
+ #resolveThemeOption = ({ themeKey, option }) => {
138
+ if (!option.media) return option.value;
139
+ if (!isClient) {
140
+ console.warn(`[${PACKAGE_NAME}] Option with key "media" cannot be resolved in server environment.`);
141
+ return option.value;
142
+ }
143
+ const [mediaQuery] = option.media;
144
+ if (!this.#mediaQueryCache[mediaQuery]) {
145
+ const mediaQueryList = window.matchMedia(mediaQuery);
146
+ this.#mediaQueryCache[mediaQuery] = mediaQueryList;
147
+ mediaQueryList.addEventListener("change", () => {
148
+ if (this.#currentThemes[themeKey] === option.value) this.#setThemesAndNotify({ ...this.#currentThemes });
149
+ }, { signal: this.#abortController.signal });
150
+ }
151
+ const [ifMatch, ifNotMatch] = this.#systemOptions[themeKey];
152
+ return this.#mediaQueryCache[mediaQuery].matches ? ifMatch : ifNotMatch;
153
+ };
154
+ #notify = () => {
155
+ for (const listener of this.#listeners) listener({
156
+ themes: this.#currentThemes,
157
+ resolvedThemes: this.#resolveThemes()
158
+ });
159
+ };
160
+ };
161
+ var Registry = class {
162
+ #registry = /* @__PURE__ */ new Map();
163
+ create = (options) => {
164
+ const storeKey = options.key || PACKAGE_NAME;
165
+ if (this.#registry.has(storeKey)) this.destroy(storeKey);
166
+ const themeStore = new ThemeStore(options);
167
+ this.#registry.set(storeKey, themeStore);
168
+ return themeStore;
169
+ };
170
+ get = (key) => {
171
+ const storeKey = key || PACKAGE_NAME;
172
+ if (!this.#registry.has(storeKey)) throw new Error(`[${PACKAGE_NAME}] Theme store with key '${storeKey}' could not be found. Please run \`createThemeStore\` with key '${storeKey}' first.`);
173
+ return this.#registry.get(storeKey);
174
+ };
175
+ destroy = (key) => {
176
+ const storeKey = key || PACKAGE_NAME;
177
+ if (!this.#registry.has(storeKey)) throw new Error(`[${PACKAGE_NAME}] Theme store with key '${storeKey}' could not be found. Please run \`createThemeStore\` with key '${storeKey}' first.`);
178
+ this.#registry.get(storeKey).___destroy();
179
+ this.#registry.delete(storeKey);
180
+ };
181
+ };
182
+ const registry = new Registry();
183
+ const createThemeStore = registry.create;
184
+ const getThemeStore = registry.get;
185
+ const destroyThemeStore = registry.destroy;
186
+
187
+ //#endregion
188
+ export { getThemeStore as a, getDefaultThemes as i, createThemeStore as n, getThemesAndOptions as o, destroyThemeStore as r, ThemeStore as t };
@@ -0,0 +1,19 @@
1
+ //#region src/storage.d.ts
2
+ type StorageAdapter = {
3
+ getItem: (key: string) => object | null | Promise<object | null>;
4
+ setItem: (key: string, value: object) => void | Promise<void>;
5
+ broadcast?: (key: string, value: object) => void;
6
+ watch?: (cb: (key: string | null, value: object) => void) => () => void;
7
+ };
8
+ type StorageAdapterCreate = ({
9
+ abortController
10
+ }: {
11
+ abortController: AbortController;
12
+ }) => StorageAdapter;
13
+ type StorageAdapterCreator<Options> = (options?: Options) => StorageAdapterCreate;
14
+ declare const localStorageAdapter: StorageAdapterCreator<{
15
+ storageType?: 'localStorage' | 'sessionStorage';
16
+ }>;
17
+ declare const memoryStorageAdapter: StorageAdapterCreator<never>;
18
+ //#endregion
19
+ export { memoryStorageAdapter as a, localStorageAdapter as i, StorageAdapterCreate as n, StorageAdapterCreator as r, StorageAdapter as t };
@@ -0,0 +1,59 @@
1
+ //#region package.json
2
+ var name = "resonare";
3
+
4
+ //#endregion
5
+ //#region src/storage.ts
6
+ const localStorageAdapter = ({ storageType = "localStorage" } = {}) => {
7
+ return ({ abortController }) => {
8
+ return {
9
+ getItem: (key) => {
10
+ return JSON.parse(window[storageType].getItem(key) || "null");
11
+ },
12
+ setItem: (key, value) => {
13
+ window[storageType].setItem(key, JSON.stringify(value));
14
+ },
15
+ watch: (cb) => {
16
+ const controller = new AbortController();
17
+ window.addEventListener("storage", (e) => {
18
+ if (e.storageArea !== window[storageType]) return;
19
+ cb(e.key, JSON.parse(e.newValue));
20
+ }, { signal: AbortSignal.any([abortController.signal, controller.signal]) });
21
+ return () => {
22
+ controller.abort();
23
+ };
24
+ }
25
+ };
26
+ };
27
+ };
28
+ const memoryStorageAdapter = () => {
29
+ return ({ abortController }) => {
30
+ const storage = /* @__PURE__ */ new Map();
31
+ const channel = new BroadcastChannel(name);
32
+ return {
33
+ getItem: (key) => {
34
+ return storage.get(key) || null;
35
+ },
36
+ setItem: (key, value) => {
37
+ storage.set(key, value);
38
+ },
39
+ broadcast: (key, value) => {
40
+ channel.postMessage({
41
+ key,
42
+ value
43
+ });
44
+ },
45
+ watch: (cb) => {
46
+ const controller = new AbortController();
47
+ channel.addEventListener("message", (e) => {
48
+ cb(e.data.key, e.data.value);
49
+ }, { signal: AbortSignal.any([abortController.signal, controller.signal]) });
50
+ return () => {
51
+ controller.abort();
52
+ };
53
+ }
54
+ };
55
+ };
56
+ };
57
+
58
+ //#endregion
59
+ export { memoryStorageAdapter as n, localStorageAdapter as t };
@@ -0,0 +1,2 @@
1
+ import { a as memoryStorageAdapter, i as localStorageAdapter, n as StorageAdapterCreate, r as StorageAdapterCreator, t as StorageAdapter } from "./storage-BP1DGXF0.js";
2
+ export { StorageAdapter, StorageAdapterCreate, StorageAdapterCreator, localStorageAdapter, memoryStorageAdapter };
@@ -0,0 +1,3 @@
1
+ import { n as memoryStorageAdapter, t as localStorageAdapter } from "./storage-ByzkGDod.js";
2
+
3
+ export { localStorageAdapter, memoryStorageAdapter };
package/dist/umd.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { a as memoryStorageAdapter, i as localStorageAdapter } from "./storage-BP1DGXF0.js";
2
+ import { c as destroyThemeStore, d as getThemesAndOptions, s as createThemeStore, u as getThemeStore } from "./index-C1bWhjq0.js";
3
+ export { createThemeStore, destroyThemeStore, getThemeStore, getThemesAndOptions, localStorageAdapter, memoryStorageAdapter };
package/dist/umd.js ADDED
@@ -0,0 +1,4 @@
1
+ import { n as memoryStorageAdapter, t as localStorageAdapter } from "./storage-ByzkGDod.js";
2
+ import { a as getThemeStore, n as createThemeStore, o as getThemesAndOptions, r as destroyThemeStore } from "./src-BsTQDM3B.js";
3
+
4
+ export { createThemeStore, destroyThemeStore, getThemeStore, getThemesAndOptions, localStorageAdapter, memoryStorageAdapter };
package/global.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ interface Window {
2
+ resonare: typeof import('./dist/umd')
3
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "resonare",
3
+ "version": "0.0.1",
4
+ "description": "Resonare",
5
+ "keywords": [
6
+ "theming"
7
+ ],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/universse/resonare.git",
11
+ "directory": "resonare"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Si Phuoc <phuoc317049@gmail.com>",
15
+ "sideEffects": false,
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./global": {
23
+ "types": "./dist/umd.d.ts"
24
+ },
25
+ "./raw": {
26
+ "import": "./dist/resonare.iife.min.js"
27
+ },
28
+ "./react": {
29
+ "types": "./dist/react.d.ts",
30
+ "import": "./dist/react.js"
31
+ },
32
+ "./storage": {
33
+ "types": "./dist/storage.d.ts",
34
+ "import": "./dist/storage.js"
35
+ }
36
+ },
37
+ "jsdelivr": "./dist/resonare.iife.min.js",
38
+ "unpkg": "./dist/resonare.iife.min.js",
39
+ "types": "./dist/index.d.ts",
40
+ "files": [
41
+ "dist",
42
+ "global.d.ts"
43
+ ],
44
+ "dependencies": {
45
+ "react-compiler-runtime": "1.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@size-limit/preset-small-lib": "12.0.0",
49
+ "@types/react": "19.2.7",
50
+ "@vitejs/plugin-react": "5.1.2",
51
+ "babel-plugin-react-compiler": "1.0.0",
52
+ "jsdom": "27.3.0",
53
+ "react": "19.2.1",
54
+ "react-dom": "19.2.1",
55
+ "size-limit": "12.0.0",
56
+ "tsdown": "0.17.2",
57
+ "typescript": "5.9.3",
58
+ "vitest": "4.0.15"
59
+ },
60
+ "peerDependencies": {
61
+ "react": "^18.0.0 || ^19.0.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "react": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "scripts": {
72
+ "build": "tsdown",
73
+ "dev": "tsdown --watch",
74
+ "size": "size-limit",
75
+ "test": "vitest"
76
+ }
77
+ }