@charstudios/pallet 0.1.0 → 0.3.0

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,479 +1,454 @@
1
- # @charstudios/pallet
2
-
3
- A runtime, hook-driven theming layer for **shadcn/ui** apps. Pallet gives you and your users advanced, live control over **colors, fonts, roundness, elevation, spacing, and motion**, plus swappable visual **variants** ("skins") — all through a small React API that works with your existing, unmodified shadcn components.
4
-
5
- Built for reuse across every Char Studios product: install it once, wrap your app, and every project shares the same theming system.
6
-
7
- - **Runtime theming** — change any token live; no rebuild, no CSS regeneration.
8
- - **Variants/skins** — ship `flat` (crisp, default shadcn look) and `launch` (soft, elevated, pill-shaped), or define your own.
9
- - **Full token control** — colors (light + dark, OKLCH), typography (with Google Fonts auto-loading), radius scale, shadow ramp, density, and motion.
10
- - **Presets** — start from a named preset and layer overrides on top; `reset()` returns to baseline.
11
- - **SSR-safe** — inline the initial theme from a Server Component to avoid a flash of the wrong theme.
12
- - **Persistence** — optionally remember a user's theme in `localStorage`.
13
-
14
- ---
15
-
16
- ## Table of contents
17
-
18
- - [How it works](#how-it-works)
19
- - [Installation](#installation)
20
- - [Quick start](#quick-start)
21
- - [Presets](#presets)
22
- - [Hooks API](#hooks-api)
23
- - [Building a theme editor](#building-a-theme-editor)
24
- - [Variants (skins)](#variants-skins)
25
- - [Server-side rendering (no flash)](#server-side-rendering-no-flash)
26
- - [Custom presets and variants](#custom-presets-and-variants)
27
- - [Token reference](#token-reference)
28
- - [TypeScript](#typescript)
29
- - [Publishing](#publishing)
30
- - [FAQ](#faq)
31
-
32
- ---
33
-
34
- ## How it works
35
-
36
- shadcn/ui styles everything through CSS custom properties (`--primary`, `--radius`, `--font-sans`, ...). Pallet:
37
-
38
- 1. Takes a **`ThemeConfig`** (a preset + your overrides).
39
- 2. **Resolves** it into a flat map of CSS variables for the active color scheme.
40
- 3. **Injects** those variables onto the document root (or a scoped element) at runtime.
41
- 4. Sets a **`data-pallet-variant`** attribute so the shipped **variant stylesheets** can restyle surfaces (shadows, borders, pill shapes) that aren't expressible through tokens alone.
42
-
43
- ```
44
- ThemeConfig (preset + overrides)
45
- │ resolveTheme()
46
-
47
- CSS variables ──inject──▶ :root / scope element
48
-
49
- │ data-pallet-variant="launch"
50
- ▼ ▼
51
- your shadcn components ◀── variant stylesheet skins
52
- ```
53
-
54
- Your components never change. You only add a provider and (optionally) a stylesheet import.
55
-
56
- ---
57
-
58
- ## Installation
59
-
60
- ```bash
61
- npm install @charstudios/pallet
62
- # or
63
- pnpm add @charstudios/pallet
64
- ```
65
-
66
- **Peer dependencies:** `react >= 18` and `react-dom >= 18` (works with React 19). Assumes a shadcn/ui + Tailwind setup using CSS variables (the shadcn default).
67
-
68
- ---
69
-
70
- ## Quick start
71
-
72
- ### 1. Import the styles
73
-
74
- Add this once to your global stylesheet (e.g. `app/globals.css`), after Tailwind:
75
-
76
- ```css
77
- @import "tailwindcss";
78
- @import "@charstudios/pallet/styles";
79
- ```
80
-
81
- This registers the base token fallbacks and all built-in variant skins. (You can import individual skins instead — see [Variants](#variants-skins).)
82
-
83
- ### 2. Wrap your app
84
-
85
- ```tsx
86
- // app/layout.tsx (or your root component)
87
- import { ThemeProvider } from "@charstudios/pallet";
88
-
89
- export default function RootLayout({ children }: { children: React.ReactNode }) {
90
- return (
91
- <html lang="en" suppressHydrationWarning>
92
- <body>
93
- <ThemeProvider defaultPreset="launch" defaultScheme="system" storageKey="app-theme">
94
- {children}
95
- </ThemeProvider>
96
- </body>
97
- </html>
98
- );
99
- }
100
- ```
101
-
102
- That's it — your shadcn components now render with the `launch` theme, honoring system light/dark, and any user changes persist under `app-theme`.
103
-
104
- ### 3. Change the theme at runtime
105
-
106
- ```tsx
107
- "use client";
108
- import { useThemeControls, usePreset, useColorScheme } from "@charstudios/pallet";
109
-
110
- export function ThemeButtons() {
111
- const { setPrimary, setRadius, reset } = useThemeControls();
112
- const { applyPreset } = usePreset();
113
- const { toggle } = useColorScheme();
114
-
115
- return (
116
- <div className="flex gap-2">
117
- <button onClick={() => applyPreset("launch")}>Launch</button>
118
- <button onClick={() => applyPreset("flat")}>Flat</button>
119
- <button onClick={() => setPrimary("oklch(0.7 0.15 300)")}>Purple</button>
120
- <button onClick={() => setRadius("0.25rem")}>Sharp corners</button>
121
- <button onClick={toggle}>Toggle dark</button>
122
- <button onClick={reset}>Reset</button>
123
- </div>
124
- );
125
- }
126
- ```
127
-
128
- ---
129
-
130
- ## Presets
131
-
132
- | Preset | Look |
133
- | -------- | ------------------------------------------------------------------- |
134
- | `flat` | Crisp hairline borders, minimal shadows, tighter radius. Default shadcn feel (mirrors Quill's current look). |
135
- | `launch` | Soft and elevated: pill-shaped controls, gentle tinted shadows, near-borderless filled inputs, glowing focus rings, periwinkle primary. |
136
-
137
- ```tsx
138
- import { presets, flat, launch, getPreset } from "@charstudios/pallet/presets";
139
-
140
- presets.flat; // ThemeConfig
141
- getPreset("launch"); // ThemeConfig | undefined
142
- ```
143
-
144
- Set the starting preset via `defaultPreset` on the provider, or switch live with `usePreset().applyPreset(...)`.
145
-
146
- ---
147
-
148
- ## Hooks API
149
-
150
- All hooks must be used within a `<ThemeProvider>`.
151
-
152
- ### `useTheme()`
153
-
154
- Returns the full context.
155
-
156
- - `theme: ThemeConfig` — the resolved theme (preset + overrides).
157
- - `basePreset: ThemeConfig` — the active preset before overrides.
158
- - `overrides: ThemeOverride` — the current overrides.
159
- - `scheme: "light" | "dark"` the actual scheme rendered.
160
- - `schemePreference: "light" | "dark" | "system"` — the user's preference.
161
- - `vars: Record<string, string>` — resolved CSS variables for the current scheme.
162
- - `setTheme(theme)` replace the whole theme (clears overrides).
163
- - `applyPreset(preset)` apply a preset by name or object (clears overrides).
164
- - `updateTheme(override)` — deep-merge a partial override.
165
- - `resetOverrides()` — clear overrides.
166
- - `setSchemePreference(pref)` set light/dark/system.
167
-
168
- ### `useThemeControls()`
169
-
170
- Granular setters for building an editor UI. Color setters default to updating **both** light and dark; pass a target (`"light" | "dark" | "both"`) to scope them.
171
-
172
- - `setColor(role, value, target?)`
173
- - `setPrimary(value, target?)`
174
- - `setAccent(value, target?)`
175
- - `setColors(partialColors, target?)`
176
- - `setRadius(base)` e.g. `"0.5rem"`, `"1.25rem"`
177
- - `setRadiusScale(partialScale)`
178
- - `setFont("sans" | "heading" | "mono", family)`
179
- - `setGoogleFonts(families)` — auto-loads them at runtime
180
- - `setVariant(name)` — switch the skin
181
- - `setElevationLevel("none" | "xs" | "sm" | "md" | "lg" | "xl")`
182
- - `setShadow(key, value)`
183
- - `setBorderStrength(0..1)`
184
- - `setDensity({ spacing?, scale? })`
185
- - `setMotion({ duration?, easing?, intensity? })`
186
- - `update(override)` — escape hatch
187
- - `reset()` — clear overrides
188
-
189
- ### `usePreset()`
190
-
191
- - `preset: ThemeConfig` — the active base preset.
192
- - `builtinNames: string[]` names of built-in presets.
193
- - `applyPreset(preset)` — switch presets.
194
-
195
- ### `useColorScheme()`
196
-
197
- - `scheme: "light" | "dark"` — the resolved scheme.
198
- - `preference: "light" | "dark" | "system"`.
199
- - `setPreference(pref)`.
200
- - `toggle()` flip light/dark.
201
-
202
- ---
203
-
204
- ## Building a theme editor
205
-
206
- Everything you need to build a settings panel is exposed as hooks. A minimal example:
207
-
208
- ```tsx
209
- "use client";
210
- import { useTheme, useThemeControls } from "@charstudios/pallet";
211
-
212
- export function ThemeEditor() {
213
- const { theme, scheme } = useTheme();
214
- const c = useThemeControls();
215
-
216
- return (
217
- <div className="space-y-4">
218
- <label>
219
- Primary
220
- <input
221
- type="color"
222
- onChange={(e) => c.setPrimary(e.target.value)}
223
- />
224
- </label>
225
-
226
- <label>
227
- Roundness: {theme.radius.base}
228
- <input
229
- type="range"
230
- min={0}
231
- max={2}
232
- step={0.05}
233
- onChange={(e) => c.setRadius(`${e.target.value}rem`)}
234
- />
235
- </label>
236
-
237
- <label>
238
- Heading font
239
- <select onChange={(e) => { c.setFont("heading", e.target.value); c.setGoogleFonts([e.target.value]); }}>
240
- <option value="Inter">Inter</option>
241
- <option value="Plus Jakarta Sans">Plus Jakarta Sans</option>
242
- <option value="Outfit">Outfit</option>
243
- </select>
244
- </label>
245
-
246
- <label>
247
- Variant
248
- <select onChange={(e) => c.setVariant(e.target.value)}>
249
- <option value="flat">Flat</option>
250
- <option value="launch">Launch</option>
251
- </select>
252
- </label>
253
-
254
- <button onClick={c.reset}>Reset</button>
255
- </div>
256
- );
257
- }
258
- ```
259
-
260
- > Tip: `<input type="color">` emits hex; that works fine as a CSS color. Prefer OKLCH strings for perceptual consistency with the presets when you can.
261
-
262
- ---
263
-
264
- ## Variants (skins)
265
-
266
- A variant is a named "skin" applied via the `data-pallet-variant` attribute (set automatically by the provider). The shipped stylesheets restyle shadcn surfaces that tokens can't reach on their own — shadows, border treatment, and pill shaping.
267
-
268
- Import all skins:
269
-
270
- ```css
271
- @import "@charstudios/pallet/styles";
272
- ```
273
-
274
- Or cherry-pick to reduce CSS:
275
-
276
- ```css
277
- @import "@charstudios/pallet/styles/base";
278
- @import "@charstudios/pallet/styles/launch";
279
- ```
280
-
281
- Switch at runtime:
282
-
283
- ```tsx
284
- useThemeControls().setVariant("launch");
285
- ```
286
-
287
- The variant stylesheets are wrapped in the `pallet-variants` cascade layer, so your own app styles always take precedence.
288
-
289
- ---
290
-
291
- ## Server-side rendering (no flash)
292
-
293
- For app-wide theming Pallet writes variables to `<html>` on mount. To avoid a brief flash of the wrong theme on first paint, inline the theme's CSS from a Server Component using the server-safe entry:
294
-
295
- ```tsx
296
- // app/layout.tsx (Server Component)
297
- import { themeToCss } from "@charstudios/pallet/server";
298
- import { launch } from "@charstudios/pallet/presets";
299
-
300
- export default function RootLayout({ children }: { children: React.ReactNode }) {
301
- return (
302
- <html lang="en" suppressHydrationWarning>
303
- <head>
304
- <style
305
- id="pallet-ssr"
306
- dangerouslySetInnerHTML={{ __html: themeToCss(launch) }}
307
- />
308
- </head>
309
- <body>{/* <ThemeProvider>…</ThemeProvider> */}</body>
310
- </html>
311
- );
312
- }
313
- ```
314
-
315
- `themeToCss` emits structural tokens + light colors on `:root` and dark colors on `.dark`. The provider then takes over for any live/persisted changes.
316
-
317
- **Scoped theming / previews:** use `scope="self"` on the provider to apply the theme to a wrapper `<div>` instead of the document root. This also emits SSR inline styles automatically — ideal for rendering a live preview of a different theme inside a settings page.
318
-
319
- ```tsx
320
- <ThemeProvider defaultPreset="launch" scope="self">
321
- <PreviewSurface />
322
- </ThemeProvider>
323
- ```
324
-
325
- ---
326
-
327
- ## Custom presets and variants
328
-
329
- ### Custom preset
330
-
331
- A preset is just a `ThemeConfig`. Start from a built-in and tweak:
332
-
333
- ```tsx
334
- import { launch } from "@charstudios/pallet/presets";
335
- import { deepMerge } from "@charstudios/pallet/server";
336
- import type { ThemeConfig } from "@charstudios/pallet/server";
337
-
338
- export const brand: ThemeConfig = deepMerge(launch, {
339
- name: "brand",
340
- variant: "launch",
341
- colors: { light: { primary: "oklch(0.62 0.2 15)" }, dark: { primary: "oklch(0.6 0.2 15)" } },
342
- radius: { base: "0.75rem" },
343
- });
344
- ```
345
-
346
- Register it on the provider so `applyPreset("brand")` works:
347
-
348
- ```tsx
349
- <ThemeProvider defaultPreset={brand} presets={{ brand }}>
350
- ```
351
-
352
- ### Custom variant (skin)
353
-
354
- 1. Pick a name and set it: `setVariant("neo")` (or `variant: "neo"` in a preset).
355
- 2. Add a stylesheet that targets `[data-pallet-variant="neo"] [data-slot="..."]`:
356
-
357
- ```css
358
- @layer pallet-variants {
359
- [data-pallet-variant="neo"] [data-slot="card"] {
360
- border-radius: var(--radius-lg);
361
- box-shadow: var(--shadow-lg);
362
- }
363
- [data-pallet-variant="neo"] [data-slot="button"] {
364
- border-radius: 0; /* brutalist square buttons */
365
- }
366
- }
367
- ```
368
-
369
- shadcn components expose `data-slot` on every part (`card`, `button`, `input`, `select-trigger`, `popover`, `dropdown-menu-content`, `dialog-content`, `sheet-content`, `badge`, ...), which is what the skins hook into.
370
-
371
- ---
372
-
373
- ## Token reference
374
-
375
- Resolved CSS variables written by Pallet:
376
-
377
- - **Colors** (per scheme): `--background`, `--foreground`, `--card(-foreground)`, `--popover(-foreground)`, `--primary(-foreground)`, `--secondary(-foreground)`, `--muted(-foreground)`, `--accent(-foreground)`, `--destructive(-foreground)`, `--border`, `--input`, `--ring`, `--chart-1..5`, `--sidebar*`.
378
- - **Radius**: `--radius` plus the derived scale `--radius-sm | md | lg | xl | 2xl | 3xl | 4xl`.
379
- - **Typography**: `--font-sans`, `--font-heading`, `--font-mono`.
380
- - **Elevation**: `--shadow-xs | sm | md | lg | xl`, `--elevation`, `--surface-border-strength` (and the helper `--pallet-surface-border`).
381
- - **Density**: `--spacing`, `--density`.
382
- - **Motion**: `--motion-duration`, `--motion-ease`, `--motion-intensity`.
383
-
384
- ---
385
-
386
- ## TypeScript
387
-
388
- Fully typed. Import types from the server entry (safe everywhere):
389
-
390
- ```ts
391
- import type {
392
- ThemeConfig,
393
- ThemeOverride,
394
- ColorTokens,
395
- Typography,
396
- RadiusConfig,
397
- ElevationConfig,
398
- Density,
399
- Motion,
400
- ColorScheme,
401
- VariantName,
402
- } from "@charstudios/pallet/server";
403
- ```
404
-
405
- ### Entry points
406
-
407
- | Import path | Contents | Environment |
408
- | -------------------------------- | ---------------------------------------------------- | ---------------------- |
409
- | `@charstudios/pallet` | `ThemeProvider`, hooks (+ re-exports of the below) | Client components |
410
- | `@charstudios/pallet/server` | `themeToCss`, `resolveTheme`, `deepMerge`, types, … | Server or client |
411
- | `@charstudios/pallet/presets` | `flat`, `launch`, `presets`, `getPreset` | Server or client |
412
- | `@charstudios/pallet/styles` | All variant CSS | CSS `@import` |
413
-
414
- ---
415
-
416
- ## Publishing
417
-
418
- Publishes to npm as `@charstudios/pallet` via [trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC) from GitHub Actions — no long-lived npm tokens.
419
-
420
- ### One-time setup on npmjs.com
421
-
422
- 1. Publish the package once (or create it on npm) so package settings exist.
423
- 2. Open **[@charstudios/pallet](https://www.npmjs.com/package/@charstudios/pallet) → Settings → Trusted Publisher**.
424
- 3. Choose **GitHub Actions** and set:
425
- - **Organization or user:** `Char-Studios`
426
- - **Repository:** `Pallet`
427
- - **Workflow filename:** `publish.yml` (filename only, not the path)
428
- - **Allowed actions:** `npm publish`
429
- 4. Optional hardening: **Settings → Publishing access → Require two-factor authentication and disallow tokens**.
430
-
431
- ### Release
432
-
433
- ```bash
434
- npm version patch # or minor | major creates a commit + tag
435
- git push && git push --tags
436
- ```
437
-
438
- Pushing a `v*` tag runs [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which authenticates with OIDC and runs `npm publish`.
439
-
440
- Notes:
441
-
442
- - Requires Node **22.14+** / npm **11.5.1+** in CI (the workflow uses Node 24).
443
- - `publishConfig.access: "public"` publishes the scoped package publicly on a free account.
444
- - Only `dist/` is published (see `.npmignore` / `files`).
445
- - Provenance attestations need a **public** GitHub repo; this repo is currently private.
446
-
447
- ### Local development / linking
448
-
449
- To use it in another project before publishing:
450
-
451
- ```bash
452
- # in the pallet folder
453
- npm run build
454
- npm link
455
- # in the consuming project
456
- npm link @charstudios/pallet
457
- ```
458
-
459
- Or reference it directly in the consumer's `package.json`:
460
-
461
- ```json
462
- { "dependencies": { "@charstudios/pallet": "file:../pallet" } }
463
- ```
464
-
465
- ---
466
-
467
- ## FAQ
468
-
469
- **Does this modify my shadcn components?** No. It only injects CSS variables and adds a `data-pallet-variant` attribute; the variant stylesheets restyle via `data-slot` selectors.
470
-
471
- **Does it replace `next-themes`?** It can. Pallet manages the `.dark` class and light/dark/system itself. If you already use `next-themes`, use `scope="self"` or let one own the `.dark` class to avoid conflicts.
472
-
473
- **Tailwind v3 or v4?** Both work — Pallet is Tailwind-version agnostic; it only reads/writes CSS variables and ships plain CSS.
474
-
475
- **Can users export/share a theme?** Yes. Use `serializeTheme(theme)` / `deserializeTheme(json)` from `@charstudios/pallet/server`, and feed a saved override back in via `defaultOverride` or `updateTheme`.
476
-
477
- ---
478
-
479
- MIT © Char Studios
1
+ # @charstudios/pallet
2
+
3
+ A runtime, hook-driven theming layer for **shadcn/ui** apps. Pallet gives you and your users advanced, live control over **colors, fonts, roundness, elevation, spacing, and motion**, plus swappable visual **variants** ("skins") — all through a small React API that works with your existing, unmodified shadcn components.
4
+
5
+ Built for reuse across every Char Studios product: install it once, wrap your app, and every project shares the same theming system.
6
+
7
+ - **Runtime theming** — change any token live; no rebuild, no CSS regeneration.
8
+ - **Variants/skins** — ship `flat` (crisp), `toon` (cel-shaded thick lips), and `frosted` (quiet glass), covering the full shadcn `data-slot` surface set — or define your own.
9
+ - **Full token control** — colors (light + dark, OKLCH), typography (with Google Fonts auto-loading), radius scale, shadow ramp, density, and motion.
10
+ - **Presets** — start from a named preset and layer overrides on top; `reset()` returns to baseline.
11
+ - **SSR-safe** — inline the initial theme from a Server Component to avoid a flash of the wrong theme.
12
+ - **Persistence** — optionally remember a user's theme in `localStorage`.
13
+
14
+ ---
15
+
16
+ ## Table of contents
17
+
18
+ - [How it works](#how-it-works)
19
+ - [Installation](#installation)
20
+ - [Quick start](#quick-start)
21
+ - [Presets](#presets)
22
+ - [Hooks API](#hooks-api)
23
+ - [Building a theme editor](#building-a-theme-editor)
24
+ - [Variants (skins)](#variants-skins)
25
+ - [Server-side rendering (no flash)](#server-side-rendering-no-flash)
26
+ - [Custom presets and variants](#custom-presets-and-variants)
27
+ - [Token reference](#token-reference)
28
+ - [TypeScript](#typescript)
29
+ - [FAQ](#faq)
30
+
31
+ ---
32
+
33
+ ## How it works
34
+
35
+ shadcn/ui styles everything through CSS custom properties (`--primary`, `--radius`, `--font-sans`, ...). Pallet:
36
+
37
+ 1. Takes a **`ThemeConfig`** (a preset + your overrides).
38
+ 2. **Resolves** it into a flat map of CSS variables for the active color scheme.
39
+ 3. **Injects** those variables onto the document root (or a scoped element) at runtime.
40
+ 4. Sets a **`data-pallet-variant`** attribute so the shipped **variant stylesheets** can restyle surfaces (shadows, borders, pill shapes) that aren't expressible through tokens alone.
41
+
42
+ ```
43
+ ThemeConfig (preset + overrides)
44
+ │ resolveTheme()
45
+
46
+ CSS variables ──inject──▶ :root / scope element
47
+ │ │
48
+ data-pallet-variant="toon"
49
+ ▼ ▼
50
+ your shadcn components ◀── variant stylesheet skins
51
+ ```
52
+
53
+ Your components never change. You only add a provider and (optionally) a stylesheet import.
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ npm install @charstudios/pallet
61
+ # or
62
+ pnpm add @charstudios/pallet
63
+ ```
64
+
65
+ **Peer dependencies:** `react >= 18` and `react-dom >= 18` (works with React 19). Assumes a shadcn/ui + Tailwind setup using CSS variables (the shadcn default).
66
+
67
+ ---
68
+
69
+ ## Quick start
70
+
71
+ ### 1. Import the styles
72
+
73
+ Add this once to your global stylesheet (e.g. `app/globals.css`), after Tailwind:
74
+
75
+ ```css
76
+ @import "tailwindcss";
77
+ @import "@charstudios/pallet/styles";
78
+ ```
79
+
80
+ This registers the base token fallbacks and all built-in variant skins. (You can import individual skins instead — see [Variants](#variants-skins).)
81
+
82
+ ### 2. Wrap your app
83
+
84
+ ```tsx
85
+ // app/layout.tsx (or your root component)
86
+ import { ThemeProvider } from "@charstudios/pallet";
87
+
88
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
89
+ return (
90
+ <html lang="en" suppressHydrationWarning>
91
+ <body>
92
+ <ThemeProvider defaultPreset="toon" defaultScheme="system" storageKey="app-theme">
93
+ {children}
94
+ </ThemeProvider>
95
+ </body>
96
+ </html>
97
+ );
98
+ }
99
+ ```
100
+
101
+ That's it — your shadcn components now render with the `toon` theme, honoring system light/dark, and any user changes persist under `app-theme`.
102
+
103
+ ### 3. Change the theme at runtime
104
+
105
+ ```tsx
106
+ "use client";
107
+ import { useThemeControls, usePreset, useColorScheme } from "@charstudios/pallet";
108
+
109
+ export function ThemeButtons() {
110
+ const { setPrimary, setRadius, reset } = useThemeControls();
111
+ const { applyPreset } = usePreset();
112
+ const { toggle } = useColorScheme();
113
+
114
+ return (
115
+ <div className="flex gap-2">
116
+ <button onClick={() => applyPreset("toon")}>Toon</button>
117
+ <button onClick={() => applyPreset("frosted")}>Frosted</button>
118
+ <button onClick={() => applyPreset("flat")}>Flat</button>
119
+ <button onClick={() => setPrimary("oklch(0.7 0.15 300)")}>Purple</button>
120
+ <button onClick={() => setRadius("0.25rem")}>Sharp corners</button>
121
+ <button onClick={toggle}>Toggle dark</button>
122
+ <button onClick={reset}>Reset</button>
123
+ </div>
124
+ );
125
+ }
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Presets
131
+
132
+ Each built-in preset pairs a token palette with a matching **variant skin**. The previews below use the **same component gallery** (buttons, inputs, badges, tabs, alerts, select, card, avatar) so you can compare Flat, Toon, and Frosted side by side.
133
+
134
+ ### `flat`
135
+
136
+ Crisp hairline borders, minimal shadows, tighter radius — the default shadcn feel.
137
+
138
+ <p>
139
+ <img src="./docs/skins/flat.png" alt="Flat skin preview" width="720" />
140
+ </p>
141
+
142
+ ### `toon`
143
+
144
+ Cel-shaded / neo-brutalist: moderate radius, **no soft shadows**, and a thick bottom border whose color is auto-derived from each surface’s fill (`color-mix` toward black via `--toon-fill`).
145
+
146
+ <p>
147
+ <img src="./docs/skins/toon.png" alt="Toon skin preview" width="720" />
148
+ </p>
149
+
150
+ ### `frosted`
151
+
152
+ Quiet glass: light backdrop blur, hairline luminous borders, translucent fills — restrained, not heavy.
153
+
154
+ <p>
155
+ <img src="./docs/skins/frosted.png" alt="Frosted skin preview" width="720" />
156
+ </p>
157
+
158
+ ```tsx
159
+ import { presets, flat, toon, frosted, getPreset } from "@charstudios/pallet/presets";
160
+
161
+ presets.flat; // ThemeConfig
162
+ getPreset("toon"); // ThemeConfig | undefined
163
+ getPreset("frosted"); // ThemeConfig | undefined
164
+ ```
165
+
166
+ Set the starting preset via `defaultPreset` on the provider, or switch live with `usePreset().applyPreset(...)`.
167
+
168
+ ---
169
+
170
+ ## Hooks API
171
+
172
+ All hooks must be used within a `<ThemeProvider>`.
173
+
174
+ ### `useTheme()`
175
+
176
+ Returns the full context.
177
+
178
+ - `theme: ThemeConfig` the resolved theme (preset + overrides).
179
+ - `basePreset: ThemeConfig` — the active preset before overrides.
180
+ - `overrides: ThemeOverride` — the current overrides.
181
+ - `scheme: "light" | "dark"` the actual scheme rendered.
182
+ - `schemePreference: "light" | "dark" | "system"` — the user's preference.
183
+ - `vars: Record<string, string>` — resolved CSS variables for the current scheme.
184
+ - `setTheme(theme)` replace the whole theme (clears overrides).
185
+ - `applyPreset(preset)` apply a preset by name or object (clears overrides).
186
+ - `updateTheme(override)` — deep-merge a partial override.
187
+ - `resetOverrides()` — clear overrides.
188
+ - `setSchemePreference(pref)` — set light/dark/system.
189
+
190
+ ### `useThemeControls()`
191
+
192
+ Granular setters for building an editor UI. Color setters default to updating **both** light and dark; pass a target (`"light" | "dark" | "both"`) to scope them.
193
+
194
+ - `setColor(role, value, target?)`
195
+ - `setPrimary(value, target?)`
196
+ - `setAccent(value, target?)`
197
+ - `setColors(partialColors, target?)`
198
+ - `setRadius(base)` e.g. `"0.5rem"`, `"1.25rem"`
199
+ - `setRadiusScale(partialScale)`
200
+ - `setFont("sans" | "heading" | "mono", family)`
201
+ - `setGoogleFonts(families)` — auto-loads them at runtime
202
+ - `setVariant(name)` — switch the skin
203
+ - `setElevationLevel("none" | "xs" | "sm" | "md" | "lg" | "xl")`
204
+ - `setShadow(key, value)`
205
+ - `setBorderStrength(0..1)`
206
+ - `setDensity({ spacing?, scale? })`
207
+ - `setMotion({ duration?, easing?, intensity? })`
208
+ - `update(override)` — escape hatch
209
+ - `reset()` — clear overrides
210
+
211
+ ### `usePreset()`
212
+
213
+ - `preset: ThemeConfig` the active base preset.
214
+ - `builtinNames: string[]` — names of built-in presets.
215
+ - `applyPreset(preset)` — switch presets.
216
+
217
+ ### `useColorScheme()`
218
+
219
+ - `scheme: "light" | "dark"` — the resolved scheme.
220
+ - `preference: "light" | "dark" | "system"`.
221
+ - `setPreference(pref)`.
222
+ - `toggle()` flip light/dark.
223
+
224
+ ---
225
+
226
+ ## Building a theme editor
227
+
228
+ Everything you need to build a settings panel is exposed as hooks. A minimal example:
229
+
230
+ ```tsx
231
+ "use client";
232
+ import { useTheme, useThemeControls } from "@charstudios/pallet";
233
+
234
+ export function ThemeEditor() {
235
+ const { theme, scheme } = useTheme();
236
+ const c = useThemeControls();
237
+
238
+ return (
239
+ <div className="space-y-4">
240
+ <label>
241
+ Primary
242
+ <input
243
+ type="color"
244
+ onChange={(e) => c.setPrimary(e.target.value)}
245
+ />
246
+ </label>
247
+
248
+ <label>
249
+ Roundness: {theme.radius.base}
250
+ <input
251
+ type="range"
252
+ min={0}
253
+ max={2}
254
+ step={0.05}
255
+ onChange={(e) => c.setRadius(`${e.target.value}rem`)}
256
+ />
257
+ </label>
258
+
259
+ <label>
260
+ Heading font
261
+ <select onChange={(e) => { c.setFont("heading", e.target.value); c.setGoogleFonts([e.target.value]); }}>
262
+ <option value="Inter">Inter</option>
263
+ <option value="Plus Jakarta Sans">Plus Jakarta Sans</option>
264
+ <option value="Outfit">Outfit</option>
265
+ </select>
266
+ </label>
267
+
268
+ <label>
269
+ Variant
270
+ <select onChange={(e) => c.setVariant(e.target.value)}>
271
+ <option value="flat">Flat</option>
272
+ <option value="toon">Toon</option>
273
+ <option value="frosted">Frosted</option>
274
+ </select>
275
+ </label>
276
+
277
+ <button onClick={c.reset}>Reset</button>
278
+ </div>
279
+ );
280
+ }
281
+ ```
282
+
283
+ > Tip: `<input type="color">` emits hex; that works fine as a CSS color. Prefer OKLCH strings for perceptual consistency with the presets when you can.
284
+
285
+ ---
286
+
287
+ ## Variants (skins)
288
+
289
+ A variant is a named "skin" applied via the `data-pallet-variant` attribute (set automatically by the provider). The shipped stylesheets restyle shadcn surfaces that tokens can't reach on their own — shadows, border treatment, pill shaping, and (for `frosted`) backdrop-blur glass.
290
+
291
+ Each built-in skin targets the full common shadcn `data-slot` set: controls, cards/alerts/tables/sidebar, overlays (dialogs, sheets, menus, popovers, tooltips), and chrome (badges, kbd, breadcrumbs, pagination, etc.).
292
+
293
+ Import all skins:
294
+
295
+ ```css
296
+ @import "@charstudios/pallet/styles";
297
+ ```
298
+
299
+ Or cherry-pick to reduce CSS:
300
+
301
+ ```css
302
+ @import "@charstudios/pallet/styles/base";
303
+ @import "@charstudios/pallet/styles/frosted";
304
+ ```
305
+
306
+ Switch at runtime:
307
+
308
+ ```tsx
309
+ useThemeControls().setVariant("frosted");
310
+ ```
311
+
312
+ The variant stylesheets are wrapped in the `pallet-variants` cascade layer, so your own app styles always take precedence.
313
+
314
+ ---
315
+
316
+ ## Server-side rendering (no flash)
317
+
318
+ For app-wide theming Pallet writes variables to `<html>` on mount. To avoid a brief flash of the wrong theme on first paint, inline the theme's CSS from a Server Component using the server-safe entry:
319
+
320
+ ```tsx
321
+ // app/layout.tsx (Server Component)
322
+ import { themeToCss } from "@charstudios/pallet/server";
323
+ import { toon } from "@charstudios/pallet/presets";
324
+
325
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
326
+ return (
327
+ <html lang="en" suppressHydrationWarning>
328
+ <head>
329
+ <style
330
+ id="pallet-ssr"
331
+ dangerouslySetInnerHTML={{ __html: themeToCss(toon) }}
332
+ />
333
+ </head>
334
+ <body>{/* <ThemeProvider>…</ThemeProvider> */}</body>
335
+ </html>
336
+ );
337
+ }
338
+ ```
339
+
340
+ `themeToCss` emits structural tokens + light colors on `:root` and dark colors on `.dark`. The provider then takes over for any live/persisted changes.
341
+
342
+ **Scoped theming / previews:** use `scope="self"` on the provider to apply the theme to a wrapper `<div>` instead of the document root. This also emits SSR inline styles automatically — ideal for rendering a live preview of a different theme inside a settings page.
343
+
344
+ ```tsx
345
+ <ThemeProvider defaultPreset="toon" scope="self">
346
+ <PreviewSurface />
347
+ </ThemeProvider>
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Custom presets and variants
353
+
354
+ ### Custom preset
355
+
356
+ A preset is just a `ThemeConfig`. Start from a built-in and tweak:
357
+
358
+ ```tsx
359
+ import { toon } from "@charstudios/pallet/presets";
360
+ import { deepMerge } from "@charstudios/pallet/server";
361
+ import type { ThemeConfig } from "@charstudios/pallet/server";
362
+
363
+ export const brand: ThemeConfig = deepMerge(toon, {
364
+ name: "brand",
365
+ variant: "toon",
366
+ colors: { light: { primary: "oklch(0.62 0.2 15)" }, dark: { primary: "oklch(0.6 0.2 15)" } },
367
+ radius: { base: "0.75rem" },
368
+ });
369
+ ```
370
+
371
+ Register it on the provider so `applyPreset("brand")` works:
372
+
373
+ ```tsx
374
+ <ThemeProvider defaultPreset={brand} presets={{ brand }}>
375
+ ```
376
+
377
+ ### Custom variant (skin)
378
+
379
+ 1. Pick a name and set it: `setVariant("neo")` (or `variant: "neo"` in a preset).
380
+ 2. Add a stylesheet that targets `[data-pallet-variant="neo"] [data-slot="..."]`:
381
+
382
+ ```css
383
+ @layer pallet-variants {
384
+ [data-pallet-variant="neo"] [data-slot="card"] {
385
+ border-radius: var(--radius-lg);
386
+ box-shadow: var(--shadow-lg);
387
+ }
388
+ [data-pallet-variant="neo"] [data-slot="button"] {
389
+ border-radius: 0; /* brutalist square buttons */
390
+ }
391
+ }
392
+ ```
393
+
394
+ shadcn components expose `data-slot` on every part (`card`, `button`, `input`, `select-trigger`, `popover`, `dropdown-menu-content`, `dialog-content`, `sheet-content`, `badge`, ...), which is what the skins hook into.
395
+
396
+ ---
397
+
398
+ ## Token reference
399
+
400
+ Resolved CSS variables written by Pallet:
401
+
402
+ - **Colors** (per scheme): `--background`, `--foreground`, `--card(-foreground)`, `--popover(-foreground)`, `--primary(-foreground)`, `--secondary(-foreground)`, `--muted(-foreground)`, `--accent(-foreground)`, `--destructive(-foreground)`, `--border`, `--input`, `--ring`, `--chart-1..5`, `--sidebar*`.
403
+ - **Radius**: `--radius` plus the derived scale `--radius-sm | md | lg | xl | 2xl | 3xl | 4xl`.
404
+ - **Typography**: `--font-sans`, `--font-heading`, `--font-mono`.
405
+ - **Elevation**: `--shadow-xs | sm | md | lg | xl`, `--elevation`, `--surface-border-strength` (and the helper `--pallet-surface-border`).
406
+ - **Density**: `--spacing`, `--density`.
407
+ - **Motion**: `--motion-duration`, `--motion-ease`, `--motion-intensity`.
408
+
409
+ ---
410
+
411
+ ## TypeScript
412
+
413
+ Fully typed. Import types from the server entry (safe everywhere):
414
+
415
+ ```ts
416
+ import type {
417
+ ThemeConfig,
418
+ ThemeOverride,
419
+ ColorTokens,
420
+ Typography,
421
+ RadiusConfig,
422
+ ElevationConfig,
423
+ Density,
424
+ Motion,
425
+ ColorScheme,
426
+ VariantName,
427
+ } from "@charstudios/pallet/server";
428
+ ```
429
+
430
+ ### Entry points
431
+
432
+ | Import path | Contents | Environment |
433
+ | -------------------------------- | ---------------------------------------------------- | ---------------------- |
434
+ | `@charstudios/pallet` | `ThemeProvider`, hooks (+ re-exports of the below) | Client components |
435
+ | `@charstudios/pallet/server` | `themeToCss`, `resolveTheme`, `deepMerge`, types, … | Server or client |
436
+ | `@charstudios/pallet/presets` | `flat`, `toon`, `frosted`, `presets`, `getPreset` | Server or client |
437
+ | `@charstudios/pallet/styles` | All variant CSS | CSS `@import` |
438
+ | `@charstudios/pallet/styles/frosted` | Frosted glass skin only | CSS `@import` |
439
+
440
+ ---
441
+
442
+ ## FAQ
443
+
444
+ **Does this modify my shadcn components?** No. It only injects CSS variables and adds a `data-pallet-variant` attribute; the variant stylesheets restyle via `data-slot` selectors across the common shadcn surface set. Components need modern shadcn `data-slot` attributes for the skins to attach.
445
+
446
+ **Does it replace `next-themes`?** It can. Pallet manages the `.dark` class and light/dark/system itself. If you already use `next-themes`, use `scope="self"` or let one own the `.dark` class to avoid conflicts.
447
+
448
+ **Tailwind v3 or v4?** Both work — Pallet is Tailwind-version agnostic; it only reads/writes CSS variables and ships plain CSS.
449
+
450
+ **Can users export/share a theme?** Yes. Use `serializeTheme(theme)` / `deserializeTheme(json)` from `@charstudios/pallet/server`, and feed a saved override back in via `defaultOverride` or `updateTheme`.
451
+
452
+ ---
453
+
454
+ MIT © Char Studios