@bug-on/m3-expressive 1.1.0 → 1.2.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/dist/core.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import * as React11 from 'react';
3
3
  import { createContext, useState, useEffect, useRef, useCallback, useMemo, useContext } from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
- import { argbFromHex, themeFromSourceColor, hexFromArgb } from '@material/material-color-utilities';
5
+ import { hexFromArgb, Hct, SchemeExpressive, SchemeNeutral, SchemeMonochrome, SchemeContent, SchemeFidelity, SchemeVibrant, SchemeTonalSpot } from '@material/material-color-utilities';
6
6
  import { clsx } from 'clsx';
7
7
  import { twMerge } from 'tailwind-merge';
8
8
  import { LazyMotion, domMax, m, useMotionValue, animate, useAnimationFrame, AnimatePresence, useReducedMotion } from 'motion/react';
@@ -151,66 +151,110 @@ function resolveMode(mode) {
151
151
  if (typeof window === "undefined") return "light";
152
152
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
153
153
  }
154
- function generateM3Theme(sourceColorHex, mode = "light") {
155
- const sourceColor = argbFromHex(sourceColorHex);
156
- const theme = themeFromSourceColor(sourceColor);
157
- const scheme = mode === "light" ? theme.schemes.light : theme.schemes.dark;
158
- const palettes = theme.palettes;
159
- const tone = (palette, t) => hexFromArgb(palette.tone(t));
154
+ function createDynamicScheme(sourceColorHex, isDark, variant = "expressive", contrastLevel = 0) {
155
+ const hct = Hct.fromInt(argbFromHex(sourceColorHex));
156
+ const spec = "2025";
157
+ switch (variant) {
158
+ case "tonal_spot":
159
+ return new SchemeTonalSpot(hct, isDark, contrastLevel, spec);
160
+ case "vibrant":
161
+ return new SchemeVibrant(hct, isDark, contrastLevel, spec);
162
+ case "fidelity":
163
+ return new SchemeFidelity(hct, isDark, contrastLevel, spec);
164
+ case "content":
165
+ return new SchemeContent(hct, isDark, contrastLevel, spec);
166
+ case "monochrome":
167
+ return new SchemeMonochrome(hct, isDark, contrastLevel, spec);
168
+ case "neutral":
169
+ return new SchemeNeutral(hct, isDark, contrastLevel, spec);
170
+ default:
171
+ return new SchemeExpressive(hct, isDark, contrastLevel, spec);
172
+ }
173
+ }
174
+ function argbFromHex(hex) {
175
+ const sanitized = hex.replace(/^#/, "");
176
+ const r = Number.parseInt(sanitized.substring(0, 2), 16);
177
+ const g = Number.parseInt(sanitized.substring(2, 4), 16);
178
+ const b = Number.parseInt(sanitized.substring(4, 6), 16);
179
+ return (4278190080 | r << 16 | g << 8 | b) >>> 0;
180
+ }
181
+ function generateM3Theme(sourceColorHex, mode = "light", options = {}) {
182
+ const { variant = "expressive", contrastLevel = 0 } = options;
183
+ const isDark = mode === "dark";
184
+ const scheme = createDynamicScheme(
185
+ sourceColorHex,
186
+ isDark,
187
+ variant,
188
+ contrastLevel
189
+ );
190
+ const hex = (argb) => hexFromArgb(argb);
160
191
  return {
161
- primary: hexFromArgb(scheme.primary),
162
- onPrimary: hexFromArgb(scheme.onPrimary),
163
- primaryContainer: hexFromArgb(scheme.primaryContainer),
164
- onPrimaryContainer: hexFromArgb(scheme.onPrimaryContainer),
165
- inversePrimary: hexFromArgb(scheme.inversePrimary),
166
- primaryFixed: tone(palettes.primary, 90),
167
- primaryFixedDim: tone(palettes.primary, 80),
168
- onPrimaryFixed: tone(palettes.primary, 10),
169
- onPrimaryFixedVariant: tone(palettes.primary, 30),
170
- secondary: hexFromArgb(scheme.secondary),
171
- onSecondary: hexFromArgb(scheme.onSecondary),
172
- secondaryContainer: hexFromArgb(scheme.secondaryContainer),
173
- onSecondaryContainer: hexFromArgb(scheme.onSecondaryContainer),
174
- secondaryFixed: tone(palettes.secondary, 90),
175
- secondaryFixedDim: tone(palettes.secondary, 80),
176
- onSecondaryFixed: tone(palettes.secondary, 10),
177
- onSecondaryFixedVariant: tone(palettes.secondary, 30),
178
- tertiary: hexFromArgb(scheme.tertiary),
179
- onTertiary: hexFromArgb(scheme.onTertiary),
180
- tertiaryContainer: hexFromArgb(scheme.tertiaryContainer),
181
- onTertiaryContainer: hexFromArgb(scheme.onTertiaryContainer),
182
- tertiaryFixed: tone(palettes.tertiary, 90),
183
- tertiaryFixedDim: tone(palettes.tertiary, 80),
184
- onTertiaryFixed: tone(palettes.tertiary, 10),
185
- onTertiaryFixedVariant: tone(palettes.tertiary, 30),
186
- error: hexFromArgb(scheme.error),
187
- onError: hexFromArgb(scheme.onError),
188
- errorContainer: hexFromArgb(scheme.errorContainer),
189
- onErrorContainer: hexFromArgb(scheme.onErrorContainer),
190
- surface: hexFromArgb(scheme.surface),
191
- onSurface: hexFromArgb(scheme.onSurface),
192
- surfaceVariant: hexFromArgb(scheme.surfaceVariant),
193
- onSurfaceVariant: hexFromArgb(scheme.onSurfaceVariant),
194
- surfaceTint: hexFromArgb(scheme.primary),
195
- // Surface container roles from neutral palette tones
196
- surfaceContainerLowest: mode === "light" ? tone(palettes.neutral, 100) : tone(palettes.neutral, 4),
197
- surfaceContainerLow: mode === "light" ? tone(palettes.neutral, 96) : tone(palettes.neutral, 10),
198
- surfaceContainer: mode === "light" ? tone(palettes.neutral, 94) : tone(palettes.neutral, 12),
199
- surfaceContainerHigh: mode === "light" ? tone(palettes.neutral, 92) : tone(palettes.neutral, 17),
200
- surfaceContainerHighest: mode === "light" ? tone(palettes.neutral, 90) : tone(palettes.neutral, 22),
201
- inverseSurface: hexFromArgb(scheme.inverseSurface),
202
- inverseOnSurface: hexFromArgb(scheme.inverseOnSurface),
203
- background: hexFromArgb(scheme.background),
204
- onBackground: hexFromArgb(scheme.onBackground),
205
- outline: hexFromArgb(scheme.outline),
206
- outlineVariant: hexFromArgb(scheme.outlineVariant),
207
- shadow: hexFromArgb(scheme.shadow),
208
- scrim: hexFromArgb(scheme.scrim)
192
+ // Primary
193
+ primary: hex(scheme.primary),
194
+ onPrimary: hex(scheme.onPrimary),
195
+ primaryContainer: hex(scheme.primaryContainer),
196
+ onPrimaryContainer: hex(scheme.onPrimaryContainer),
197
+ inversePrimary: hex(scheme.inversePrimary),
198
+ primaryFixed: hex(scheme.primaryFixed),
199
+ primaryFixedDim: hex(scheme.primaryFixedDim),
200
+ onPrimaryFixed: hex(scheme.onPrimaryFixed),
201
+ onPrimaryFixedVariant: hex(scheme.onPrimaryFixedVariant),
202
+ primaryDim: hex(scheme.primaryDim),
203
+ // Secondary
204
+ secondary: hex(scheme.secondary),
205
+ onSecondary: hex(scheme.onSecondary),
206
+ secondaryContainer: hex(scheme.secondaryContainer),
207
+ onSecondaryContainer: hex(scheme.onSecondaryContainer),
208
+ secondaryFixed: hex(scheme.secondaryFixed),
209
+ secondaryFixedDim: hex(scheme.secondaryFixedDim),
210
+ onSecondaryFixed: hex(scheme.onSecondaryFixed),
211
+ onSecondaryFixedVariant: hex(scheme.onSecondaryFixedVariant),
212
+ secondaryDim: hex(scheme.secondaryDim),
213
+ // Tertiary
214
+ tertiary: hex(scheme.tertiary),
215
+ onTertiary: hex(scheme.onTertiary),
216
+ tertiaryContainer: hex(scheme.tertiaryContainer),
217
+ onTertiaryContainer: hex(scheme.onTertiaryContainer),
218
+ tertiaryFixed: hex(scheme.tertiaryFixed),
219
+ tertiaryFixedDim: hex(scheme.tertiaryFixedDim),
220
+ onTertiaryFixed: hex(scheme.onTertiaryFixed),
221
+ onTertiaryFixedVariant: hex(scheme.onTertiaryFixedVariant),
222
+ tertiaryDim: hex(scheme.tertiaryDim),
223
+ // Error
224
+ error: hex(scheme.error),
225
+ onError: hex(scheme.onError),
226
+ errorContainer: hex(scheme.errorContainer),
227
+ onErrorContainer: hex(scheme.onErrorContainer),
228
+ // Surface
229
+ surface: hex(scheme.surface),
230
+ onSurface: hex(scheme.onSurface),
231
+ surfaceVariant: hex(scheme.surfaceVariant),
232
+ onSurfaceVariant: hex(scheme.onSurfaceVariant),
233
+ surfaceTint: hex(scheme.surfaceTint),
234
+ surfaceDim: hex(scheme.surfaceDim),
235
+ surfaceBright: hex(scheme.surfaceBright),
236
+ surfaceContainerLowest: hex(scheme.surfaceContainerLowest),
237
+ surfaceContainerLow: hex(scheme.surfaceContainerLow),
238
+ surfaceContainer: hex(scheme.surfaceContainer),
239
+ surfaceContainerHigh: hex(scheme.surfaceContainerHigh),
240
+ surfaceContainerHighest: hex(scheme.surfaceContainerHighest),
241
+ // Inverse
242
+ inverseSurface: hex(scheme.inverseSurface),
243
+ inverseOnSurface: hex(scheme.inverseOnSurface),
244
+ // Background
245
+ background: hex(scheme.background),
246
+ onBackground: hex(scheme.onBackground),
247
+ // Outline
248
+ outline: hex(scheme.outline),
249
+ outlineVariant: hex(scheme.outlineVariant),
250
+ // Utility
251
+ shadow: hex(scheme.shadow),
252
+ scrim: hex(scheme.scrim)
209
253
  };
210
254
  }
211
- function applyTheme(sourceColorHex, mode = "light", root = document.documentElement) {
255
+ function applyTheme(sourceColorHex, mode = "light", root = document.documentElement, options = {}) {
212
256
  const resolved = resolveMode(mode);
213
- const colors = generateM3Theme(sourceColorHex, resolved);
257
+ const colors = generateM3Theme(sourceColorHex, resolved, options);
214
258
  for (const [key, value] of Object.entries(colors)) {
215
259
  const kebabKey = key.replace(/[A-Z]/g, (m7) => `-${m7.toLowerCase()}`);
216
260
  root.style.setProperty(`--md-sys-color-${kebabKey}`, value);
@@ -2469,6 +2513,8 @@ function MD3ThemeProvider({
2469
2513
  sourceColor: initialSourceColor = "#6750A4",
2470
2514
  defaultMode = "light",
2471
2515
  persistToLocalStorage = false,
2516
+ variant = "expressive",
2517
+ contrastLevel = 0,
2472
2518
  typography: typographyProp,
2473
2519
  fontFamily,
2474
2520
  fontVariationAxes,
@@ -2477,6 +2523,17 @@ function MD3ThemeProvider({
2477
2523
  const [sourceColor, setSourceColor] = useState(initialSourceColor);
2478
2524
  const [mode, setMode] = useState(defaultMode);
2479
2525
  const [isHydrated, setIsHydrated] = useState(!persistToLocalStorage);
2526
+ const [systemMode, setSystemMode] = useState("light");
2527
+ useEffect(() => {
2528
+ if (typeof window === "undefined") return;
2529
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2530
+ const updateSystemMode = () => {
2531
+ setSystemMode(mediaQuery.matches ? "dark" : "light");
2532
+ };
2533
+ updateSystemMode();
2534
+ mediaQuery.addEventListener("change", updateSystemMode);
2535
+ return () => mediaQuery.removeEventListener("change", updateSystemMode);
2536
+ }, []);
2480
2537
  useEffect(() => {
2481
2538
  if (!persistToLocalStorage) return;
2482
2539
  const savedColor = localStorage.getItem(STORAGE_KEY_COLOR);
@@ -2488,25 +2545,37 @@ function MD3ThemeProvider({
2488
2545
  setMode(savedMode);
2489
2546
  setIsHydrated(true);
2490
2547
  }, [persistToLocalStorage]);
2548
+ const effectiveMode = mode === "system" ? systemMode : mode;
2491
2549
  useEffect(() => {
2492
2550
  if (!isHydrated) return;
2493
- applyTheme(sourceColor, mode);
2551
+ applyTheme(sourceColor, effectiveMode, document.documentElement, {
2552
+ variant,
2553
+ contrastLevel
2554
+ });
2494
2555
  if (persistToLocalStorage) {
2495
2556
  localStorage.setItem(STORAGE_KEY_COLOR, sourceColor);
2496
2557
  localStorage.setItem(STORAGE_KEY_MODE, mode);
2497
2558
  }
2498
- }, [sourceColor, mode, persistToLocalStorage, isHydrated]);
2499
- useEffect(() => {
2500
- if (mode !== "system" || typeof window === "undefined") return;
2501
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2502
- const handleChange = () => applyTheme(sourceColor, "system");
2503
- mediaQuery.addEventListener("change", handleChange);
2504
- return () => mediaQuery.removeEventListener("change", handleChange);
2505
- }, [mode, sourceColor]);
2506
- const effectiveMode = resolveMode(mode);
2559
+ }, [
2560
+ sourceColor,
2561
+ mode,
2562
+ effectiveMode,
2563
+ persistToLocalStorage,
2564
+ isHydrated,
2565
+ variant,
2566
+ contrastLevel
2567
+ ]);
2507
2568
  const themeValue = useMemo(
2508
- () => ({ sourceColor, setSourceColor, mode, setMode, effectiveMode }),
2509
- [sourceColor, mode, effectiveMode]
2569
+ () => ({
2570
+ sourceColor,
2571
+ setSourceColor,
2572
+ mode,
2573
+ setMode,
2574
+ effectiveMode,
2575
+ variant,
2576
+ contrastLevel
2577
+ }),
2578
+ [sourceColor, mode, effectiveMode, variant, contrastLevel]
2510
2579
  );
2511
2580
  const typographyValue = useMemo(() => {
2512
2581
  if (typographyProp) return typographyProp;