@soybeanjs/shadcn-theme 0.0.11 → 0.1.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.en_US.md CHANGED
@@ -1,19 +1,18 @@
1
1
  # @soybeanjs/shadcn-theme
2
2
 
3
- A powerful and flexible shadcn/ui theme generator with support for dynamic CSS variable injection, preset color schemes, and light/dark mode switching.
3
+ A powerful and flexible shadcn/ui theme CSS variables generator with preset color schemes, light/dark output, and optional custom preset extension.
4
4
 
5
5
  [中文 README](README.md)
6
6
 
7
7
  ## ✨ Features
8
8
 
9
- - 🎨 **Rich Preset Themes** - Multiple base palettes, theme colors, and feedback color presets
10
- - 🌗 **Light/Dark Mode Support** - Built-in dark mode with automatic dark variant generation
11
- - 🎯 **Flexible Color Schemes** - Support for both HSL and OKLCH color formats
12
- - 🔧 **Highly Customizable** - Full control over custom theme color configurations
13
- - 📦 **Zero Runtime Dependencies** - Only depends on `@soybeanjs/colord` for color processing
14
- - 🚀 **Plug and Play** - Automatically injects CSS variables into the DOM
15
- - 🎭 **Extended Palettes** - Theme customization support for sidebars, charts, and more
16
- - 🌈 **Color Palette Generation** - Automatically generates gradient palettes (50-950) for primary colors
9
+ - 🎨 **Rich Preset Themes** - Built-in base / primary / feedback preset combinations
10
+ - 🌗 **Light/Dark Output** - Supports `.dark` / `@media (prefers-color-scheme: dark)` / custom selector
11
+ - 🎯 **Flexible Color Formats** - Supports `hsl` and `oklch` output
12
+ - 🔧 **Extensible** - Add custom colors via `preset` (including sidebar / chart fields)
13
+ - 🌈 **Palette Variables** - Generates 50-950 palette vars for primary/destructive/success/warning/info/carbon
14
+ - 📦 **Lightweight Dependency** - Only depends on `@soybeanjs/colord`
15
+ - 🧩 **Pure Generator** - Returns CSS strings only; does not automatically mutate the DOM
17
16
 
18
17
  ## 📦 Installation
19
18
 
