@newtonedev/editor 0.1.12 → 0.2.1

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.
Files changed (116) hide show
  1. package/dist/Editor.d.ts.map +1 -1
  2. package/dist/components/CodeBlock.d.ts.map +1 -1
  3. package/dist/components/ConfiguratorPanel.d.ts +6 -3
  4. package/dist/components/ConfiguratorPanel.d.ts.map +1 -1
  5. package/dist/components/EditorHeader.d.ts +3 -2
  6. package/dist/components/EditorHeader.d.ts.map +1 -1
  7. package/dist/components/EditorShell.d.ts.map +1 -1
  8. package/dist/components/PresetSelector.d.ts +3 -2
  9. package/dist/components/PresetSelector.d.ts.map +1 -1
  10. package/dist/components/PreviewWindow.d.ts.map +1 -1
  11. package/dist/components/RightSidebar.d.ts.map +1 -1
  12. package/dist/components/Sidebar.d.ts +8 -1
  13. package/dist/components/Sidebar.d.ts.map +1 -1
  14. package/dist/components/TableOfContents.d.ts.map +1 -1
  15. package/dist/components/sections/ColorsSection.d.ts +6 -3
  16. package/dist/components/sections/ColorsSection.d.ts.map +1 -1
  17. package/dist/components/sections/DynamicRangeSection.d.ts +2 -2
  18. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
  19. package/dist/components/sections/FontsSection.d.ts +2 -2
  20. package/dist/components/sections/FontsSection.d.ts.map +1 -1
  21. package/dist/components/sections/IconsSection.d.ts +2 -2
  22. package/dist/components/sections/IconsSection.d.ts.map +1 -1
  23. package/dist/components/sections/OthersSection.d.ts +2 -2
  24. package/dist/components/sections/OthersSection.d.ts.map +1 -1
  25. package/dist/components/sections/ScalePlots.d.ts +11 -0
  26. package/dist/components/sections/ScalePlots.d.ts.map +1 -0
  27. package/dist/components/sections/index.d.ts +1 -0
  28. package/dist/components/sections/index.d.ts.map +1 -1
  29. package/dist/configurator/bridge/toCSS.d.ts +7 -0
  30. package/dist/configurator/bridge/toCSS.d.ts.map +1 -0
  31. package/dist/configurator/bridge/toJSON.d.ts +15 -0
  32. package/dist/configurator/bridge/toJSON.d.ts.map +1 -0
  33. package/dist/configurator/bridge/toThemeConfig.d.ts +8 -0
  34. package/dist/configurator/bridge/toThemeConfig.d.ts.map +1 -0
  35. package/dist/configurator/constants.d.ts +13 -0
  36. package/dist/configurator/constants.d.ts.map +1 -0
  37. package/dist/configurator/hex-conversion.d.ts +21 -0
  38. package/dist/configurator/hex-conversion.d.ts.map +1 -0
  39. package/dist/configurator/hooks/useConfigurator.d.ts +11 -0
  40. package/dist/configurator/hooks/useConfigurator.d.ts.map +1 -0
  41. package/dist/configurator/hooks/usePreviewColors.d.ts +8 -0
  42. package/dist/configurator/hooks/usePreviewColors.d.ts.map +1 -0
  43. package/dist/configurator/hooks/useWcagValidation.d.ts +20 -0
  44. package/dist/configurator/hooks/useWcagValidation.d.ts.map +1 -0
  45. package/dist/configurator/hue-conversion.d.ts +10 -0
  46. package/dist/configurator/hue-conversion.d.ts.map +1 -0
  47. package/dist/configurator/state/actions.d.ts +107 -0
  48. package/dist/configurator/state/actions.d.ts.map +1 -0
  49. package/dist/configurator/state/defaults.d.ts +7 -0
  50. package/dist/configurator/state/defaults.d.ts.map +1 -0
  51. package/dist/configurator/state/reducer.d.ts +19 -0
  52. package/dist/configurator/state/reducer.d.ts.map +1 -0
  53. package/dist/configurator/types.d.ts +60 -0
  54. package/dist/configurator/types.d.ts.map +1 -0
  55. package/dist/hooks/useEditorState.d.ts +8 -6
  56. package/dist/hooks/useEditorState.d.ts.map +1 -1
  57. package/dist/hooks/usePresets.d.ts +7 -6
  58. package/dist/hooks/usePresets.d.ts.map +1 -1
  59. package/dist/index.cjs +30372 -808
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.d.ts +17 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +30351 -799
  64. package/dist/index.js.map +1 -1
  65. package/dist/preview/CategoryView.d.ts.map +1 -1
  66. package/dist/preview/ComponentDetailView.d.ts.map +1 -1
  67. package/dist/preview/ComponentRenderer.d.ts.map +1 -1
  68. package/dist/preview/IconBrowserView.d.ts.map +1 -1
  69. package/dist/preview/OverviewView.d.ts.map +1 -1
  70. package/dist/preview/PaletteScaleView.d.ts +11 -0
  71. package/dist/preview/PaletteScaleView.d.ts.map +1 -0
  72. package/dist/types.d.ts +4 -3
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +7 -4
  75. package/src/Editor.tsx +43 -19
  76. package/src/components/CodeBlock.tsx +7 -11
  77. package/src/components/ConfiguratorPanel.tsx +25 -18
  78. package/src/components/EditorHeader.tsx +29 -39
  79. package/src/components/EditorShell.tsx +17 -29
  80. package/src/components/FontPicker.tsx +7 -7
  81. package/src/components/PresetSelector.tsx +211 -129
  82. package/src/components/PreviewWindow.tsx +5 -12
  83. package/src/components/PrimaryNav.tsx +6 -6
  84. package/src/components/RightSidebar.tsx +24 -25
  85. package/src/components/Sidebar.tsx +54 -60
  86. package/src/components/TableOfContents.tsx +4 -5
  87. package/src/components/sections/ColorsSection.tsx +109 -121
  88. package/src/components/sections/DynamicRangeSection.tsx +61 -75
  89. package/src/components/sections/FontsSection.tsx +17 -28
  90. package/src/components/sections/IconsSection.tsx +2 -2
  91. package/src/components/sections/OthersSection.tsx +4 -5
  92. package/src/components/sections/ScalePlots.tsx +221 -0
  93. package/src/components/sections/index.ts +1 -0
  94. package/src/configurator/bridge/toCSS.ts +44 -0
  95. package/src/configurator/bridge/toJSON.ts +24 -0
  96. package/src/configurator/bridge/toThemeConfig.ts +114 -0
  97. package/src/configurator/constants.ts +13 -0
  98. package/src/configurator/hex-conversion.ts +67 -0
  99. package/src/configurator/hooks/useConfigurator.ts +33 -0
  100. package/src/configurator/hooks/usePreviewColors.ts +47 -0
  101. package/src/configurator/hooks/useWcagValidation.ts +133 -0
  102. package/src/configurator/hue-conversion.ts +25 -0
  103. package/src/configurator/state/actions.ts +43 -0
  104. package/src/configurator/state/defaults.ts +107 -0
  105. package/src/configurator/state/reducer.ts +399 -0
  106. package/src/configurator/types.ts +65 -0
  107. package/src/hooks/useEditorState.ts +25 -11
  108. package/src/hooks/usePresets.ts +54 -33
  109. package/src/index.ts +33 -0
  110. package/src/preview/CategoryView.tsx +8 -11
  111. package/src/preview/ComponentDetailView.tsx +24 -54
  112. package/src/preview/ComponentRenderer.tsx +2 -4
  113. package/src/preview/IconBrowserView.tsx +9 -10
  114. package/src/preview/OverviewView.tsx +9 -12
  115. package/src/preview/PaletteScaleView.tsx +122 -0
  116. package/src/types.ts +4 -3
