@micha.bigler/ui-core-micha 2.29.2 → 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,320 @@
1
+ import { createTheme, getContrastRatio as getMuiContrastRatio } from '@mui/material/styles';
2
+ import { STATUS_KEYS, TYPOGRAPHY_VARIANTS } from './tokens';
3
+ const MUI_DEFAULT_THEME = createTheme();
4
+ function getPath(value, path) {
5
+ return path.split('.').reduce((current, key) => current === null || current === void 0 ? void 0 : current[key], value);
6
+ }
7
+ function sameValue(left, right) {
8
+ if (left === right)
9
+ return true;
10
+ if (left === undefined || right === undefined)
11
+ return false;
12
+ return JSON.stringify(left) === JSON.stringify(right);
13
+ }
14
+ const pathSurface = (surface, path = surface) => ({
15
+ surface,
16
+ get: (theme) => getPath(theme, path),
17
+ });
18
+ const keyedSurface = (surface, get) => ({ surface, get });
19
+ const STATUS_SURFACES = STATUS_KEYS.flatMap((status) => ['text', 'fill', 'fillText', 'bg'].map((token) => pathSurface(`palette.${status}.${token}`)));
20
+ const TYPOGRAPHY_SURFACES = TYPOGRAPHY_VARIANTS.flatMap((variant) => ['fontSize', 'fontWeight', 'lineHeight', 'letterSpacing'].map((token) => pathSurface(`typography.${variant}.${token}`)));
21
+ const componentPath = (component, path) => pathSurface(`components.${component}.${path}`);
22
+ const componentKeyLeaf = (component, path, key, leaf) => keyedSurface(`components.${component}.${path}.${key}.${leaf}`, (theme) => { var _a, _b; return (_b = (_a = getPath(theme, `components.${component}.${path}`)) === null || _a === void 0 ? void 0 : _a[key]) === null || _b === void 0 ? void 0 : _b[leaf]; });
23
+ const COMPONENT_SURFACES = [
24
+ keyedSurface('components.MuiCssBaseline.reduced-motion.transitionDuration', (theme) => {
25
+ var _a, _b, _c, _d, _e;
26
+ return (_e = (_d = (_c = (_b = (_a = theme.components) === null || _a === void 0 ? void 0 : _a.MuiCssBaseline) === null || _b === void 0 ? void 0 : _b.styleOverrides) === null || _c === void 0 ? void 0 : _c['@media (prefers-reduced-motion: reduce)']) === null || _d === void 0 ? void 0 : _d['*, *::before, *::after']) === null || _e === void 0 ? void 0 : _e.transitionDuration;
27
+ }),
28
+ componentPath('MuiButton', 'defaultProps.disableElevation'),
29
+ componentPath('MuiButton', 'styleOverrides.root.minHeight'),
30
+ componentPath('MuiButton', 'styleOverrides.root.borderRadius'),
31
+ componentPath('MuiButton', 'styleOverrides.root.textTransform'),
32
+ componentKeyLeaf('MuiButton', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
33
+ componentKeyLeaf('MuiButton', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
34
+ componentPath('MuiButton', 'styleOverrides.contained.boxShadow'),
35
+ componentPath('MuiButton', 'styleOverrides.outlined.borderColor'),
36
+ componentPath('MuiTableCell', 'styleOverrides.root.padding'),
37
+ componentPath('MuiIconButton', 'styleOverrides.root.minHeight'),
38
+ componentPath('MuiIconButton', 'styleOverrides.root.minWidth'),
39
+ componentKeyLeaf('MuiIconButton', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
40
+ componentKeyLeaf('MuiIconButton', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
41
+ componentPath('MuiDivider', 'styleOverrides.root.borderColor'),
42
+ componentPath('MuiTooltip', 'styleOverrides.tooltip.boxShadow'),
43
+ componentPath('MuiTooltip', 'styleOverrides.tooltip.transitionDuration'),
44
+ componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.enter'),
45
+ componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.exit'),
46
+ componentPath('MuiSelect', 'defaultProps.variant'),
47
+ componentPath('MuiSelect', 'styleOverrides.select.minHeight'),
48
+ componentKeyLeaf('MuiSelect', 'styleOverrides.select', '@media (any-pointer: coarse)', 'minHeight'),
49
+ componentPath('MuiCheckbox', 'styleOverrides.root.color'),
50
+ componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
51
+ componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
52
+ componentPath('MuiFormControlLabel', 'styleOverrides.root.minHeight'),
53
+ componentKeyLeaf('MuiFormControlLabel', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
54
+ componentKeyLeaf('MuiFormControlLabel', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
55
+ componentPath('MuiFormControlLabel', 'styleOverrides.label.fontSize'),
56
+ componentPath('MuiContainer', 'styleOverrides.root.paddingLeft'),
57
+ componentPath('MuiContainer', 'styleOverrides.root.paddingRight'),
58
+ componentKeyLeaf('MuiContainer', 'styleOverrides.root', '@media (min-width:1200px)', 'paddingLeft'),
59
+ componentKeyLeaf('MuiContainer', 'styleOverrides.root', '@media (min-width:1200px)', 'paddingRight'),
60
+ componentPath('MuiPaper', 'defaultProps.elevation'),
61
+ componentPath('MuiPaper', 'defaultProps.variant'),
62
+ componentPath('MuiPaper', 'styleOverrides.root.borderRadius'),
63
+ componentPath('MuiPaper', 'styleOverrides.root.boxShadow'),
64
+ componentPath('MuiCard', 'defaultProps.elevation'),
65
+ componentPath('MuiCard', 'defaultProps.variant'),
66
+ componentPath('MuiCard', 'styleOverrides.root.borderRadius'),
67
+ componentPath('MuiCard', 'styleOverrides.root.boxShadow'),
68
+ componentPath('MuiChip', 'styleOverrides.root.height'),
69
+ componentPath('MuiChip', 'styleOverrides.root.borderRadius'),
70
+ componentPath('MuiChip', 'styleOverrides.root.fontSize'),
71
+ keyedSurface('components.MuiChip.styleOverrides.root.coarse-hit-area.inset', (theme) => {
72
+ var _a, _b, _c, _d, _e, _f;
73
+ return (_f = (_e = (_d = (_c = (_b = (_a = theme.components) === null || _a === void 0 ? void 0 : _a.MuiChip) === null || _b === void 0 ? void 0 : _b.styleOverrides) === null || _c === void 0 ? void 0 : _c.root) === null || _d === void 0 ? void 0 : _d['@media (any-pointer: coarse)']) === null || _e === void 0 ? void 0 : _e['&::after']) === null || _f === void 0 ? void 0 : _f.inset;
74
+ }),
75
+ componentPath('MuiTextField', 'defaultProps.variant'),
76
+ componentPath('MuiTextField', 'styleOverrides.root.minHeight'),
77
+ componentKeyLeaf('MuiTextField', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
78
+ componentKeyLeaf('MuiTextField', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
79
+ componentPath('MuiOutlinedInput', 'styleOverrides.root.minHeight'),
80
+ componentPath('MuiOutlinedInput', 'styleOverrides.root.borderRadius'),
81
+ keyedSurface('components.MuiOutlinedInput.variants.non-multiline-height', (theme) => {
82
+ var _a, _b, _c, _d, _e;
83
+ return (_e = (_d = (_c = (_b = (_a = theme.components) === null || _a === void 0 ? void 0 : _a.MuiOutlinedInput) === null || _b === void 0 ? void 0 : _b.variants) === null || _c === void 0 ? void 0 : _c.find(({ props }) => (props === null || props === void 0 ? void 0 : props.multiline) === false)) === null || _d === void 0 ? void 0 : _d.style) === null || _e === void 0 ? void 0 : _e.height;
84
+ }),
85
+ componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
86
+ componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
87
+ componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '& .MuiOutlinedInput-notchedOutline', 'borderColor'),
88
+ componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '&:hover .MuiOutlinedInput-notchedOutline', 'borderColor'),
89
+ componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '&.Mui-focused .MuiOutlinedInput-notchedOutline', 'borderColor'),
90
+ componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '&.Mui-error .MuiOutlinedInput-notchedOutline', 'borderColor'),
91
+ componentPath('MuiDialog', 'defaultProps.transitionDuration.enter'),
92
+ componentPath('MuiDialog', 'defaultProps.transitionDuration.exit'),
93
+ componentPath('MuiDialog', 'styleOverrides.paper.borderRadius'),
94
+ componentPath('MuiDialog', 'styleOverrides.paper.boxShadow'),
95
+ componentPath('MuiDrawer', 'styleOverrides.paper.boxShadow'),
96
+ componentPath('MuiMenu', 'styleOverrides.paper.boxShadow'),
97
+ ...['success', 'warning', 'error', 'info'].flatMap((status) => {
98
+ const slot = `standard${status[0].toUpperCase()}${status.slice(1)}`;
99
+ return [
100
+ componentPath('MuiAlert', `styleOverrides.${slot}.color`),
101
+ componentPath('MuiAlert', `styleOverrides.${slot}.backgroundColor`),
102
+ componentKeyLeaf('MuiAlert', `styleOverrides.${slot}`, '& .MuiAlert-icon', 'color'),
103
+ ];
104
+ }),
105
+ ...['MuiOutlinedInput', 'MuiFilledInput', 'MuiInput'].flatMap((component) => [
106
+ componentKeyLeaf(component, 'styleOverrides.input', '&:-webkit-autofill', 'WebkitBoxShadow'),
107
+ componentKeyLeaf(component, 'styleOverrides.input', '&:-webkit-autofill', 'WebkitTextFillColor'),
108
+ componentKeyLeaf(component, 'styleOverrides.input', '&:autofill', 'boxShadow'),
109
+ componentKeyLeaf(component, 'styleOverrides.input', '&:autofill', 'WebkitTextFillColor'),
110
+ ]),
111
+ ];
112
+ /** The versioned surface inventory used by every adopting application. */
113
+ export const THEME_COMPLETENESS_SURFACES = [
114
+ pathSurface('palette.primary.main'),
115
+ ...STATUS_SURFACES,
116
+ pathSurface('palette.ink.primary'),
117
+ pathSurface('palette.ink.secondary'),
118
+ pathSurface('palette.ink.muted'),
119
+ pathSurface('palette.background.default'),
120
+ pathSurface('palette.background.paper'),
121
+ pathSurface('palette.divider'),
122
+ pathSurface('palette.controlBorder.main'),
123
+ pathSurface('palette.controlBorder.hover'),
124
+ pathSurface('palette.controlBorder.focus'),
125
+ pathSurface('palette.controlBorder.error'),
126
+ pathSurface('palette.dataSeries.categorical'),
127
+ pathSurface('radius.control'),
128
+ pathSurface('radius.card'),
129
+ pathSurface('typography.fontFamily'),
130
+ ...TYPOGRAPHY_SURFACES,
131
+ pathSurface('shape.borderRadius'),
132
+ {
133
+ surface: 'spacing.unit',
134
+ get: (theme) => (typeof theme.spacing === 'function' ? theme.spacing(1) : undefined),
135
+ },
136
+ pathSurface('breakpoints.values'),
137
+ pathSurface('shadow.rest'),
138
+ pathSurface('shadow.overlay'),
139
+ pathSurface('motion.duration.fast'),
140
+ pathSurface('motion.duration.base'),
141
+ pathSurface('motion.duration.overlayEnter'),
142
+ pathSurface('motion.duration.overlayExit'),
143
+ pathSurface('motion.duration.chart'),
144
+ pathSurface('motion.easing.enter'),
145
+ pathSurface('motion.easing.exit'),
146
+ pathSurface('motion.easing.state'),
147
+ pathSurface('motion.reduced'),
148
+ pathSurface('fontLoading.family'),
149
+ pathSurface('fontLoading.weights'),
150
+ ...COMPONENT_SURFACES,
151
+ ];
152
+ function parseColour(colour) {
153
+ if (typeof colour !== 'string')
154
+ return null;
155
+ const value = colour.trim();
156
+ if (value.startsWith('#')) {
157
+ const hex = value.slice(1);
158
+ const expanded = hex.length === 3
159
+ ? hex.split('').map((part) => part + part).join('')
160
+ : hex;
161
+ if (!/^[\da-f]{6}([\da-f]{2})?$/i.test(expanded))
162
+ return null;
163
+ return {
164
+ r: parseInt(expanded.slice(0, 2), 16),
165
+ g: parseInt(expanded.slice(2, 4), 16),
166
+ b: parseInt(expanded.slice(4, 6), 16),
167
+ a: expanded.length === 8 ? parseInt(expanded.slice(6, 8), 16) / 255 : 1,
168
+ };
169
+ }
170
+ const match = value.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
171
+ if (!match)
172
+ return null;
173
+ return {
174
+ r: Number(match[1]),
175
+ g: Number(match[2]),
176
+ b: Number(match[3]),
177
+ a: match[4] === undefined ? 1 : Number(match[4]),
178
+ };
179
+ }
180
+ function composite(foreground, background) {
181
+ const alpha = foreground.a + background.a * (1 - foreground.a);
182
+ if (alpha === 0)
183
+ return { r: 0, g: 0, b: 0, a: 0 };
184
+ return {
185
+ r: (foreground.r * foreground.a + background.r * background.a * (1 - foreground.a)) / alpha,
186
+ g: (foreground.g * foreground.a + background.g * background.a * (1 - foreground.a)) / alpha,
187
+ b: (foreground.b * foreground.a + background.b * background.a * (1 - foreground.a)) / alpha,
188
+ a: alpha,
189
+ };
190
+ }
191
+ function luminance(colour) {
192
+ const channels = [colour.r, colour.g, colour.b].map((channel) => {
193
+ const value = channel / 255;
194
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
195
+ });
196
+ return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
197
+ }
198
+ /** Calculates WCAG contrast, including alpha compositing over the background. */
199
+ export function calculateContrastRatio(foreground, background) {
200
+ const foregroundColour = parseColour(foreground);
201
+ const backgroundColour = parseColour(background);
202
+ if (!foregroundColour || !backgroundColour) {
203
+ try {
204
+ const ratio = getMuiContrastRatio(foreground, background);
205
+ return Number.isFinite(ratio) ? ratio : Number.NaN;
206
+ }
207
+ catch (_a) {
208
+ return Number.NaN;
209
+ }
210
+ }
211
+ const opaqueBackground = backgroundColour.a < 1
212
+ ? composite(backgroundColour, { r: 255, g: 255, b: 255, a: 1 })
213
+ : backgroundColour;
214
+ const opaqueForeground = foregroundColour.a < 1
215
+ ? composite(foregroundColour, opaqueBackground)
216
+ : foregroundColour;
217
+ const light = Math.max(luminance(opaqueForeground), luminance(opaqueBackground));
218
+ const dark = Math.min(luminance(opaqueForeground), luminance(opaqueBackground));
219
+ return (light + 0.05) / (dark + 0.05);
220
+ }
221
+ function contrastFindings(theme) {
222
+ var _a, _b, _c, _d, _e;
223
+ const findings = [];
224
+ for (const status of STATUS_KEYS) {
225
+ const tokens = (_a = theme.palette) === null || _a === void 0 ? void 0 : _a[status];
226
+ if (!tokens)
227
+ continue;
228
+ const textRatio = calculateContrastRatio(tokens.text, tokens.bg);
229
+ if (!Number.isFinite(textRatio) || textRatio < 4.5) {
230
+ findings.push({
231
+ surface: `contrast.${status}.text-on-bg`,
232
+ reason: Number.isFinite(textRatio)
233
+ ? `Expected at least 4.5:1; received ${textRatio.toFixed(2)}:1.`
234
+ : 'Contrast could not be calculated from the declared colours.',
235
+ });
236
+ }
237
+ const fillRatio = calculateContrastRatio(tokens.fillText, tokens.fill);
238
+ if (!Number.isFinite(fillRatio) || fillRatio < 4.5) {
239
+ findings.push({
240
+ surface: `contrast.${status}.fillText-on-fill`,
241
+ reason: Number.isFinite(fillRatio)
242
+ ? `Expected at least 4.5:1; received ${fillRatio.toFixed(2)}:1.`
243
+ : 'Contrast could not be calculated from the declared colours.',
244
+ });
245
+ }
246
+ }
247
+ const backgrounds = {
248
+ white: '#FFFFFF',
249
+ page: (_c = (_b = theme.palette) === null || _b === void 0 ? void 0 : _b.background) === null || _c === void 0 ? void 0 : _c.default,
250
+ };
251
+ for (const state of ['main', 'hover', 'focus', 'error']) {
252
+ const colour = (_e = (_d = theme.palette) === null || _d === void 0 ? void 0 : _d.controlBorder) === null || _e === void 0 ? void 0 : _e[state];
253
+ if (!colour)
254
+ continue;
255
+ for (const [surface, background] of Object.entries(backgrounds)) {
256
+ const ratio = calculateContrastRatio(colour, background);
257
+ if (!Number.isFinite(ratio) || ratio < 3) {
258
+ findings.push({
259
+ surface: `contrast.controlBorder.${state}-on-${surface}`,
260
+ reason: Number.isFinite(ratio)
261
+ ? `Expected at least 3:1; received ${ratio.toFixed(2)}:1.`
262
+ : 'Contrast could not be calculated from the declared colours.',
263
+ });
264
+ }
265
+ }
266
+ }
267
+ return findings;
268
+ }
269
+ /**
270
+ * Returns completeness findings. Adopting applications make this a hard CI
271
+ * assertion in their own suite; non-adopters may call it for reporting only.
272
+ */
273
+ export function assertThemeComplete(theme, { exemptions = [] } = {}) {
274
+ var _a, _b;
275
+ const declaredExemptions = [
276
+ ...(((_a = theme === null || theme === void 0 ? void 0 : theme.themeCompleteness) === null || _a === void 0 ? void 0 : _a.exemptions) || []),
277
+ ...exemptions,
278
+ ];
279
+ const validExemptions = new Set();
280
+ const findings = [];
281
+ for (const exemption of declaredExemptions) {
282
+ if (!(exemption === null || exemption === void 0 ? void 0 : exemption.surface) || !((_b = exemption === null || exemption === void 0 ? void 0 : exemption.reason) === null || _b === void 0 ? void 0 : _b.trim())) {
283
+ findings.push({
284
+ surface: `exemption.${(exemption === null || exemption === void 0 ? void 0 : exemption.surface) || 'unknown'}`,
285
+ reason: 'A completeness exemption must name a surface and include a reason.',
286
+ });
287
+ continue;
288
+ }
289
+ validExemptions.add(exemption.surface);
290
+ }
291
+ for (const descriptor of THEME_COMPLETENESS_SURFACES) {
292
+ const value = descriptor.get(theme);
293
+ const defaultValue = descriptor.get(MUI_DEFAULT_THEME);
294
+ if ((value === undefined || sameValue(value, defaultValue)) && !validExemptions.has(descriptor.surface)) {
295
+ findings.push({
296
+ surface: descriptor.surface,
297
+ reason: value === undefined
298
+ ? 'The surface is not deliberately defined.'
299
+ : 'The surface still equals MUI\'s untouched default.',
300
+ });
301
+ }
302
+ }
303
+ findings.push(...contrastFindings(theme));
304
+ return { findings };
305
+ }
306
+ /** Reports adoption signals as numbers; it never changes completeness. */
307
+ export function reportThemeAdoption(sources = []) {
308
+ const normalized = sources.map((entry, index) => typeof entry === 'string'
309
+ ? { path: `source-${index + 1}`, source: entry }
310
+ : { path: entry.path || `source-${index + 1}`, source: entry.source || '' });
311
+ const themeBypassPaths = normalized
312
+ .filter(({ source }) => /\bcreateTheme\s*\(/.test(source) && !/\bcreateAppTheme\s*\(/.test(source))
313
+ .map(({ path }) => path);
314
+ const inlineHexCount = normalized.reduce((count, { source }) => { var _a; return count + (((_a = source.match(/#[\da-f]{3,8}\b/gi)) === null || _a === void 0 ? void 0 : _a.length) || 0); }, 0);
315
+ return {
316
+ inlineHexCount,
317
+ themeBypassPathCount: themeBypassPaths.length,
318
+ themeBypassPaths,
319
+ };
320
+ }
@@ -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.2",
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",