@@ -28,39 +27,225 @@ pnpm add @soybeanjs/shadcn-theme
28
27
  ```typescript
29
28
  import { createShadcnTheme } from '@soybeanjs/shadcn-theme';
30
29
 
31
- // Use default presets (slate + indigo + classic)
32
- createShadcnTheme();
30
+ // Use default presets (gray + indigo + classic + extended)
31
+ const theme = createShadcnTheme();
32
+ const css = theme.getCss();
33
33
 
34
34
  // Custom preset combination
35
- createShadcnTheme({
36
- presets: {
37
- base: 'zinc', // Base palette: stone | zinc | neutral | gray | slate
38
- theme: 'blue', // Theme color: any Tailwind palette name
39
- feedback: 'vivid' // Feedback color style: classic | vivid | subtle | warm | cool, etc.
40
- },
41
- radius: '0.5rem', // Border radius size
42
- darkSelector: 'class', // Dark mode selector: 'class' | 'media' | custom
43
- format: 'hsl' // Color format: 'hsl' | 'oklch'
35
+ const custom = createShadcnTheme({
36
+ base: 'zinc',
37
+ primary: 'blue',
38
+ feedback: 'vivid',
39
+ sidebar: 'extended',
40
+ radius: '0.5rem',
41
+ darkSelector: 'class',
42
+ format: 'hsl'
43
+ });
44
+
45
+ const customCss = custom.getCss();
46
+ ```
47
+
48
+ ### (Optional) Inject into the DOM
49
+
50
+ This library generates CSS strings. If you want runtime theme switching, you can inject the result yourself:
51
+
52
+ ```ts
53
+ import { createShadcnTheme } from '@soybeanjs/shadcn-theme';
54
+
55
+ const theme = createShadcnTheme({ primary: 'indigo' });
56
+
57
+ function applyTheme(cssText: string, styleId = 'SHADCN_THEME_STYLE') {
58
+ const el = document.getElementById(styleId) ?? document.createElement('style');
59
+ el.id = styleId;
60
+ el.textContent = cssText;
61
+ document.head.appendChild(el);
62
+ }
63
+
64
+ applyTheme(theme.getCss());
65
+ applyTheme(theme.getCss({ primary: 'emerald' }));
66
+ ```
67
+
68
+ ### Custom Preset
69
+
70
+ `preset` lets you add or override named preset entries on top of the built-in presets. You can extend only one group (e.g. add a custom `primary` preset), or extend `base / primary / feedback / sidebar` together.
71
+
72
+ #### 1) Structure and how to reference a preset
73
+
74
+ - `preset` is an object that may contain `base / primary / feedback / sidebar`. Each group is `{ [key: string]: PresetItem }`.
75
+ - To use your custom preset, set the corresponding `base` / `primary` / `feedback` / `sidebar` option to the key you defined.
76
+ - Example: if you define `preset.primary.brandPrimary`, then pass `primary: 'brandPrimary'`.
77
+
78
+ #### 2) Merge/override rules
79
+
80
+ The library shallow-merges your `preset` into the built-ins:
81
+
82
+ - Same key is overridden by your `preset` (e.g. defining `primary.indigo` overrides built-in `indigo`).
83
+ - New keys are added as extra presets (e.g. `primary.brandPrimary`).
84
+
85
+ #### 3) Two sidebar modes
86
+
87
+ - `sidebar: 'extended'` (default): ignores `preset.sidebar` and derives sidebar colors from base/primary.
88
+ - `sidebar: '<yourKey>'`: uses `preset.sidebar[<yourKey>]` as the sidebar colors.
89
+
90
+ #### 4) Color values and `format`
91
+
92
+ - Each color value supports: Tailwind palette reference (e.g. `slate.500`), `hsl(...)`, or `oklch(...)`.
93
+ - `format: 'hsl'`: outputs `h s l [/ alpha]` (no outer `hsl(...)`); `oklch(...)` inputs are converted to HSL.
94
+ - `format: 'oklch'`: outputs values with the outer `oklch(...)`; `hsl(...)` inputs are converted to OKLCH.
95
+
96
+ #### Quick example: add a custom primary preset (good starting point)
97
+
98
+ `primary` presets only need `primary / ring / chart1~chart5`:
99
+
100
+ ```ts
101
+ const theme = createShadcnTheme({
102
+ primary: 'brandPrimary',
103
+ preset: {
104
+ primary: {
105
+ brandPrimary: {
106
+ light: {
107
+ primary: 'blue.600',
108
+ ring: 'blue.400',
109
+ chart1: 'orange.600',
110
+ chart2: 'teal.600',
111
+ chart3: 'cyan.900',
112
+ chart4: 'amber.400',
113
+ chart5: 'amber.500'
114
+ },
115
+ dark: {
116
+ primary: 'blue.400',
117
+ ring: 'blue.500',
118
+ chart1: 'orange.500',
119
+ chart2: 'teal.500',
120
+ chart3: 'cyan.400',
121
+ chart4: 'amber.500',
122
+ chart5: 'amber.600'
123
+ }
124
+ }
125
+ }
126
+ }
127
+ });
128
+
129
+ const css = theme.getCss();
130
+ ```
131
+
132
+ #### Quick example: add a custom feedback preset
133
+
134
+ ```ts
135
+ const theme = createShadcnTheme({
136
+ feedback: 'brandFeedback',
137
+ preset: {
138
+ feedback: {
139
+ brandFeedback: {
140
+ light: {
141
+ destructive: 'red.500',
142
+ success: 'emerald.500',
143
+ warning: 'amber.500',
144
+ info: 'sky.500'
145
+ },
146
+ dark: {
147
+ destructive: 'red.400',
148
+ success: 'emerald.400',
149
+ warning: 'amber.400',
150
+ info: 'sky.400'
151
+ }
152
+ }
153
+ }
154
+ }
155
+ });
156
+ ```
157
+
158
+ #### Quick example: custom sidebar (non-extended)
159
+
160
+ ```ts
161
+ const theme = createShadcnTheme({
162
+ sidebar: 'brandSidebar',
163
+ preset: {
164
+ sidebar: {
165
+ brandSidebar: {
166
+ light: {
167
+ sidebar: 'slate.50',
168
+ sidebarForeground: 'slate.900',
169
+ sidebarPrimary: 'blue.600',
170
+ sidebarPrimaryForeground: 'slate.50',
171
+ sidebarAccent: 'slate.100',
172
+ sidebarAccentForeground: 'slate.900',
173
+ sidebarBorder: 'slate.200',
174
+ sidebarRing: 'blue.400'
175
+ },
176
+ dark: {
177
+ sidebar: 'slate.950',
178
+ sidebarForeground: 'slate.50',
179
+ sidebarPrimary: 'blue.400',
180
+ sidebarPrimaryForeground: 'slate.950',
181
+ sidebarAccent: 'slate.900',
182
+ sidebarAccentForeground: 'slate.50',
183
+ sidebarBorder: 'slate.800',
184
+ sidebarRing: 'blue.500'
185
+ }
186
+ }
187
+ }
188
+ }
44
189
  });
45
190
  ```
