@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.d.ts CHANGED
@@ -1,422 +1,6 @@
1
- import * as React$1 from 'react';
2
- import { ReactNode } from 'react';
3
- import * as react_jsx_runtime from 'react/jsx-runtime';
4
- import { ClassValue } from 'clsx';
5
- export { M as MD3ColorStyle, a as MD3Shape, b as MD3Size, P as PolymorphicProps, c as PolymorphicRef } from './md3-DFhj-NZj.js';
6
- import { a as Typography, F as FontVariationAxes } from './typography-339RV6v7.js';
7
-
8
- /**
9
- * useMediaQuery — SSR-safe responsive hook.
10
- *
11
- * Prevents hydration mismatch by initializing with `false`.
12
- *
13
- * @example
14
- * ```tsx
15
- * const isMobile = useMediaQuery('(max-width: 768px)');
16
- * ```
17
- */
18
- declare function useMediaQuery(query: string): boolean;
19
-
20
- interface RippleOptions {
21
- /** Ripple animation duration (ms). Default: 600 */
22
- duration?: number;
23
- /** Ripple color. Default: 'currentColor' */
24
- color?: string;
25
- /** Opacity. Default: 0.12 (MD3 standard) */
26
- opacity?: number;
27
- /** Disable ripple (e.g. when component is disabled) */
28
- disabled?: boolean;
29
- }
30
- /**
31
- * useRipple — Material Design 3 Expressive Ripple Effect
32
- *
33
- * Pure DOM hook with zero external animation dependencies.
34
- * Note: Requires element to have `position: relative` and `overflow: hidden`.
35
- *
36
- * @example
37
- * ```tsx
38
- * const { rippleRef, onPointerDown } = useRipple();
39
- * <button ref={rippleRef} onPointerDown={onPointerDown}>Click me</button>
40
- * ```
41
- */
42
- declare function useRipple$1<T extends HTMLElement = HTMLElement>(options?: RippleOptions): {
43
- rippleRef: React$1.RefObject<T | null>;
44
- onPointerDown: (event: React.PointerEvent<T>) => void;
45
- };
46
-
47
- /**
48
- * MaterialSymbolsPreconnect
49
- *
50
- * Injects preconnect resource hints for Google Fonts CDN into <head>.
51
- * Place this component AS EARLY AS POSSIBLE in the app tree, ideally
52
- * directly inside <head> or root layout.
53
- *
54
- * WHY THIS MATTERS:
55
- * If @import url() is inside a CSS file, browser must:
56
- * 1. Parse HTML -> download JS bundle -> execute CSS -> hit @import -> start Google Fonts connection
57
- * Preconnect hints allow browser to establish TCP handshake + TLS connection early at step 1,
58
- * saving 100-500ms connection latency depending on network.
59
- *
60
- * USAGE:
61
- * ```tsx
62
- * // app/layout.tsx (Next.js) or index.html equivalent
63
- * import { MaterialSymbolsPreconnect } from '@bug-on/m3-expressive';
64
- *
65
- * export default function RootLayout({ children }) {
66
- * return (
67
- * <html>
68
- * <head>
69
- * <MaterialSymbolsPreconnect />
70
- * </head>
71
- * <body>{children}</body>
72
- * </html>
73
- * );
74
- * }
75
- * ```
76
- *
77
- * NOTE: Use this component only with CDN mode.
78
- * Self-hosted fonts do not require preconnecting to external origins.
79
- */
80
- interface MaterialSymbolsPreconnectProps {
81
- /**
82
- * Array of Material Symbols font variants to load.
83
- * Include only variants used by application to minimize bandwidth.
84
- * @default ["outlined"]
85
- */
86
- variants?: Array<"outlined" | "rounded" | "sharp">;
87
- }
88
- declare function MaterialSymbolsPreconnect({ variants, }: MaterialSymbolsPreconnectProps): react_jsx_runtime.JSX.Element;
89
-
90
- type ThemeMode = "light" | "dark" | "system";
91
- /**
92
- * Resolves the effective color scheme from a ThemeMode.
93
- * When mode is "system", reads the OS preference via matchMedia.
94
- * Returns "light" as the safe default in SSR environments.
95
- */
96
- declare function resolveMode(mode: ThemeMode): "light" | "dark";
97
- interface MD3ColorScheme {
98
- primary: string;
99
- onPrimary: string;
100
- primaryContainer: string;
101
- onPrimaryContainer: string;
102
- inversePrimary: string;
103
- primaryFixed: string;
104
- primaryFixedDim: string;
105
- onPrimaryFixed: string;
106
- onPrimaryFixedVariant: string;
107
- secondary: string;
108
- onSecondary: string;
109
- secondaryContainer: string;
110
- onSecondaryContainer: string;
111
- secondaryFixed: string;
112
- secondaryFixedDim: string;
113
- onSecondaryFixed: string;
114
- onSecondaryFixedVariant: string;
115
- tertiary: string;
116
- onTertiary: string;
117
- tertiaryContainer: string;
118
- onTertiaryContainer: string;
119
- tertiaryFixed: string;
120
- tertiaryFixedDim: string;
121
- onTertiaryFixed: string;
122
- onTertiaryFixedVariant: string;
123
- error: string;
124
- onError: string;
125
- errorContainer: string;
126
- onErrorContainer: string;
127
- surface: string;
128
- onSurface: string;
129
- surfaceVariant: string;
130
- onSurfaceVariant: string;
131
- surfaceTint: string;
132
- surfaceContainerLowest: string;
133
- surfaceContainerLow: string;
134
- surfaceContainer: string;
135
- surfaceContainerHigh: string;
136
- surfaceContainerHighest: string;
137
- inverseSurface: string;
138
- inverseOnSurface: string;
139
- background: string;
140
- onBackground: string;
141
- outline: string;
142
- outlineVariant: string;
143
- shadow: string;
144
- scrim: string;
145
- }
146
- /**
147
- * Generate a complete MD3 color scheme from a source color hex string.
148
- * Uses the HCT color space algorithm — same as Material You on Android.
149
- */
150
- declare function generateM3Theme(sourceColorHex: string, mode?: "light" | "dark"): MD3ColorScheme;
151
- /**
152
- * Apply an MD3 dynamic color scheme to the document root as CSS custom properties.
153
- * Sets both `--md-sys-color-*` tokens (used by components) and
154
- * `--color-m3-*` tokens (used by Tailwind arbitrary values in apps).
155
- *
156
- * Also sets `data-theme` attribute for dark mode CSS selectors.
157
- */
158
- declare function applyTheme(sourceColorHex: string, mode?: ThemeMode, root?: HTMLElement): void;
159
-
160
- declare function cn(...inputs: ClassValue[]): string;
161
-
162
- /**
163
- * Props cho component {@link Icon}.
164
- *
165
- * All variable font axes are mapped directly to `font-variation-settings`.
166
- */
167
- interface IconProps extends React$1.HTMLAttributes<HTMLSpanElement> {
168
- /**
169
- * Name of the Material Symbol in snake_case format.
170
- * @example "home", "arrow_forward", "settings"
171
- * @see https://fonts.google.com/icons
172
- */
173
- name: string;
174
- /**
175
- * Geometric style variant — maps to font family.
176
- * @default "outlined"
177
- */
178
- variant?: "outlined" | "rounded" | "sharp";
179
- /**
180
- * `FILL` axis. `0` = outlined, `1` = filled.
181
- * Features spring animation when `animateFill` is true.
182
- * @default 0
183
- */
184
- fill?: 0 | 1;
185
- /**
186
- * `wght` axis — stroke weight. Should match surrounding text weight.
187
- * @default 400
188
- */
189
- weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700;
190
- /**
191
- * `GRAD` axis — fine-tunes visual weight without affecting layout dimensions.
192
- * Use `-25` on dark backgrounds to offset light halation effects.
193
- * @default 0
194
- */
195
- grade?: -50 | -25 | 0 | 100 | 200;
196
- /**
197
- * `opsz` axis — optical size in dp. Used to set `font-size` if `size` is not specified.
198
- * Match rendered pixel size for best quality.
199
- * @default 24
200
- */
201
- opticalSize?: 20 | 24 | 40 | 48;
202
- /**
203
- * Direct `font-size` override in px. `opsz` axis still respects `opticalSize`.
204
- * @example size={18} opticalSize={20}
205
- */
206
- size?: number | "inherit";
207
- /**
208
- * Enables smooth spring animation when toggling FILL axis (uses `SPRING_TRANSITION_FAST`).
209
- * Requires `motion` dependency.
210
- * @default false
211
- * @example <Icon name="favorite" fill={isLiked ? 1 : 0} animateFill />
212
- */
213
- animateFill?: boolean;
214
- }
215
- /**
216
- * Material Symbols (variable font) Icon component.
217
- *
218
- * Make sure to import the icon font CSS before usage:
219
- * ```ts
220
- * import '@bug-on/m3-expressive/material-symbols-cdn.css';
221
- * ```
222
- *
223
- * @remarks
224
- * - Use snake_case for icon names: `"arrow_forward"`, NOT `"ArrowForward"`.
225
- * - `aria-hidden="true"` is attached automatically — provide screen reader labels on parent elements.
226
- *
227
- * @example
228
- * ```tsx
229
- * // Basic Icon
230
- * <Icon name="home" />
231
- *
232
- * // Visual customization (filled, heavy weight)
233
- * <Icon name="favorite" variant="rounded" fill={1} weight={300} />
234
- *
235
- * // Animated transition on state change
236
- * <Icon name="bookmark" fill={saved ? 1 : 0} animateFill />
237
- *
238
- * // Custom size override
239
- * <Icon name="close" size={18} opticalSize={20} />
240
- *
241
- * // Combination with other components
242
- * <Button icon={<Icon name="add" />}>Add to cart</Button>
243
- * ```
244
- *
245
- * @see https://fonts.google.com/icons
246
- * @see https://m3.material.io/styles/icons/overview
247
- */
248
- declare const Icon: React$1.NamedExoticComponent<IconProps & React$1.RefAttributes<HTMLSpanElement>>;
249
-
250
- /**
251
- * Represents a single ripple wave instance with position and size metadata.
252
- *
253
- * @example
254
- * ```tsx
255
- * const ripple: RippleOrigin = { id: Date.now(), x: 50, y: 30, size: 200 };
256
- * ```
257
- */
258
- interface RippleOrigin {
259
- /** Unique identifier used as React key and for removal. */
260
- id: number;
261
- /** X coordinate of the pointer event relative to the container's left edge (px). */
262
- x: number;
263
- /** Y coordinate of the pointer event relative to the container's top edge (px). */
264
- y: number;
265
- /**
266
- * Diameter of the ripple circle (px).
267
- * Typically `Math.hypot(width, height) * 2` to ensure it fills the container.
268
- */
269
- size: number;
270
- }
271
- /**
272
- * Props for the `Ripple` presentation component.
273
- */
274
- interface RippleProps {
275
- /** Active ripple instances to render. Managed by the parent via `useRipple`. */
276
- ripples: RippleOrigin[];
277
- /** Called when a ripple's exit animation completes — remove it from state. */
278
- onRippleDone: (id: number) => void;
279
- /**
280
- * Completely disables the ripple effect.
281
- * Use this when the parent element is disabled or interaction is not desired.
282
- * @default false
283
- */
284
- disabled?: boolean;
285
- /**
286
- * When `true`, the ripple respects the user's OS-level
287
- * `prefers-reduced-motion` accessibility setting and renders nothing if active.
288
- *
289
- * Set to `false` to always show ripples regardless of system preference.
290
- * @default true
291
- */
292
- respectSystemMotion?: boolean;
293
- }
294
- /**
295
- * MD3 Expressive Ripple — animated touch-feedback wave layer.
296
- *
297
- * Renders absolutely-positioned ripple circles inside an `overflow-hidden`
298
- * container. Must be placed as a direct child of the interactive element.
299
- *
300
- * @remarks
301
- * - The parent element **must** have `overflow: hidden` and `position: relative`
302
- * (or equivalent) for clipping to work correctly.
303
- * - Set `disabled` to `true` on parent's disabled state to avoid stale ripples.
304
- * - The ripple color is `currentColor` at 12% opacity — matching MD3 state layer spec.
305
- *
306
- * @example
307
- * ```tsx
308
- * const { ripples, onPointerDown, removeRipple } = useRippleState();
309
- *
310
- * <button onPointerDown={onPointerDown} className="relative overflow-hidden">
311
- * <Ripple ripples={ripples} onRippleDone={removeRipple} />
312
- * Click me
313
- * </button>
314
- * ```
315
- *
316
- * @see {@link useRippleState} for the state management hook
317
- * @see https://m3.material.io/foundations/interaction/states/overview
318
- */
319
- declare function Ripple({ ripples, onRippleDone, disabled, respectSystemMotion, }: RippleProps): react_jsx_runtime.JSX.Element | null;
320
- /**
321
- * Options for configuring `useRippleState` behaviour.
322
- */
323
- interface UseRippleStateOptions {
324
- /**
325
- * When `true`, the ripple is suppressed — `onPointerDown` becomes a no-op.
326
- * Use this to sync the ripple with the parent element's `disabled` state.
327
- * @default false
328
- */
329
- disabled?: boolean;
330
- }
331
- /**
332
- * `useRippleState` — state manager for MD3 Expressive ripple waves.
333
- *
334
- * Tracks active ripple instances and provides pointer event handlers.
335
- * Pair with the `<Ripple>` component for rendering.
336
- *
337
- * @remarks
338
- * This hook only manages ripple *state* (coordinates, size, lifecycle).
339
- * The actual animation is handled by `<Ripple>` via Framer Motion.
340
- * Respecting `prefers-reduced-motion` is handled by `<Ripple>` itself.
341
- *
342
- * @param options - Configuration options. See {@link UseRippleStateOptions}.
343
- * @returns `{ ripples, onPointerDown, removeRipple }` — bind to the interactive element.
344
- *
345
- * @example
346
- * ```tsx
347
- * function MyButton({ disabled, children }) {
348
- * const { ripples, onPointerDown, removeRipple } = useRippleState({ disabled });
349
- *
350
- * return (
351
- * <button
352
- * disabled={disabled}
353
- * onPointerDown={onPointerDown}
354
- * className="relative overflow-hidden"
355
- * >
356
- * <Ripple ripples={ripples} onRippleDone={removeRipple} disabled={disabled} />
357
- * {children}
358
- * </button>
359
- * );
360
- * }
361
- * ```
362
- *
363
- * @see {@link Ripple} for the rendering component
364
- */
365
- declare function useRippleState(options?: UseRippleStateOptions): {
366
- ripples: RippleOrigin[];
367
- onPointerDown: (e: React$1.PointerEvent<HTMLElement>) => void;
368
- removeRipple: (id: number) => void;
369
- };
370
- /**
371
- * @deprecated Use `useRippleState` instead. This alias will be removed in a future version.
372
- * @see {@link useRippleState}
373
- */
374
- declare const useRipple: typeof useRippleState;
375
-
376
- interface ThemeContextValue {
377
- sourceColor: string;
378
- setSourceColor: (color: string) => void;
379
- mode: ThemeMode;
380
- setMode: (mode: ThemeMode) => void;
381
- /** The resolved color scheme actually applied — always "light" or "dark". */
382
- effectiveMode: "light" | "dark";
383
- }
384
- interface MD3ThemeProviderProps {
385
- children: ReactNode;
386
- sourceColor?: string;
387
- defaultMode?: ThemeMode;
388
- persistToLocalStorage?: boolean;
389
- /**
390
- * A fully custom `Typography` instance.
391
- * When provided, `fontFamily` and `fontVariationAxes` are ignored.
392
- */
393
- typography?: Typography;
394
- /**
395
- * Override the CSS `font-family` for all typography styles.
396
- * Ignored when `typography` prop is provided.
397
- * @example "'Inter', sans-serif"
398
- */
399
- fontFamily?: string;
400
- /**
401
- * Variable font axes applied globally via `font-variation-settings`.
402
- * Merged on top of defaults (`ROND: 100`). Ignored when `typography` is provided.
403
- * @example { ROND: 50 }
404
- */
405
- fontVariationAxes?: FontVariationAxes;
406
- /**
407
- * When `true`, mounts `SnackbarHost` inside the provider and exposes
408
- * `useSnackbar()` to all descendants — no separate `<SnackbarProvider>` needed.
409
- *
410
- * Opt-in, default `false`. For advanced usage (e.g., scoped snackbars or
411
- * custom host positioning), keep this `false` and use `<SnackbarProvider>`
412
- * or `<SnackbarHost>` directly.
413
- *
414
- * @default false
415
- */
416
- enableSnackbar?: boolean;
417
- }
418
- declare function MD3ThemeProvider({ children, sourceColor: initialSourceColor, defaultMode, persistToLocalStorage, typography: typographyProp, fontFamily, fontVariationAxes, enableSnackbar, }: MD3ThemeProviderProps): react_jsx_runtime.JSX.Element;
419
- declare function useTheme(): ThemeContextValue;
420
- declare function useThemeMode(): Pick<ThemeContextValue, "mode" | "setMode" | "effectiveMode">;
421
-
422
- export { Icon, type IconProps, type MD3ColorScheme, MD3ThemeProvider, type MD3ThemeProviderProps, MaterialSymbolsPreconnect, Ripple, type RippleOrigin, type RippleProps, type ThemeMode, type UseRippleStateOptions, applyTheme, cn, generateM3Theme, resolveMode, useRipple$1 as useDOMRipple, useMediaQuery, useRipple, useRippleState, useTheme, useThemeMode };
1
+ export { I as Icon, a as IconProps, M as MD3ColorScheme, b as MD3ThemeProvider, c as MD3ThemeProviderProps, d as MaterialSymbolsPreconnect, R as Ripple, e as RippleOrigin, f as RippleProps, T as ThemeMode, U as UseRippleStateOptions, g as applyTheme, h as cn, i as generateM3Theme, r as resolveMode, u as useDOMRipple, j as useMediaQuery, k as useRipple, l as useRippleState, m as useTheme, n as useThemeMode } from './core-Bc5Wj_pc.js';
2
+ export { M as MD3ColorStyle, a as MD3Shape, b as MD3Size, P as PolymorphicProps, c as PolymorphicRef } from './md3-Dty-Qcad.js';
3
+ import 'react';
4
+ import 'react/jsx-runtime';
5
+ import 'clsx';
6
+ import './typography-339RV6v7.js';
package/dist/core.js CHANGED
@@ -172,66 +172,110 @@ function resolveMode(mode) {
172
172
  if (typeof window === "undefined") return "light";
173
173
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
174
174
  }
