material-theme-builder 3.2.0 → 4.0.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
@@ -48,6 +48,8 @@ theme.toCss();
48
48
  theme.toTailwind();
49
49
  theme.toFlutter();
50
50
  theme.toShadcn();
51
+ theme.toShadcnAliases();
52
+ theme.toShadcnRegistryItem({ fallback: true });
51
53
  ```
52
54
 
53
55
  ## CLI
@@ -95,19 +97,43 @@ import { Mtb } from "material-theme-builder/react";
95
97
  >
96
98
  > Typically wrapping `{children}` in a
97
99
  > [layout](https://nextjs.org/docs/app/getting-started/layouts-and-pages#creating-a-layout).
100
+ >
101
+ > `<Mtb>` renders its `<style>`, so it works both server- and client-side.
102
+ > Client-side is what you want when the theme has to be interactive through
103
+ > `setMtbConfig`.
98
104
 
99
105
  > [!NOTE]
100
106
  >
101
- > CSS varnames are always kebab-cased, e.g. `myCustomColor1` →
102
- > `--md-sys-color-my-custom-color-1` / `--md-ref-palette-my-custom-color-1-<tone>`
107
+ > For a theme that is not interactive / never changes at runtime, skip the
108
+ > component entirely: the root entry holds `builder` alone, so a Server
109
+ > Component can call it and emit `toCss()` into the document itself — no client
110
+ > JS, and no `useMtb`.
111
+ >
112
+ > ```tsx
113
+ > import { builder } from "material-theme-builder";
114
+ >
115
+ > const css = builder("#0e1216", { scheme: "vibrant" }).toCss();
116
+ >
117
+ > export default function RootLayout({
118
+ > children,
119
+ > }: {
120
+ > children: React.ReactNode;
121
+ > }) {
122
+ > return (
123
+ > <html lang="en">
124
+ > <head>
125
+ > <style dangerouslySetInnerHTML={{ __html: css }} />
126
+ > </head>
127
+ > <body>{children}</body>
128
+ > </html>
129
+ > );
130
+ > }
131
+ > ```
103
132
 
104
133
  > [!NOTE]
105
134
  >
106
- > `<Mtb>` injects the CSS from the client, and is the only thing here carrying
107
- > `"use client"`. The root entry holds `builder` alone — so from a
108
- > [React Server Component](https://react.dev/reference/rsc/server-components)
109
- > you can call it and emit `toCss()` into the document yourself, without
110
- > shipping components the page never renders.
135
+ > CSS varnames are always kebab-cased, e.g. `myCustomColor1` →
136
+ > `--md-sys-color-my-custom-color-1` / `--md-ref-palette-my-custom-color-1-<tone>`
111
137
 
112
138
  ## `useMtb`
113
139
 
@@ -127,247 +153,69 @@ return (
127
153
 
128
154
  ## Tailwind
129
155
 
130
- Compatible through [theme variables](https://tailwindcss.com/docs/theme):
156
+ Compatible through [theme variables](https://tailwindcss.com/docs/theme) — a
157
+ stylesheet for the standard tokens, and a plugin for the custom colors:
158
+
159
+ ```css
160
+ @import "tailwindcss";
161
+
162
+ @import "material-theme-builder/tailwind.css";
163
+ @plugin "material-theme-builder/tailwind" {
164
+ custom-colors: myCustomColor1, myCustomColor2;
165
+ }
166
+ ```
167
+
168
+ Drop the `@plugin` line if you have no custom colors.
169
+
170
+ <details>
171
+
172
+ Each name listed brings its four scheme roles and eleven shades —
173
+ `bg-myCustomColor1`, `text-on-myCustomColor1`, `bg-myCustomColor1-container`,
174
+ `bg-myCustomColor1-300`.
175
+
176
+ `prefix` mirrors `builder({ prefix })`:
177
+
178
+ ```css
179
+ @plugin "material-theme-builder/tailwind" {
180
+ prefix: my;
181
+ custom-colors: myCustomColor1;
182
+ }
183
+ ```
184
+
185
+ </details>
186
+
187
+ > [!TIP]
188
+ >
189
+ > Colors are declared as
190
+ > [inlined theme values](https://tailwindcss.com/docs/theme#referencing-other-variables):
191
+ > `bg-primary` compiles to `background-color: var(--md-sys-color-primary)`, with
192
+ > no `--color-primary` in between. That one would sit on `:root`, out of reach
193
+ > of a nested `<Mtb>`.
194
+
195
+ <details>
196
+ <summary>The theme variables the stylesheet declares</summary>
197
+
198
+ Generated from [`toTailwind()`](#programmatic-api), so the two cannot drift:
131
199
 
132
200
  ```css
