material-theme-builder 3.3.0 → 5.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
@@ -97,19 +97,43 @@ import { Mtb } from "material-theme-builder/react";
97
97
  >
98
98
  > Typically wrapping `{children}` in a
99
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`.
100
104
 
101
105
  > [!NOTE]
102
106
  >
103
- > CSS varnames are always kebab-cased, e.g. `myCustomColor1` →
104
- > `--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
+ > ```
105
132
 
106
133
  > [!NOTE]
107
134
  >
108
- > `<Mtb>` injects the CSS from the client, and is the only thing here carrying
109
- > `"use client"`. The root entry holds `builder` alone — so from a
110
- > [React Server Component](https://react.dev/reference/rsc/server-components)
111
- > you can call it and emit `toCss()` into the document yourself, without
112
- > 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>`
113
137
 
114
138
  ## `useMtb`
115
139
 
@@ -129,25 +153,26 @@ return (
129
153
 
130
154
  ## Tailwind
131
155
 
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:
156
+ Compatible through [theme variables](https://tailwindcss.com/docs/theme) — one
157
+ plugin, one line:
135
158
 
136
159
  ```css
137
160
  @import "tailwindcss";
138
161
 
139
- @import "material-theme-builder/tailwind.css";
140
162
  @plugin "material-theme-builder/tailwind" {
141
163
  custom-colors: myCustomColor1, myCustomColor2;
142
164
  }
143
165
  ```
144
166
 
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.
167
+ Drop the `custom-colors` block if you have none.
168
+
169
+ <details>
149
170
 
150
- The plugin takes a `prefix` too, mirroring `builder({ prefix })`:
171
+ Each name listed brings its four scheme roles and eleven shades —
172
+ `bg-myCustomColor1`, `text-on-myCustomColor1`, `bg-myCustomColor1-container`,
173
+ `bg-myCustomColor1-300`.
174
+
175
+ `prefix` mirrors `builder({ prefix })`:
151
176
 
152
177
  ```css
153
178
  @plugin "material-theme-builder/tailwind" {
@@ -156,65 +181,27 @@ The plugin takes a `prefix` too, mirroring `builder({ prefix })`:
156
181
  }
157
182
  ```
158
183
 
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.
184
+ </details>
162
185
 
163
186
  > [!TIP]
164
187
  >
165
- > Both halves declare their colors as
188
+ > Colors are declared as
166
189
  > [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]
173
- >
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.
177
- >
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:
183
- >
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
- > }
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`.
190
+ > `bg-primary` compiles to `background-color: var(--md-sys-color-primary)`, with
191
+ > no `--color-primary` in between. That one would sit on `:root`, out of reach
192
+ > of a nested `<Mtb>`.
195
193
 
196
194
  <details>
197
- <summary>The theme variables the stylesheet declares</summary>
195
+ <summary>The names it declares</summary>
198
196
 
199
- Generated from [`toTailwind()`](#programmatic-api), so the two cannot drift:
197
+ 115 standard ones — every M3 scheme token (`bg-surface-container-low`,
198
+ `text-on-primary`, `border-outline-variant`…), plus eleven Tailwind shades for
199
+ each of `primary`, `secondary`, `tertiary`, `error`, `neutral` and
200
+ `neutral-variant` (`bg-primary-300`). Then four roles and eleven shades per
201
+ custom color you name.
200
202
 
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`.
203
+ They are theme _defaults_, so an `@theme` block of your own wins over them
204
+ whatever the order. See [shadcn](#shadcn), where three names collide.
218
205
 
219
206
  </details>
220
207
 
@@ -225,157 +212,110 @@ Pre-requisites:
225
212
  - You should use
226
213
  [`tailwind.cssVariables`](https://ui.shadcn.com/docs/theming#css-variables)
227
214
 
228
- One command, from inside your project:
215
+ In your
216
+ [`globals.css`](https://ui.shadcn.com/docs/installation/manual#configure-styles):
229
217
 
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
- ```
218
+ ```css
219
+ @import "tailwindcss";
220
+ @import "tw-animate-css";
221
+ @import "shadcn/tailwind.css";
240
222
 
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.
223
+ /* 👇🏻 ADD THIS 👇🏻 */
224
+ @import "material-theme-builder/shadcn.css"; /* shadcn's variables remapping on M3 */
225
+ @plugin "material-theme-builder/tailwind" { /* the M3 tw classNames (optional) */
226
+ custom-colors: myCustomColor1, myCustomColor2;
227
+ }
228
+ /* 👆🏻 ADD THIS 👆🏻 */
264
229
 
265
- ```sh
266
- $ npx material-theme-builder "#6750A4" --format registry-item > mtb.json
267
- $ npx shadcn@latest add ./mtb.json && rm mtb.json
268
- ```
230
+ @custom-variant dark (&:is(.dark *));
269
231
 
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:
232
+ @theme inline {
233
+ --color-background: var(--background);
234
+ ...
235
+ }
276
236
 
277
- ```css
278
237
  :root {
279
- --card: var(--md-sys-color-surface-container-low, oklch(0.968 0.012 317.742));
238
+ --radius: 0.625rem;
239
+ --background: oklch(1 0 0);
240
+ ...
280
241
  }
281
242
 
282
243
  .dark {
283
- --card: var(--md-sys-color-surface-container-low, oklch(0.227 0.01 303.714));
244
+ --background: oklch(0.145 0 0);
245
+ ...
284
246
  }
285
247
  ```
286
248
 
287
- So it works both ways round: live under an `<Mtb>`, and static — server-rendered,
288
- zero client JS — anywhere there is none.
249
+ `shadcn.css` is the one that matters: it points
250
+ [shadcn's variables](https://ui.shadcn.com/docs/theming#list-of-variables) at
251
+ the M3 custom properties, so every shadcn component follows whichever `<Mtb>` is
252
+ above it in the tree. It carries no colors of its own — mount an `<Mtb>`, or
253
+ emit [`toCss()`](#programmatic-api) server-side, or nothing resolves.
289
254
 
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
- ```
255
+ The `@plugin` line is optional. It is the [Tailwind](#tailwind) recipe
256
+ unchanged, and what it adds is names to write yourself —
257
+ `bg-surface-container-low`, `text-on-primary`, your custom colors. Drop it and
258
+ every shadcn component still follows the theme.
297
259
 
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.
260
+ For the opposite trade — concrete `oklch()` values and no `var()` at all, frozen
261
+ at build time — see [`toShadcn()`](#programmatic-api).
302
262
 
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.
263
+ <details>
264
+ <summary>The three names both halves claim</summary>
310
265
 
311
266
  > [!NOTE]
312
267
  >
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.
268
+ > Written down for the record. It moves one utility by one role, and you almost
269
+ > certainly do not need to care.
317
270
 
318
- For the opposite trade — concrete `oklch()` values and no `var()` at all, frozen
319
- at build time — see [`toShadcn()`](#programmatic-api).
271
+ Material and shadcn picked the same name for three things — `background`,
272
+ `primary`, `secondary`. The plugin's colors are theme defaults, so on those
273
+ three shadcn's `@theme inline` wins, and the utility goes through the mapping
274
+ above:
320
275
 
321
- <details>
322
- <summary>Install the mapping alone, without generating anything</summary>
276
+ ```
277
+ bg-secondary → --color-secondary → var(--secondary) → var(--md-sys-color-secondary-container)
278
+ ```
323
279
 
324
- The package publishes one too, so `shadcn add` has something to fetch without a
325
- build step of yours:
280
+ Without shadcn it is one hop shorter, and lands on the role of the same name:
326
281
 
327
- ```sh
328
- $ npx shadcn@latest add https://unpkg.com/material-theme-builder/registry-item.json
282
+ ```
283
+ bg-secondary → --color-secondary → var(--md-sys-color-secondary)
329
284
  ```
330
285
 
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.
286
+ Same destination either way, M3 — just not the same role. And only for
287
+ `secondary`: `primary` maps to `primary`, and M3 `background` and `surface` are
288
+ the same color.
337
289
 
338
- </details>
290
+ If you ever want the M3 role itself, `<Mtb>` still emits it:
339
291
 
340
- <details>
341
- <summary>Rather import a stylesheet than let the CLI edit your file</summary>
292
+ ```html
293
+ <div class="bg-[var(--md-sys-color-secondary)]"></div>
294
+ ```
342
295
 
343
- A stylesheet is shipped too, for setups that would rather keep the mapping in
344
- one line they can delete:
296
+ or give it a name of its own:
345
297
 
346
298
  ```css
347
- @import "tailwindcss";
348
- @import "./shadcn.css"; /* shadcn's own `:root` and `.dark` */
349
- @import "material-theme-builder/shadcn.css"; /* ...then ours */
299
+ @theme inline {
300
+ --color-m3-secondary: var(--md-sys-color-secondary);
301
+ }
350
302
  ```
351
303
 
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
304
  </details>
368
305
 
369
306
  <details>
370
307
  <summary>The variables it remaps</summary>
371
308
 
372
309
  Both halves are generated from [`toShadcnAliases()`](#programmatic-api) and
373
- [`toShadcnRegistryItem()`](#programmatic-api), off one mapping, so neither can
374
- drift from the other:
310
+ [`toShadcnRegistryItem()`](#programmatic-api), off one mapping, so they cannot
311
+ drift. The selectors are doubled so the block outranks shadcn's own `:root` and
312
+ `.dark` on
313
+ [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Specificity#increasing_specificity_by_duplicating_selector)
314
+ rather than on order — which is what lets the `@import` sit with your others.
375
315
 
376
316
  ```css
377
- :root,
378
- .dark {
317
+ :root:root,
318
+ .dark.dark {
379
319
  --background: var(--md-sys-color-surface);
380
320
  --foreground: var(--md-sys-color-on-surface);
381
321
  --card: var(--md-sys-color-surface-container-low);
@@ -412,12 +352,106 @@ drift from the other:
412
352
 
413
353
  </details>
414
354
 
355
+ ### `shadcn-apply`
356
+
357
+ The alternative, for colors to fall back on and no import to place. One command,
358
+ from inside your project:
359
+
360
+ ```sh
361
+ $ npx material-theme-builder shadcn-apply "#6750A4"
362
+ ```
363
+
364
+ From nothing at all, scaffold with shadcn's own CLI first — what this repo
365
+ dogfoods:
366
+
367
+ ```sh
368
+ $ npx shadcn@latest init --preset b0 --name material-theme-app
369
+ $ cd material-theme-app && npx material-theme-builder shadcn-apply "#6750A4"
370
+ ```
371
+
372
+ It generates a registry item for your source color and hands it to `shadcn add`,
373
+ which rewrites the values inside your existing `:root` and `.dark` blocks, in
374
+ place. Same mapping as the stylesheet, with that theme's own colors left in as
375
+ the `var()` fallbacks:
376
+
377
+ ```css
378
+ :root {
379
+ --card: var(--md-sys-color-surface-container-low, oklch(0.968 0.012 317.742));
380
+ }
381
+
382
+ .dark {
383
+ --card: var(--md-sys-color-surface-container-low, oklch(0.227 0.01 303.714));
384
+ }
385
+ ```
386
+
387
+ So it works with no `<Mtb>` at all — the fallbacks render the theme statically,
388
+ server-rendered, zero client JS. Your old values are overwritten, not kept
389
+ anywhere: `git diff` is the undo.
390
+
391
+ Both steps by hand, if you would rather:
392
+
393
+ ```sh
394
+ $ npx material-theme-builder "#6750A4" --format registry-item > mtb.json
395
+ $ npx shadcn@latest add ./mtb.json && rm mtb.json
396
+ ```
397
+
398
+ `shadcn-apply` takes every theme option `material-theme-builder` itself takes,
399
+ and they all land in those fallbacks:
400
+
401
+ ```sh
402
+ $ npx material-theme-builder shadcn-apply "#6750A4" --scheme vibrant --contrast 0.5
403
+ ```
404
+
405
+ <details>
406
+ <summary>The rest of the options</summary>
407
+
408
+ `--no-fallback` leaves the fallbacks out, on both — so shadcn's own colors are
409
+ dropped rather than kept in reserve. Nothing then declares those variables
410
+ except an `<Mtb>` or a [`toCss()`](#programmatic-api): without one, they resolve
411
+ to nothing and the components render transparent.
412
+
413
+ `--custom-colors` is the one option missing: shadcn's variable set is fixed, so
414
+ a registry item cannot carry one.
415
+
416
+ Anything after a `--` is forwarded verbatim to `shadcn add`. Our options go
417
+ before it:
418
+
419
+ ```sh
420
+ $ npx material-theme-builder shadcn-apply "#6750A4" -- --overwrite --dry-run
421
+ ```
422
+
423
+ > [!NOTE]
424
+ >
425
+ > shadcn's CLI also appends a self-referential `--card: var(--card);` per
426
+ > variable to your `@theme inline` block. Noise, not a bug: they land _above_
427
+ > your `:root`, so the real values win. Delete them if they bother you.
428
+
429
+ </details>
430
+
431
+ <details>
432
+ <summary>Install the mapping alone, without generating anything</summary>
433
+
434
+ The package publishes a registry item too, so `shadcn add` has something to
435
+ fetch without a build step:
436
+
437
+ ```sh
438
+ $ npx shadcn@latest add https://unpkg.com/material-theme-builder/registry-item.json
439
+ ```
440
+
441
+ It is the stylesheet's content, installed the registry way: the mapping and
442
+ nothing else, no colors to fall back on. Generate your own, as above, to have
443
+ some.
444
+
445
+ </details>
446
+
415
447
  <details>
416
- <summary>mapping details</summary>
417
- see:
418
-
419
- - https://chatgpt.com/share/6899f20a-422c-8011-a072-62fb649589a0
420
- - https://gemini.google.com/share/51e072b6f1d2
448
+ <summary>mapping details</summary>
449
+
450
+ see:
451
+
452
+ - https://chatgpt.com/share/6899f20a-422c-8011-a072-62fb649589a0
453
+ - https://gemini.google.com/share/51e072b6f1d2
454
+
421
455
  </details>
422
456
 
423
457
  # Dev
@@ -460,18 +494,17 @@ $ pnpm run lgtm
460
494
  ## CONTRIBUTING
461
495
 
462
496
  ```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
497
+ pnpm run storybook # the day-to-day loop -- no build needed, `shadcn.css` regenerates as you edit
498
+ pnpm run build # dist/, plus the generated files -- all gitignored
465
499
  pnpm run lgtm # everything CI checks
466
500
  ```
467
501
 
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.
502
+ `shadcn.css` and `registry-item.json` are generated — from `toShadcnAliases()`
503
+ and `toShadcnRegistryItem()` — and gitignored. `pnpm run build` writes them
504
+ (`scripts/generate.mjs`); `shadcn.css` also gets a `src/` copy, which is what
505
+ Storybook `@import`s, and in Storybook a Vite plugin (`.storybook/main.ts`)
506
+ rewrites it at server start and again on every edit under `src/lib/`, so the
507
+ stories never show a stale vocabulary.
475
508
 
476
509
  `generate.mjs` builds the registry item without `{ fallback: true }`, which is
477
510
  what keeps every one of those outputs a function of the _mapping_ rather than of