@@ -0,0 +1,47 @@
1
+ import { useMemo } from 'react';
2
+ import { generateScale, oklchToSrgb, clampSrgb } from '@newtonedev/colors';
3
+ import type { Grading } from '@newtonedev/colors';
4
+ import type { ColorResult } from 'newtone';
5
+ import type { ConfiguratorState } from '../types';
6
+
7
+ /**
8
+ * Compute preview swatches for all palettes.
9
+ * Returns an array of arrays: one preview row per palette.
10
+ */
11
+ export function usePreviewColors(state: ConfiguratorState, useP3 = false): readonly (readonly ColorResult[])[] {
12
+ return useMemo(() => {
13
+ const g = state.globalHueGrading;
14
+ const grading: Grading | undefined =
15
+ g.lightIntensity === 0 && g.darkIntensity === 0
16
+ ? undefined
17
+ : {
18
+ light: { hue: g.lightHue, amount: g.lightIntensity },
19
+ dark: { hue: g.darkHue, amount: g.darkIntensity },
20
+ };
21
+
22
+ return state.palettes.map(p => {
23
+ const shift = p.localHueGrade
24
+ ? {
25
+ hue: p.localHueGrade.hue,
26
+ amount: p.localHueGrade.intensity,
27
+ light: p.localHueGrade.side === 'light',
28
+ }
29
+ : undefined;
30
+
31
+ const scale = generateScale({
32
+ hue: p.hue,
33
+ contrast: { light: state.dynamicRange.lightest, dark: state.dynamicRange.darkest },
34
+ chroma: { amount: p.chromaRatio, balance: p.chromaPeak },
35
+ grading,
36
+ shift,
37
+ isP3: useP3,
38
+ });
39
+
40
+ // Convert Oklch[] → ColorResult[]
41
+ return scale.map(oklch => {
42
+ const srgb = clampSrgb(oklchToSrgb(oklch));
43
+ return { srgb, oklch } as ColorResult;
44
+ });
45
+ });
46
+ }, [state, useP3]);
47
+ }
@@ -0,0 +1,133 @@
1
+ import { useMemo } from 'react';
2
+ import { generateScale, oklchToSrgb, clampSrgb } from '@newtonedev/colors';
3
+ import type { Oklch, Grading, Shift } from '@newtonedev/colors';
4
+ import { getWcagContrastRatio } from 'newtone';
5
+ import type { ConfiguratorState } from '../types';
6
+
7
+ /** Number of positions to sample when searching for the best contrast match. */
8
+ const SEARCH_STEPS = 200;
9
+ const AA_TARGET = 4.5;
10
+
11
+ /**
12
+ * Linearly interpolate between steps of a pre-generated scale.
13
+ * Gives continuous resolution from the fixed-step scale returned by generateScale.
14
+ */
15
+ function interpolateOklch(scale: Oklch[], t: number): Oklch {
16
+ const N = scale.length;
17
+ const fIdx = Math.max(0, Math.min(N - 1, t * (N - 1)));
18
+ const i0 = Math.floor(fIdx);
19
+ const i1 = Math.min(i0 + 1, N - 1);
20
+ const f = fIdx - i0;
21
+ const a = scale[i0];
22
+ const b = scale[i1];
23
+ return {
24
+ L: a.L + (b.L - a.L) * f,
25
+ C: a.C + (b.C - a.C) * f,
26
+ h: a.h + (b.h - a.h) * f,
27
+ };
28
+ }
29
+
30
+ export interface WcagValidation {
31
+ /** WCAG contrast ratio at the key color position (null if no keyColor set) */
32
+ readonly keyColorContrast: number | null;
33
+ /** Whether the key color passes WCAG AA for normal text (>= 4.5:1) */
34
+ readonly passesAA: boolean;
35
+ /** Whether the key color passes WCAG AA for large text (>= 3:1) */
36
+ readonly passesAALargeText: boolean;
37
+ /** The step index where auto contrast search would place the key color [0, 25] */
38
+ readonly autoStep: number;
39
+ }
40
+
41
+ function buildShift(lg: { hue: number; intensity: number; side: 'light' | 'dark' }): Shift {
42
+ return { hue: lg.hue, amount: lg.intensity, light: lg.side === 'light' };
43
+ }
44
+
45
+ /**
46
+ * Compute WCAG validation info for a non-primary palette's key color.
47
+ *
48
+ * Validates the palette's key color against the primary background
49
+ * in the current preview mode. Also computes where the auto contrast
50
+ * search would place the key color (for showing the default position).
51
+ */
52
+ export function useWcagValidation(
53
+ state: ConfiguratorState,
54
+ paletteIndex: number,
55
+ ): WcagValidation {
56
+ return useMemo(() => {
57
+ const palette = state.palettes[paletteIndex];
58
+ const primary = state.palettes[0];
59
+ if (!palette || !primary) {
60
+ return { keyColorContrast: null, passesAA: true, passesAALargeText: true, autoStep: 13 };
61
+ }
62
+
63
+ const mode = state.preview.mode;
64
+ const contrast = { light: state.dynamicRange.lightest, dark: state.dynamicRange.darkest };
65
+
66
+ const g = state.globalHueGrading;
67
+ const grading: Grading | undefined =
68
+ g.lightIntensity === 0 && g.darkIntensity === 0
69
+ ? undefined
70
+ : {
71
+ light: { hue: g.lightHue, amount: g.lightIntensity },
72
+ dark: { hue: g.darkHue, amount: g.darkIntensity },
73
+ };
74
+
75
+ // Generate primary scale to find background
76
+ const primaryScale = generateScale({
77
+ hue: primary.hue,
78
+ contrast,
79
+ chroma: { amount: primary.chromaRatio, balance: primary.chromaPeak },
80
+ grading,
81
+ shift: primary.localHueGrade ? buildShift(primary.localHueGrade) : undefined,
82
+ });
83
+
84
+ // Background position: light=near-lightest (engineNv≈0.95), dark=near-darkest (engineNv≈0.1)
85
+ const bgEngineNv = mode === 'light' ? 0.95 : 0.1;
86
+ const bgT = 1 - bgEngineNv;
87
+ const bgSrgb = clampSrgb(oklchToSrgb(interpolateOklch(primaryScale, bgT)));
88
+
89
+ // Generate palette scale to find key color and auto position
90
+ const paletteScale = generateScale({
91
+ hue: palette.hue,
92
+ contrast,
93
+ chroma: { amount: palette.chromaRatio, balance: palette.chromaPeak },
94
+ grading,
95
+ shift: palette.localHueGrade ? buildShift(palette.localHueGrade) : undefined,
96
+ });
97
+
98
+ // Find auto position: search at fine-grained t intervals for best 4.5:1 contrast
99
+ let bestSearchIdx = 0;
100
+ let bestDelta = Infinity;
101
+ for (let i = 0; i < SEARCH_STEPS; i++) {
102
+ const t = i / (SEARCH_STEPS - 1);
103
+ const srgb = clampSrgb(oklchToSrgb(interpolateOklch(paletteScale, t)));
104
+ const cr = getWcagContrastRatio(srgb, bgSrgb);
105
+ const delta = Math.abs(cr - AA_TARGET);
106
+ if (delta < bestDelta) {
107
+ bestDelta = delta;
108
+ bestSearchIdx = i;
109
+ }
110
+ }
111
+ // Convert search index to step index: t=0→step 0 (lightest), t=1→step 25 (darkest)
112
+ const autoStep = Math.round(bestSearchIdx / (SEARCH_STEPS - 1) * 25);
113
+
114
+ // Resolve per-mode key color step
115
+ const effectiveKeyStep = mode === 'dark' ? palette.keyColorStepDark : palette.keyColorStep;
116
+
117
+ if (effectiveKeyStep === undefined) {
118
+ return { keyColorContrast: null, passesAA: true, passesAALargeText: true, autoStep };
119
+ }
120
+
121
+ // Look up the key color in the palette scale: step 0=lightest (t=0), step 25=darkest (t=1)
122
+ const keyT = Math.max(0, Math.min(25, effectiveKeyStep)) / 25;
123
+ const keySrgb = clampSrgb(oklchToSrgb(interpolateOklch(paletteScale, keyT)));
124
+ const cr = getWcagContrastRatio(keySrgb, bgSrgb);
125
+
126
+ return {
127
+ keyColorContrast: cr,
128
+ passesAA: cr >= 4.5,
129
+ passesAALargeText: cr >= 3.0,
130
+ autoStep,
131
+ };
132
+ }, [state, paletteIndex]);
133
+ }
@@ -0,0 +1,25 @@
1
+ import { srgbToOklch } from 'newtone';
2
+ import type { Srgb } from 'newtone';
3
+
4
+ /**
5
+ * Convert a traditional HSL color wheel hue (0-359) to its equivalent OKLCH hue.
6
+ *
7
+ * Uses the fully saturated HSL color (S=1, L=0.5) at the given hue,
8
+ * converts to sRGB, then extracts the OKLCH hue component.
9
+ *
10
+ * Traditional hues: 0=red, 60=yellow, 120=green, 180=cyan, 240=blue, 300=magenta.
11
+ */
12
+ export function traditionalHueToOklch(hue: number): number {
13
+ const h = ((hue % 360) + 360) % 360;
14
+ const x = 1 - Math.abs((h / 60) % 2 - 1);
15
+
16
+ let r: number, g: number, b: number;
17
+ if (h < 60) { r = 1; g = x; b = 0; }
18
+ else if (h < 120) { r = x; g = 1; b = 0; }
19
+ else if (h < 180) { r = 0; g = 1; b = x; }
20
+ else if (h < 240) { r = 0; g = x; b = 1; }
21
+ else if (h < 300) { r = x; g = 0; b = 1; }
22
+ else { r = 1; g = 0; b = x; }
23
+
24
+ return srgbToOklch({ r, g, b } as Srgb).h;
25
+ }
@@ -0,0 +1,43 @@
1
+ import type { ColorMode } from '@newtonedev/components';
2
+ import type { TextRole } from '@newtonedev/fonts';
3
+ import type { ConfiguratorState, FontSlotConfig, FontScope, SpacingPreset } from '../types';
4
+
5
+ export type ConfiguratorAction =
6
+ // Palette actions
7
+ | { readonly type: 'SET_PALETTE_HUE'; readonly index: number; readonly hue: number }
8
+ | { readonly type: 'SET_PALETTE_CHROMA_RATIO'; readonly index: number; readonly chromaRatio: number }
9
+ | { readonly type: 'SET_PALETTE_CHROMA_PEAK'; readonly index: number; readonly chromaPeak: number }
10
+ | { readonly type: 'SET_PALETTE_LOCAL_HUE_GRADE_INTENSITY'; readonly index: number; readonly intensity: number }
11
+ | { readonly type: 'SET_PALETTE_LOCAL_HUE_GRADE_HUE'; readonly index: number; readonly hue: number }
12
+ | { readonly type: 'SET_PALETTE_LOCAL_HUE_GRADE_SIDE'; readonly index: number; readonly side: 'light' | 'dark' }
13
+ | { readonly type: 'SET_PALETTE_KEY_COLOR_STEP'; readonly index: number; readonly step: number }
14
+ | { readonly type: 'CLEAR_PALETTE_KEY_COLOR_STEP'; readonly index: number }
15
+ | { readonly type: 'SET_PALETTE_FROM_HEX'; readonly index: number; readonly hue: number; readonly chromaRatio: number; readonly keyColorStep: number }
16
+ | { readonly type: 'SET_PALETTE_KEY_COLOR_STEP_DARK'; readonly index: number; readonly step: number }
17
+ | { readonly type: 'CLEAR_PALETTE_KEY_COLOR_STEP_DARK'; readonly index: number }
18
+ | { readonly type: 'SET_PALETTE_FROM_HEX_DARK'; readonly index: number; readonly hue: number; readonly chromaRatio: number; readonly keyColorStep: number }
19
+ // Dynamic range actions
20
+ | { readonly type: 'SET_LIGHTEST'; readonly value: number }
21
+ | { readonly type: 'SET_DARKEST'; readonly value: number }
22
+ // Global hue grading actions
23
+ | { readonly type: 'SET_GLOBAL_GRADE_LIGHT_INTENSITY'; readonly intensity: number }
24
+ | { readonly type: 'SET_GLOBAL_GRADE_LIGHT_HUE'; readonly hue: number }
25
+ | { readonly type: 'SET_GLOBAL_GRADE_DARK_INTENSITY'; readonly intensity: number }
26
+ | { readonly type: 'SET_GLOBAL_GRADE_DARK_HUE'; readonly hue: number }
27
+ // Spacing actions
28
+ | { readonly type: 'SET_SPACING_PRESET'; readonly preset: SpacingPreset }
29
+ // Roundness actions
30
+ | { readonly type: 'SET_ROUNDNESS_INTENSITY'; readonly intensity: number }
31
+ // Typography actions
32
+ | { readonly type: 'SET_FONT'; readonly scope: FontScope; readonly font: FontSlotConfig }
33
+ | { readonly type: 'SET_TYPE_SCALE_OFFSET'; readonly offset: number }
34
+ | { readonly type: 'SET_ROLE_WEIGHT'; readonly role: TextRole; readonly weight: number }
35
+ // Icons actions
36
+ | { readonly type: 'SET_ICON_VARIANT'; readonly variant: 'outlined' | 'rounded' | 'sharp' }
37
+ | { readonly type: 'SET_ICON_WEIGHT'; readonly weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 }
38
+ | { readonly type: 'SET_ICON_AUTO_GRADE'; readonly autoGrade: boolean }
39
+ // Preview actions
40
+ | { readonly type: 'SET_PREVIEW_MODE'; readonly mode: ColorMode }
41
+ // Control actions
42
+ | { readonly type: 'RESET' }
43
+ | { readonly type: 'LOAD_STATE'; readonly state: ConfiguratorState };
@@ -0,0 +1,107 @@
1
+ import type { ConfiguratorState } from '../types';
2
+
3
+ /**
4
+ * Default configurator state.
5
+ * All hue values are OKLCH hues [0, 360).
6
+ */
7
+ export const DEFAULT_CONFIGURATOR_STATE: ConfiguratorState = {
8
+ hueSpace: 'oklch',
9
+ palettes: [
10
+ {
11
+ name: 'Primary',
12
+ hue: 262.64,
13
+ chromaRatio: 0.24,
14
+ chromaPeak: 0.5,
15
+ },
16
+ {
17
+ name: 'Secondary',
18
+ hue: 43.49,
19
+ chromaRatio: 0.8,
20
+ chromaPeak: 0.5,
21
+ },
22
+ {
23
+ name: 'Tertiary',
24
+ hue: 195,
25
+ chromaRatio: 0.5,
26
+ chromaPeak: 0.5,
27
+ },
28
+ {
29
+ name: 'Success',
30
+ hue: 148.13,
31
+ chromaRatio: 0.64,
32
+ chromaPeak: 0.5,
33
+ },
34
+ {
35
+ name: 'Warning',
36
+ hue: 73.27,
37
+ chromaRatio: 0.8,
38
+ chromaPeak: 0.5,
39
+ },
40
+ {
41
+ name: 'Error',
42
+ hue: 29.23,
43
+ chromaRatio: 0.8,
44
+ chromaPeak: 0.5,
45
+ },
46
+ ],
47
+ dynamicRange: {
48
+ lightest: 1,
49
+ darkest: 1,
50
+ },
51
+ globalHueGrading: {
52
+ lightHue: 52.78,
53
+ lightIntensity: 0,
54
+ darkHue: 262.64,
55
+ darkIntensity: 0,
56
+ },
57
+ preview: {
58
+ mode: 'light',
59
+ },
60
+ spacing: {
61
+ preset: 'md', // Medium (8px base): default/balanced spacing
62
+ },
63
+ roundness: {
64
+ intensity: 0.5, // 1.0x multiplier = preserves current hardcoded values
65
+ },
66
+ typography: {
67
+ fonts: {
68
+ main: {
69
+ config: {
70
+ type: 'system',
71
+ family: 'system-ui',
72
+ fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
73
+ },
74
+ weights: { regular: 400, medium: 500, bold: 700 },
75
+ },
76
+ display: {
77
+ config: {
78
+ type: 'system',
79
+ family: 'system-ui',
80
+ fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
81
+ },
82
+ weights: { regular: 400, medium: 500, bold: 700 },
83
+ },
84
+ mono: {
85
+ config: {
86
+ type: 'system',
87
+ family: 'ui-monospace',
88
+ fallback: 'SFMono-Regular, Menlo, Monaco, Consolas, monospace',
89
+ },
90
+ weights: { regular: 400, medium: 500, bold: 700 },
91
+ },
92
+ currency: {
93
+ config: {
94
+ type: 'system',
95
+ family: 'system-ui',
96
+ fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
97
+ },
98
+ weights: { regular: 400, medium: 500, bold: 700 },
99
+ },
100
+ },
101
+ },
102
+ icons: {
103
+ variant: 'rounded', // Material Design 3 aesthetic
104
+ weight: 400, // Normal weight
105
+ autoGrade: true, // Enable mode-aware grade
106
+ },
107
+ };