133
201
  @theme inline {
134
202
  --color-background: var(--md-sys-color-background);
135
- --color-on-background: var(--md-sys-color-on-background);
136
- --color-surface: var(--md-sys-color-surface);
137
- --color-surface-dim: var(--md-sys-color-surface-dim);
138
- --color-surface-bright: var(--md-sys-color-surface-bright);
139
- --color-surface-container-lowest: var(
140
- --md-sys-color-surface-container-lowest
141
- );
142
- --color-surface-container-low: var(--md-sys-color-surface-container-low);
143
- --color-surface-container: var(--md-sys-color-surface-container);
144
- --color-surface-container-high: var(--md-sys-color-surface-container-high);
145
- --color-surface-container-highest: var(
146
- --md-sys-color-surface-container-highest
147
- );
148
- --color-on-surface: var(--md-sys-color-on-surface);
149
- --color-on-surface-variant: var(--md-sys-color-on-surface-variant);
150
- --color-outline: var(--md-sys-color-outline);
151
- --color-outline-variant: var(--md-sys-color-outline-variant);
152
- --color-inverse-surface: var(--md-sys-color-inverse-surface);
203
+ --color-error: var(--md-sys-color-error);
204
+ --color-error-container: var(--md-sys-color-error-container);
153
205
  --color-inverse-on-surface: var(--md-sys-color-inverse-on-surface);
154
- --color-primary: var(--md-sys-color-primary);
155
- --color-on-primary: var(--md-sys-color-on-primary);
156
- --color-primary-container: var(--md-sys-color-primary-container);
157
- --color-on-primary-container: var(--md-sys-color-on-primary-container);
158
- --color-primary-fixed: var(--md-sys-color-primary-fixed);
159
- --color-primary-fixed-dim: var(--md-sys-color-primary-fixed-dim);
160
- --color-on-primary-fixed: var(--md-sys-color-on-primary-fixed);
161
- --color-on-primary-fixed-variant: var(
162
- --md-sys-color-on-primary-fixed-variant
163
- );
164
206
  --color-inverse-primary: var(--md-sys-color-inverse-primary);
165
- --color-secondary: var(--md-sys-color-secondary);
166
- --color-on-secondary: var(--md-sys-color-on-secondary);
167
- --color-secondary-container: var(--md-sys-color-secondary-container);
168
- --color-on-secondary-container: var(--md-sys-color-on-secondary-container);
169
- --color-secondary-fixed: var(--md-sys-color-secondary-fixed);
170
- --color-secondary-fixed-dim: var(--md-sys-color-secondary-fixed-dim);
171
- --color-on-secondary-fixed: var(--md-sys-color-on-secondary-fixed);
172
- --color-on-secondary-fixed-variant: var(
173
- --md-sys-color-on-secondary-fixed-variant
174
- );
175
- --color-tertiary: var(--md-sys-color-tertiary);
176
- --color-on-tertiary: var(--md-sys-color-on-tertiary);
177
- --color-tertiary-container: var(--md-sys-color-tertiary-container);
178
- --color-on-tertiary-container: var(--md-sys-color-on-tertiary-container);
179
- --color-tertiary-fixed: var(--md-sys-color-tertiary-fixed);
180
- --color-tertiary-fixed-dim: var(--md-sys-color-tertiary-fixed-dim);
181
- --color-on-tertiary-fixed: var(--md-sys-color-on-tertiary-fixed);
182
- --color-on-tertiary-fixed-variant: var(
183
- --md-sys-color-on-tertiary-fixed-variant
184
- );
185
- --color-error: var(--md-sys-color-error);
207
+ --color-inverse-surface: var(--md-sys-color-inverse-surface);
208
+ --color-on-background: var(--md-sys-color-on-background);
186
209
  --color-on-error: var(--md-sys-color-on-error);
187
- --color-error-container: var(--md-sys-color-error-container);
188
- --color-on-error-container: var(--md-sys-color-on-error-container);
189
- --color-scrim: var(--md-sys-color-scrim);
190
- --color-shadow: var(--md-sys-color-shadow);
191
-
192
- /* Shades */
193
-
194
- --color-primary-50: var(--md-ref-palette-primary-95);
195
- --color-primary-100: var(--md-ref-palette-primary-90);
196
- --color-primary-200: var(--md-ref-palette-primary-80);
197
- --color-primary-300: var(--md-ref-palette-primary-70);
198
- --color-primary-400: var(--md-ref-palette-primary-60);
199
- --color-primary-500: var(--md-ref-palette-primary-50);
200
- --color-primary-600: var(--md-ref-palette-primary-40);
201
- --color-primary-700: var(--md-ref-palette-primary-30);
202
- --color-primary-800: var(--md-ref-palette-primary-20);
203
- --color-primary-900: var(--md-ref-palette-primary-10);
204
- --color-primary-950: var(--md-ref-palette-primary-5);
205
-
206
- --color-secondary-50: var(--md-ref-palette-secondary-95);
207
- --color-secondary-100: var(--md-ref-palette-secondary-90);
208
- --color-secondary-200: var(--md-ref-palette-secondary-80);
209
- --color-secondary-300: var(--md-ref-palette-secondary-70);
210
- --color-secondary-400: var(--md-ref-palette-secondary-60);
211
- --color-secondary-500: var(--md-ref-palette-secondary-50);
212
- --color-secondary-600: var(--md-ref-palette-secondary-40);
213
- --color-secondary-700: var(--md-ref-palette-secondary-30);
214
- --color-secondary-800: var(--md-ref-palette-secondary-20);
215
- --color-secondary-900: var(--md-ref-palette-secondary-10);
216
- --color-secondary-950: var(--md-ref-palette-secondary-5);
217
-
218
- --color-tertiary-50: var(--md-ref-palette-tertiary-95);
219
- --color-tertiary-100: var(--md-ref-palette-tertiary-90);
220
- --color-tertiary-200: var(--md-ref-palette-tertiary-80);
221
- --color-tertiary-300: var(--md-ref-palette-tertiary-70);
222
- --color-tertiary-400: var(--md-ref-palette-tertiary-60);
223
- --color-tertiary-500: var(--md-ref-palette-tertiary-50);
224
- --color-tertiary-600: var(--md-ref-palette-tertiary-40);
225
- --color-tertiary-700: var(--md-ref-palette-tertiary-30);
226
- --color-tertiary-800: var(--md-ref-palette-tertiary-20);
227
- --color-tertiary-900: var(--md-ref-palette-tertiary-10);
228
- --color-tertiary-950: var(--md-ref-palette-tertiary-5);
229
-
230
- --color-error-50: var(--md-ref-palette-error-95);
231
- --color-error-100: var(--md-ref-palette-error-90);
232
- --color-error-200: var(--md-ref-palette-error-80);
233
- --color-error-300: var(--md-ref-palette-error-70);
234
- --color-error-400: var(--md-ref-palette-error-60);
235
- --color-error-500: var(--md-ref-palette-error-50);
236
- --color-error-600: var(--md-ref-palette-error-40);
237
- --color-error-700: var(--md-ref-palette-error-30);
238
- --color-error-800: var(--md-ref-palette-error-20);
239
- --color-error-900: var(--md-ref-palette-error-10);
240
- --color-error-950: var(--md-ref-palette-error-5);
241
-
242
- --color-neutral-50: var(--md-ref-palette-neutral-95);
243
- --color-neutral-100: var(--md-ref-palette-neutral-90);
244
- --color-neutral-200: var(--md-ref-palette-neutral-80);
245
- --color-neutral-300: var(--md-ref-palette-neutral-70);
246
- --color-neutral-400: var(--md-ref-palette-neutral-60);
247
- --color-neutral-500: var(--md-ref-palette-neutral-50);
248
- --color-neutral-600: var(--md-ref-palette-neutral-40);
249
- --color-neutral-700: var(--md-ref-palette-neutral-30);
250
- --color-neutral-800: var(--md-ref-palette-neutral-20);
251
- --color-neutral-900: var(--md-ref-palette-neutral-10);
252
- --color-neutral-950: var(--md-ref-palette-neutral-5);
253
-
254
- --color-neutral-variant-50: var(--md-ref-palette-neutral-variant-95);
255
- --color-neutral-variant-100: var(--md-ref-palette-neutral-variant-90);
256
- --color-neutral-variant-200: var(--md-ref-palette-neutral-variant-80);
257
- --color-neutral-variant-300: var(--md-ref-palette-neutral-variant-70);
258
- --color-neutral-variant-400: var(--md-ref-palette-neutral-variant-60);
259
- --color-neutral-variant-500: var(--md-ref-palette-neutral-variant-50);
260
- --color-neutral-variant-600: var(--md-ref-palette-neutral-variant-40);
261
- --color-neutral-variant-700: var(--md-ref-palette-neutral-variant-30);
262
- --color-neutral-variant-800: var(--md-ref-palette-neutral-variant-20);
263
- --color-neutral-variant-900: var(--md-ref-palette-neutral-variant-10);
264
- --color-neutral-variant-950: var(--md-ref-palette-neutral-variant-5);
265
-
266
- /*
267
- * Custom colors
268
- */
269
-
270
- --color-myCustomColor1: var(--md-sys-color-my-custom-color-1);
271
- --color-on-myCustomColor1: var(--md-sys-color-on-my-custom-color-1);
272
- --color-myCustomColor1-container: var(
273
- --md-sys-color-my-custom-color-1-container
274
- );
275
- --color-on-myCustomColor1-container: var(
276
- --md-sys-color-on-my-custom-color-1-container
277
- );
278
- /* Shades */
279
- --color-myCustomColor1-50: var(--md-ref-palette-my-custom-color-1-95);
280
- --color-myCustomColor1-100: var(--md-ref-palette-my-custom-color-1-90);
281
- --color-myCustomColor1-200: var(--md-ref-palette-my-custom-color-1-80);
282
- --color-myCustomColor1-300: var(--md-ref-palette-my-custom-color-1-70);
283
- --color-myCustomColor1-400: var(--md-ref-palette-my-custom-color-1-60);
284
- --color-myCustomColor1-500: var(--md-ref-palette-my-custom-color-1-50);
285
- --color-myCustomColor1-600: var(--md-ref-palette-my-custom-color-1-40);
286
- --color-myCustomColor1-700: var(--md-ref-palette-my-custom-color-1-30);
287
- --color-myCustomColor1-800: var(--md-ref-palette-my-custom-color-1-20);
288
- --color-myCustomColor1-900: var(--md-ref-palette-my-custom-color-1-10);
289
- --color-myCustomColor1-950: var(--md-ref-palette-my-custom-color-1-5);
290
-
291
- --color-myCustomColor2: var(--md-sys-color-my-custom-color-2);
292
- --color-on-myCustomColor2: var(--md-sys-color-on-my-custom-color-2);
293
- --color-myCustomColor2-container: var(
294
- --md-sys-color-my-custom-color-2-container
295
- );
296
- --color-on-myCustomColor2-container: var(
297
- --md-sys-color-on-my-custom-color-2-container
298
- );
299
- /* Shades */
300
- --color-myCustomColor2-50: var(--md-ref-palette-my-custom-color-2-95);
301
- --color-myCustomColor2-100: var(--md-ref-palette-my-custom-color-2-90);
302
- --color-myCustomColor2-200: var(--md-ref-palette-my-custom-color-2-80);
303
- --color-myCustomColor2-300: var(--md-ref-palette-my-custom-color-2-70);
304
- --color-myCustomColor2-400: var(--md-ref-palette-my-custom-color-2-60);
305
- --color-myCustomColor2-500: var(--md-ref-palette-my-custom-color-2-50);
306
- --color-myCustomColor2-600: var(--md-ref-palette-my-custom-color-2-40);
307
- --color-myCustomColor2-700: var(--md-ref-palette-my-custom-color-2-30);
308
- --color-myCustomColor2-800: var(--md-ref-palette-my-custom-color-2-20);
309
- --color-myCustomColor2-900: var(--md-ref-palette-my-custom-color-2-10);
310
- --color-myCustomColor2-950: var(--md-ref-palette-my-custom-color-2-5);
210
+ /* ... */
311
211
  }
