@recursica/mui-adapter 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +3 -0
- package/CHANGELOG.md +52 -0
- package/dist/index.d.ts +90 -35
- package/dist/mui-adapter.cjs +68 -68
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +5660 -5582
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +63 -0
- package/src/components/Accordion/Accordion.module.css +21 -2
- package/src/components/Accordion/Accordion.stories.tsx +38 -0
- package/src/components/Accordion/Accordion.tsx +27 -12
- package/src/components/AssistiveElement/ASSISTIVEELEMENT_IMPLEMENTATION_NOTES.md +59 -0
- package/src/components/AssistiveElement/AssistiveElement.module.css +8 -1
- package/src/components/AssistiveElement/AssistiveElement.tsx +17 -7
- package/src/components/Autocomplete/Autocomplete.tsx +18 -2
- package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +36 -0
- package/src/components/Avatar/Avatar.tsx +6 -9
- package/src/components/Badge/Badge.module.css +0 -12
- package/src/components/Badge/Badge.tsx +0 -4
- package/src/components/Card/Card.tsx +5 -5
- package/src/components/Checkbox/Checkbox.module.css +17 -21
- package/src/components/Checkbox/Checkbox.tsx +24 -18
- package/src/components/Chip/Chip.module.css +16 -7
- package/src/components/Chip/Chip.tsx +1 -1
- package/src/components/Flex/Flex.tsx +1 -1
- package/src/components/FormControlWrapper/FORMCONTROLWRAPPER_IMPLEMENTATION_NOTES.md +35 -0
- package/src/components/FormControlWrapper/FormControlWrapper.tsx +24 -8
- package/src/components/Grid/Grid.tsx +1 -1
- package/src/components/Group/Group.tsx +1 -1
- package/src/components/HoverCard/HoverCard.tsx +1 -1
- package/src/components/Label/Label.module.css +15 -2
- package/src/components/Label/Label.tsx +7 -6
- package/src/components/Menu/Menu.tsx +1 -1
- package/src/components/Modal/Modal.tsx +1 -1
- package/src/components/NumberInput/NumberInput.tsx +1 -1
- package/src/components/Pagination/Pagination.tsx +1 -1
- package/src/components/Panel/Panel.tsx +1 -1
- package/src/components/Radio/Radio.module.css +33 -38
- package/src/components/Radio/Radio.tsx +58 -44
- package/src/components/Radio/RadioGroup.tsx +5 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +0 -7
- package/src/components/SegmentedControl/SegmentedControl.tsx +4 -8
- package/src/components/Slider/Slider.tsx +1 -1
- package/src/components/Stack/Stack.tsx +1 -1
- package/src/components/Stepper/Stepper.tsx +3 -3
- package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +123 -0
- package/src/components/Switch/Switch.module.css +152 -89
- package/src/components/Switch/Switch.tsx +140 -33
- package/src/components/Switch/SwitchGroup.tsx +37 -9
- package/src/components/Table/Table.tsx +1 -1
- package/src/components/Tabs/IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Tabs/Tabs.module.css +120 -43
- package/src/components/Tabs/Tabs.stories.tsx +5 -2
- package/src/components/Tabs/Tabs.tsx +1 -1
- package/src/components/Tabs/USAGE.md +27 -11
- package/src/components/TextField/TextField.tsx +1 -1
- package/src/components/TimePicker/TimePicker.tsx +1 -1
- package/src/components/Timeline/Timeline.tsx +1 -1
- package/src/components/Timeline/TimelineItem.tsx +2 -2
- package/src/components/Toast/Toast.tsx +4 -4
- package/src/components/Tooltip/Tooltip.tsx +1 -1
- package/src/components/Tree/Tree.tsx +1 -1
- package/src/types/mantine.d.ts +0 -7
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
FormControlLayout,
|
|
18
18
|
type FormControlLayoutProps,
|
|
19
19
|
} from "../FormControlLayout/FormControlLayout";
|
|
20
|
+
import { AssistiveElement } from "../AssistiveElement/AssistiveElement";
|
|
20
21
|
|
|
21
22
|
import styles from "./Radio.module.css";
|
|
22
23
|
|
|
@@ -78,38 +79,36 @@ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
|
|
|
78
79
|
delete restRecord["size"];
|
|
79
80
|
delete restRecord["color"];
|
|
80
81
|
|
|
82
|
+
// NOTE: MUI's actual prop is "classes", not "classNames" (that's Mantine's naming) — this
|
|
83
|
+
// was reading the wrong key and silently doing nothing. Fixed. Also "body"/"inner"/"radio"/
|
|
84
|
+
// "icon"/"labelWrapper"/"label" below were never real MUI `classes` slots (MUI's Radio only
|
|
85
|
+
// recognizes root/checked/disabled/colorPrimary/colorSecondary/sizeSmall, plus "input" from
|
|
86
|
+
// the underlying SwitchBase) — those slot names are Mantine's own Radio classNames API,
|
|
87
|
+
// copy-pasted over verbatim. Since none of them existed as real slots, MUI silently dropped
|
|
88
|
+
// them, so the circle (all its background/border CSS lived under `.radio`) never rendered —
|
|
89
|
+
// only the label showed. Fixed by keeping only the real slots here and drawing the circle as
|
|
90
|
+
// our own combined icon/checkedIcon node instead (see radioNode below), same pattern already
|
|
91
|
+
// used by Checkbox.
|
|
81
92
|
const mergedClassNames: Partial<Record<string, string>> = {
|
|
82
93
|
root: styles.root,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
radio: styles.radio,
|
|
86
|
-
icon: styles.icon,
|
|
87
|
-
labelWrapper: styles.labelWrapper,
|
|
88
|
-
label: styles.label,
|
|
94
|
+
checked: styles.checked,
|
|
95
|
+
disabled: styles.disabled,
|
|
89
96
|
};
|
|
90
97
|
|
|
91
|
-
const
|
|
98
|
+
const classesProp = restRecord.classes;
|
|
92
99
|
if (
|
|
93
|
-
|
|
94
|
-
typeof
|
|
95
|
-
!Array.isArray(
|
|
100
|
+
classesProp &&
|
|
101
|
+
typeof classesProp === "object" &&
|
|
102
|
+
!Array.isArray(classesProp)
|
|
96
103
|
) {
|
|
97
|
-
const o =
|
|
104
|
+
const o = classesProp as Partial<Record<string, string>>;
|
|
98
105
|
mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
|
|
99
|
-
mergedClassNames.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
: styles.radio;
|
|
106
|
-
mergedClassNames.icon = o.icon ? `${styles.icon} ${o.icon}` : styles.icon;
|
|
107
|
-
mergedClassNames.labelWrapper = o.labelWrapper
|
|
108
|
-
? `${styles.labelWrapper} ${o.labelWrapper}`
|
|
109
|
-
: styles.labelWrapper;
|
|
110
|
-
mergedClassNames.label = o.label
|
|
111
|
-
? `${styles.label} ${o.label}`
|
|
112
|
-
: styles.label;
|
|
106
|
+
mergedClassNames.checked = o.checked
|
|
107
|
+
? `${styles.checked} ${o.checked}`
|
|
108
|
+
: styles.checked;
|
|
109
|
+
mergedClassNames.disabled = o.disabled
|
|
110
|
+
? `${styles.disabled} ${o.disabled}`
|
|
111
|
+
: styles.disabled;
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
const classNameProp = restRecord.className as string | undefined;
|
|
@@ -144,17 +143,28 @@ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
|
|
|
144
143
|
return <>{roNode}</>;
|
|
145
144
|
}
|
|
146
145
|
|
|
147
|
-
//
|
|
146
|
+
// MUI's icon/checkedIcon fully replace the visual (they're the only child SwitchBase
|
|
147
|
+
// renders alongside the invisible native input) — so the ring + dot both have to live in
|
|
148
|
+
// one combined node per state, same pattern as Checkbox's icon/checkedIcon divs.
|
|
149
|
+
// sanitizedProps spreads FIRST: these render-critical props (icon/checkedIcon/className/
|
|
150
|
+
// classes/disabled/disableRipple/sx) must win over anything of the same name coming through
|
|
151
|
+
// from the caller, or they'd silently clobber the exact rendering this component depends on.
|
|
148
152
|
const radioNode = (
|
|
149
153
|
<MuiRadio
|
|
150
154
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
155
|
ref={ref as any}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
{...(sanitizedProps as unknown as MuiRadioProps)}
|
|
157
|
+
icon={<div className={styles.radio} />}
|
|
158
|
+
checkedIcon={
|
|
159
|
+
<div className={`${styles.radio} ${styles.radioChecked}`}>
|
|
160
|
+
<RadioIcon className={styles.icon} />
|
|
161
|
+
</div>
|
|
162
|
+
}
|
|
163
|
+
className={!label ? `${finalClass} ${styles.inner}` : styles.inner}
|
|
155
164
|
classes={mergedClassNames}
|
|
156
165
|
disabled={readOnly || disabled}
|
|
157
|
-
|
|
166
|
+
disableRipple
|
|
167
|
+
sx={label ? { padding: 0 } : undefined}
|
|
158
168
|
/>
|
|
159
169
|
);
|
|
160
170
|
|
|
@@ -170,27 +180,31 @@ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
|
|
|
170
180
|
>
|
|
171
181
|
{label as React.ReactNode}
|
|
172
182
|
</label>
|
|
173
|
-
{description
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{description}
|
|
179
|
-
</div>
|
|
180
|
-
)}
|
|
181
|
-
{error && (
|
|
182
|
-
<div
|
|
183
|
-
className={styles.error}
|
|
184
|
-
data-disabled={readOnly || disabled ? true : undefined}
|
|
183
|
+
{/* description/error are mutually exclusive — error takes precedence */}
|
|
184
|
+
{error ? (
|
|
185
|
+
<AssistiveElement
|
|
186
|
+
assistiveVariant="error"
|
|
187
|
+
assistiveWithIcon={false}
|
|
185
188
|
>
|
|
186
189
|
{error}
|
|
187
|
-
</
|
|
190
|
+
</AssistiveElement>
|
|
191
|
+
) : (
|
|
192
|
+
description && (
|
|
193
|
+
<AssistiveElement
|
|
194
|
+
assistiveVariant="help"
|
|
195
|
+
assistiveWithIcon={false}
|
|
196
|
+
>
|
|
197
|
+
{description}
|
|
198
|
+
</AssistiveElement>
|
|
199
|
+
)
|
|
188
200
|
)}
|
|
189
201
|
</div>
|
|
190
202
|
</div>
|
|
191
203
|
</div>
|
|
192
204
|
) : (
|
|
193
|
-
|
|
205
|
+
<div className={finalClass} style={style as React.CSSProperties}>
|
|
206
|
+
{radioNode}
|
|
207
|
+
</div>
|
|
194
208
|
);
|
|
195
209
|
|
|
196
210
|
if (formLayout) {
|
|
@@ -70,6 +70,7 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
|
70
70
|
emptyValueComponent,
|
|
71
71
|
value,
|
|
72
72
|
defaultValue,
|
|
73
|
+
onChange,
|
|
73
74
|
...rest
|
|
74
75
|
} = props;
|
|
75
76
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
@@ -111,6 +112,10 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
|
111
112
|
{...(sanitizedProps as unknown as MuiRadioGroupProps)}
|
|
112
113
|
value={value}
|
|
113
114
|
defaultValue={defaultValue}
|
|
115
|
+
// MUI natively calls onChange(event, value); the Recursica contract is
|
|
116
|
+
// single-argument (value only), matching Mantine's native RadioGroup
|
|
117
|
+
// onChange (the cross-adapter source of truth) — normalize here.
|
|
118
|
+
onChange={onChange ? (_event, val) => onChange(val) : undefined}
|
|
114
119
|
className={styles.groupRoot}
|
|
115
120
|
data-layout={formLayout}
|
|
116
121
|
>
|
|
@@ -98,13 +98,6 @@
|
|
|
98
98
|
display: flex;
|
|
99
99
|
align-items: center;
|
|
100
100
|
justify-content: center;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/* Mantine natively wraps label contents in an innerLabel span */
|
|
104
|
-
.root .label :global(.mantine-SegmentedControl-innerLabel) {
|
|
105
|
-
display: flex;
|
|
106
|
-
align-items: center;
|
|
107
|
-
justify-content: center;
|
|
108
101
|
gap: var(
|
|
109
102
|
--recursica_ui-kit_components_segmented-control-item_properties_item_icon-text-gap
|
|
110
103
|
);
|
|
@@ -38,7 +38,6 @@ function useSegmentedControlClassNames(restRecord: Record<string, unknown>): {
|
|
|
38
38
|
root: styles.root,
|
|
39
39
|
control: styles.control,
|
|
40
40
|
label: styles.label,
|
|
41
|
-
indicator: styles.indicator,
|
|
42
41
|
};
|
|
43
42
|
|
|
44
43
|
const classNamesProp = restRecord.classNames;
|
|
@@ -55,9 +54,6 @@ function useSegmentedControlClassNames(restRecord: Record<string, unknown>): {
|
|
|
55
54
|
mergedClassNames.label = o.label
|
|
56
55
|
? `${styles.label} ${o.label}`
|
|
57
56
|
: styles.label;
|
|
58
|
-
mergedClassNames.indicator = o.indicator
|
|
59
|
-
? `${styles.indicator} ${o.indicator}`
|
|
60
|
-
: styles.indicator;
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
const classNameProp = restRecord.className as string | undefined;
|
|
@@ -101,6 +97,10 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
101
97
|
return (
|
|
102
98
|
<MuiSegmentedControl
|
|
103
99
|
ref={ref}
|
|
100
|
+
{...(sanitizedProps as Omit<
|
|
101
|
+
MuiSegmentedControlProps,
|
|
102
|
+
"variant" | "size" | "value" | "onChange"
|
|
103
|
+
>)}
|
|
104
104
|
className={stylingParams.className}
|
|
105
105
|
classes={stylingParams.classNames}
|
|
106
106
|
orientation={orientation}
|
|
@@ -113,10 +113,6 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
113
113
|
typeof MuiSegmentedControl
|
|
114
114
|
>["onChange"]
|
|
115
115
|
}
|
|
116
|
-
{...(sanitizedProps as Omit<
|
|
117
|
-
MuiSegmentedControlProps,
|
|
118
|
-
"variant" | "size" | "value" | "onChange"
|
|
119
|
-
>)}
|
|
120
116
|
>
|
|
121
117
|
{data.map((item) => {
|
|
122
118
|
const itemValue = typeof item === "string" ? item : item.value;
|
|
@@ -235,6 +235,7 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
235
235
|
|
|
236
236
|
<div className={styles.sliderTrackWrapper}>
|
|
237
237
|
<MuiSlider
|
|
238
|
+
{...(sanitizedProps as unknown as MuiSliderProps)}
|
|
238
239
|
classes={mergedClassNames}
|
|
239
240
|
disabled={disabled}
|
|
240
241
|
value={resolvedValue}
|
|
@@ -257,7 +258,6 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
257
258
|
) => React.ReactNode)
|
|
258
259
|
: undefined
|
|
259
260
|
}
|
|
260
|
-
{...(sanitizedProps as unknown as MuiSliderProps)}
|
|
261
261
|
/>
|
|
262
262
|
</div>
|
|
263
263
|
|
|
@@ -29,10 +29,10 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(function Stack(
|
|
|
29
29
|
return (
|
|
30
30
|
<MUIStack
|
|
31
31
|
ref={ref}
|
|
32
|
+
{...safeProps}
|
|
32
33
|
spacing={resolvedGap}
|
|
33
34
|
alignItems={align}
|
|
34
35
|
justifyContent={justify}
|
|
35
|
-
{...safeProps}
|
|
36
36
|
>
|
|
37
37
|
{children}
|
|
38
38
|
</MUIStack>
|
|
@@ -48,6 +48,7 @@ export const Stepper = forwardRef<HTMLDivElement, StepperProps>(
|
|
|
48
48
|
<MuiStepper
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
50
|
ref={ref as any}
|
|
51
|
+
{...(sanitizedProps as MuiStepperProps)}
|
|
51
52
|
orientation={orientation}
|
|
52
53
|
connector={
|
|
53
54
|
<MuiStepConnector
|
|
@@ -62,7 +63,6 @@ export const Stepper = forwardRef<HTMLDivElement, StepperProps>(
|
|
|
62
63
|
classes={{
|
|
63
64
|
root: styles.steps,
|
|
64
65
|
}}
|
|
65
|
-
{...(sanitizedProps as MuiStepperProps)}
|
|
66
66
|
/>
|
|
67
67
|
</div>
|
|
68
68
|
);
|
|
@@ -79,9 +79,9 @@ export const Step = forwardRef<HTMLDivElement, StepProps>(
|
|
|
79
79
|
return (
|
|
80
80
|
<MuiStep
|
|
81
81
|
ref={ref}
|
|
82
|
+
{...(filterStylingProps(rest, overStyled) as MuiStepProps)}
|
|
82
83
|
classes={{ root: styles.step }}
|
|
83
84
|
className={className || ""}
|
|
84
|
-
{...(filterStylingProps(rest, overStyled) as MuiStepProps)}
|
|
85
85
|
/>
|
|
86
86
|
);
|
|
87
87
|
},
|
|
@@ -116,6 +116,7 @@ export const StepLabel = forwardRef<HTMLDivElement, StepLabelProps>(
|
|
|
116
116
|
return (
|
|
117
117
|
<MuiStepLabel
|
|
118
118
|
ref={ref}
|
|
119
|
+
{...(filterStylingProps(rest, overStyled) as MuiStepLabelProps)}
|
|
119
120
|
className={className || ""}
|
|
120
121
|
classes={{
|
|
121
122
|
label: styles.stepLabel,
|
|
@@ -128,7 +129,6 @@ export const StepLabel = forwardRef<HTMLDivElement, StepLabelProps>(
|
|
|
128
129
|
rest.optional
|
|
129
130
|
)
|
|
130
131
|
}
|
|
131
|
-
{...(filterStylingProps(rest, overStyled) as MuiStepLabelProps)}
|
|
132
132
|
/>
|
|
133
133
|
);
|
|
134
134
|
},
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Switch Implementation Notes
|
|
2
|
+
|
|
3
|
+
Decisions and gotchas specific to wrapping `@mui/material`'s `Switch` for the UI Kit's Switch.
|
|
4
|
+
|
|
5
|
+
## Label/description/error
|
|
6
|
+
|
|
7
|
+
MUI's `Switch` has no built-in label/description/error rendering — built by hand here.
|
|
8
|
+
The label is a plain `<label>`; description/error render through the shared
|
|
9
|
+
`AssistiveElement` component (`assistiveWithIcon={false}`) instead of local styled divs,
|
|
10
|
+
so they inherit real design tokens rather than hardcoded Mantine defaults.
|
|
11
|
+
|
|
12
|
+
## `Switch.Group` compound export
|
|
13
|
+
|
|
14
|
+
`SwitchGroup.tsx` existed but wasn't attached as `Switch.Group`. Fixed.
|
|
15
|
+
|
|
16
|
+
## `SwitchGroup` didn't control its children — now fixed
|
|
17
|
+
|
|
18
|
+
MUI's `FormGroup` has no controlled-value concept — no equivalent to Mantine's real
|
|
19
|
+
`Switch.Group`, which threads a `string[]` of checked values and `checked`/`onChange` to each
|
|
20
|
+
child via context. This adapter's `SwitchGroup` used to destructure and discard
|
|
21
|
+
`value`/`defaultValue`/`onChange` entirely — purely cosmetic.
|
|
22
|
+
|
|
23
|
+
Fixed the same way this adapter's own `Checkbox`/`CheckboxGroup` already solved the identical
|
|
24
|
+
problem (a closer, in-repo precedent than `Accordion`, which the notes previously pointed to
|
|
25
|
+
without checking Checkbox first): a `SwitchGroupContext` provider on `SwitchGroup` carrying
|
|
26
|
+
`value`/`onChange`/`name`/`readOnly`, consumed by `Switch` to compute `isChecked` from array
|
|
27
|
+
membership and to translate each toggle into an add/remove against that array before calling
|
|
28
|
+
the group's `onChange`. Standalone (ungrouped) `Switch` usage is untouched — the group-derived
|
|
29
|
+
`checked`/`onChange` are only spread onto `<MuiSwitch>` when a `SwitchGroupContext` is present.
|
|
30
|
+
Verified live: initial `value`/`defaultValue` now renders correctly, clicking a sibling
|
|
31
|
+
switch toggles only its own entry in the array, and `readOnly` on the group disables every
|
|
32
|
+
child — all confirmed against the existing `SwitchGroup.stories.tsx` scenarios via Playwright.
|
|
33
|
+
|
|
34
|
+
## Thumb icon (check/x glyph)
|
|
35
|
+
|
|
36
|
+
MUI's `icon`/`checkedIcon` props are the _same mechanism_ MUI uses internally to render its
|
|
37
|
+
own default circular thumb (`icon: <the circle>, checkedIcon: <the circle>`, then
|
|
38
|
+
spread-overwritten by whatever public `icon`/`checkedIcon` the caller passes) — confirmed
|
|
39
|
+
against MUI's own `Switch.js`/`SwitchBase.js` source. Passing a raw glyph into them replaces
|
|
40
|
+
the whole thumb circle, it doesn't add an icon inside it.
|
|
41
|
+
|
|
42
|
+
Fixed the same way `Checkbox` in this adapter already solved the identical problem: build our
|
|
43
|
+
own combined node — a `.thumb`-styled `<span>` wrapping `.thumbIconWrapper` with both
|
|
44
|
+
`CheckIcon`/`CloseIcon` (inline SVGs, same paths as Mantine's, copied rather than imported
|
|
45
|
+
since mui-adapter doesn't depend on `@mantine/core`) — and pass _that same node reference_ as
|
|
46
|
+
both `icon` and `checkedIcon` (mirroring MUI's own `icon={icon} checkedIcon={icon}` pattern).
|
|
47
|
+
Same reference means it never unmounts/remounts on toggle, which is what lets the two glyphs
|
|
48
|
+
crossfade via CSS opacity instead of a hard swap — matching Mantine's `thumbIcon` behavior.
|
|
49
|
+
Verified against a live Storybook via adapter-tester (geometry + SVG count now match Mantine
|
|
50
|
+
1:1 across Default/Static Variations stories).
|
|
51
|
+
|
|
52
|
+
## Track/thumb geometry, checked-state selectors, focus ring
|
|
53
|
+
|
|
54
|
+
Original CSS was a copy of mantine-adapter's file, but MUI's DOM is structurally different
|
|
55
|
+
(`track`/`switchBase` are siblings, not nested; MUI hardcodes its own root box, hit-area
|
|
56
|
+
padding, and thumb travel distance for its own 20px default thumb). Fixed by driving
|
|
57
|
+
`.switchRoot`/`.switchBase`/`.thumb` sizing entirely from our tokens, replacing
|
|
58
|
+
`input:checked + .track` sibling selectors (never matched in MUI's DOM) with `:has()`-scoped
|
|
59
|
+
selectors, adding an explicit focus ring from `--recursica_brand_states_focus_*` tokens, and
|
|
60
|
+
hardcoding `color="default"` so MUI's own primary-blue hover/focus circle never activates.
|
|
61
|
+
Verified via live measurement (adapter-tester): track/thumb box dimensions now match Mantine
|
|
62
|
+
exactly (64×28 track, 20×20 thumb).
|
|
63
|
+
|
|
64
|
+
Two gotchas worth flagging for future MUI adapter work:
|
|
65
|
+
|
|
66
|
+
1. `classes.root` targets MUI's own switch span, but this component's outer wrapper `<div>`
|
|
67
|
+
(switch+label row) also uses `.root` — sizing one squeezes the other. Split into
|
|
68
|
+
`.switchRoot` (MUI switch only) vs `.root` (outer wrapper, cursor only).
|
|
69
|
+
2. CSS Modules hashes literal MUI state classes (`Mui-checked`, `Mui-focusVisible`) unless
|
|
70
|
+
wrapped in `:global()` — same pattern already used in `Accordion`/`Button`.
|
|
71
|
+
|
|
72
|
+
## Disabled-state opacity didn't reach the thumb
|
|
73
|
+
|
|
74
|
+
**Found 2026-08-14, reported by Matt:** the Read Only story's thumb stayed full-color/opacity
|
|
75
|
+
while Mantine's dimmed along with the track. Same root cause as the geometry work above:
|
|
76
|
+
Mantine nests `.thumb` inside `.track`, so an `opacity` on `.track` visually dims the thumb for
|
|
77
|
+
free; MUI has them as siblings, so the disabled-opacity selectors (which only targeted
|
|
78
|
+
`.track`) never touched the thumb at all. Fixed by adding `.thumb` to both selectors alongside
|
|
79
|
+
`.track`. Verified live: thumb now dims identically to Mantine in both the plain-disabled and
|
|
80
|
+
disabled-checked cases.
|
|
81
|
+
|
|
82
|
+
## Track opacity — MUI's own always-on defaults, unrelated to our tokens
|
|
83
|
+
|
|
84
|
+
MUI's `SwitchTrack` styled component sets `opacity: 0.38` unconditionally (its own faded-pill
|
|
85
|
+
baseline), and separately `.Mui-checked + .track { opacity: 0.5 }` when checked — neither has
|
|
86
|
+
an equivalent in our token schema, and neither was being cancelled. Result: every enabled track
|
|
87
|
+
was dimmed by MUI's own opacity, most visibly on the checked state (deep red washed out to
|
|
88
|
+
pink). Fixed with an explicit `opacity: 1` in both the base `.track` rule and the
|
|
89
|
+
`.root:has(input:checked) .track` rule (needed in both — MUI's checked-specific selector is
|
|
90
|
+
more specific than its own base rule, so overriding only the base wasn't enough). Disabled-state
|
|
91
|
+
opacity is unaffected; those selectors are already more specific than either of these.
|
|
92
|
+
|
|
93
|
+
## Focus ring was on the thumb's hit-area circle, not the track
|
|
94
|
+
|
|
95
|
+
`.switchBase:global(.Mui-focusVisible)` is MUI's actual focusable element, but it's sized to
|
|
96
|
+
the thumb's 20×20 hit area, not the 64×28 track — so the ring drew a small circle instead of
|
|
97
|
+
outlining the switch. Moved to `.switchBase:global(.Mui-focusVisible) ~ .track` (siblings under
|
|
98
|
+
`.switchRoot`, switchBase first) so the ring renders on the track itself. Mantine-adapter had
|
|
99
|
+
the mirror-image bug: no override at all, so Mantine's own default outline (blue) showed on the
|
|
100
|
+
hidden `<input>` — fixed there with `.root input:focus-visible { outline: none }` +
|
|
101
|
+
`.root input:focus-visible + .track { ... }`, drawing the same Recursica-token ring in the same
|
|
102
|
+
place.
|
|
103
|
+
|
|
104
|
+
## `classNames` prop-name bug — external override was silently inert
|
|
105
|
+
|
|
106
|
+
The external override merge read `restRecord.classNames`, but MUI's actual prop is `classes`
|
|
107
|
+
(that's Mantine's naming, copied over by mistake) — fixed. Note only `root`/`track`/
|
|
108
|
+
`switchBase` are real MUI `classes` slots; `body`/`trackLabel`/`labelWrapper`/`label` are this
|
|
109
|
+
component's own hardcoded elements (styled directly in JSX, not via MUI's `classes` prop) and
|
|
110
|
+
remain unreachable through this override path — a separate, pre-existing gap, not something the
|
|
111
|
+
prop-name fix could address.
|
|
112
|
+
|
|
113
|
+
## `SwitchGroup` side-by-side layout always rendered as if stacked
|
|
114
|
+
|
|
115
|
+
**Found 2026-08-14, reported by Matt:** the `SideBySideLayout` story showed the group label
|
|
116
|
+
above the switches instead of beside them. Root cause: `SwitchGroup` passed the switch-item's
|
|
117
|
+
own inline label max-width token (200px) as the group's `controlMaxWidth` — but the mandatory
|
|
118
|
+
side-by-side label column is a fixed 224px, wider than that cap, so the label always overflowed
|
|
119
|
+
onto its own line regardless of layout mode. Reproduced identically in Mantine (same code
|
|
120
|
+
pattern, not a MUI-only bug). Fixed by not capping the group's control width at all — each
|
|
121
|
+
switch's own label already wraps at 200px via `.labelWrapper`, so no group-level cap is needed.
|
|
122
|
+
Verified live in both adapters: side-by-side now shows the label column at left with the
|
|
123
|
+
switches beside it; stacked layout unaffected.
|