46
191
 
47
- ### Custom Theme Colors
192
+ #### Full example (custom base + primary + feedback)
48
193
 
49
194
  ```typescript
50
195
  createShadcnTheme({
51
- theme: {
52
- light: {
53
- background: 'oklch(100% 0 0)',
54
- foreground: 'oklch(20% 0 0)',
55
- primary: 'oklch(50% 0.2 250)',
56
- primaryForeground: 'oklch(100% 0 0)',
57
- // ... more color configurations
58
- },
59
- dark: {
60
- // Optional, will automatically generate dark variants if not provided
61
- background: 'oklch(20% 0 0)',
62
- foreground: 'oklch(100% 0 0)',
63
- // ...
196
+ base: 'demoBase',
197
+ primary: 'demoPrimary',
198
+ feedback: 'demoFeedback',
199
+ preset: {
200
+ base: {
201
+ demoBase: {
202
+ light: {
203
+ background: 'oklch(100% 0 0)',
204
+ foreground: 'stone.950',
205
+ card: 'oklch(100% 0 0)',
206
+ cardForeground: 'stone.950',
207
+ popover: 'oklch(100% 0 0)',
208
+ popoverForeground: 'stone.950',
209
+ primaryForeground: 'stone.50',
210
+ secondary: 'stone.100',
211
+ secondaryForeground: 'stone.900',
212
+ muted: 'stone.100',
213
+ mutedForeground: 'stone.500',
214
+ accent: 'stone.100',
215
+ accentForeground: 'stone.900',
216
+ destructiveForeground: 'stone.50',
217
+ successForeground: 'stone.50',
218
+ warningForeground: 'stone.50',
219
+ infoForeground: 'stone.50',
220
+ carbon: 'stone.800',
221
+ carbonForeground: 'stone.50',
222
+ border: 'stone.200',
223
+ input: 'stone.200'
224
+ },
225
+ dark: {
226
+ background: 'stone.950',
227
+ foreground: 'stone.50',
228
+ card: 'stone.900',
229
+ cardForeground: 'stone.50',
230
+ popover: 'stone.900',
231
+ popoverForeground: 'stone.50',
232
+ primaryForeground: 'stone.900',
233
+ secondary: 'stone.800',
234
+ secondaryForeground: 'stone.50',
235
+ muted: 'stone.800',
236
+ mutedForeground: 'stone.400',
237
+ accent: 'stone.800',
238
+ accentForeground: 'stone.50',
239
+ destructiveForeground: 'stone.900',
240
+ successForeground: 'stone.900',
241
+ warningForeground: 'stone.900',
242
+ infoForeground: 'stone.900',
243
+ carbon: 'stone.100',
244
+ carbonForeground: 'stone.900',
245
+ border: 'oklch(100% 0 0 / 0.1)',
246
+ input: 'oklch(100% 0 0 / 0.15)'
247
+ }
248
+ }
64
249
  }
65
250
  }
66
251
  });
@@ -70,27 +255,40 @@ createShadcnTheme({
70
255
 
71
256
  ### `createShadcnTheme(options?: ThemeOptions)`
72
257
 
73
- Main function to create and apply themes.
258
+ Main function to create a theme CSS generator.
259
+
260
+ Return value:
261
+
262
+ ```ts
263
+ const theme = createShadcnTheme();
264
+
265
+ theme.getCss(config?: PresetConfig, radius?: string): string
266
+ theme.getColorCss(config: PresetConfig): string
267
+ theme.getRadiusCss(radius?: string): string
268
+ ```
74
269
 
75
270
  #### ThemeOptions
76
271
 
77
272
  | Parameter | Type | Default | Description |
78
273
  |------|------|--------|------|
79
- | `presets` | `PresetConfig` | - | Preset configuration, takes priority over `theme` |
80
- | `theme` | `ThemeConfig` | - | Custom theme color configuration |
81
- | `radius` | `string` | `'0.625rem'` | Global border radius size |
82
- | `styleId` | `string` | `'SHADCN_THEME_STYLES'` | ID of the injected style tag |
83
- | `styleTarget` | `'html' \| ':root'` | `':root'` | CSS variable mount target |
84
- | `darkSelector` | `string` | `'class'` | Dark mode selector |
85
- | `format` | `'hsl' \| 'oklch'` | `'hsl'` | Color output format |
274
+ | `base` | `BuiltinBasePresetKey \| string` | `'gray'` | Base preset key |
275
+ | `primary` | `BuiltinPrimaryPresetKey \| string` | `'indigo'` | Primary preset key |
276
+ | `feedback` | `BuiltinFeedbackPresetKey \| string` | `'classic'` | Feedback preset key |
277
+ | `sidebar` | `'extended' \| string` | `'extended'` | Sidebar mode; `extended` derives from base/primary |
278
+ | `preset` | `CustomPreset` | - | Custom preset collection (extends base/primary/feedback/sidebar) |
279
+ | `radius` | `string` | `'0.625rem'` | Global border radius |
280
+ | `styleTarget` | `'html' \| ':root'` | `':root'` | CSS variables mount selector |
281
+ | `darkSelector` | `'class' \| 'media' \| string` | `'class'` | Dark mode selector (custom string supported) |
282
+ | `format` | `'hsl' \| 'oklch'` | `'hsl'` | Output color format |
86
283
 
87
284
  ### Preset Configuration (PresetConfig)
88
285
 
89
286
  ```typescript