312
212
  ```
313
213
 
314
- Or simply:
315
-
316
- ```css
317
- @import "material-theme-builder/tailwind.css";
318
- ```
214
+ 115 names in all — every M3 scheme token, plus eleven Tailwind shades for each
215
+ of `primary`, `secondary`, `tertiary`, `error`, `neutral` and
216
+ `neutral-variant`.
319
217
 
320
- > [!IMPORTANT]
321
- >
322
- > Do not forget to manually add your custom colors, as in:
323
- >
324
- > ```css
325
- > /*
326
- > * Custom colors
327
- > */
328
- >
329
- > --color-myCustomColor1: var(--md-sys-color-my-custom-color-1);
330
- > --color-on-myCustomColor1: var(--md-sys-color-on-my-custom-color-1);
331
- > --color-myCustomColor1-container: var(
332
- > --md-sys-color-my-custom-color-1-container
333
- > );
334
- > --color-on-myCustomColor1-container: var(
335
- > --md-sys-color-on-my-custom-color-1-container
336
- > );
337
- > /* Shades */
338
- > --color-myCustomColor1-50: var(--md-ref-palette-my-custom-color-1-95);
339
- > --color-myCustomColor1-100: var(--md-ref-palette-my-custom-color-1-90);
340
- > --color-myCustomColor1-200: var(--md-ref-palette-my-custom-color-1-80);
341
- > --color-myCustomColor1-300: var(--md-ref-palette-my-custom-color-1-70);
342
- > --color-myCustomColor1-400: var(--md-ref-palette-my-custom-color-1-60);
343
- > --color-myCustomColor1-500: var(--md-ref-palette-my-custom-color-1-50);
344
- > --color-myCustomColor1-600: var(--md-ref-palette-my-custom-color-1-40);
345
- > --color-myCustomColor1-700: var(--md-ref-palette-my-custom-color-1-30);
346
- > --color-myCustomColor1-800: var(--md-ref-palette-my-custom-color-1-20);
347
- > --color-myCustomColor1-900: var(--md-ref-palette-my-custom-color-1-10);
348
- > --color-myCustomColor1-950: var(--md-ref-palette-my-custom-color-1-5);
349
- >
350
- > --color-myCustomColor2: var(--md-sys-color-my-custom-color-2);
351
- > --color-on-myCustomColor2: var(--md-sys-color-on-my-custom-color-2);
352
- > --color-myCustomColor2-container: var(
353
- > --md-sys-color-my-custom-color-2-container
354
- > );
355
- > --color-on-myCustomColor2-container: var(
356
- > --md-sys-color-on-my-custom-color-2-container
357
- > );
358
- > /* Shades */
359
- > --color-myCustomColor2-50: var(--md-ref-palette-my-custom-color-2-95);
360
- > --color-myCustomColor2-100: var(--md-ref-palette-my-custom-color-2-90);
361
- > --color-myCustomColor2-200: var(--md-ref-palette-my-custom-color-2-80);
362
- > --color-myCustomColor2-300: var(--md-ref-palette-my-custom-color-2-70);
363
- > --color-myCustomColor2-400: var(--md-ref-palette-my-custom-color-2-60);
364
- > --color-myCustomColor2-500: var(--md-ref-palette-my-custom-color-2-50);
365
- > --color-myCustomColor2-600: var(--md-ref-palette-my-custom-color-2-40);
366
- > --color-myCustomColor2-700: var(--md-ref-palette-my-custom-color-2-30);
367
- > --color-myCustomColor2-800: var(--md-ref-palette-my-custom-color-2-20);
368
- > --color-myCustomColor2-900: var(--md-ref-palette-my-custom-color-2-10);
369
- > --color-myCustomColor2-950: var(--md-ref-palette-my-custom-color-2-5);
370
- > ```
218
+ </details>
371
219
 
372
220
  ## shadcn
373
221
 
@@ -376,19 +224,110 @@ Pre-requisites:
376
224
  - You should use
377
225
  [`tailwind.cssVariables`](https://ui.shadcn.com/docs/theming#css-variables)
378
226
 
379
- Simply override/remap
380
- [shadcn's CSS variables](https://ui.shadcn.com/docs/theming#list-of-variables):
227
+ In your
228
+ [`globals.css`](https://ui.shadcn.com/docs/installation/manual#configure-styles):
381
229
 
382
230
  ```css
