@micha.bigler/ui-core-micha 2.29.1 → 2.30.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.
@@ -0,0 +1,325 @@
1
+ import { darken, lighten } from '@mui/material/styles';
2
+ // MUI's palette augmentation (createPalette -> augmentColor) only runs on the
3
+ // FIRST argument passed to createTheme; every later argument -- which is how
4
+ // createAppTheme layers BASELINE_PALETTE on top -- is deep-merged as a raw
5
+ // object with no augmentation. A status entry missing `main`/`contrastText`
6
+ // therefore silently keeps MUI's stock hue for `color="success"` etc. even
7
+ // though `text`/`fill`/`bg` are set correctly, so every status channel MUI
8
+ // recognises (success/warning/error/info) must carry its own explicit
9
+ // main/light/dark/contrastText, computed the same way augmentColor would.
10
+ const withMainShades = (main, contrastText) => ({
11
+ main,
12
+ light: lighten(main, 0.2),
13
+ dark: darken(main, 0.3),
14
+ contrastText,
15
+ });
16
+ export const SERIES_COLOURS = [
17
+ '#3D5A99',
18
+ '#3E80B8',
19
+ '#2E8F8A',
20
+ '#7A5FA8',
21
+ '#9C4F86',
22
+ '#8A7355',
23
+ ];
24
+ export const STATUS_KEYS = ['success', 'warning', 'error', 'info', 'stale'];
25
+ export const TYPOGRAPHY_VARIANTS = [
26
+ 'h1',
27
+ 'h2',
28
+ 'h3',
29
+ 'h4',
30
+ 'h5',
31
+ 'h6',
32
+ 'subtitle1',
33
+ 'subtitle2',
34
+ 'body1',
35
+ 'body2',
36
+ 'button',
37
+ 'caption',
38
+ 'overline',
39
+ ];
40
+ const typeVariant = (fontSize, fontWeight, lineHeight, extra = {}) => (Object.assign({ fontSize,
41
+ fontWeight,
42
+ lineHeight, letterSpacing: 0 }, extra));
43
+ export const BASELINE_PALETTE = {
44
+ ink: {
45
+ primary: '#212529',
46
+ secondary: '#5B6670',
47
+ muted: '#6A7178',
48
+ },
49
+ text: {
50
+ primary: '#212529',
51
+ secondary: '#5B6670',
52
+ disabled: '#6A7178',
53
+ },
54
+ background: {
55
+ default: '#FAFAFA',
56
+ paper: '#FFFFFF',
57
+ },
58
+ divider: 'rgba(33,37,41,.10)',
59
+ controlBorder: {
60
+ main: 'rgba(33,37,41,.50)',
61
+ hover: 'rgba(33,37,41,.65)',
62
+ error: '#BF3227',
63
+ },
64
+ success: Object.assign(Object.assign({}, withMainShades('#1B8038', '#FFFFFF')), { text: '#35794A', fill: '#1B8038', fillText: '#FFFFFF', bg: '#E5F4E9' }),
65
+ warning: Object.assign(Object.assign({}, withMainShades('#C08A2C', '#212529')), { text: '#976100', fill: '#C08A2C', fillText: '#212529', bg: '#FBF0DC' }),
66
+ error: Object.assign(Object.assign({}, withMainShades('#BF3227', '#FFFFFF')), { text: '#BF3227', fill: '#BF3227', fillText: '#FFFFFF', bg: '#FBEAE8' }),
67
+ // The canonical sheet introduces no new info hue. These values reuse
68
+ // MUI's own info.dark/main family while adding the shared status shape.
69
+ info: Object.assign(Object.assign({}, withMainShades('#01579B', '#FFFFFF')), { text: '#01579B', fill: '#01579B', fillText: '#FFFFFF', bg: '#E5F6FD' }),
70
+ stale: {
71
+ text: '#5B6B7D',
72
+ fill: '#5B6B7D',
73
+ fillText: '#FFFFFF',
74
+ bg: '#EAEDF1',
75
+ },
76
+ dataSeries: {
77
+ categorical: SERIES_COLOURS,
78
+ },
79
+ };
80
+ export const OVERLAY_SHADOW = '0 8px 24px rgba(20,26,31,.16), 0 2px 8px rgba(20,26,31,.08)';
81
+ export const MOTION = {
82
+ duration: {
83
+ fast: 120,
84
+ base: 180,
85
+ overlayEnter: 220,
86
+ overlayExit: 180,
87
+ chart: 300,
88
+ },
89
+ easing: {
90
+ enter: 'cubic-bezier(0.0, 0, 0.2, 1)',
91
+ exit: 'cubic-bezier(0.4, 0, 1, 1)',
92
+ state: 'cubic-bezier(0.4, 0, 0.2, 1)',
93
+ },
94
+ reduced: '0.01ms',
95
+ };
96
+ const coarseHitArea = {
97
+ '@media (any-pointer: coarse)': {
98
+ minHeight: 44,
99
+ minWidth: 44,
100
+ },
101
+ };
102
+ const autofill = {
103
+ '&:-webkit-autofill': {
104
+ WebkitBoxShadow: '0 0 0 100px #FFFFFF inset',
105
+ WebkitTextFillColor: '#212529',
106
+ caretColor: '#212529',
107
+ },
108
+ '&:autofill': {
109
+ boxShadow: '0 0 0 100px #FFFFFF inset',
110
+ WebkitTextFillColor: '#212529',
111
+ caretColor: '#212529',
112
+ },
113
+ };
114
+ export const BASELINE_STATIC = {
115
+ typography: {
116
+ fontFamily: "'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
117
+ h1: typeVariant('32px', 600, 1.2),
118
+ h2: typeVariant('28px', 600, 1.22),
119
+ h3: typeVariant('24px', 600, 1.25),
120
+ h4: typeVariant('20px', 600, 1.3),
121
+ h5: typeVariant('18px', 600, 1.3),
122
+ h6: typeVariant('16px', 600, 1.35),
123
+ subtitle1: typeVariant('15px', 600, 1.4),
124
+ subtitle2: typeVariant('13px', 500, 1.4),
125
+ body1: typeVariant('14px', 400, 1.55),
126
+ body2: typeVariant('13px', 400, 1.55),
127
+ button: typeVariant('14px', 500, 1.4, { textTransform: 'none' }),
128
+ caption: typeVariant('12px', 400, 1.4),
129
+ overline: typeVariant('11px', 600, 1.4, {
130
+ textTransform: 'uppercase',
131
+ letterSpacing: '0.4px',
132
+ }),
133
+ },
134
+ shape: {
135
+ borderRadius: 3,
136
+ },
137
+ radius: {
138
+ control: 3,
139
+ card: 8,
140
+ },
141
+ spacing: 8,
142
+ breakpoints: {
143
+ values: { xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 },
144
+ },
145
+ density: {
146
+ controlHeight: 40,
147
+ touchTarget: 44,
148
+ tableCellPadding: '10px 16px',
149
+ chip: { height: 32, radius: 16, fontSize: 13 },
150
+ containerGutter: { default: 24, wide: 32 },
151
+ },
152
+ fontLoading: {
153
+ family: 'DM Sans',
154
+ weights: [400, 500, 600],
155
+ source: '@fontsource/dm-sans',
156
+ },
157
+ shadow: {
158
+ rest: 'none',
159
+ overlay: OVERLAY_SHADOW,
160
+ },
161
+ motion: MOTION,
162
+ transitions: {
163
+ duration: {
164
+ fast: MOTION.duration.fast,
165
+ base: MOTION.duration.base,
166
+ overlayEnter: MOTION.duration.overlayEnter,
167
+ overlayExit: MOTION.duration.overlayExit,
168
+ chart: MOTION.duration.chart,
169
+ },
170
+ easing: {
171
+ enter: MOTION.easing.enter,
172
+ exit: MOTION.easing.exit,
173
+ state: MOTION.easing.state,
174
+ },
175
+ },
176
+ // MUI's 25-entry shadows array is intentionally retained intact. Resting
177
+ // surfaces are made shadowless via the Paper/Card defaults below, while
178
+ // named transient surfaces receive shadow.overlay explicitly.
179
+ components: {
180
+ MuiCssBaseline: {
181
+ styleOverrides: {
182
+ '@media (prefers-reduced-motion: reduce)': {
183
+ '*, *::before, *::after': {
184
+ animationDuration: '0.01ms !important',
185
+ animationIterationCount: '1 !important',
186
+ transitionDuration: '0.01ms !important',
187
+ scrollBehavior: 'auto !important',
188
+ },
189
+ },
190
+ },
191
+ },
192
+ MuiButton: {
193
+ defaultProps: { disableElevation: true },
194
+ styleOverrides: {
195
+ root: Object.assign({ minHeight: 40, borderRadius: 3, textTransform: 'none', transitionDuration: '120ms' }, coarseHitArea),
196
+ contained: { boxShadow: 'none', '&:hover': { boxShadow: 'none' } },
197
+ },
198
+ },
199
+ MuiTableCell: {
200
+ styleOverrides: { root: { padding: '10px 16px' } },
201
+ },
202
+ MuiIconButton: {
203
+ styleOverrides: { root: Object.assign({ minHeight: 40, minWidth: 40 }, coarseHitArea) },
204
+ },
205
+ MuiDivider: {
206
+ styleOverrides: { root: { borderColor: 'rgba(33,37,41,.10)' } },
207
+ },
208
+ MuiSelect: {
209
+ defaultProps: { variant: 'outlined' },
210
+ styleOverrides: {
211
+ select: {
212
+ minHeight: 40,
213
+ boxSizing: 'border-box',
214
+ '@media (any-pointer: coarse)': { minHeight: 44 },
215
+ },
216
+ },
217
+ },
218
+ MuiCheckbox: {
219
+ styleOverrides: { root: Object.assign({ minHeight: 40, minWidth: 40 }, coarseHitArea) },
220
+ },
221
+ MuiFormControlLabel: {
222
+ styleOverrides: {
223
+ root: Object.assign({ minHeight: 40 }, coarseHitArea),
224
+ label: { fontSize: '14px', lineHeight: 1.55 },
225
+ },
226
+ },
227
+ MuiContainer: {
228
+ styleOverrides: {
229
+ root: {
230
+ paddingLeft: 24,
231
+ paddingRight: 24,
232
+ '@media (min-width:1200px)': { paddingLeft: 32, paddingRight: 32 },
233
+ },
234
+ },
235
+ },
236
+ MuiPaper: {
237
+ defaultProps: { elevation: 0, variant: 'outlined' },
238
+ styleOverrides: {
239
+ root: { borderRadius: 8, boxShadow: 'none' },
240
+ },
241
+ },
242
+ MuiCard: {
243
+ defaultProps: { elevation: 0, variant: 'outlined' },
244
+ styleOverrides: { root: { borderRadius: 8, boxShadow: 'none' } },
245
+ },
246
+ MuiChip: {
247
+ styleOverrides: {
248
+ root: {
249
+ height: 32,
250
+ borderRadius: 16,
251
+ fontSize: '13px',
252
+ position: 'relative',
253
+ '@media (any-pointer: coarse)': {
254
+ '&::after': {
255
+ content: '""',
256
+ position: 'absolute',
257
+ inset: '-6px',
258
+ },
259
+ },
260
+ },
261
+ },
262
+ },
263
+ MuiTextField: {
264
+ defaultProps: { variant: 'outlined' },
265
+ styleOverrides: { root: Object.assign({ minHeight: 40 }, coarseHitArea) },
266
+ },
267
+ MuiOutlinedInput: {
268
+ styleOverrides: {
269
+ root: Object.assign({ minHeight: 40, borderRadius: 3 }, coarseHitArea),
270
+ input: autofill,
271
+ },
272
+ // `variants` is a sibling of `styleOverrides`, not nested inside it -
273
+ // MUI matches these by `props` and applies `style` independently of the
274
+ // styleOverrides merge, so this is the correct placement (a `variants`
275
+ // array embedded inside styleOverrides.root would just be inert CSS-in-JS).
276
+ variants: [
277
+ { props: { multiline: false }, style: { height: 40 } },
278
+ ],
279
+ },
280
+ MuiFilledInput: {
281
+ styleOverrides: { input: autofill },
282
+ },
283
+ MuiInput: {
284
+ styleOverrides: { input: autofill },
285
+ },
286
+ MuiDialog: {
287
+ defaultProps: { transitionDuration: { enter: 220, exit: 180 } },
288
+ styleOverrides: { paper: { borderRadius: 8, boxShadow: OVERLAY_SHADOW } },
289
+ },
290
+ MuiDrawer: {
291
+ defaultProps: { transitionDuration: { enter: 220, exit: 180 } },
292
+ styleOverrides: { paper: { boxShadow: OVERLAY_SHADOW } },
293
+ },
294
+ MuiMenu: {
295
+ defaultProps: { transitionDuration: { enter: 220, exit: 180 } },
296
+ styleOverrides: { paper: { boxShadow: OVERLAY_SHADOW } },
297
+ },
298
+ MuiTooltip: {
299
+ defaultProps: {
300
+ slotProps: { transition: { timeout: { enter: 220, exit: 180 } } },
301
+ },
302
+ styleOverrides: {
303
+ tooltip: { boxShadow: OVERLAY_SHADOW, transitionDuration: '220ms' },
304
+ },
305
+ },
306
+ },
307
+ };
308
+ export const BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS = [
309
+ {
310
+ surface: 'palette.background.paper',
311
+ reason: 'The canonical surface is deliberately MUI white.',
312
+ },
313
+ {
314
+ surface: 'spacing.unit',
315
+ reason: 'The canonical spacing rhythm deliberately retains MUI\'s 8px unit.',
316
+ },
317
+ {
318
+ surface: 'breakpoints.values',
319
+ reason: 'The canonical breakpoint values deliberately retain MUI\'s defaults.',
320
+ },
321
+ ...['subtitle2', 'body1', 'body2', 'button', 'caption'].map((variant) => ({
322
+ surface: `typography.${variant}.fontWeight`,
323
+ reason: `The canonical ${variant} weight deliberately equals MUI's numeric default.`,
324
+ })),
325
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.29.1",
3
+ "version": "2.30.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "repository": {
@@ -8,6 +8,9 @@
8
8
  "url": "https://github.com/bigler-webapps/ui-core-micha"
9
9
  },
