@cerebruminc/cerebellum 20.0.6 → 20.0.7
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/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/mantine/components/Button.module.css +14 -2
- package/src/mantine/mantineTheme.test.tsx +249 -0
- package/src/mantine/mantineTheme.ts +55 -33
- package/src/mantine/services/NotificationAction.module.css +10 -0
- package/src/mantine/services/notifications.tsx +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# react-component-lib-boilerplate
|
|
2
2
|
|
|
3
|
+
## [20.0.7](https://github.com/cerebruminc/cerebellum/compare/v20.0.6...v20.0.7) (2026-08-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **mantine:** merge Modal and Notification classNames instead of replacing them ([5718033](https://github.com/cerebruminc/cerebellum/commit/57180332c8a1ca206e9e52c9eba4e4f936c5cad7))
|
|
9
|
+
* **mantine:** stop the inline filled-button shadow from outranking its :disabled reset ([dbcdd46](https://github.com/cerebruminc/cerebellum/commit/dbcdd460d81401bbe8ca403e76cf13ae20e1f73d))
|
|
10
|
+
* **notifications:** stop inline styles from blocking the stylesheet rules meant to override them ([0fee4cd](https://github.com/cerebruminc/cerebellum/commit/0fee4cd1212eb62aa82c365cabb4dd1524cdd1b1))
|
|
11
|
+
|
|
3
12
|
## [20.0.6](https://github.com/cerebruminc/cerebellum/compare/v20.0.5...v20.0.6) (2026-08-18)
|
|
4
13
|
|
|
5
14
|
|
package/package.json
CHANGED
|
@@ -52,8 +52,20 @@
|
|
|
52
52
|
display: none;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
.
|
|
56
|
-
|
|
55
|
+
/* The filled variant's glow. The base value lives here, not in the theme's `styles`:
|
|
56
|
+
Mantine applies `styles` inline, and an inline box-shadow would outrank the
|
|
57
|
+
`:disabled` reset below. `--button-shadow` is set in the theme's Button `vars`,
|
|
58
|
+
which Mantine also applies inline -- but a custom property is only a value, so the
|
|
59
|
+
cascade below still decides who gets to use it. Scoped to the filled variant so no
|
|
60
|
+
other variant gains a box-shadow declaration it did not have before. */
|
|
61
|
+
.root[data-variant="filled"] {
|
|
62
|
+
box-shadow: var(--button-shadow, none);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Doubled so this outranks the rule above regardless of source order, and beats a
|
|
66
|
+
consuming app's own single-class rule too. */
|
|
67
|
+
.root.root:disabled,
|
|
68
|
+
.root.root[data-disabled] {
|
|
57
69
|
box-shadow: none;
|
|
58
70
|
}
|
|
59
71
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/mantine/mantineTheme.test.tsx
|
|
3
|
+
* @summary Guards the theme against inline styles that break the stylesheet.
|
|
4
|
+
* @remarks
|
|
5
|
+
* Mantine applies `styles` and `vars` INLINE — get-style.mjs flat-spreads every
|
|
6
|
+
* resolved slot into one `style` object — and @mantine/emotion is not installed.
|
|
7
|
+
* That fails two ways, and this file guards both:
|
|
8
|
+
*
|
|
9
|
+
* 1. A key that only means something in a stylesheet (`@media`, `:hover`, `::before`,
|
|
10
|
+
* a descendant selector) is silently discarded.
|
|
11
|
+
* 2. A *base* value left in `styles` is silently applied inline, where it outranks
|
|
12
|
+
* the stylesheet rule meant to override it. This is the one that shipped four
|
|
13
|
+
* visual bugs, so it gets the STYLESHEET_OWNED table below.
|
|
14
|
+
*
|
|
15
|
+
* Anything needing a selector or a breakpoint belongs in a component CSS Module,
|
|
16
|
+
* wired through the theme's `classNames`.
|
|
17
|
+
*
|
|
18
|
+
* Both scans run over PROPS_MATRIX, not just each block's defaults. `styles` and
|
|
19
|
+
* `vars` branch on props, so resolving a block once leaves every other branch
|
|
20
|
+
* unscanned — which is how an inline `boxShadow` on `variant="filled"` outranked
|
|
21
|
+
* Button.module.css's `:disabled` reset without any test noticing.
|
|
22
|
+
*/
|
|
23
|
+
import { DEFAULT_THEME, MantineProvider, type MantineTheme, Modal, mergeMantineTheme } from "@mantine/core";
|
|
24
|
+
import { render } from "@testing-library/react";
|
|
25
|
+
import modalClasses from "./components/Modal.module.css";
|
|
26
|
+
import { createCustomMantineTheme } from "./mantineTheme";
|
|
27
|
+
import type { Color, NotificationTone, NotificationType, TextVariant } from "./types";
|
|
28
|
+
|
|
29
|
+
const theme: MantineTheme = mergeMantineTheme(DEFAULT_THEME, createCustomMantineTheme());
|
|
30
|
+
|
|
31
|
+
type Props = Record<string, unknown>;
|
|
32
|
+
type Slots = Record<string, Props>;
|
|
33
|
+
|
|
34
|
+
/** The shape this file relies on. Mantine types every one of these as `any`. */
|
|
35
|
+
type ThemeBlock = {
|
|
36
|
+
classNames?: Record<string, string>;
|
|
37
|
+
defaultProps?: Props;
|
|
38
|
+
styles?: (theme: MantineTheme, props: Props) => Slots;
|
|
39
|
+
vars?: (theme: MantineTheme, props: Props) => Slots;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const HOOKS = ["styles", "vars"] as const;
|
|
43
|
+
|
|
44
|
+
const componentNames = Object.keys(theme.components);
|
|
45
|
+
const blockFor = (name: string): ThemeBlock => theme.components[name] as ThemeBlock;
|
|
46
|
+
|
|
47
|
+
/** Keys that cannot work as an inline style property. `--custom-props` can, so exempt them. */
|
|
48
|
+
const STYLESHEET_ONLY = /^[@.]|[&:\s>[\]{},]/;
|
|
49
|
+
const isNestedKey = (key: string) => !key.startsWith("--") && STYLESHEET_ONLY.test(key);
|
|
50
|
+
|
|
51
|
+
const nestedKeysIn = (value: unknown, path: string, seen = new WeakSet<object>()): string[] => {
|
|
52
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || seen.has(value)) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
seen.add(value);
|
|
56
|
+
|
|
57
|
+
return Object.entries(value).flatMap(([key, child]) => {
|
|
58
|
+
const here = `${path}.${key}`;
|
|
59
|
+
return isNestedKey(key) ? [here] : nestedKeysIn(child, here, seen);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Enumerating a union as a Record forces the compiler to flag a member added later,
|
|
65
|
+
* so the matrix below cannot quietly fall behind the types it covers.
|
|
66
|
+
*/
|
|
67
|
+
const membersOf = <T extends string>(union: Record<T, true>) => Object.keys(union) as T[];
|
|
68
|
+
|
|
69
|
+
const TEXT_VARIANTS = membersOf<TextVariant>({
|
|
70
|
+
"body-xs": true,
|
|
71
|
+
"body-sm": true,
|
|
72
|
+
"body-md": true,
|
|
73
|
+
"body-lg": true,
|
|
74
|
+
"title-xl": true,
|
|
75
|
+
"title-lg": true,
|
|
76
|
+
"title-md": true,
|
|
77
|
+
"title-sm": true,
|
|
78
|
+
"title-xs": true,
|
|
79
|
+
"title-2xs": true,
|
|
80
|
+
"semibold-sm": true,
|
|
81
|
+
});
|
|
82
|
+
const NOTIFICATION_TYPES = membersOf<NotificationType>({ toast: true, bar: true, dialog: true });
|
|
83
|
+
const NOTIFICATION_TONES = membersOf<NotificationTone>({ positive: true, negative: true, neutral: true });
|
|
84
|
+
const PILL_COLORS = membersOf<Color>({
|
|
85
|
+
aqua: true,
|
|
86
|
+
blue: true,
|
|
87
|
+
brand: true,
|
|
88
|
+
gray: true,
|
|
89
|
+
orange: true,
|
|
90
|
+
peach: true,
|
|
91
|
+
purple: true,
|
|
92
|
+
red: true,
|
|
93
|
+
yellow: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/** Every block reading `props.size` funnels into the same `getInputVars` branches. */
|
|
97
|
+
const INPUT_SIZES: Props[] = ["xs", "sm", "md", "lg", "xl", undefined].map((size) => ({ size }));
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* One entry per theme block whose `styles`/`vars` reads props. Each row is merged
|
|
101
|
+
* over that block's own `defaultProps`, and the block's bare defaults are always
|
|
102
|
+
* scanned too, so a row only has to name what it changes.
|
|
103
|
+
*/
|
|
104
|
+
const PROPS_MATRIX: Record<string, Props[]> = {
|
|
105
|
+
Button: [
|
|
106
|
+
...INPUT_SIZES,
|
|
107
|
+
{ variant: "filled" },
|
|
108
|
+
{ variant: "filled", color: "gray" },
|
|
109
|
+
{ variant: "filled", w: 200 },
|
|
110
|
+
{ variant: "light" },
|
|
111
|
+
{ variant: "light", color: "gray" },
|
|
112
|
+
{ variant: "subtle" },
|
|
113
|
+
{ variant: "subtle", color: "gray" },
|
|
114
|
+
{ variant: "outline" },
|
|
115
|
+
{ variant: "default" },
|
|
116
|
+
{ boxed: true },
|
|
117
|
+
{ boxed: true, variant: "subtle" },
|
|
118
|
+
],
|
|
119
|
+
Text: [...TEXT_VARIANTS.map((variant) => ({ variant })), { variant: undefined }, { variant: "not-a-variant" }],
|
|
120
|
+
Notification: [
|
|
121
|
+
...NOTIFICATION_TYPES.flatMap((notificationType) =>
|
|
122
|
+
NOTIFICATION_TONES.flatMap((tone) => [
|
|
123
|
+
{ notificationType, tone },
|
|
124
|
+
{ notificationType, tone, icon: "icon" },
|
|
125
|
+
])
|
|
126
|
+
),
|
|
127
|
+
{ notificationType: undefined, tone: undefined },
|
|
128
|
+
],
|
|
129
|
+
Pill: [...PILL_COLORS.flatMap((color) => [{ color }, { color, withRemoveButton: true }]), { color: undefined }],
|
|
130
|
+
TextInput: INPUT_SIZES,
|
|
131
|
+
PasswordInput: INPUT_SIZES,
|
|
132
|
+
Textarea: INPUT_SIZES,
|
|
133
|
+
Select: INPUT_SIZES,
|
|
134
|
+
MultiSelect: INPUT_SIZES,
|
|
135
|
+
DateInput: INPUT_SIZES,
|
|
136
|
+
DatePickerInput: INPUT_SIZES,
|
|
137
|
+
Autocomplete: INPUT_SIZES,
|
|
138
|
+
InputBase: INPUT_SIZES,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** A block's defaults, plus every matrix row layered over them. */
|
|
142
|
+
const permutationsFor = (name: string) => {
|
|
143
|
+
const defaults = { ...blockFor(name).defaultProps };
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
{ label: "defaultProps", props: defaults },
|
|
147
|
+
...(PROPS_MATRIX[name] ?? []).map((row) => ({ label: JSON.stringify(row), props: { ...defaults, ...row } })),
|
|
148
|
+
];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
describe("no stylesheet-only keys in inline styles", () => {
|
|
152
|
+
it.each(["@media (max-width: 600px)", "&:hover", ":hover", "::before", "> .thumb", "[data-x] span"])("rejects %s", (key) => {
|
|
153
|
+
expect(isNestedKey(key)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it.each(["padding", "backgroundColor", "--badge-fz"])("allows %s", (key) => {
|
|
157
|
+
expect(isNestedKey(key)).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("finds no stylesheet-only key in any theme block, under any props in the matrix", () => {
|
|
161
|
+
const offenders = componentNames.flatMap((name) => {
|
|
162
|
+
const block = blockFor(name);
|
|
163
|
+
|
|
164
|
+
return HOOKS.flatMap((hook) => {
|
|
165
|
+
const resolve = block[hook];
|
|
166
|
+
if (typeof resolve !== "function") {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return permutationsFor(name).flatMap(({ label, props }) => nestedKeysIn(resolve(theme, props), `${name}.${hook} [${label}]`));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(offenders.sort()).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/* A block that reads props but has no matrix row would be scanned once, under its
|
|
178
|
+
defaults, leaving every other branch dark. Fail until someone adds the row. */
|
|
179
|
+
it("has a matrix entry for every block that reads props", () => {
|
|
180
|
+
const unscanned = componentNames.flatMap((name) =>
|
|
181
|
+
HOOKS.filter((hook) => {
|
|
182
|
+
const resolve = blockFor(name)[hook];
|
|
183
|
+
return typeof resolve === "function" && resolve.length >= 2 && !PROPS_MATRIX[name]?.length;
|
|
184
|
+
}).map((hook) => `${name}.${hook} reads props but PROPS_MATRIX has no entry for ${name}`)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(unscanned.sort()).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* Each row: this slot's CSS owns these properties, so the theme must not set them.
|
|
193
|
+
* Setting one inline is what killed the Modal's responsive padding, the notification
|
|
194
|
+
* close button's hover, and the filled Button's `:disabled` shadow reset.
|
|
195
|
+
*/
|
|
196
|
+
const STYLESHEET_OWNED = [
|
|
197
|
+
{ component: "Modal", slot: "header", properties: [/^padding/], owner: "components/Modal.module.css" },
|
|
198
|
+
{ component: "Modal", slot: "body", properties: [/^padding/], owner: "components/Modal.module.css" },
|
|
199
|
+
{ component: "Button", slot: "root", properties: [/^boxShadow/], owner: "components/Button.module.css" },
|
|
200
|
+
{
|
|
201
|
+
component: "Notification",
|
|
202
|
+
slot: "closeButton",
|
|
203
|
+
properties: ["opacity", /^transition/, /^background/],
|
|
204
|
+
owner: "services/NotificationAction.module.css",
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
describe("styles the stylesheet owns", () => {
|
|
209
|
+
it.each(STYLESHEET_OWNED)("leaves the $slot slot of $component to $owner", ({ component, slot, properties, owner }) => {
|
|
210
|
+
const block = blockFor(component);
|
|
211
|
+
const offenders = permutationsFor(component).flatMap(({ label, props }) =>
|
|
212
|
+
Object.keys(block.styles?.(theme, props)?.[slot] ?? {})
|
|
213
|
+
.filter((key) => properties.some((p) => (typeof p === "string" ? p === key : p.test(key))))
|
|
214
|
+
.map((key) => `${component}.styles.${slot}.${key} [${label}] is applied inline, outranking ${owner}`)
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(offenders).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/* The CSS is only reachable if the module class is wired to the slot. */
|
|
221
|
+
it.each(STYLESHEET_OWNED)("wires $owner to the $slot slot of $component", ({ component, slot }) => {
|
|
222
|
+
expect(blockFor(component).classNames?.[slot]).toEqual(expect.any(String));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/* `defaultProps` goes through a shallow spread in use-props.mjs, so a caller passing
|
|
227
|
+
any `classNames` replaces the theme's wholesale. Top-level `classNames` merges per
|
|
228
|
+
slot instead. This ran for Modal and Notification only; every block needs it. */
|
|
229
|
+
it("keeps classNames out of defaultProps in every theme block", () => {
|
|
230
|
+
const nested = componentNames.filter((name) => blockFor(name).defaultProps?.classNames !== undefined);
|
|
231
|
+
|
|
232
|
+
expect(nested).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/* The regression test for the above: openConfirmModal passes its own classNames,
|
|
236
|
+
which used to replace the theme's outright. */
|
|
237
|
+
it("keeps Modal's module classes when the caller passes classNames too", () => {
|
|
238
|
+
render(
|
|
239
|
+
<MantineProvider theme={createCustomMantineTheme()}>
|
|
240
|
+
<Modal opened onClose={() => {}} title="Confirm" classNames={{ content: "caller-content" }}>
|
|
241
|
+
Body copy
|
|
242
|
+
</Modal>
|
|
243
|
+
</MantineProvider>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(document.querySelector(".caller-content")).toHaveClass(modalClasses.content);
|
|
247
|
+
expect(document.querySelectorAll(`.${modalClasses.header}`)).toHaveLength(1);
|
|
248
|
+
expect(document.querySelectorAll(`.${modalClasses.body}`)).toHaveLength(1);
|
|
249
|
+
});
|
|
@@ -32,8 +32,8 @@ import buttonClasses from "./components/Button.module.css";
|
|
|
32
32
|
import modalClasses from "./components/Modal.module.css";
|
|
33
33
|
import pillClasses from "./components/Pill.module.css";
|
|
34
34
|
import scrollAreaClasses from "./components/ScrollArea.module.css";
|
|
35
|
-
import notificationActionClasses from "./services/NotificationAction.module.css";
|
|
36
35
|
import notificationClasses from "./services/Notification.module.css";
|
|
36
|
+
import notificationActionClasses from "./services/NotificationAction.module.css";
|
|
37
37
|
|
|
38
38
|
type TonedNotificationProps = NotificationProps &
|
|
39
39
|
Record<`data-${string}`, unknown> & {
|
|
@@ -106,10 +106,8 @@ const getInputVars = (size?: MantineSize | (string & {})) => {
|
|
|
106
106
|
wrapper["--input-height-lg"] = "calc(3.4375rem * var(--mantine-scale))"; // 55px
|
|
107
107
|
wrapper["--input-height-xl"] = "calc(3.75rem * var(--mantine-scale))"; // 60px
|
|
108
108
|
|
|
109
|
-
wrapper["--input-fz"] =
|
|
110
|
-
|
|
111
|
-
wrapper["--input-height"] =
|
|
112
|
-
wrapper[`--input-height-${resolvedSize}` as keyof typeof wrapper];
|
|
109
|
+
wrapper["--input-fz"] = wrapper[`--input-fz-${resolvedSize}` as keyof typeof wrapper];
|
|
110
|
+
wrapper["--input-height"] = wrapper[`--input-height-${resolvedSize}` as keyof typeof wrapper];
|
|
113
111
|
|
|
114
112
|
return { wrapper };
|
|
115
113
|
};
|
|
@@ -470,6 +468,7 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
470
468
|
| "--button-hover"
|
|
471
469
|
| "--button-hover-color"
|
|
472
470
|
| "--button-color"
|
|
471
|
+
| "--button-shadow"
|
|
473
472
|
| "--button-subtle-bg"
|
|
474
473
|
| "--button-subtle-underline",
|
|
475
474
|
string | undefined
|
|
@@ -520,6 +519,21 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
520
519
|
root["--button-subtle-underline"] = subtleUnderlineColor;
|
|
521
520
|
}
|
|
522
521
|
|
|
522
|
+
/*
|
|
523
|
+
* The filled variant's glow, as a custom property rather than a `styles`
|
|
524
|
+
* entry. `styles` is applied inline, and an inline box-shadow outranks
|
|
525
|
+
* Button.module.css's `:disabled` reset no matter how specific that
|
|
526
|
+
* selector is -- which is why disabled buttons kept glowing. Custom
|
|
527
|
+
* properties do work inline, so this keeps the value colour-derived while
|
|
528
|
+
* leaving the stylesheet able to override it.
|
|
529
|
+
*/
|
|
530
|
+
if (props.variant === "filled") {
|
|
531
|
+
const color = props.color || theme.primaryColor;
|
|
532
|
+
const colorScale = theme.colors[color] || theme.colors[theme.primaryColor];
|
|
533
|
+
|
|
534
|
+
root["--button-shadow"] = theme.other.highContrast ? "none" : `0 0 20px -6px ${rgba(colorScale[6], 1)}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
523
537
|
// gray does not follow the same tone progression as the other colors;
|
|
524
538
|
// filled variant gets white text via variantColorResolver, so skip it here
|
|
525
539
|
if (props.color === "gray" && props.variant !== "filled") {
|
|
@@ -529,16 +543,14 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
529
543
|
return { root };
|
|
530
544
|
},
|
|
531
545
|
styles: (theme: MantineTheme, props: ButtonProps) => {
|
|
532
|
-
const color = props.color || theme.primaryColor;
|
|
533
|
-
const colorScale = theme.colors[color] || theme.colors[theme.primaryColor];
|
|
534
|
-
const colorValue = colorScale[6];
|
|
535
|
-
|
|
536
546
|
return {
|
|
547
|
+
/*
|
|
548
|
+
* No `boxShadow` here. The filled variant's glow is set as
|
|
549
|
+
* `--button-shadow` in `vars` above and applied by Button.module.css,
|
|
550
|
+
* beside the `:disabled` rule that clears it. Setting it here would land
|
|
551
|
+
* inline and that reset could never win.
|
|
552
|
+
*/
|
|
537
553
|
root: {
|
|
538
|
-
// Add box-shadow to filled variant buttons using button's color
|
|
539
|
-
...(props.variant === "filled" && {
|
|
540
|
-
boxShadow: theme.other.highContrast ? "none" : `0 0 20px -6px ${rgba(colorValue, 1)}`,
|
|
541
|
-
}),
|
|
542
554
|
// Light variant gets a border in high contrast to match SecondaryButton/BoxedButton
|
|
543
555
|
...(props.variant === "light" &&
|
|
544
556
|
theme.other.highContrast && {
|
|
@@ -616,12 +628,19 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
616
628
|
vars: (_theme: MantineTheme, props: SelectProps) => getInputVars(props.size),
|
|
617
629
|
},
|
|
618
630
|
Modal: {
|
|
631
|
+
/*
|
|
632
|
+
* Top level, not inside `defaultProps`. Mantine merges
|
|
633
|
+
* `theme.components.X.classNames` additively per slot, but `defaultProps` goes
|
|
634
|
+
* through a shallow spread in `use-props.mjs`, so a caller passing any
|
|
635
|
+
* `classNames` replaces these wholesale. `openConfirmModal` does exactly that,
|
|
636
|
+
* which is why confirm modals were losing their padding and scrollbar.
|
|
637
|
+
*/
|
|
638
|
+
classNames: {
|
|
639
|
+
header: modalClasses.header,
|
|
640
|
+
body: modalClasses.body,
|
|
641
|
+
content: modalClasses.content,
|
|
642
|
+
},
|
|
619
643
|
defaultProps: {
|
|
620
|
-
classNames: {
|
|
621
|
-
header: modalClasses.header,
|
|
622
|
-
body: modalClasses.body,
|
|
623
|
-
content: modalClasses.content,
|
|
624
|
-
},
|
|
625
644
|
withCloseButton: false,
|
|
626
645
|
radius: "10px",
|
|
627
646
|
centered: true,
|
|
@@ -634,18 +653,18 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
634
653
|
color: highContrast ? "#4F6CEA" : "var(--mantine-color-blue-7)",
|
|
635
654
|
},
|
|
636
655
|
},
|
|
656
|
+
/*
|
|
657
|
+
* Header/body padding is owned entirely by Modal.module.css, base values
|
|
658
|
+
* included. A base value set here lands inline, and inline `padding` outranks
|
|
659
|
+
* any stylesheet rule without `!important`, so the module's `@media`
|
|
660
|
+
* padding-inline overrides could never apply.
|
|
661
|
+
*/
|
|
637
662
|
styles: (theme: MantineTheme) => ({
|
|
638
663
|
title: {
|
|
639
664
|
fontSize: "30px",
|
|
640
665
|
fontWeight: 600,
|
|
641
666
|
lineHeight: 1.33333,
|
|
642
667
|
},
|
|
643
|
-
header: {
|
|
644
|
-
padding: "49px 71px 0",
|
|
645
|
-
},
|
|
646
|
-
body: {
|
|
647
|
-
padding: "20px 71px 61px",
|
|
648
|
-
},
|
|
649
668
|
close: {
|
|
650
669
|
position: "absolute",
|
|
651
670
|
top: "19px",
|
|
@@ -766,11 +785,11 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
766
785
|
},
|
|
767
786
|
},
|
|
768
787
|
Notification: {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
788
|
+
// Top level so a caller's `classNames` merges with these instead of
|
|
789
|
+
// replacing them -- see the Modal block above.
|
|
790
|
+
classNames: {
|
|
791
|
+
root: notificationClasses.root,
|
|
792
|
+
closeButton: notificationActionClasses.closeButton,
|
|
774
793
|
},
|
|
775
794
|
vars: (theme: MantineTheme, props: TonedNotificationProps) => {
|
|
776
795
|
const notificationType = String(props.notificationType || "toast");
|
|
@@ -850,10 +869,13 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
850
869
|
// Make the button and icon larger
|
|
851
870
|
"--cb-size": "calc(2rem * var(--mantine-scale))",
|
|
852
871
|
"--cb-icon-size": "calc(1.5rem * var(--mantine-scale))",
|
|
853
|
-
backgroundColor: "transparent",
|
|
854
872
|
color: theme.colors.gray[8],
|
|
855
|
-
|
|
856
|
-
|
|
873
|
+
/*
|
|
874
|
+
* opacity, transition and background-color belong to
|
|
875
|
+
* NotificationAction.module.css, beside the :hover state. They have to:
|
|
876
|
+
* an inline `opacity: 0.6` here outranks the stylesheet's
|
|
877
|
+
* `:hover { opacity: 1 }` no matter how specific that selector is.
|
|
878
|
+
*/
|
|
857
879
|
...(isToast && {
|
|
858
880
|
display: "none",
|
|
859
881
|
}),
|
|
@@ -874,7 +896,7 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
874
896
|
icon: React.createElement(
|
|
875
897
|
"span",
|
|
876
898
|
{ style: { display: "block", width: "12px", height: "12px", position: "relative" } },
|
|
877
|
-
React.createElement(X, { fill: "currentColor" })
|
|
899
|
+
React.createElement(X, { fill: "currentColor" })
|
|
878
900
|
),
|
|
879
901
|
},
|
|
880
902
|
},
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
.action.action {
|
|
2
|
+
color: var(--mantine-color-gray-8);
|
|
2
3
|
text-underline-offset: 5px;
|
|
4
|
+
transition: color 350ms ease;
|
|
3
5
|
white-space: nowrap;
|
|
4
6
|
}
|
|
5
7
|
|
|
@@ -7,6 +9,14 @@
|
|
|
7
9
|
color: var(--mantine-color-gray-9);
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
/* Base state lives here, not in the theme's `styles`: Mantine applies `styles`
|
|
13
|
+
inline, and an inline opacity would outrank the :hover rule below. */
|
|
14
|
+
.closeButton.closeButton {
|
|
15
|
+
background-color: transparent;
|
|
16
|
+
opacity: 0.6;
|
|
17
|
+
transition: opacity 350ms ease, background-color 350ms ease;
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
.closeButton.closeButton:hover {
|
|
11
21
|
background-color: transparent;
|
|
12
22
|
opacity: 1;
|
|
@@ -64,7 +64,11 @@ export const showBar = (notification: BarNotification, store?: NotificationsStor
|
|
|
64
64
|
<Anchor
|
|
65
65
|
component="button"
|
|
66
66
|
variant="body-sm"
|
|
67
|
-
|
|
67
|
+
/*
|
|
68
|
+
* No `c` prop and no `styles.root` colour: both land as inline styles, which
|
|
69
|
+
* outrank any stylesheet rule, so the hover colour would never apply. The
|
|
70
|
+
* colour and its transition live in NotificationAction.module.css.
|
|
71
|
+
*/
|
|
68
72
|
classNames={{ root: classes.action }}
|
|
69
73
|
onClick={(event) => {
|
|
70
74
|
buttonClick?.(event);
|