material-theme-builder 3.2.0 → 3.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
@@ -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
@@ -127,247 +129,94 @@ return (
127
129
 
128
130
  ## Tailwind
129
131
 
130
- Compatible through [theme variables](https://tailwindcss.com/docs/theme):
132
+ Compatible through [theme variables](https://tailwindcss.com/docs/theme), in two
133
+ halves — a stylesheet for the standard tokens, and a plugin for the custom
134
+ colors, the one part a shipped file cannot know:
131
135
 
132
136
  ```css
133
- @theme inline {
134
- --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);
153
- --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
- --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);
186
- --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);
137
+ @import "tailwindcss";
138
+
139
+ @import "material-theme-builder/tailwind.css";
140
+ @plugin "material-theme-builder/tailwind" {
141
+ custom-colors: myCustomColor1, myCustomColor2;
311
142
  }
312
143
  ```
313
144
 
314
- Or simply:
145
+ No hand-written block either side. Each name listed brings its four scheme roles
146
+ and eleven shades — `bg-myCustomColor1`, `text-on-myCustomColor1`,
147
+ `bg-myCustomColor1-container`, `bg-myCustomColor1-300`. Drop the `@plugin` line
148
+ if you have no custom colors.
149
+
150
+ The plugin takes a `prefix` too, mirroring `builder({ prefix })`:
315
151
 
316
152
  ```css
317
- @import "material-theme-builder/tailwind.css";
153
+ @plugin "material-theme-builder/tailwind" {
154
+ prefix: my;
155
+ custom-colors: myCustomColor1;
156
+ }
318
157
  ```
319
158
 
320
- > [!IMPORTANT]
159
+ The plugin can carry the standard tokens on its own — `@plugin` without the
160
+ `@import` — for a setup that would rather not import CSS at all. Read the
161
+ warning below first if you also use shadcn.
162
+
163
+ > [!TIP]
321
164
  >
322
- > Do not forget to manually add your custom colors, as in:
165
+ > Both halves declare their colors as
166
+ > [inlined theme values](https://tailwindcss.com/docs/theme#referencing-other-variables):
167
+ > `bg-primary` compiles to `background-color: var(--md-sys-color-primary)`,
168
+ > with no `--color-primary` in between. That matters for nesting — a
169
+ > `--color-primary` declared on `:root` would resolve against `:root`, out of
170
+ > reach of a nested `<Mtb>` re-declaring the M3 properties.
171
+
172
+ > [!WARNING]
323
173
  >
324
- > ```css
325
- > /*
326
- > * Custom colors
327
- > */
174
+ > Theme values a plugin contributes are _defaults_: an `@theme` block of your
175
+ > own wins over them whatever the order, where the stylesheet — being CSS —
176
+ > wins by import order.
328
177
  >
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);
178
+ > That is why the standard tokens are left to the stylesheet. shadcn's
179
+ > `@theme inline` claims three names M3 also uses — `background`, `primary`,
180
+ > `secondary` — and with the two halves above the stylesheet takes them back,
181
+ > so shadcn changes nothing. Only if you drop the `@import` and let the plugin
182
+ > carry the standard tokens do you have to hand those three back yourself:
349
183
  >
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);
184
+ > ```css
185
+ > @theme inline {
186
+ > --color-background: var(--md-sys-color-background);
187
+ > --color-primary: var(--md-sys-color-primary);
188
+ > --color-secondary: var(--md-sys-color-secondary);
189
+ > }
370
190
  > ```
191
+ >
192
+ > Left alone, `bg-secondary` resolves through shadcn's `--secondary`, which the
193
+ > [shadcn](#shadcn) section below remaps to `secondary-container` — a tone 90
194
+ > where you asked for a tone 40, under text still colored `on-secondary`.
195
+
196
+ <details>
197
+ <summary>The theme variables the stylesheet declares</summary>
198
+
199
+ Generated from [`toTailwind()`](#programmatic-api), so the two cannot drift:
200
+
201
+ ```css
202
+ @theme inline {
203
+ --color-background: var(--md-sys-color-background);
204
+ --color-error: var(--md-sys-color-error);
205
+ --color-error-container: var(--md-sys-color-error-container);
206
+ --color-inverse-on-surface: var(--md-sys-color-inverse-on-surface);
207
+ --color-inverse-primary: var(--md-sys-color-inverse-primary);
208
+ --color-inverse-surface: var(--md-sys-color-inverse-surface);
209
+ --color-on-background: var(--md-sys-color-on-background);
210
+ --color-on-error: var(--md-sys-color-on-error);
211
+ /* ... */
212
+ }
213
+ ```
214
+
215
+ 115 names in all — every M3 scheme token, plus eleven Tailwind shades for each
216
+ of `primary`, `secondary`, `tertiary`, `error`, `neutral` and
217
+ `neutral-variant`.
218
+
219
+ </details>
371
220
 
372
221
  ## shadcn
373
222
 
@@ -376,17 +225,155 @@ Pre-requisites:
376
225
  - You should use
377
226
  [`tailwind.cssVariables`](https://ui.shadcn.com/docs/theming#css-variables)
378
227
 
379
- Simply override/remap
380
- [shadcn's CSS variables](https://ui.shadcn.com/docs/theming#list-of-variables):
228
+ One command, from inside your project:
229
+
230
+ ```sh
231
+ $ npx material-theme-builder shadcn-apply "#6750A4"
232
+ ```
233
+
234
+ Or from nothing at all — `shadcn-init` scaffolds a stock shadcn app
235
+ (`shadcn init --preset b0 --template vite`), themes it and starts it:
236
+
237
+ ```sh
238
+ $ npx material-theme-builder shadcn-init "#6750A4"
239
+ ```
240
+
241
+ The verbs are shadcn's own — there, `init` is the new project and `apply` the
242
+ existing one — and they are prefixed because shadcn is one integration here
243
+ among Figma, CSS, Tailwind and Flutter: a bare `init` would read as "initialize
244
+ material-theme-builder", and would leave no room for a `tailwind-init` later.
245
+
246
+ Anything after a `--` is forwarded verbatim to the shadcn command underneath —
247
+ `shadcn init` for `shadcn-init`, `shadcn add` for `shadcn-apply` — so
248
+ `shadcn-init "#6750A4" -- --template next -n my-app` scaffolds Next instead.
249
+ Options of ours go before the separator; one written after it is refused, rather
250
+ than forwarded into an error from shadcn about a flag it has never heard of.
251
+ `--print` writes the equivalent shell chain and runs nothing.
252
+
253
+ `--shadcn-cli <spec>` pins which shadcn runs — `--shadcn-cli shadcn@4.18.0`, a
254
+ tag, a fork, anything `npx` resolves — defaulting to `shadcn@latest`. (Not
255
+ `--shadcn`: the root command has used that name since 3.2.0 for something else
256
+ entirely, a boolean that appends the alias block to `--format tailwind`.) It
257
+ reaches for neighbouring versions rather than back in time, though: the defaults
258
+ these commands pass are shadcn 4.x vocabulary (`--preset b0` is a 4.x preset
259
+ code), so pinning far enough back also means passing that era's preset after the
260
+ `--`.
261
+
262
+ Both do the same two things by hand, if you would rather: generate a registry
263
+ item for your source color, and install it the way you install any shadcn theme.
264
+
265
+ ```sh
266
+ $ npx material-theme-builder "#6750A4" --format registry-item > mtb.json
267
+ $ npx shadcn@latest add ./mtb.json && rm mtb.json
268
+ ```
269
+
270
+ Either way, that rewrites the values inside your existing `:root` and `.dark`
271
+ blocks, in place, pointing
272
+ [shadcn's CSS variables](https://ui.shadcn.com/docs/theming#list-of-variables)
273
+ at the M3 custom properties `<Mtb>` emits — so every shadcn component follows
274
+ whichever theme is above it in the tree — and leaves that theme's own colors in
275
+ as the `var()` fallbacks:
381
276
 
382
277
  ```css
383
278
  :root {
384
- /* ... */
279
+ --card: var(--md-sys-color-surface-container-low, oklch(0.968 0.012 317.742));
385
280
  }
281
+
386
282
  .dark {
387
- /* ... */
283
+ --card: var(--md-sys-color-surface-container-low, oklch(0.227 0.01 303.714));
388
284
  }
285
+ ```
286
+
287
+ So it works both ways round: live under an `<Mtb>`, and static — server-rendered,
288
+ zero client JS — anywhere there is none.
289
+
290
+ Every option lands in those fallbacks, `--scheme` and `--contrast` included, and
291
+ `shadcn-init` and `shadcn-apply` take them all — so the item they generate is the one the
292
+ by-hand route would have produced:
293
+
294
+ ```sh
295
+ $ npx material-theme-builder shadcn-apply "#6750A4" --scheme vibrant --contrast 0.5
296
+ ```
297
+
298
+ `--no-fallback` leaves the fallbacks out, on all three. `--custom-colors` is the
299
+ one option the two subcommands do not take, and that is not an oversight:
300
+ shadcn's variable set is fixed, so no component reads a custom color and a
301
+ registry item cannot carry one.
302
+
303
+ > [!WARNING]
304
+ >
305
+ > `shadcn add` overwrites shadcn's own `oklch()` values rather than keeping them
306
+ > anywhere, so they are not a safety net. Where nothing declares the M3
307
+ > properties _and_ there are no fallbacks, every variable resolves to nothing and
308
+ > components render transparent — `git diff` your CSS, or re-run `shadcn init`,
309
+ > to get shadcn's defaults back.
310
+
311
+ > [!NOTE]
312
+ >
313
+ > shadcn's CLI also appends a self-referential `--card: var(--card);` per
314
+ > variable to your `@theme inline` block. It is noise, not a bug on your side:
315
+ > those land _above_ your `:root`, so the real values win. Delete them if they
316
+ > bother you.
317
+
318
+ For the opposite trade — concrete `oklch()` values and no `var()` at all, frozen
319
+ at build time — see [`toShadcn()`](#programmatic-api).
320
+
321
+ <details>
322
+ <summary>Install the mapping alone, without generating anything</summary>
323
+
324
+ The package publishes one too, so `shadcn add` has something to fetch without a
325
+ build step of yours:
326
+
327
+ ```sh
328
+ $ npx shadcn@latest add https://unpkg.com/material-theme-builder/registry-item.json
329
+ ```
330
+
331
+ It is the mapping and nothing else — 31 `var()` references, no colors. Which is
332
+ why it can be published at all: it is the same file whatever your source color,
333
+ scheme or contrast, because those arrive at runtime from `<Mtb>`. And that is
334
+ also its one requirement — mount an `<Mtb>`, or emit
335
+ [`toCss()`](#programmatic-api) server-side, or nothing resolves. Generate your
336
+ own, as above, to have colors to fall back on.
337
+
338
+ </details>
339
+
340
+ <details>
341
+ <summary>Rather import a stylesheet than let the CLI edit your file</summary>
342
+
343
+ A stylesheet is shipped too, for setups that would rather keep the mapping in
344
+ one line they can delete:
389
345
 
346
+ ```css
347
+ @import "tailwindcss";
348
+ @import "./shadcn.css"; /* shadcn's own `:root` and `.dark` */
349
+ @import "material-theme-builder/shadcn.css"; /* ...then ours */
350
+ ```
351
+
352
+ > [!IMPORTANT]
353
+ >
354
+ > It has to come AFTER shadcn's own `:root { ... }` and `.dark { ... }`, which
355
+ > it overrides — so **not** at the top with your other imports, which is where
356
+ > an `@import` normally goes and where this one silently loses:
357
+ >
358
+ > ```css
359
+ > /* ✗ `--card` falls back to shadcn's grey; nothing warns you */
360
+ > @import "tailwindcss";
361
+ > @import "material-theme-builder/shadcn.css";
362
+ > @import "./shadcn.css";
363
+ > ```
364
+ >
365
+ > The registry item above exists to make this impossible to get wrong.
366
+
367
+ </details>
368
+
369
+ <details>
370
+ <summary>The variables it remaps</summary>
371
+
372
+ Both halves are generated from [`toShadcnAliases()`](#programmatic-api) and
373
+ [`toShadcnRegistryItem()`](#programmatic-api), off one mapping, so neither can
374
+ drift from the other:
375
+
376
+ ```css
390
377
  :root,
391
378
  .dark {
392
379
  --background: var(--md-sys-color-surface);
@@ -423,6 +410,8 @@ Simply override/remap
423
410
  }
424
411
  ```
425
412
 
413
+ </details>
414
+
426
415
  <details>
427
416
  <summary>mapping details</summary>
428
417
  see:
@@ -431,11 +420,6 @@ Simply override/remap
431
420
  - https://gemini.google.com/share/51e072b6f1d2
432
421
  </details>
433
422
 
434
- > [!IMPORTANT]
435
- >
436
- > Make sure `:root, .dark { ... }` comes AFTER `.root { ... } .dark { ... }` to
437
- > take precedence.
438
-
439
423
  # Dev
440
424
 
441
425
  ## INSTALL
@@ -475,6 +459,39 @@ $ pnpm run lgtm
475
459
 
476
460
  ## CONTRIBUTING
477
461
 
462
+ ```bash
463
+ pnpm run storybook # the day-to-day loop -- no build needed, the stylesheets regenerate as you edit
464
+ pnpm run build # dist/, plus the generated stylesheets -- both gitignored
465
+ pnpm run lgtm # everything CI checks
466
+ ```
467
+
468
+ `tailwind.css`, `shadcn.css` and `registry-item.json` are generated — from
469
+ `toTailwind()`, `toShadcnAliases()` and `toShadcnRegistryItem()` — and
470
+ gitignored. `pnpm run build` writes them (`scripts/generate.mjs`); the two
471
+ stylesheets also get a `src/` copy, which is what Storybook `@import`s, and in
472
+ Storybook a Vite plugin (`.storybook/main.ts`) rewrites those at server start
473
+ and again on every edit under `src/lib/`, so the stories never show a stale
474
+ vocabulary.
475
+
476
+ `generate.mjs` builds the registry item without `{ fallback: true }`, which is
477
+ what keeps every one of those outputs a function of the _mapping_ rather than of
478
+ a color: `SOURCE` there is arbitrary, and has to stay able to be. The fallback
479
+ variant belongs to whoever knows a real source color — the CLI's
480
+ `--format registry-item`.
481
+
482
+ `src/styles/shadcn.css` is the other half of that arrangement, and is _not_
483
+ generated from anything here: it is pristine `shadcn init --preset b0` output,
484
+ committed verbatim — regenerate it with the recipe in its own header. Same for
485
+ the components, via `pnpm dlx shadcn@latest add <item> --overwrite`. All of it
486
+ is exempt from Prettier and from the repo's own lint conventions, so that a
487
+ regeneration diffs to nothing; see `.prettierignore` and `SHADCN_FILES` in
488
+ `eslint.config.mjs` for which paths `components.json` makes shadcn's territory.
489
+
490
+ The `Shadcn/dashboard-01` story is what checks the shadcn mapping end to end: it
491
+ renders one of [shadcn's blocks](https://ui.shadcn.com/blocks), unmodified,
492
+ under `<Mtb>`. Every other story paints from the M3 vocabulary directly, so none
493
+ of them would notice `shadcn.css` pointing a variable at the wrong role.
494
+
478
495
  When submitting a pull request, please include a changeset to document your
479
496
  changes:
480
497
 
@@ -491,3 +508,34 @@ m3 references:
491
508
  | builder | roles |
492
509
  | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
493
510
  | [<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) |
511
+
512
+ The spec itself, deep-linked to the sections that matter. `m3.material.io` is a
513
+ client-rendered SPA, so `#:~:text=` fragments get stripped on load — only these
514
+ section anchors work:
515
+
516
+ - [Color roles](https://m3.material.io/styles/color/roles) — the inventory:
517
+ _"26 standard color roles organized into six groups"_, which is what
518
+ `tokenDescriptions` is checked against
519
+ - [Color roles § Surface](https://m3.material.io/styles/color/roles#89f972b1-e372-494c-aabc-69aea34ed591)
520
+ — _"three surface roles: Surface / On surface / On surface variant"_. No
521
+ `surface variant`: the ink outlived its own background, hence the asymmetry
522
+ - [Color roles § Add-on color roles](https://m3.material.io/styles/color/roles#a5f6ea3d-d457-4c5d-94f4-55f3cdf6470b)
523
+ — fixed accents and surface dim/bright are add-ons, and _"most products won't
524
+ need to use these"_
525
+ - [Color system § What's new](https://m3.material.io/styles/color/system/overview#ca18ba03-a1ec-4bbb-a531-ae5396d3ee4a)
526
+ — the changelog. Feb 2023 is when tone-based surfaces replaced the +1…+5
527
+ elevation model
528
+
529
+ The Material Design blog is where the reasoning behind the color system lives —
530
+ and where changes to it get announced before the spec pages catch up:
531
+
532
+ - [Tone-based Surfaces in Material 3](https://m3.material.io/blog/tone-based-surface-color-m3)
533
+ — the surface roles replacing elevation overlays. The only first-party text
534
+ stating that `Surface Variant` gives way to `Surface Container Highest`
535
+ - [The science of color & design](https://m3.material.io/blog/science-of-color-design)
536
+ — HCT, and why a tone means the same contrast across hues: the basis of the
537
+ tonal palettes
538
+ - [Designing Harmony into Dynamic Color](https://m3.material.io/blog/dynamic-color-harmony)
539
+ — what `customColors[].blend` actually does to a custom color
540
+ - [Introducing Material Theme Builder](https://m3.material.io/blog/material-theme-builder)
541
+ — the tool this package reimplements