resonare 0.0.11 → 0.0.12

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 CHANGED
@@ -1,25 +1,25 @@
1
1
  # Resonare [![Version](https://img.shields.io/npm/v/resonare.svg?labelColor=black&color=blue)](https://www.npmjs.com/package/resonare)
2
2
 
3
- A configuration-based theme store for building deeply personal user interface.
3
+ A configuration-based theme store for building deeply personal user interfaces.
4
4
 
5
5
  ## Features
6
6
 
7
- - Define and manage multi-dimensional themes, eg:
7
+ - Define and manage multi-dimensional themes, e.g.:
8
8
  - Color scheme: system, light, dark
9
9
  - Contrast preference: standard, high
10
10
  - Spacing: compact, comfortable, spacious
11
- - etc
11
+ - etc.
12
12
  - Framework-agnostic
13
13
  - Prevent theme flicker on page load
14
14
  - Honor system preferences
15
15
  - Create sections with independent theming
16
16
  - Sync theme selection across tabs and windows
17
- - Flexible persistence options, defaulting to localStorage
18
- - SSR-friendly
17
+ - Flexible client-side persistence options, defaulting to localStorage
18
+ - Support server-side persistence
19
19
 
20
20
  ## Demo
21
21
 
22
- - [Demo](https://resonare.universse.workers.dev)
22
+ [Check it out](https://resonare.universse.workers.dev).
23
23
 
24
24
  ## Installation
25
25
 
@@ -35,7 +35,7 @@ pnpm add resonare
35
35
 
36
36
  ## Basic Usage
37
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.
38
+ It's recommended to initialize Resonare in a synchronous script to avoid theme flicker on page load. Load via CDN or inline the stringified version to reduce the number of HTTP requests.
39
39
 
40
40
  ```html
41
41
  <script src="https://unpkg.com/resonare"></script>
@@ -54,14 +54,14 @@ It's recommended to initialize Resonare in a synchronous script to avoid theme f
54
54
  },
55
55
  'light',
56
56
  'dark',
57
- ]
57
+ ],
58
58
  },
59
59
  },
60
60
  })
61
61
 
62
62
  themeStore.subscribe(({ resolvedThemes }) => {
63
- for (const [theme, optionKey] of Object.entries(resolvedThemes)) {
64
- document.documentElement.dataset[theme] = optionKey
63
+ for (const [theme, option] of Object.entries(resolvedThemes)) {
64
+ document.documentElement.dataset[theme] = option
65
65
  }
66
66
  })
67
67
 
@@ -71,15 +71,50 @@ It's recommended to initialize Resonare in a synchronous script to avoid theme f
71
71
  </script>
72
72
  ```
73
73
 
74
- If you are using TypeScript, add `node_modules/resonare/global.d.ts` to `include` in `tsconfig.json`.
74
+ ```ts
75
+ import { type ThemeConfig, type ThemeStore } from 'resonare'
76
+ import { resonareInlineScript } from 'resonare/inline-script'
77
+
78
+ const themeConfig = {
79
+ colorScheme: {
80
+ options: [
81
+ {
82
+ value: 'system',
83
+ media: ['(prefers-color-scheme: dark)', 'dark', 'light'],
84
+ },
85
+ 'light',
86
+ 'dark',
87
+ ],
88
+ },
89
+ } as const satisfies ThemeConfig
75
90
 
76
- ```json
77
- {
78
- "include": [
79
- "node_modules/resonare/global.d.ts",
80
- // ...
81
- ]
91
+ declare module 'resonare' {
92
+ interface ThemeStoreRegistry {
93
+ resonare: ThemeStore<typeof themeConfig>
94
+ }
95
+ }
96
+
97
+ function initTheme({ config }: { config: ThemeConfig }) {
98
+ const themeStore = window.resonare.createThemeStore({ config })
99
+
100
+ themeStore.subscribe(({ resolvedThemes }) => {
101
+ Object.entries(resolvedThemes).forEach(([theme, option]) => {
102
+ document.documentElement.dataset[theme] = option
103
+ })
104
+ })
105
+
106
+ themeStore.restore()
107
+ themeStore.sync()
82
108
  }
109
+
110
+ export const themeScript = `${resonareInlineScript};
111
+ (${initTheme.toString()})(${JSON.stringify({ config: themeConfig })})`
112
+ ```
113
+
114
+ Add a triple-slash directive to any `.d.ts` file in your project (e.g. `env.d.ts`):
115
+
116
+ ```ts
117
+ /// <reference types="resonare/global" />
83
118
  ```
84
119
 
85
120
  ## API
@@ -89,7 +124,7 @@ If you are using TypeScript, add `node_modules/resonare/global.d.ts` to `include
89
124
  ```ts
90
125
  import { createThemeStore, type ThemeConfig, type ThemeStore } from 'resonare'
91
126
 
92
- const config = {
127
+ const themeConfig = {
93
128
  colorScheme: {
94
129
  options: [
95
130
  {
@@ -98,7 +133,7 @@ const config = {
98
133
  },
99
134
  'light',
100
135
  'dark',
101
- ]
136
+ ],
102
137
  },
103
138
  contrast: {
104
139
  options: [
@@ -114,31 +149,25 @@ const config = {
114
149
  'high',
115
150
  ],
116
151
  initialValue: 'standard',
117
- }
152
+ },
118
153
  } as const satisfies ThemeConfig
119
154
 
120
155
  declare module 'resonare' {
121
156
  interface ThemeStoreRegistry {
122
- resonare: ThemeStore<typeof config>
157
+ resonare: ThemeStore<typeof themeConfig>
123
158
  }
124
159
  }
125
160
 
126
-
127
161
  const themeStore = createThemeStore({
128
162
  // optional, default 'resonare'
129
163
  // should be unique, also used as client storage key
130
164
  key: 'resonare',
131
165
 
132
166
  // required, specify theme and options
133
- config,
167
+ config: themeConfig,
134
168
 
135
- // optional, useful for SSR
136
- initialState: {
137
- themes: {
138
- colorScheme: 'dark',
139
- contrast: 'high',
140
- },
141
- },
169
+ // optional, useful for server-side persistence
170
+ initialState: persistedStateFromDb, // persisted state returned by themeStore.toPersist()
142
171
 
143
172
  // optional, specify your own client storage
144
173
  // localStorage is used by default
@@ -157,7 +186,7 @@ const themeStore = createThemeStore({
157
186
  window.addEventListener(
158
187
  'storage',
159
188
  (e) => {
160
- if (e.storageArea !== localStorage) return
189
+ if (e.storageArea !== window.localStorage) return
161
190
 
162
191
  cb(e.key, JSON.parse(e.newValue!))
163
192
  },
@@ -182,8 +211,8 @@ const themeStore = createThemeStore({
182
211
  ```ts
183
212
  import { getThemeStore } from 'resonare'
184
213
 
185
- // Get an existing theme store by key
186
- const themeStore = getThemeStore('resonare')
214
+ // get an existing theme store by key
215
+ const themeStore = getThemeStore('resonare')
187
216
  ```
188
217
 
189
218
  ### `destroyThemeStore`
@@ -191,8 +220,8 @@ const themeStore = getThemeStore('resonare')
191
220
  ```ts
192
221
  import { destroyThemeStore } from 'resonare'
193
222
 
194
- // Get an existing theme store by key
195
- const themeStore = destroyThemeStore('resonare')
223
+ // destroy an existing theme store by key
224
+ destroyThemeStore('resonare')
196
225
  ```
197
226
 
198
227
  ### ThemeStore Methods
@@ -206,14 +235,14 @@ interface ThemeStore<T> {
206
235
  getResolvedThemes(): Record<string, string>
207
236
 
208
237
  // update theme
209
- setThemes(themes: Partial<Record<string, string>>): Promise<void>
238
+ setThemes(themes: Partial<Record<string, string>>): void
210
239
 
211
240
  // get state to persist, useful for server-side persistence
212
- // to restore, pass the returned object to initialState
241
+ // to restore, pass the returned object to createThemeStore's initialState
213
242
  toPersist(): object
214
243
 
215
- // restore persisted theme selection from client storage
216
- restore(): Promise<void>
244
+ // restore persisted state from client-side storage
245
+ restore(): void
217
246
 
218
247
  // sync theme selection across tabs/windows
219
248
  sync(): () => void
@@ -232,21 +261,36 @@ interface ThemeStore<T> {
232
261
  }
233
262
  ```
234
263
 
235
- ### React Integration
264
+ ## Framework Integrations
265
+
266
+ ### React
236
267
 
237
268
  Ensure that you have initialized Resonare as per instructions under [Basic Usage](#basic-usage).
238
269
 
239
270
  ```tsx
240
271
  import * as React from 'react'
241
- import { getThemesAndOptions } from 'resonare'
272
+ import { getThemesAndOptions, type ThemeConfig } from 'resonare'
242
273
  import { useResonare } from 'resonare/react'
243
274
 
275
+ const themeConfig = {
276
+ colorScheme: {
277
+ options: [
278
+ {
279
+ value: 'system',
280
+ media: ['(prefers-color-scheme: dark)', 'dark', 'light'],
281
+ },
282
+ 'light',
283
+ 'dark',
284
+ ],
285
+ },
286
+ } as const satisfies ThemeConfig
287
+
244
288
  function ThemeSelect() {
245
289
  const { themes, setThemes } = useResonare(() =>
246
290
  window.resonare.getThemeStore(),
247
291
  )
248
292
 
249
- return getThemesAndOptions(config).map(([theme, options]) => (
293
+ return getThemesAndOptions(themeConfig).map(([theme, options]) => (
250
294
  <div key={theme}>
251
295
  <label htmlFor={theme}>{theme}</label>
252
296
  <select
@@ -268,9 +312,9 @@ function ThemeSelect() {
268
312
  }
269
313
  ```
270
314
 
271
- ### Server-side persistence
315
+ ## Server-side Persistence
272
316
 
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`.
317
+ Use `memoryStorageAdapter` to avoid storing any data client-side. The synchronous script is not required.
274
318
 
275
319
  ```tsx
276
320
  import {
@@ -278,12 +322,11 @@ import {
278
322
  getThemesAndOptions,
279
323
  memoryStorageAdapter,
280
324
  type ThemeConfig,
281
- type Themes,
282
325
  } from 'resonare'
283
326
  import { useResonare } from 'resonare/react'
284
327
  import * as React from 'react'
285
328
 
286
- const config = {
329
+ const themeConfig = {
287
330
  colorScheme: {
288
331
  options: ['light', 'dark'],
289
332
  },
@@ -292,15 +335,11 @@ const config = {
292
335
  },
293
336
  } as const satisfies ThemeConfig
294
337
 
295
- export function ThemeSelect({
296
- persistedServerThemes,
297
- }: { persistedServerThemes: Themes<typeof config> }) {
338
+ export function ThemeSelect({ persistedStateFromDb }) {
298
339
  const [themeStore] = React.useState(() =>
299
340
  createThemeStore({
300
- config,
301
- initialState: {
302
- themes: persistedServerThemes,
303
- },
341
+ config: themeConfig,
342
+ initialState: persistedStateFromDb,
304
343
  storage: memoryStorageAdapter(),
305
344
  }),
306
345
  )
@@ -309,14 +348,16 @@ export function ThemeSelect({
309
348
  initOnMount: true,
310
349
  })
311
350
 
312
- return getThemesAndOptions(config).map(([theme, options]) => (
351
+ return getThemesAndOptions(themeConfig).map(([theme, options]) => (
313
352
  <div key={theme}>
314
353
  <label htmlFor={theme}>{theme}</label>
315
354
  <select
316
355
  id={theme}
317
356
  name={theme}
318
- onChange={(e) => {
357
+ onChange={async (e) => {
319
358
  setThemes({ [theme]: e.target.value })
359
+
360
+ await saveToDb(themeStore.toPersist())
320
361
  }}
321
362
  value={themes[theme]}
322
363
  >
@@ -1 +1 @@
1
- export const resonareInlineScript = "/**\n* resonare v0.0.11\n*\n* This source code is licensed under the MIT license found in the\n* LICENSE file in the root directory of this source tree.\n*/\nvar resonare=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`resonare`;let n=({type:e=`localStorage`}={})=>({abortController:t})=>({get:t=>JSON.parse(window[e].getItem(t)||`null`),set:(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()}}});function r(e){return Object.fromEntries(Object.entries(e).map(([e,t])=>[e,t.initialValue??t.options[0].value??t.options[0]]))}var i=class{#e;#t;#n;#r;#i;#a=new Set;#o;#s=new AbortController;constructor({key:e=t,config:i,initialState:a={},storage:o=n()}){let s={},c={...a.systemOptions};Object.entries(i).forEach(([e,t])=>{s[e]={},`options`in t&&t.options.forEach(t=>{typeof t==`object`?(t.media&&!Object.hasOwn(c,e)&&(c[e]=[t.media[1],t.media[2]]),s[e][String(t.value)]=t):s[e][String(t)]={value:t}})}),this.#n={key:e,config:s},this.#r=c,this.#e=r(i),this.#t={...this.#e,...a.themes},this.#i=o?.({abortController:this.#s})??null,this.#o={}}getThemes=()=>this.#t;getResolvedThemes=()=>this.#l();setThemes=e=>{let t=typeof e==`function`?e(this.#t):e;this.#c({...this.#t,...t});let n=this.toPersist();this.#i&&(this.#i.set(this.#n.key,n),this.#i.broadcast?.(this.#n.key,n))};updateSystemOption=(e,[t,n])=>{this.#r[e]=[t,n],this.setThemes({...this.#t})};toPersist=()=>({version:1,themes:this.#t,systemOptions:this.#r});restore=()=>{let e=this.#i?.get(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)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,n?this.#u({themeKey:e,option:n}):t]}));#u=({themeKey:e,option:t})=>{if(!t.media)return 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,i]=this.#r[e];return this.#o[n].matches?r:i};#d=()=>{for(let e of this.#a)e({themes:this.#t,resolvedThemes:this.#l()})}};let a=new class{#e=new Map;create=e=>{let n=e.key||t,r=this.#e.get(n);return r||(r=new i(e),this.#e.set(n,r)),r};get=e=>{let n=e||t;if(!this.#e.has(n))throw Error(`Theme store '${n}' not found.`);return this.#e.get(n)};destroy=e=>{let n=e||t;this.#e.has(n)&&(this.#e.get(n).___destroy(),this.#e.delete(n))}},o=a.create,s=a.get,c=a.destroy;return e.createThemeStore=o,e.destroyThemeStore=c,e.getThemeStore=s,e})({});"
1
+ export const resonareInlineScript = "/**\n* resonare v0.0.12\n*\n* This source code is licensed under the MIT license found in the\n* LICENSE file in the root directory of this source tree.\n*/\nvar resonare=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`resonare`;let n=({type:e=`localStorage`}={})=>({abortController:t})=>({get:t=>JSON.parse(window[e].getItem(t)||`null`),set:(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()}}});function r(e){return Object.fromEntries(Object.entries(e).map(([e,t])=>[e,t.initialValue??t.options[0].value??t.options[0]]))}var i=class{#e;#t;#n;#r;#i;#a=new Set;#o;#s=new AbortController;constructor({key:e=t,config:i,initialState:a={},storage:o=n()}){let s={},c={...a.systemOptions};Object.entries(i).forEach(([e,t])=>{s[e]={},`options`in t&&t.options.forEach(t=>{typeof t==`object`?(t.media&&!Object.hasOwn(c,e)&&(c[e]=[t.media[1],t.media[2]]),s[e][String(t.value)]=t):s[e][String(t)]={value:t}})}),this.#n={key:e,config:s},this.#r=c,this.#e=r(i),this.#t={...this.#e,...a.themes},this.#i=o?.({abortController:this.#s})??null,this.#o={}}getThemes=()=>this.#t;getResolvedThemes=()=>this.#l();setThemes=e=>{let t=typeof e==`function`?e(this.#t):e;this.#c({...this.#t,...t});let n=this.toPersist();this.#i&&(this.#i.set(this.#n.key,n),this.#i.broadcast?.(this.#n.key,n))};updateSystemOption=(e,[t,n])=>{this.#r[e]=[t,n],this.setThemes({...this.#t})};toPersist=()=>({version:1,themes:this.#t,systemOptions:this.#r});restore=()=>{let e=this.#i?.get(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)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,n?this.#u({themeKey:e,option:n}):t]}));#u=({themeKey:e,option:t})=>{if(!t.media)return 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,i]=this.#r[e];return this.#o[n].matches?r:i};#d=()=>{for(let e of this.#a)e({themes:this.#t,resolvedThemes:this.#l()})}};let a=new class{#e=new Map;create=e=>{let n=e.key||t,r=this.#e.get(n);return r||(r=new i(e),this.#e.set(n,r)),r};get=e=>{let n=e||t;if(!this.#e.has(n))throw Error(`Theme store '${n}' not found.`);return this.#e.get(n)};destroy=e=>{let n=e||t;this.#e.has(n)&&(this.#e.get(n).___destroy(),this.#e.delete(n))}},o=a.create,s=a.get,c=a.destroy;return e.createThemeStore=o,e.destroyThemeStore=c,e.getThemeStore=s,e})({});"
@@ -1,5 +1,5 @@
1
1
  /**
2
- * resonare v0.0.11
2
+ * resonare v0.0.12
3
3
  *
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resonare",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Resonare",
5
5
  "keywords": [
6
6
  "theming"
@@ -19,6 +19,9 @@
19
19
  "types": "./dist/index.d.ts",
20
20
  "import": "./dist/index.js"
21
21
  },
22
+ "./global": {
23
+ "types": "./global.d.ts"
24
+ },
22
25
  "./inline-script": {
23
26
  "import": "./dist/inline-script.ts"
24
27
  },