231
+ @import "tailwindcss";
232
+ @import "tw-animate-css";
233
+ @import "shadcn/tailwind.css";
234
+
235
+ /* 👇🏻 ADD THIS 👇🏻 */
236
+ @import "material-theme-builder/tailwind.css"; /* the M3 tw classNames (optional) */
237
+ @import "material-theme-builder/shadcn.css"; /* shadcn's variables remapping on M3 */
238
+ @plugin "material-theme-builder/tailwind" { /* your custom colors (optional) */
239
+ custom-colors: myCustomColor1, myCustomColor2;
240
+ }
241
+ /* 👆🏻 ADD THIS 👆🏻 */
242
+
243
+ @custom-variant dark (&:is(.dark *));
244
+
245
+ @theme inline {
246
+ --color-background: var(--background);
247
+ ...
248
+ }
249
+
383
250
  :root {
384
- /* ... */
251
+ --radius: 0.625rem;
252
+ --background: oklch(1 0 0);
253
+ ...
385
254
  }
255
+
386
256
  .dark {
387
- /* ... */
257
+ --background: oklch(0.145 0 0);
258
+ ...
388
259
  }
260
+ ```
389
261
 
390
- :root,
391
- .dark {
262
+ `shadcn.css` is the one that matters: it points
263
+ [shadcn's variables](https://ui.shadcn.com/docs/theming#list-of-variables) at
264
+ the M3 custom properties, so every shadcn component follows whichever `<Mtb>` is
265
+ above it in the tree. It carries no colors of its own — mount an `<Mtb>`, or
266
+ emit [`toCss()`](#programmatic-api) server-side, or nothing resolves.
267
+
268
+ The other two are optional. They are the [Tailwind](#tailwind) recipe unchanged,
269
+ and what they add is names to write yourself — `bg-surface-container-low`,
270
+ `text-on-primary`, your custom colors. Drop them and every shadcn component
271
+ still follows the theme.
272
+
273
+ For the opposite trade — concrete `oklch()` values and no `var()` at all, frozen
274
+ at build time — see [`toShadcn()`](#programmatic-api).
275
+
276
+ <details>
277
+ <summary>The three names both halves claim</summary>
278
+
279
+ > [!NOTE]
280
+ >
281
+ > Written down for the record. It moves one utility by one role, and you almost
282
+ > certainly do not need to care.
283
+
284
+ Material and shadcn picked the same name for three things — `background`,
285
+ `primary`, `secondary`. shadcn's `@theme inline` is the later of the two, so on
286
+ those three it wins, and the utility goes through the mapping above:
287
+
288
+ ```
289
+ bg-secondary → --color-secondary → var(--secondary) → var(--md-sys-color-secondary-container)
290
+ ```
291
+
292
+ Without shadcn it is one hop shorter, and lands on the role of the same name:
293
+
294
+ ```
295
+ bg-secondary → --color-secondary → var(--md-sys-color-secondary)
296
+ ```
297
+
298
+ Same destination either way, M3 — just not the same role. And only for
299
+ `secondary`: `primary` maps to `primary`, and M3 `background` and `surface` are
300
+ the same color.
301
+
302
+ If you ever want the M3 role itself, `<Mtb>` still emits it:
303
+
304
+ ```html
305
+ <div class="bg-[var(--md-sys-color-secondary)]"></div>
306
+ ```
307
+
308
+ or give it a name of its own:
309
+
310
+ ```css
311
+ @theme inline {
312
+ --color-m3-secondary: var(--md-sys-color-secondary);
313
+ }
314
+ ```
315
+
316
+ </details>
317
+
318
+ <details>
319
+ <summary>The variables it remaps</summary>
320
+
321
+ Both halves are generated from [`toShadcnAliases()`](#programmatic-api) and
322
+ [`toShadcnRegistryItem()`](#programmatic-api), off one mapping, so they cannot
323
+ drift. The selectors are doubled so the block outranks shadcn's own `:root` and
324
+ `.dark` on
325
+ [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Specificity#increasing_specificity_by_duplicating_selector)
326
+ rather than on order — which is what lets the `@import` sit with your others.
327
+
328
+ ```css
329
+ :root:root,
330
+ .dark.dark {
392
331
  --background: var(--md-sys-color-surface);
393
332
  --foreground: var(--md-sys-color-on-surface);
394
333
  --card: var(--md-sys-color-surface-container-low);
@@ -423,18 +362,109 @@ Simply override/remap
423
362
  }
