|
1 |
| -interface Window { |
2 |
| - palettez: typeof import('palettez') |
3 |
| -} |
| 1 | +# Palettez |
| 2 | + |
| 3 | +A flexible and powerful theme management library for JavaScript applications. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- Manage parallel themes with multiple options, for eg: |
| 8 | + - Color scheme: light, dark, system |
| 9 | + - Contrast preference: standard, high |
| 10 | + - Spacing: compact, comfortable, spacious |
| 11 | +- Persist theme selection to client or server storage |
| 12 | +- Dynamically change themes based on system settings |
| 13 | +- Sync theme selection across tabs and windows |
| 14 | +- No theme flicker on page load |
| 15 | + |
| 16 | +## Demo |
| 17 | + |
| 18 | +- Client-side persistence with localStorage and server-side persistence with cookies |
| 19 | + |
| 20 | + [](https://stackblitz.com/fork/github/universse/palettez/tree/main/demo?title=Palettez%20Demo&file=src%2Fpages%2Findex.astro,src%2Fpages%2Fssr.astro) |
| 21 | + |
| 22 | +## Installation |
| 23 | + |
| 24 | +To install: |
| 25 | + |
| 26 | +```bash |
| 27 | +npm i palettez |
| 28 | +# or |
| 29 | +yarn add palettez |
| 30 | +# or |
| 31 | +pnpm add palettez |
| 32 | +``` |
| 33 | + |
| 34 | +## Basic Usage |
| 35 | + |
| 36 | +For client-side persistence (eg. localStorage), it's recommended to initialize Palettez 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 Palettez to reduce the number of HTTP requests. Check out the demo for example usage with Astro and Vite. |
| 37 | + |
| 38 | +```html |
| 39 | +<script src="https://unpkg.com/palettez"></script> |
| 40 | +<!-- or --> |
| 41 | +<script src="https://cdn.jsdelivr.net/npm/palettez"></script> |
| 42 | + |
| 43 | +<script> |
| 44 | + ;(() => { |
| 45 | + const themeManager = window.palettez.create({ |
| 46 | + config: { |
| 47 | + colorScheme: { |
| 48 | + label: 'Color scheme', |
| 49 | + options: { |
| 50 | + system: { |
| 51 | + value: 'System', |
| 52 | + isDefault: true, |
| 53 | + media: { |
| 54 | + query: '(prefers-color-scheme: dark)', |
| 55 | + ifMatch: 'dark', |
| 56 | + ifNotMatch: 'light', |
| 57 | + }, |
| 58 | + }, |
| 59 | + light: { value: 'Light' }, |
| 60 | + dark: { value: 'Dark' }, |
| 61 | + }, |
| 62 | + }, |
| 63 | + }, |
| 64 | + }) |
| 65 | +
|
| 66 | + themeManager.subscribe((_, resolvedThemes) => { |
| 67 | + Object.entries(resolvedThemes).forEach(([theme, optionKey]) => { |
| 68 | + document.documentElement.dataset[theme] = optionKey |
| 69 | + }) |
| 70 | + }) |
| 71 | +
|
| 72 | + themeManager.restore() |
| 73 | + themeManager.sync() |
| 74 | + })() |
| 75 | +</script> |
| 76 | +``` |
| 77 | + |
| 78 | +If you are using TypeScript, add `palettez/global` to `compilerOptions.types` in `tsconfig.json`. |
| 79 | + |
| 80 | +```json |
| 81 | +{ |
| 82 | + "compilerOptions": { |
| 83 | + "types": ["palettez/global"] |
| 84 | + } |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +## API |
| 89 | + |
| 90 | +### `create` |
| 91 | + |
| 92 | +```ts |
| 93 | +import { create } from 'palettez' |
| 94 | + |
| 95 | +const themeManager = create({ |
| 96 | + // optional, default: 'palettez' |
| 97 | + key: 'palettez', |
| 98 | + |
| 99 | + // required, specify theme and options |
| 100 | + config: { |
| 101 | + colorScheme: { |
| 102 | + label: 'Color scheme', |
| 103 | + options: { |
| 104 | + system: { |
| 105 | + value: 'System', |
| 106 | + isDefault: true, |
| 107 | + media: { |
| 108 | + query: '(prefers-color-scheme: dark)', |
| 109 | + ifMatch: 'dark', |
| 110 | + ifNotMatch: 'light', |
| 111 | + }, |
| 112 | + }, |
| 113 | + light: { value: 'Light' }, |
| 114 | + dark: { value: 'Dark' }, |
| 115 | + }, |
| 116 | + }, |
| 117 | + |
| 118 | + contrast: { |
| 119 | + label: 'Contrast', |
| 120 | + options: { |
| 121 | + system: { |
| 122 | + value: 'System', |
| 123 | + isDefault: true, |
| 124 | + media: { |
| 125 | + query: '(prefers-contrast: more) and (forced-colors: none)', |
| 126 | + ifMatch: 'more', |
| 127 | + ifNotMatch: 'standard', |
| 128 | + }, |
| 129 | + }, |
| 130 | + standard: { value: 'Standard' }, |
| 131 | + high: { value: 'High' }, |
| 132 | + }, |
| 133 | + }, |
| 134 | + }, |
| 135 | + |
| 136 | + // optional, specify your own storage solution |
| 137 | + getStorage: () => { |
| 138 | + return { |
| 139 | + getItem: (key: string) => { |
| 140 | + return JSON.parse(window.localStorage.getItem(key) || 'null') |
| 141 | + }, |
| 142 | + |
| 143 | + setItem: (key: string, value: object) => { |
| 144 | + window.localStorage.setItem(key, JSON.stringify(value)) |
| 145 | + }, |
| 146 | + |
| 147 | + removeItem: (key: string) => { |
| 148 | + window.localStorage.removeItem(key) |
| 149 | + }, |
| 150 | + |
| 151 | + // optional, useful for syncing theme selection across tabs and windows |
| 152 | + watch: (cb) => { |
| 153 | + const controller = new AbortController() |
| 154 | + |
| 155 | + window.addEventListener( |
| 156 | + 'storage', |
| 157 | + (e) => { |
| 158 | + const persistedThemes = JSON.parse(e.newValue || 'null') |
| 159 | + cb(e.key, persistedThemes) |
| 160 | + }, |
| 161 | + { signal: controller.signal }, |
| 162 | + ) |
| 163 | + |
| 164 | + return () => { |
| 165 | + controller.abort() |
| 166 | + } |
| 167 | + }, |
| 168 | + } |
| 169 | + } |
| 170 | +}) |
| 171 | +``` |
| 172 | + |
| 173 | +### `read` |
| 174 | + |
| 175 | +```ts |
| 176 | +import { read } from 'palettez' |
| 177 | + |
| 178 | +const themeManager = read('palettez') |
| 179 | +``` |
| 180 | + |
| 181 | +### Methods |
| 182 | + |
| 183 | +```ts |
| 184 | +themeManager.getThemes() // { colorScheme: 'system', contrast: 'standard' } |
| 185 | +themeManager.getResolvedThemes() // { colorScheme: 'light', contrast: 'standard' } |
| 186 | +themeManager.setThemes({ contrast: 'high' }) |
| 187 | +themeManager.restore() // restore persisted theme selection |
| 188 | +themeManager.sync() // useful for syncing theme selection across tabs and windows |
| 189 | +themeManager.clear() // clear persisted theme selection |
| 190 | +themeManager.subscribe((themes, resolvedThemes) => { /* ... */ }) |
| 191 | +``` |
| 192 | + |
| 193 | +## React Integration |
| 194 | + |
| 195 | +Ensure that you have called `create` before `usePalettez`. |
| 196 | + |
| 197 | +```tsx |
| 198 | +import { usePalettez } from 'palettez/react' |
| 199 | + |
| 200 | +export function ThemeSelect() { |
| 201 | + const { |
| 202 | + themesAndOptions, |
| 203 | + themes, |
| 204 | + setThemes, |
| 205 | + |
| 206 | + getResolvedThemes, |
| 207 | + restore, |
| 208 | + sync, |
| 209 | + clear, |
| 210 | + subscribe, |
| 211 | + } = usePalettez('palettez') |
| 212 | + |
| 213 | + return themesAndOptions.map((theme) => ( |
| 214 | + <div key={theme.key}> |
| 215 | + <label htmlFor={theme.key}>{theme.label}</label> |
| 216 | + <select |
| 217 | + id={theme.key} |
| 218 | + name={theme.key} |
| 219 | + onChange={(e) => { |
| 220 | + setThemes({ [theme.key]: e.target.value }) |
| 221 | + }} |
| 222 | + value={themes[theme.key]} |
| 223 | + > |
| 224 | + {theme.options.map((option) => ( |
| 225 | + <option key={option.key} value={option.key}> |
| 226 | + {option.value} |
| 227 | + </option> |
| 228 | + ))} |
| 229 | + </select> |
| 230 | + </div> |
| 231 | + )) |
| 232 | +} |
| 233 | +``` |
0 commit comments