175
- function generateM3Theme(sourceColorHex, mode = "light") {
176
- const sourceColor = materialColorUtilities.argbFromHex(sourceColorHex);
177
- const theme = materialColorUtilities.themeFromSourceColor(sourceColor);
178
- const scheme = mode === "light" ? theme.schemes.light : theme.schemes.dark;
179
- const palettes = theme.palettes;
180
- const tone = (palette, t) => materialColorUtilities.hexFromArgb(palette.tone(t));
175
+ function createDynamicScheme(sourceColorHex, isDark, variant = "expressive", contrastLevel = 0) {
176
+ const hct = materialColorUtilities.Hct.fromInt(argbFromHex(sourceColorHex));
177
+ const spec = "2025";
178
+ switch (variant) {
179
+ case "tonal_spot":
180
+ return new materialColorUtilities.SchemeTonalSpot(hct, isDark, contrastLevel, spec);
181
+ case "vibrant":
182
+ return new materialColorUtilities.SchemeVibrant(hct, isDark, contrastLevel, spec);
183
+ case "fidelity":
184
+ return new materialColorUtilities.SchemeFidelity(hct, isDark, contrastLevel, spec);
185
+ case "content":
186
+ return new materialColorUtilities.SchemeContent(hct, isDark, contrastLevel, spec);
187
+ case "monochrome":
188
+ return new materialColorUtilities.SchemeMonochrome(hct, isDark, contrastLevel, spec);
189
+ case "neutral":
190
+ return new materialColorUtilities.SchemeNeutral(hct, isDark, contrastLevel, spec);
191
+ default:
192
+ return new materialColorUtilities.SchemeExpressive(hct, isDark, contrastLevel, spec);
193
+ }
194
+ }
195
+ function argbFromHex(hex) {
196
+ const sanitized = hex.replace(/^#/, "");
197
+ const r = Number.parseInt(sanitized.substring(0, 2), 16);
198
+ const g = Number.parseInt(sanitized.substring(2, 4), 16);
199
+ const b = Number.parseInt(sanitized.substring(4, 6), 16);
200
+ return (4278190080 | r << 16 | g << 8 | b) >>> 0;
201
+ }
202
+ function generateM3Theme(sourceColorHex, mode = "light", options = {}) {
203
+ const { variant = "expressive", contrastLevel = 0 } = options;
204
+ const isDark = mode === "dark";
205
+ const scheme = createDynamicScheme(
206
+ sourceColorHex,
207
+ isDark,
208
+ variant,
209
+ contrastLevel
210
+ );
211
+ const hex = (argb) => materialColorUtilities.hexFromArgb(argb);
181
212
  return {
182
- primary: materialColorUtilities.hexFromArgb(scheme.primary),
183
- onPrimary: materialColorUtilities.hexFromArgb(scheme.onPrimary),
184
- primaryContainer: materialColorUtilities.hexFromArgb(scheme.primaryContainer),
185
- onPrimaryContainer: materialColorUtilities.hexFromArgb(scheme.onPrimaryContainer),
186
- inversePrimary: materialColorUtilities.hexFromArgb(scheme.inversePrimary),
187
- primaryFixed: tone(palettes.primary, 90),
188
- primaryFixedDim: tone(palettes.primary, 80),
189
- onPrimaryFixed: tone(palettes.primary, 10),
190
- onPrimaryFixedVariant: tone(palettes.primary, 30),
191
- secondary: materialColorUtilities.hexFromArgb(scheme.secondary),
192
- onSecondary: materialColorUtilities.hexFromArgb(scheme.onSecondary),
193
- secondaryContainer: materialColorUtilities.hexFromArgb(scheme.secondaryContainer),
194
- onSecondaryContainer: materialColorUtilities.hexFromArgb(scheme.onSecondaryContainer),
195
- secondaryFixed: tone(palettes.secondary, 90),
196
- secondaryFixedDim: tone(palettes.secondary, 80),
197
- onSecondaryFixed: tone(palettes.secondary, 10),
198
- onSecondaryFixedVariant: tone(palettes.secondary, 30),
199
- tertiary: materialColorUtilities.hexFromArgb(scheme.tertiary),
200
- onTertiary: materialColorUtilities.hexFromArgb(scheme.onTertiary),
201
- tertiaryContainer: materialColorUtilities.hexFromArgb(scheme.tertiaryContainer),
202
- onTertiaryContainer: materialColorUtilities.hexFromArgb(scheme.onTertiaryContainer),
203
- tertiaryFixed: tone(palettes.tertiary, 90),
204
- tertiaryFixedDim: tone(palettes.tertiary, 80),
205
- onTertiaryFixed: tone(palettes.tertiary, 10),
206
- onTertiaryFixedVariant: tone(palettes.tertiary, 30),
207
- error: materialColorUtilities.hexFromArgb(scheme.error),
208
- onError: materialColorUtilities.hexFromArgb(scheme.onError),
209
- errorContainer: materialColorUtilities.hexFromArgb(scheme.errorContainer),
210
- onErrorContainer: materialColorUtilities.hexFromArgb(scheme.onErrorContainer),
211
- surface: materialColorUtilities.hexFromArgb(scheme.surface),
212
- onSurface: materialColorUtilities.hexFromArgb(scheme.onSurface),
213
- surfaceVariant: materialColorUtilities.hexFromArgb(scheme.surfaceVariant),
214
- onSurfaceVariant: materialColorUtilities.hexFromArgb(scheme.onSurfaceVariant),
215
- surfaceTint: materialColorUtilities.hexFromArgb(scheme.primary),
216
- // Surface container roles from neutral palette tones
217
- surfaceContainerLowest: mode === "light" ? tone(palettes.neutral, 100) : tone(palettes.neutral, 4),
218
- surfaceContainerLow: mode === "light" ? tone(palettes.neutral, 96) : tone(palettes.neutral, 10),
219
- surfaceContainer: mode === "light" ? tone(palettes.neutral, 94) : tone(palettes.neutral, 12),
220
- surfaceContainerHigh: mode === "light" ? tone(palettes.neutral, 92) : tone(palettes.neutral, 17),
221
- surfaceContainerHighest: mode === "light" ? tone(palettes.neutral, 90) : tone(palettes.neutral, 22),
222
- inverseSurface: materialColorUtilities.hexFromArgb(scheme.inverseSurface),
223
- inverseOnSurface: materialColorUtilities.hexFromArgb(scheme.inverseOnSurface),
224
- background: materialColorUtilities.hexFromArgb(scheme.background),
225
- onBackground: materialColorUtilities.hexFromArgb(scheme.onBackground),
226
- outline: materialColorUtilities.hexFromArgb(scheme.outline),
227
- outlineVariant: materialColorUtilities.hexFromArgb(scheme.outlineVariant),
228
- shadow: materialColorUtilities.hexFromArgb(scheme.shadow),
229
- scrim: materialColorUtilities.hexFromArgb(scheme.scrim)
213
+ // Primary
214
+ primary: hex(scheme.primary),
215
+ onPrimary: hex(scheme.onPrimary),
216
+ primaryContainer: hex(scheme.primaryContainer),
217
+ onPrimaryContainer: hex(scheme.onPrimaryContainer),
218
+ inversePrimary: hex(scheme.inversePrimary),
219
+ primaryFixed: hex(scheme.primaryFixed),
220
+ primaryFixedDim: hex(scheme.primaryFixedDim),
221
+ onPrimaryFixed: hex(scheme.onPrimaryFixed),
222
+ onPrimaryFixedVariant: hex(scheme.onPrimaryFixedVariant),
223
+ primaryDim: hex(scheme.primaryDim),
224
+ // Secondary
225
+ secondary: hex(scheme.secondary),
226
+ onSecondary: hex(scheme.onSecondary),
227
+ secondaryContainer: hex(scheme.secondaryContainer),
228
+ onSecondaryContainer: hex(scheme.onSecondaryContainer),
229
+ secondaryFixed: hex(scheme.secondaryFixed),
230
+ secondaryFixedDim: hex(scheme.secondaryFixedDim),
231
+ onSecondaryFixed: hex(scheme.onSecondaryFixed),
232
+ onSecondaryFixedVariant: hex(scheme.onSecondaryFixedVariant),
233
+ secondaryDim: hex(scheme.secondaryDim),
234
+ // Tertiary
235
+ tertiary: hex(scheme.tertiary),
236
+ onTertiary: hex(scheme.onTertiary),
237
+ tertiaryContainer: hex(scheme.tertiaryContainer),
238
+ onTertiaryContainer: hex(scheme.onTertiaryContainer),
239
+ tertiaryFixed: hex(scheme.tertiaryFixed),
240
+ tertiaryFixedDim: hex(scheme.tertiaryFixedDim),
241
+ onTertiaryFixed: hex(scheme.onTertiaryFixed),
242
+ onTertiaryFixedVariant: hex(scheme.onTertiaryFixedVariant),
243
+ tertiaryDim: hex(scheme.tertiaryDim),
244
+ // Error
245
+ error: hex(scheme.error),
246
+ onError: hex(scheme.onError),
247
+ errorContainer: hex(scheme.errorContainer),
248
+ onErrorContainer: hex(scheme.onErrorContainer),
249
+ // Surface
250
+ surface: hex(scheme.surface),
251
+ onSurface: hex(scheme.onSurface),
252
+ surfaceVariant: hex(scheme.surfaceVariant),
253
+ onSurfaceVariant: hex(scheme.onSurfaceVariant),
254
+ surfaceTint: hex(scheme.surfaceTint),
255
+ surfaceDim: hex(scheme.surfaceDim),
256
+ surfaceBright: hex(scheme.surfaceBright),
257
+ surfaceContainerLowest: hex(scheme.surfaceContainerLowest),
258
+ surfaceContainerLow: hex(scheme.surfaceContainerLow),
259
+ surfaceContainer: hex(scheme.surfaceContainer),
260
+ surfaceContainerHigh: hex(scheme.surfaceContainerHigh),
261
+ surfaceContainerHighest: hex(scheme.surfaceContainerHighest),
262
+ // Inverse
263
+ inverseSurface: hex(scheme.inverseSurface),
264
+ inverseOnSurface: hex(scheme.inverseOnSurface),
265
+ // Background
266
+ background: hex(scheme.background),
267
+ onBackground: hex(scheme.onBackground),
268
+ // Outline
269
+ outline: hex(scheme.outline),
270
+ outlineVariant: hex(scheme.outlineVariant),
271
+ // Utility
272
+ shadow: hex(scheme.shadow),
273
+ scrim: hex(scheme.scrim)
230
274
  };
231
275
  }
232
- function applyTheme(sourceColorHex, mode = "light", root = document.documentElement) {
276
+ function applyTheme(sourceColorHex, mode = "light", root = document.documentElement, options = {}) {
233
277
  const resolved = resolveMode(mode);
234
- const colors = generateM3Theme(sourceColorHex, resolved);
278
+ const colors = generateM3Theme(sourceColorHex, resolved, options);
235
279
  for (const [key, value] of Object.entries(colors)) {
236
280
  const kebabKey = key.replace(/[A-Z]/g, (m7) => `-${m7.toLowerCase()}`);
237
281
  root.style.setProperty(`--md-sys-color-${kebabKey}`, value);
@@ -2490,6 +2534,8 @@ function MD3ThemeProvider({
2490
2534
  sourceColor: initialSourceColor = "#6750A4",
2491
2535
  defaultMode = "light",
2492
2536
  persistToLocalStorage = false,
2537
+ variant = "expressive",
2538
+ contrastLevel = 0,
2493
2539
  typography: typographyProp,
2494
2540
  fontFamily,
2495
2541
  fontVariationAxes,
@@ -2498,6 +2544,17 @@ function MD3ThemeProvider({
2498
2544
  const [sourceColor, setSourceColor] = React11.useState(initialSourceColor);
2499
2545
  const [mode, setMode] = React11.useState(defaultMode);
2500
2546
  const [isHydrated, setIsHydrated] = React11.useState(!persistToLocalStorage);
2547
+ const [systemMode, setSystemMode] = React11.useState("light");
2548
+ React11.useEffect(() => {
2549
+ if (typeof window === "undefined") return;
2550
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2551
+ const updateSystemMode = () => {
2552
+ setSystemMode(mediaQuery.matches ? "dark" : "light");
2553
+ };
2554
+ updateSystemMode();
2555
+ mediaQuery.addEventListener("change", updateSystemMode);
2556
+ return () => mediaQuery.removeEventListener("change", updateSystemMode);
2557
+ }, []);
2501
2558
  React11.useEffect(() => {
2502
2559
  if (!persistToLocalStorage) return;
2503
2560
  const savedColor = localStorage.getItem(STORAGE_KEY_COLOR);
@@ -2509,25 +2566,37 @@ function MD3ThemeProvider({
2509
2566
  setMode(savedMode);
2510
2567
  setIsHydrated(true);
2511
2568
  }, [persistToLocalStorage]);
2569
+ const effectiveMode = mode === "system" ? systemMode : mode;
2512
2570
  React11.useEffect(() => {
2513
2571
  if (!isHydrated) return;
2514
- applyTheme(sourceColor, mode);
2572
+ applyTheme(sourceColor, effectiveMode, document.documentElement, {
2573
+ variant,
2574
+ contrastLevel
2575
+ });
2515
2576
  if (persistToLocalStorage) {
2516
2577
  localStorage.setItem(STORAGE_KEY_COLOR, sourceColor);
2517
2578
  localStorage.setItem(STORAGE_KEY_MODE, mode);
2518
2579
  }
2519
- }, [sourceColor, mode, persistToLocalStorage, isHydrated]);
2520
- React11.useEffect(() => {
2521
- if (mode !== "system" || typeof window === "undefined") return;
2522
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2523
- const handleChange = () => applyTheme(sourceColor, "system");
2524
- mediaQuery.addEventListener("change", handleChange);
2525
- return () => mediaQuery.removeEventListener("change", handleChange);
2526
- }, [mode, sourceColor]);
2527
- const effectiveMode = resolveMode(mode);
2580
+ }, [
2581
+ sourceColor,
2582
+ mode,
2583
+ effectiveMode,
2584
+ persistToLocalStorage,
2585
+ isHydrated,
2586
+ variant,
2587
+ contrastLevel
2588
+ ]);
2528
2589
  const themeValue = React11.useMemo(
2529
- () => ({ sourceColor, setSourceColor, mode, setMode, effectiveMode }),
2530
- [sourceColor, mode, effectiveMode]
2590
+ () => ({
2591
+ sourceColor,
2592
+ setSourceColor,
2593
+ mode,
2594
+ setMode,
2595
+ effectiveMode,
2596
+ variant,
2597
+ contrastLevel
2598
+ }),
2599
+ [sourceColor, mode, effectiveMode, variant, contrastLevel]
2531
2600
  );
2532
2601
  const typographyValue = React11.useMemo(() => {
2533
2602
  if (typographyProp) return typographyProp;