424
363
  ```
425
364
 
426
- <details>
427
- <summary>mapping details</summary>
428
- see:
429
-
430
- - https://chatgpt.com/share/6899f20a-422c-8011-a072-62fb649589a0
431
- - https://gemini.google.com/share/51e072b6f1d2
432
365
  </details>
433
366
 
434
- > [!IMPORTANT]
367
+ ### `shadcn-apply`
368
+
369
+ The alternative, for colors to fall back on and no import to place. One command,
370
+ from inside your project:
371
+
372
+ ```sh
373
+ $ npx material-theme-builder shadcn-apply "#6750A4"
374
+ ```
375
+
376
+ From nothing at all, scaffold with shadcn's own CLI first — what this repo
377
+ dogfoods:
378
+
379
+ ```sh
380
+ $ npx shadcn@latest init --preset b0 --name material-theme-app
381
+ $ cd material-theme-app && npx material-theme-builder shadcn-apply "#6750A4"
382
+ ```
383
+
384
+ It generates a registry item for your source color and hands it to `shadcn add`,
385
+ which rewrites the values inside your existing `:root` and `.dark` blocks, in
386
+ place. Same mapping as the stylesheet, with that theme's own colors left in as
387
+ the `var()` fallbacks:
388
+
389
+ ```css
390
+ :root {
391
+ --card: var(--md-sys-color-surface-container-low, oklch(0.968 0.012 317.742));
392
+ }
393
+
394
+ .dark {
395
+ --card: var(--md-sys-color-surface-container-low, oklch(0.227 0.01 303.714));
396
+ }
397
+ ```
398
+
399
+ So it works with no `<Mtb>` at all — the fallbacks render the theme statically,
400
+ server-rendered, zero client JS. Your old values are overwritten, not kept
401
+ anywhere: `git diff` is the undo.
402
+
403
+ Both steps by hand, if you would rather:
404
+
405
+ ```sh
406
+ $ npx material-theme-builder "#6750A4" --format registry-item > mtb.json
407
+ $ npx shadcn@latest add ./mtb.json && rm mtb.json
408
+ ```
409
+
410
+ `shadcn-apply` takes every theme option `material-theme-builder` itself takes,
411
+ and they all land in those fallbacks:
412
+
413
+ ```sh
414
+ $ npx material-theme-builder shadcn-apply "#6750A4" --scheme vibrant --contrast 0.5
415
+ ```
416
+
417
+ <details>
418
+ <summary>The rest of the options</summary>
419
+
420
+ `--no-fallback` leaves the fallbacks out, on both — so shadcn's own colors are
421
+ dropped rather than kept in reserve. Nothing then declares those variables
422
+ except an `<Mtb>` or a [`toCss()`](#programmatic-api): without one, they resolve
423
+ to nothing and the components render transparent.
424
+
425
+ `--custom-colors` is the one option missing: shadcn's variable set is fixed, so
426
+ a registry item cannot carry one.
427
+
428
+ Anything after a `--` is forwarded verbatim to `shadcn add`. Our options go
429
+ before it:
430
+
431
+ ```sh
432
+ $ npx material-theme-builder shadcn-apply "#6750A4" -- --overwrite --dry-run
433
+ ```
434
+
435
+ > [!NOTE]
435
436
  >
436
- > Make sure `:root, .dark { ... }` comes AFTER `.root { ... } .dark { ... }` to
437
- > take precedence.
437
+ > shadcn's CLI also appends a self-referential `--card: var(--card);` per
438
+ > variable to your `@theme inline` block. Noise, not a bug: they land _above_
439
+ > your `:root`, so the real values win. Delete them if they bother you.
440
+
441
+ </details>
442
+
443
+ <details>
444
+ <summary>Install the mapping alone, without generating anything</summary>
445
+
446
+ The package publishes a registry item too, so `shadcn add` has something to
447
+ fetch without a build step:
448
+
449
+ ```sh
450
+ $ npx shadcn@latest add https://unpkg.com/material-theme-builder/registry-item.json
451
+ ```
452
+
453
+ It is the stylesheet's content, installed the registry way: the mapping and
454
+ nothing else, no colors to fall back on. Generate your own, as above, to have
455
+ some.
456
+
457
+ </details>
458
+
459
+ <details>
460
+ <summary>mapping details</summary>
461
+
462
+ see:
463
+
464
+ - https://chatgpt.com/share/6899f20a-422c-8011-a072-62fb649589a0
465
+ - https://gemini.google.com/share/51e072b6f1d2
466
+
467
+ </details>
438
468
 
439
469
  # Dev
440
470
 
@@ -475,6 +505,39 @@ $ pnpm run lgtm
475
505
 
476
506
  ## CONTRIBUTING
477
507
 
508
+ ```bash
509
+ pnpm run storybook # the day-to-day loop -- no build needed, the stylesheets regenerate as you edit
510
+ pnpm run build # dist/, plus the generated stylesheets -- both gitignored
511
+ pnpm run lgtm # everything CI checks
512
+ ```
513
+
514
+ `tailwind.css`, `shadcn.css` and `registry-item.json` are generated — from
515
+ `toTailwind()`, `toShadcnAliases()` and `toShadcnRegistryItem()` — and
516
+ gitignored. `pnpm run build` writes them (`scripts/generate.mjs`); the two
517
+ stylesheets also get a `src/` copy, which is what Storybook `@import`s, and in
518
+ Storybook a Vite plugin (`.storybook/main.ts`) rewrites those at server start
519
+ and again on every edit under `src/lib/`, so the stories never show a stale
520
+ vocabulary.
521
+
522
+ `generate.mjs` builds the registry item without `{ fallback: true }`, which is
523
+ what keeps every one of those outputs a function of the _mapping_ rather than of
524
+ a color: `SOURCE` there is arbitrary, and has to stay able to be. The fallback
525
+ variant belongs to whoever knows a real source color — the CLI's
526
+ `--format registry-item`.
527
+
528
+ `src/styles/shadcn.css` is the other half of that arrangement, and is _not_
529
+ generated from anything here: it is pristine `shadcn init --preset b0` output,
530
+ committed verbatim — regenerate it with the recipe in its own header. Same for
531
+ the components, via `pnpm dlx shadcn@latest add <item> --overwrite`. All of it
532
+ is exempt from Prettier and from the repo's own lint conventions, so that a
533
+ regeneration diffs to nothing; see `.prettierignore` and `SHADCN_FILES` in
534
+ `eslint.config.mjs` for which paths `components.json` makes shadcn's territory.
535
+
536
+ The `Shadcn/dashboard-01` story is what checks the shadcn mapping end to end: it
537
+ renders one of [shadcn's blocks](https://ui.shadcn.com/blocks), unmodified,
538
+ under `<Mtb>`. Every other story paints from the M3 vocabulary directly, so none
539
+ of them would notice `shadcn.css` pointing a variable at the wrong role.
540
+
478
541
  When submitting a pull request, please include a changeset to document your
479
542
  changes:
480
543
 
@@ -491,3 +554,34 @@ m3 references:
491
554
  | builder | roles |
492
555
  | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
493
556
  | [<img width="2836" height="2266" alt="CleanShot 2026-01-14 at 08 58 40@2x" src="https://github.com/user-attachments/assets/e4b47c00-716f-4b08-b393-de306d5ce302" />](https://material-foundation.github.io/material-theme-builder/) | [<img width="2836" height="2266" alt="CleanShot 2026-01-14 at 09 01 23@2x" src="https://github.com/user-attachments/assets/826e502d-e173-43c4-807a-53d0ba075a88" />](https://m3.material.io/styles/color/roles) |
557
+
558
+ The spec itself, deep-linked to the sections that matter. `m3.material.io` is a
559
+ client-rendered SPA, so `#:~:text=` fragments get stripped on load — only these
560
+ section anchors work:
561
+
562
+ - [Color roles](https://m3.material.io/styles/color/roles) — the inventory:
563
+ _"26 standard color roles organized into six groups"_, which is what
564
+ `tokenDescriptions` is checked against
565
+ - [Color roles § Surface](https://m3.material.io/styles/color/roles#89f972b1-e372-494c-aabc-69aea34ed591)
566
+ — _"three surface roles: Surface / On surface / On surface variant"_. No
567
+ `surface variant`: the ink outlived its own background, hence the asymmetry
568
+ - [Color roles § Add-on color roles](https://m3.material.io/styles/color/roles#a5f6ea3d-d457-4c5d-94f4-55f3cdf6470b)
569
+ — fixed accents and surface dim/bright are add-ons, and _"most products won't
570
+ need to use these"_
571
+ - [Color system § What's new](https://m3.material.io/styles/color/system/overview#ca18ba03-a1ec-4bbb-a531-ae5396d3ee4a)
572
+ — the changelog. Feb 2023 is when tone-based surfaces replaced the +1…+5
573
+ elevation model
574
+
575
+ The Material Design blog is where the reasoning behind the color system lives —
576
+ and where changes to it get announced before the spec pages catch up:
577
+
578
+ - [Tone-based Surfaces in Material 3](https://m3.material.io/blog/tone-based-surface-color-m3)
579
+ — the surface roles replacing elevation overlays. The only first-party text
580
+ stating that `Surface Variant` gives way to `Surface Container Highest`
581
+ - [The science of color & design](https://m3.material.io/blog/science-of-color-design)
582
+ — HCT, and why a tone means the same contrast across hues: the basis of the
583
+ tonal palettes
584
+ - [Designing Harmony into Dynamic Color](https://m3.material.io/blog/dynamic-color-harmony)
585
+ — what `customColors[].blend` actually does to a custom color
586
+ - [Introducing Material Theme Builder](https://m3.material.io/blog/material-theme-builder)
587
+ — the tool this package reimplements