90
287
  interface PresetConfig {
91
- base?: 'stone' | 'zinc' | 'neutral' | 'gray' | 'slate'; // Default: 'slate'
92
- theme?: TailwindPaletteKey; // Any Tailwind palette, default: 'indigo'
93
- feedback?: FeedbackPaletteKey; // Feedback color style, default: 'classic'
288
+ base?: string;
289
+ primary?: string;
290
+ feedback?: string;
291
+ sidebar?: 'extended' | string;
94
292
  }
95
293
  ```
96
294
 
@@ -186,78 +384,75 @@ Supports three color value formats:
186
384
  ### Example 1: Classic Blue Theme
187
385
 
188
386
  ```typescript
189
- createShadcnTheme({
190
- presets: {
191
- base: 'slate',
192
- theme: 'blue',
193
- feedback: 'classic'
194
- },
387
+ const theme = createShadcnTheme({
388
+ base: 'slate',
389
+ primary: 'blue',
390
+ feedback: 'classic',
195
391
  radius: '0.5rem',
196
392
  darkSelector: 'class'
197
393
  });
394
+
395
+ const css = theme.getCss();
198
396
  ```
199
397
 
200
398
  ### Example 2: Modern Purple Theme
201
399
 
202
400
  ```typescript
203
- createShadcnTheme({
204
- presets: {
205
- base: 'zinc',
206
- theme: 'violet',
207
- feedback: 'modern'
208
- },
401
+ const theme = createShadcnTheme({
402
+ base: 'zinc',
403
+ primary: 'violet',
404
+ feedback: 'modern',
209
405
  radius: '0.75rem',
210
406
  darkSelector: 'class',
211
407
  format: 'oklch'
212
408
  });
409
+
410
+ const css = theme.getCss();
213
411
  ```
214
412
 
215
- ### Example 3: Custom Brand Colors
413
+ ### Example 3: Override Per Generation
216
414
 
217
415
  ```typescript
218
- createShadcnTheme({
219
- theme: {
220
- light: {
221
- background: 'oklch(100% 0 0)',
222
- foreground: 'oklch(20% 0 0)',
223
- primary: 'oklch(50% 0.25 280)', // Custom brand purple
224
- primaryForeground: 'oklch(100% 0 0)',
225
- secondary: 'oklch(95% 0.01 280)',
226
- secondaryForeground: 'oklch(30% 0 0)',
227
- // ... other colors
228
- }
229
- // dark is optional, will be auto-generated if not provided
230
- }
231
- });
416
+ const theme = createShadcnTheme({ base: 'slate', primary: 'indigo', feedback: 'classic' });
417
+
418
+ const css1 = theme.getCss();
419
+ const css2 = theme.getCss({ primary: 'emerald', feedback: 'vivid' });
232
420
  ```
233
421
 
234
422
  ### Example 4: Media Query Dark Mode
235
423
 
236
424
  ```typescript
237
- createShadcnTheme({
238
- presets: {
239
- base: 'slate',
240
- theme: 'indigo'
241
- },
425
+ const theme = createShadcnTheme({
426
+ base: 'slate',
427
+ primary: 'indigo',
242
428
  darkSelector: 'media' // Use system preference
243
429
  });
430
+
431
+ const css = theme.getCss();
244
432
  ```
245
433
 
246
434
  ### Example 5: Custom Dark Mode Selector
247
435
 
248
436
  ```typescript
249
- createShadcnTheme({
250
- presets: {
251
- base: 'slate',
252
- theme: 'emerald'
253
- },
437
+ const theme = createShadcnTheme({
438
+ base: 'slate',
439
+ primary: 'emerald',
254
440
  darkSelector: '[data-theme="dark"]' // Custom selector
255
441
  });
442
+
443
+ const css = theme.getCss();
256
444
  ```
257
445
 
258
446
  ## 🎯 Generated CSS Variables
259
447
 
260
- After calling `createShadcnTheme()`, a `<style>` tag containing the following variables will be automatically injected into `<head>`:
448
+ `getCss()` returns a CSS string containing variables like the following.
449
+
450
+ When `format: 'hsl'`, variable values are `h s l [/ alpha]` (without the outer `hsl(...)` wrapper):
451
+
452
+ Notes:
453
+
454
+ - When `format: 'hsl'` and the color key is `border`, `input`, or `sidebarBorder`, if the value includes opacity (e.g. `hsl(... / 0.1)` or `oklch(... / 0.1)`), the library also emits alpha variables: `--border-alpha`, `--input-alpha`, `--sidebar-border-alpha`. Meanwhile `--border` / `--input` / `--sidebar-border` keep only the `h s l` part (without the `/ alpha`).
455
+ - It generates 11 palette variables (50-950) for `primary`, `destructive`, `success`, `warning`, `info`, and `carbon`: `50/100/200/300/400/500/600/700/800/900/950`.
261
456
 
262
457
  ```css
263
458
  :root {
@@ -265,9 +460,13 @@ After calling `createShadcnTheme()`, a `<style>` tag containing the following va
265
460
  --background: 0 0% 100%;
266
461
  --foreground: 222.2 84% 4.9%;
267
462
  --primary: 221.2 83.2% 53.3%;
463
+
464
+ /* In hsl mode: border/input/sidebarBorder also output alpha vars */
465
+ --border: 214.3 31.8% 91.4%;
466
+ --border-alpha: 0.1;
268
467
  /* ... more variables */
269
468
 
270
- /* Auto-generated palettes */
469
+ /* Auto-generated palettes (11 levels: 50-950) */
271
470
  --primary-50: 239 84% 97%;
272
471
  --primary-100: 237 84% 94%;
273
472
  /* ... primary-200 to primary-950 */
@@ -287,23 +486,36 @@ After calling `createShadcnTheme()`, a `<style>` tag containing the following va
287
486
  }
288
487
  ```
289
488
 
489
+ When `format: 'oklch'`, variable values include the outer `oklch(...)` wrapper:
490
+
491
+ ```css
492
+ :root {
493
+ --background: oklch(100% 0 0);
494
+ --foreground: oklch(20% 0 0);
495
+ --border: oklch(100% 0 0 / 0.1);
496
+ }
497
+ ```
498
+
290
499
  ## 💡 Advanced Usage
291
500
 
292
501
  ### Dynamic Theme Switching
293
502
 
294
503
  ```typescript
295
- // Switch to light theme
296
- createShadcnTheme({
297
- presets: { base: 'slate', theme: 'blue' }
298
- });
504
+ const theme = createShadcnTheme();
505
+
506
+ function apply(cssText: string) {
507
+ const id = 'SHADCN_THEME_STYLE';
508
+ const el = document.getElementById(id) ?? document.createElement('style');
509
+ el.id = id;
510
+ el.textContent = cssText;
511
+ document.head.appendChild(el);
512
+ }
299
513
 
300
- // Runtime switch to dark theme (by toggling class)
301
- document.documentElement.classList.add('dark');
514
+ apply(theme.getCss());
515
+ apply(theme.getCss({ base: 'zinc', primary: 'purple' }));
302
516
 
303
- // Switch to another theme
304
- createShadcnTheme({
305
- presets: { base: 'zinc', theme: 'purple' }
306
- });
517
+ // Dark mode switching is controlled by your darkSelector (e.g. default is adding .dark)
518
+ document.documentElement.classList.add('dark');
307
519
  ```
308
520
 
309
521
  ### Using with Tailwind CSS
@@ -336,6 +548,29 @@ module.exports = {
336
548
  }
337
549
  ```
338
550
 
551
+ When you use `format: 'hsl'`, opacity must be handled separately, especially for `border` / `input` / `sidebarBorder`:
552
+
553
+ - These variables output `h s l` (without `/ alpha`). If opacity is present, the library also emits `--border-alpha` / `--input-alpha` / `--sidebar-border-alpha`.
554
+ - In Tailwind, you can compose them using the slash syntax:
555
+
556
+ ```js
557
+ // Use the generated alpha value
558
+ border: 'hsl(var(--border) / var(--border-alpha))'
559
+ ```
560
+
561
+ If you want Tailwind opacity modifiers to work (e.g. `border-border/50`), use the `<alpha-value>` placeholder (in this case you typically don't use `--border-alpha`):
562
+
563
+ ```js
564
+ // Let Tailwind inject opacity
565
+ border: 'hsl(var(--border) / <alpha-value>)'
566
+ ```
567
+
568
+ If you use `format: 'oklch'`, since the variable value already contains `oklch(...)`, use `var(--xxx)` directly in Tailwind (no extra `oklch(...)` wrapper needed):
569
+
570
+ ```js
571
+ background: 'var(--background)'
572
+ ```
573
+
339
574
  ### Using in CSS
340
575
 
341
576
  ```css