10
10
  "private": false,
11
+ "dependencies": {
12
+ "@fontsource/dm-sans": "^5.2.8"
13
+ },
11
14
  "peerDependencies": {
12
15
  "@emotion/react": "^11.14.0",
13
16
  "@emotion/styled": "^11.14.1",
@@ -7,11 +7,14 @@ import { useTheme } from '@mui/material/styles';
7
7
  export function getNeutralChartPalette(theme) {
8
8
  const palette = theme.palette;
9
9
  const isDark = palette.mode === 'dark';
10
+ const dataSeries = palette.dataSeries?.categorical;
10
11
 
11
12
  return {
12
- categorical: isDark
13
- ? [palette.primary.light, palette.secondary.light, palette.info.light, palette.success.light, palette.warning.light]
14
- : [palette.primary.main, palette.secondary.main, palette.info.main, palette.success.main, palette.warning.main],
13
+ categorical: Array.isArray(dataSeries) && dataSeries.length > 0
14
+ ? [...dataSeries]
15
+ : isDark
16
+ ? [palette.primary.light, palette.secondary.light, palette.info.light, palette.success.light, palette.warning.light]
17
+ : [palette.primary.main, palette.secondary.main, palette.info.main, palette.success.main, palette.warning.main],
15
18
  sequential: isDark
16
19
  ? [palette.action.disabled, palette.text.secondary, palette.primary.light, palette.primary.main, palette.primary.dark]
17
20
  : [palette.action.disabled, palette.divider, palette.text.secondary, palette.primary.light, palette.primary.main],
@@ -100,10 +100,10 @@ export const authTranslations = {
100
100
  },
101
101
 
102
102
  "Auth.INVITE_SENT": {
103
- "de": "Die Einladung wurde versendet.",
104
- "fr": "L'invitation a été envoyée.",
105
- "en": "The invitation has been sent.",
106
- "sw": "Mwaliko umetumwa."
103
+ "de": "Die Einladung wurde an {{email}} gesendet.",
104
+ "fr": "L'invitation a été envoyée à {{email}}.",
105
+ "en": "The invitation has been sent to {{email}}.",
106
+ "sw": "Mwaliko umetumwa kwa {{email}}."
107
107
  },
108
108
 
109
109
  "Auth.RESET_REQUEST_ACCEPTED": {
package/src/index.js CHANGED
@@ -1,5 +1,14 @@
1
1
  // index.js (Entry Point deiner Library)
2
2
 
3
+ // --- 0. Theme ---
4
+ export {
5
+ createAppTheme,
6
+ assertThemeComplete,
7
+ calculateContrastRatio,
8
+ reportThemeAdoption,
9
+ THEME_COMPLETENESS_SURFACES,
10
+ } from './theme';
11
+
3
12
  // --- 1. Auth Context (Essentiell für den Wrapper) ---
4
13
  export { AuthContext, AuthProvider } from './auth/AuthContext';
5
14
  export { UserMenu } from './auth/UserMenu';
@@ -123,14 +123,18 @@ export function SignUpPage() {
123
123
 
124
124
  setSubmitting(true);
125
125
  try {
126
- await submitRegistrationRequest({
126
+ const res = await submitRegistrationRequest({
127
127
  email,
128
128
  mode,
129
129
  accessCode,
130
130
  registrationContextToken: mode === 'self_signup_qr' ? tokenFromUrl : null,
131
131
  turnstileToken: turnstileRequired ? turnstileToken : undefined,
132
132
  });
133
- setSuccessKey('Auth.INVITE_REQUEST_SUCCESS');
133
+ // The backend's own response code is already mode-agnostic and accurate
134
+ // (Auth.INVITE_SENT). Falling back to the access-code-specific "if valid,
135
+ // an invite was sent" wording here was wrong for open/email-domain/QR
136
+ // signup, which have no code whose validity needs hiding.
137
+ setSuccessKey(res?.code || 'Auth.INVITE_SENT');
134
138
  } catch (err) {
135
139
  setErrorKey(err.code || 'Auth.INVITE_FAILED');
136
140
  if (turnstileRequired) {
@@ -0,0 +1,212 @@
1
+ import '@fontsource/dm-sans/400.css';
2
+ import '@fontsource/dm-sans/500.css';
3
+ import '@fontsource/dm-sans/600.css';
4
+
5
+ import { createTheme, darken, getContrastRatio } from '@mui/material/styles';
6
+
7
+ import {
8
+ BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS,
9
+ BASELINE_PALETTE,
10
+ BASELINE_STATIC,
11
+ MOTION,
12
+ } from './tokens';
13
+
14
+ function findFunction(value, path) {
15
+ if (typeof value === 'function') return path;
16
+ if (!value || typeof value !== 'object') return null;
17
+
18
+ for (const [key, child] of Object.entries(value)) {
19
+ const found = findFunction(child, `${path}.${key}`);
20
+ if (found) return found;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function validateStyleOverrides(components = {}) {
26
+ for (const [componentName, component] of Object.entries(components)) {
27
+ if (!component?.styleOverrides) continue;
28
+ const slot = findFunction(
29
+ component.styleOverrides,
30
+ `components.${componentName}.styleOverrides`,
31
+ );
32
+ if (slot) {
33
+ throw new TypeError(
34
+ `createAppTheme: ${slot} must be an object, not a function. ` +
35
+ 'Use object styleOverrides and MUI variants for prop- or state-dependent styles.',
36
+ );
37
+ }
38
+ }
39
+ }
40
+
41
+ function clearsContrast(colour, surfaces) {
42
+ return surfaces.every((surface) => getContrastRatio(colour, surface) >= 3);
43
+ }
44
+
45
+ // Darkened until it clears 3:1 against BOTH '#FFFFFF' and background.default
46
+ // -- the exact two surfaces assertThemeComplete's contrastFindings checks --
47
+ // so deriving against background.paper alone (which need not be white for an
48
+ // app that overrides it) could pass here and still fail there.
49
+ function deriveFocusColour(primary, surfaces) {
50
+ let focus = primary;
51
+ for (let coefficient = 0.05; !clearsContrast(focus, surfaces); coefficient += 0.05) {
52
+ focus = darken(primary, Math.min(coefficient, 0.9));
53
+ if (coefficient >= 0.9) break;
54
+ }
55
+ return focus;
56
+ }
57
+
58
+ function createPaletteAwareComponents(palette) {
59
+ const stateTransition = `border-color ${MOTION.duration.fast}ms ${MOTION.easing.state}, color ${MOTION.duration.fast}ms ${MOTION.easing.state}, background-color ${MOTION.duration.fast}ms ${MOTION.easing.state}`;
60
+ const autofill = {
61
+ '&:-webkit-autofill': {
62
+ WebkitBoxShadow: `0 0 0 100px ${palette.background.paper} inset`,
63
+ WebkitTextFillColor: palette.ink.primary,
64
+ caretColor: palette.ink.primary,
65
+ },
66
+ '&:autofill': {
67
+ boxShadow: `0 0 0 100px ${palette.background.paper} inset`,
68
+ WebkitTextFillColor: palette.ink.primary,
69
+ caretColor: palette.ink.primary,
70
+ },
71
+ };
72
+ const alertStyles = Object.fromEntries(
73
+ ['success', 'warning', 'error', 'info'].map((status) => [
74
+ `standard${status[0].toUpperCase()}${status.slice(1)}`,
75
+ {
76
+ color: palette[status].text,
77
+ backgroundColor: palette[status].bg,
78
+ '& .MuiAlert-icon': { color: palette[status].text },
79
+ },
80
+ ]),
81
+ );
82
+
83
+ return {
84
+ MuiButton: {
85
+ styleOverrides: {
86
+ root: { transition: stateTransition },
87
+ outlined: {
88
+ borderColor: palette.controlBorder.main,
89
+ '&:hover': { borderColor: palette.controlBorder.hover },
90
+ },
91
+ },
92
+ },
93
+ MuiDivider: {
94
+ styleOverrides: { root: { borderColor: palette.divider } },
95
+ },
96
+ MuiCheckbox: {
97
+ styleOverrides: {
98
+ root: {
99
+ color: palette.controlBorder.main,
100
+ transition: stateTransition,
101
+ '&:hover': { color: palette.controlBorder.hover },
102
+ '&.Mui-checked': { color: palette.primary.main },
103
+ '&.MuiCheckbox-indeterminate': { color: palette.primary.main },
104
+ '&.Mui-error': { color: palette.controlBorder.error },
105
+ },
106
+ },
107
+ },
108
+ MuiPaper: {
109
+ styleOverrides: { root: { borderColor: palette.divider } },
110
+ },
111
+ MuiCard: {
112
+ styleOverrides: { root: { borderColor: palette.divider } },
113
+ },
114
+ MuiChip: {
115
+ styleOverrides: {
116
+ outlined: { borderColor: palette.controlBorder.main },
117
+ },
118
+ },
119
+ MuiOutlinedInput: {
120
+ styleOverrides: {
121
+ root: {
122
+ transition: stateTransition,
123
+ '& .MuiOutlinedInput-notchedOutline': {
124
+ borderColor: palette.controlBorder.main,
125
+ },
126
+ '&:hover .MuiOutlinedInput-notchedOutline': {
127
+ borderColor: palette.controlBorder.hover,
128
+ },
129
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
130
+ borderColor: palette.controlBorder.focus,
131
+ borderWidth: 2,
132
+ },
133
+ '&.Mui-error .MuiOutlinedInput-notchedOutline': {
134
+ borderColor: palette.controlBorder.error,
135
+ },
136
+ },
137
+ input: autofill,
138
+ },
139
+ },
140
+ MuiFilledInput: {
141
+ styleOverrides: { input: autofill },
142
+ },
143
+ MuiInput: {
144
+ styleOverrides: { input: autofill },
145
+ },
146
+ MuiAlert: {
147
+ styleOverrides: alertStyles,
148
+ },
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Builds the shared application theme while leaving app identity in primary
154
+ * and fontFamily. Palette resolution happens before palette-aware component
155
+ * objects are created, so runtime app/site overrides cannot leave stale values.
156
+ */
157
+ export function createAppTheme(appConfig = {}) {
158
+ if (!appConfig?.palette?.primary) {
159
+ throw new TypeError('createAppTheme: appConfig.palette.primary is required.');
160
+ }
161
+ validateStyleOverrides(appConfig.components);
162
+
163
+ const paletteTheme = createTheme(
164
+ { palette: appConfig.palette },
165
+ { palette: BASELINE_PALETTE },
166
+ { palette: appConfig.palette },
167
+ );
168
+ const finalPalette = paletteTheme.palette;
169
+ const computedPalette = {
170
+ controlBorder: {
171
+ ...finalPalette.controlBorder,
172
+ focus: deriveFocusColour(finalPalette.primary.main, [
173
+ '#FFFFFF',
174
+ finalPalette.background.default,
175
+ ]),
176
+ },
177
+ };
178
+ const resolvedPalette = { ...finalPalette, ...computedPalette };
179
+ const fontFamily = appConfig.typography?.fontFamily || BASELINE_STATIC.typography.fontFamily;
180
+
181
+ return createTheme(
182
+ {
183
+ ...BASELINE_STATIC,
184
+ typography: {
185
+ ...BASELINE_STATIC.typography,
186
+ fontFamily,
187
+ },
188
+ fontLoading: {
189
+ ...BASELINE_STATIC.fontLoading,
190
+ weights: [...BASELINE_STATIC.fontLoading.weights],
191
+ },
192
+ palette: resolvedPalette,
193
+ themeCompleteness: {
194
+ baseline: true,
195
+ exemptions: BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS.map((exemption) => ({
196
+ ...exemption,
197
+ })),
198
+ },
199
+ },
200
+ {
201
+ components: createPaletteAwareComponents(resolvedPalette),
202
+ },
203
+ appConfig,
204
+ {
205
+ palette: {
206
+ dataSeries: {
207
+ categorical: [...resolvedPalette.dataSeries.categorical],
208
+ },
209
+ },
210
+ },
211
+ );
212
+ }
@@ -0,0 +1,7 @@
1
+ export { createAppTheme } from './createAppTheme';
2
+ export {
3
+ assertThemeComplete,
4
+ calculateContrastRatio,
5
+ reportThemeAdoption,
6
+ THEME_COMPLETENESS_SURFACES,
7
+ } from './themeCompleteness';