@recursica/mui-adapter 0.26.0 → 0.27.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/index.d.ts +82 -0
  3. package/dist/mui-adapter.cjs +73 -73
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.js +9164 -9078
  6. package/dist/mui-adapter.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/Autocomplete/Autocomplete.tsx +46 -34
  9. package/src/components/Avatar/Avatar.tsx +11 -21
  10. package/src/components/Badge/Badge.tsx +9 -13
  11. package/src/components/Breadcrumb/Breadcrumb.tsx +12 -18
  12. package/src/components/Button/Button.tsx +17 -11
  13. package/src/components/Card/Card.tsx +9 -19
  14. package/src/components/Checkbox/Checkbox.tsx +22 -26
  15. package/src/components/Checkbox/CheckboxGroup.tsx +3 -3
  16. package/src/components/Chip/Chip.tsx +12 -19
  17. package/src/components/Dropdown/BareDropdown.tsx +14 -8
  18. package/src/components/Dropdown/Dropdown.tsx +13 -6
  19. package/src/components/FileInput/FileInput.tsx +1 -1
  20. package/src/components/FileUpload/FileUpload.tsx +1 -1
  21. package/src/components/FormControlLayout/FormControlLayout.tsx +1 -1
  22. package/src/components/FormControlWrapper/FormControlWrapper.tsx +1 -1
  23. package/src/components/HoverCard/HoverCard.tsx +13 -21
  24. package/src/components/Menu/Menu.tsx +14 -6
  25. package/src/components/Modal/Modal.tsx +12 -6
  26. package/src/components/NumberInput/NumberInput.tsx +30 -15
  27. package/src/components/Pagination/Pagination.tsx +5 -15
  28. package/src/components/Panel/Panel.tsx +17 -25
  29. package/src/components/Popover/Popover.tsx +10 -18
  30. package/src/components/Radio/Radio.tsx +21 -26
  31. package/src/components/Radio/RadioGroup.tsx +3 -4
  32. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +1 -1
  33. package/src/components/SegmentedControl/SegmentedControl.tsx +20 -25
  34. package/src/components/Slider/Slider.tsx +40 -43
  35. package/src/components/Stepper/Stepper.tsx +34 -14
  36. package/src/components/Switch/Switch.tsx +33 -49
  37. package/src/components/Switch/SwitchGroup.tsx +3 -3
  38. package/src/components/Tabs/Tabs.tsx +12 -4
  39. package/src/components/TextArea/TextArea.tsx +30 -7
  40. package/src/components/TextField/TextField.tsx +30 -26
  41. package/src/components/Timeline/Timeline.tsx +9 -1
  42. package/src/components/Timeline/TimelineItem.tsx +2 -2
  43. package/src/components/Toast/Toast.tsx +14 -23
  44. package/src/components/Tooltip/Tooltip.tsx +14 -27
  45. package/src/components/TransferList/TransferList.tsx +1 -1
  46. package/src/utils/filterStylingProps.ts +4 -0
@@ -14,6 +14,7 @@ import {
14
14
  } from "@mui/material";
15
15
  import {
16
16
  filterStylingProps,
17
+ mergeClassNames,
17
18
  type RecursicaOverStyled,
18
19
  } from "../../utils/filterStylingProps";
19
20
  import styles from "./Stepper.module.css";
@@ -83,6 +84,13 @@ export const Stepper = forwardRef<HTMLDivElement, StepperProps>(
83
84
  const sanitizedProps = filterStylingProps(rest, overStyled);
84
85
  const isHorizontal = orientation === "horizontal";
85
86
 
87
+ const mergedClassNames = mergeClassNames(
88
+ { root: styles.steps },
89
+ (sanitizedProps as Record<string, unknown>).classes as
90
+ | Partial<Record<string, string>>
91
+ | undefined,
92
+ );
93
+
86
94
  return (
87
95
  <div
88
96
  className={`${styles.root} ${isHorizontal ? styles.horizontal : styles.vertical} ${size === "large" ? styles.large : styles.small} ${className || ""}`}
@@ -117,9 +125,7 @@ export const Stepper = forwardRef<HTMLDivElement, StepperProps>(
117
125
  <></>
118
126
  )
119
127
  }
120
- classes={{
121
- root: styles.steps,
122
- }}
128
+ classes={mergedClassNames}
123
129
  />
124
130
  </div>
125
131
  );
@@ -133,11 +139,18 @@ export type StepProps = RecursicaOverStyled<MuiStepProps>;
133
139
  export const Step = forwardRef<HTMLDivElement, StepProps>(
134
140
  function Step(props, ref) {
135
141
  const { overStyled = false, className, ...rest } = props;
142
+ const sanitizedProps = filterStylingProps(rest, overStyled);
143
+ const mergedClassNames = mergeClassNames(
144
+ { root: styles.step },
145
+ (sanitizedProps as Record<string, unknown>).classes as
146
+ | Partial<Record<string, string>>
147
+ | undefined,
148
+ );
136
149
  return (
137
150
  <MuiStep
138
151
  ref={ref}
139
- {...(filterStylingProps(rest, overStyled) as MuiStepProps)}
140
- classes={{ root: styles.step }}
152
+ {...(sanitizedProps as MuiStepProps)}
153
+ classes={mergedClassNames}
141
154
  className={className || ""}
142
155
  />
143
156
  );
@@ -173,21 +186,28 @@ export const StepLabel = forwardRef<HTMLDivElement, StepLabelProps>(
173
186
  overStyled = false,
174
187
  className,
175
188
  description,
176
- StepIconComponent,
189
+ StepIconComponent = RecursicaStepIcon,
177
190
  ...rest
178
191
  } = props;
192
+ const sanitizedProps = filterStylingProps(rest, overStyled);
193
+ const mergedClassNames = mergeClassNames(
194
+ {
195
+ root: styles.stepLabelRoot,
196
+ label: styles.stepLabel,
197
+ iconContainer: styles.stepIcon,
198
+ labelContainer: styles.stepBody,
199
+ },
200
+ (sanitizedProps as Record<string, unknown>).classes as
201
+ | Partial<Record<string, string>>
202
+ | undefined,
203
+ );
179
204
  return (
180
205
  <MuiStepLabel
181
206
  ref={ref}
182
- {...(filterStylingProps(rest, overStyled) as MuiStepLabelProps)}
207
+ {...(sanitizedProps as MuiStepLabelProps)}
183
208
  className={className || ""}
184
- classes={{
185
- root: styles.stepLabelRoot,
186
- label: styles.stepLabel,
187
- iconContainer: styles.stepIcon,
188
- labelContainer: styles.stepBody,
189
- }}
190
- StepIconComponent={StepIconComponent ?? RecursicaStepIcon}
209
+ classes={mergedClassNames}
210
+ StepIconComponent={StepIconComponent}
191
211
  optional={
192
212
  description ? (
193
213
  <div className={styles.stepDescription}>{description}</div>
@@ -7,6 +7,8 @@ import { type ReadOnlyControlProps } from "@recursica/adapter-common";
7
7
 
8
8
  import {
9
9
  filterStylingProps,
10
+ omitUnsupportedProps,
11
+ mergeClassNames,
10
12
  type RecursicaOverStyled,
11
13
  } from "../../utils/filterStylingProps";
12
14
  import {
@@ -100,14 +102,19 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
100
102
  style,
101
103
  ...rest
102
104
  } = props;
103
- const sanitizedProps = filterStylingProps(rest, overStyled);
104
- const restRecord = sanitizedProps as Record<string, unknown>;
105
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
106
+ // through even if a caller forces them via plain JavaScript, bypassing the Omit<> above. We
107
+ // rely strictly on variables from Switch.module.css instead of MUI's own dimension/palette props.
108
+ const UNSUPPORTED_PROPS = [
109
+ "size", // Recursica controls sizing via design tokens, not MUI's native small/medium size
110
+ "color", // Recursica hardcodes color="default" below; MUI's palette-driven colors are unused
111
+ ] as const satisfies readonly (keyof MuiSwitchProps)[];
105
112
 
106
- // Actively delete dimension bindings that bypass the abstraction
107
- delete restRecord["size"];
108
- delete restRecord["color"];
109
- delete restRecord["radius"];
110
- delete restRecord["variant"];
113
+ const sanitizedProps = omitUnsupportedProps(
114
+ filterStylingProps(rest, overStyled),
115
+ UNSUPPORTED_PROPS,
116
+ );
117
+ const restRecord = sanitizedProps as Record<string, unknown>;
111
118
 
112
119
  // NOTE: classes.root must stay keyed "root" (MUI's own slot name), but it targets a
113
120
  // dedicated .switchRoot CSS class, not .root — .root is this component's own outer
@@ -119,47 +126,24 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
119
126
  // replaces that slot outright (confirmed against MUI's SwitchBase/Switch source: icon and
120
127
  // checkedIcon substitute the whole default thumb, they don't add to it), so the .thumb
121
128
  // class is applied directly to our own node instead.
122
- const mergedClassNames: Partial<Record<string, string>> = {
123
- root: styles.switchRoot,
124
- body: styles.body,
125
- track: styles.track,
126
- switchBase: styles.switchBase,
127
- trackLabel: styles.trackLabel,
128
- labelWrapper: styles.labelWrapper,
129
- label: styles.label,
130
- };
131
-
132
- // NOTE: MUI's actual prop is "classes", not "classNames" (that's Mantine's naming) — this
133
- // was reading the wrong key and silently doing nothing. Fixed. Also note "body"/
134
- // "trackLabel"/"labelWrapper"/"label" below aren't real MUI `classes` slots at all (MUI
135
- // only recognizes root/switchBase/track/thumb/input/checked/disabled/colorX/sizeX/edgeX) —
136
- // those four are this component's own hardcoded elements, styled directly in the JSX below,
137
- // not through MUI's classes mechanism. Only root/track/switchBase actually take effect via
138
- // this override path; the rest are pre-existing dead weight, unrelated to the classNames bug.
139
- const classesProp = restRecord.classes;
140
- if (
141
- classesProp &&
142
- typeof classesProp === "object" &&
143
- !Array.isArray(classesProp)
144
- ) {
145
- const o = classesProp as Partial<Record<string, string>>;
146
- mergedClassNames.root = o.root
147
- ? `${styles.switchRoot} ${o.root}`
148
- : styles.switchRoot;
149
- mergedClassNames.body = o.body ? `${styles.body} ${o.body}` : styles.body;
150
- mergedClassNames.track = o.track
151
- ? `${styles.track} ${o.track}`
152
- : styles.track;
153
- mergedClassNames.trackLabel = o.trackLabel
154
- ? `${styles.trackLabel} ${o.trackLabel}`
155
- : styles.trackLabel;
156
- mergedClassNames.labelWrapper = o.labelWrapper
157
- ? `${styles.labelWrapper} ${o.labelWrapper}`
158
- : styles.labelWrapper;
159
- mergedClassNames.label = o.label
160
- ? `${styles.label} ${o.label}`
161
- : styles.label;
162
- }
129
+ // NOTE: MUI's actual prop is "classes", not "classNames" (that's Mantine's naming). Also
130
+ // note "body"/"trackLabel"/"labelWrapper"/"label" below aren't real MUI `classes` slots at
131
+ // all (MUI only recognizes root/switchBase/track/thumb/input/checked/disabled/colorX/sizeX/
132
+ // edgeX) — those four are this component's own hardcoded elements, styled directly in the
133
+ // JSX below, not through MUI's classes mechanism. Only root/track/switchBase actually take
134
+ // effect via this merge; the rest are pre-existing dead weight, unrelated to the merge itself.
135
+ const mergedClassNames = mergeClassNames(
136
+ {
137
+ root: styles.switchRoot,
138
+ body: styles.body,
139
+ track: styles.track,
140
+ switchBase: styles.switchBase,
141
+ trackLabel: styles.trackLabel,
142
+ labelWrapper: styles.labelWrapper,
143
+ label: styles.label,
144
+ },
145
+ restRecord.classes as Partial<Record<string, string>> | undefined,
146
+ );
163
147
 
164
148
  const classNameProp = restRecord.className as string | undefined;
165
149
  const finalClass = classNameProp
@@ -237,6 +221,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
237
221
  <MuiSwitch
238
222
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
239
223
  ref={ref as any}
224
+ {...(sanitizedProps as unknown as MuiSwitchProps)}
240
225
  className={!label ? `${finalClass} ${styles.inner}` : styles.inner}
241
226
  classes={mergedClassNames}
242
227
  color="default"
@@ -244,7 +229,6 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
244
229
  data-disabled={isSwitchDisabled || undefined}
245
230
  icon={thumbNode}
246
231
  checkedIcon={thumbNode}
247
- {...(sanitizedProps as unknown as MuiSwitchProps)}
248
232
  {...(isGrouped
249
233
  ? { checked: isChecked, onChange: handleGroupChange }
250
234
  : {})}
@@ -80,12 +80,12 @@ export const SwitchGroup = forwardRef<HTMLDivElement, SwitchGroupProps>(
80
80
  onChange,
81
81
  ...rest
82
82
  } = props;
83
+ // NOTE: MuiFormGroupProps has no native "size" (or other Recursica-unsupported) prop to
84
+ // leak through here — nothing to add to an UNSUPPORTED_PROPS list beyond what
85
+ // filterStylingProps already blocks.
83
86
  const sanitizedProps = filterStylingProps(rest, overStyled);
84
87
  const restRecord = sanitizedProps as Record<string, unknown>;
85
88
 
86
- // Delete prohibited sizing hooks from bypassing the variables
87
- delete restRecord["size"];
88
-
89
89
  const handleChange = (
90
90
  _event: React.SyntheticEvent,
91
91
  childValue: string[],
@@ -7,6 +7,7 @@ import {
7
7
  } from "@mui/material";
8
8
  import {
9
9
  filterStylingProps,
10
+ mergeClassNames,
10
11
  type RecursicaOverStyled,
11
12
  } from "../../utils/filterStylingProps";
12
13
  import styles from "./Tabs.module.css";
@@ -32,6 +33,16 @@ export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
32
33
 
33
34
  const sanitizedProps = filterStylingProps(rest, overStyled);
34
35
 
36
+ const mergedClassNames = mergeClassNames(
37
+ {
38
+ flexContainer: styles.list,
39
+ indicator: styles.indicator,
40
+ },
41
+ (sanitizedProps as Record<string, unknown>).classes as
42
+ | Partial<Record<string, string>>
43
+ | undefined,
44
+ );
45
+
35
46
  return (
36
47
  <MuiTabs
37
48
  ref={ref}
@@ -41,10 +52,7 @@ export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
41
52
  data-variant={variant}
42
53
  data-orientation={orientation}
43
54
  data-inverted={inverted || undefined}
44
- classes={{
45
- flexContainer: styles.list,
46
- indicator: styles.indicator,
47
- }}
55
+ classes={mergedClassNames}
48
56
  />
49
57
  );
50
58
  },
@@ -7,6 +7,8 @@ import { type ReadOnlyControlProps } from "@recursica/adapter-common";
7
7
  // Removed unused TextField import
8
8
  import {
9
9
  filterStylingProps,
10
+ omitUnsupportedProps,
11
+ mergeClassNames,
10
12
  type RecursicaOverStyled,
11
13
  } from "../../utils/filterStylingProps";
12
14
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
@@ -69,13 +71,32 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
69
71
  autosize,
70
72
  ...rest
71
73
  } = props;
72
- const sanitizedProps = filterStylingProps(rest, overStyled);
74
+
75
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
76
+ // through even if a caller forces them via plain JavaScript, bypassing the Omit<> above.
77
+ const UNSUPPORTED_PROPS = [
78
+ "size", // Recursica controls sizing via design tokens, not MUI's native small/medium size
79
+ "variant", // Recursica styles the naked textarea directly; MUI's standard/filled/outlined unused
80
+ ] as const satisfies readonly (keyof MuiTextareaProps)[];
81
+
82
+ const sanitizedProps = omitUnsupportedProps(
83
+ filterStylingProps(rest, overStyled),
84
+ UNSUPPORTED_PROPS,
85
+ );
73
86
  const restRecord = sanitizedProps as Record<string, unknown>;
74
87
 
75
- // Delete prohibited sizing hooks from bypassing variables natively
76
- delete restRecord["size"];
77
- delete restRecord["variant"];
78
- delete restRecord["radius"];
88
+ // The `slotProps` prop is NOT omitted from this component's public type, so a caller can
89
+ // legitimately supply it — merge it (rather than letting our own `slotProps.input` spread
90
+ // after theirs and clobber `disableUnderline`/`classes`/`data-autosize` wholesale).
91
+ const callerSlotProps = restRecord.slotProps as
92
+ | { input?: Record<string, unknown> }
93
+ | undefined;
94
+ const mergedInputClasses = mergeClassNames(
95
+ { root: styles.root, input: styles.input },
96
+ callerSlotProps?.input?.classes as
97
+ | Partial<Record<string, string>>
98
+ | undefined,
99
+ );
79
100
 
80
101
  const wrapperClass = className
81
102
  ? `${styles.layoutOverride} ${className}`
@@ -109,6 +130,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
109
130
  activeComponent={
110
131
  /* Naked Input execution safely decoupled from Mui's macro Input.Wrapper DOM hooks */
111
132
  <MuiTextarea
133
+ {...(sanitizedProps as unknown as MuiTextareaProps)}
112
134
  multiline
113
135
  variant="standard"
114
136
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -120,15 +142,16 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
120
142
  error={!!error}
121
143
  required={undefined}
122
144
  slotProps={{
145
+ ...callerSlotProps,
123
146
  input: {
147
+ ...callerSlotProps?.input,
124
148
  disableUnderline: true,
125
- classes: { root: styles.root, input: styles.input },
149
+ classes: mergedInputClasses,
126
150
  ...({
127
151
  "data-autosize": autosize ? "true" : undefined,
128
152
  } as Record<string, unknown>),
129
153
  },
130
154
  }}
131
- {...(sanitizedProps as unknown as MuiTextareaProps)}
132
155
  />
133
156
  }
134
157
  />
@@ -3,6 +3,8 @@ import { InputBase, type InputBaseProps } from "@mui/material";
3
3
  import { type ReadOnlyControlProps } from "@recursica/adapter-common";
4
4
  import {
5
5
  filterStylingProps,
6
+ omitUnsupportedProps,
7
+ mergeClassNames,
6
8
  type RecursicaOverStyled,
7
9
  } from "../../utils/filterStylingProps";
8
10
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
@@ -77,34 +79,36 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
77
79
  ...rest
78
80
  } = props;
79
81
 
80
- const sanitizedProps = filterStylingProps(rest, overStyled);
81
- const restRecord = sanitizedProps as Record<string, unknown>;
82
-
83
- // Delete prohibited sizing hooks from bypassing variables natively
84
- delete restRecord["size"];
85
- delete restRecord["color"];
82
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
83
+ // through even if a caller forces them via plain JavaScript, bypassing the Omit<> above.
84
+ const UNSUPPORTED_PROPS = [
85
+ "size", // Recursica controls sizing via design tokens, not MUI's native small/medium size
86
+ "color", // Colors are token-driven; MUI's native palette isn't exposed
87
+ ] as const satisfies readonly (keyof InputBaseProps)[];
86
88
 
87
- // Securely map core native blocks down ensuring nested CSS modules map precisely
88
- const mergedClassNames: Partial<Record<string, string>> = {
89
- wrapper: styles.root, // The nested Input internal relative wrapper bounding box
90
- input: styles.input,
91
- section: styles.section,
92
- };
89
+ const sanitizedProps = omitUnsupportedProps(
90
+ filterStylingProps(rest, overStyled),
91
+ UNSUPPORTED_PROPS,
92
+ );
93
+ const restRecord = sanitizedProps as Record<string, unknown>;
93
94
 
94
- const classesProp = restRecord.classes;
95
- if (
96
- classesProp &&
97
- typeof classesProp === "object" &&
98
- !Array.isArray(classesProp)
99
- ) {
100
- const o = classesProp as Partial<Record<string, string>>;
101
- mergedClassNames.wrapper = o.root
102
- ? `${styles.root} ${o.root}`
103
- : styles.root;
104
- mergedClassNames.input = o.input
105
- ? `${styles.input} ${o.input}`
106
- : styles.input;
107
- }
95
+ // Securely map core native blocks down ensuring nested CSS modules map precisely. The
96
+ // caller-facing "root"/"input" slot names (MUI's own InputBase `classes` shape) are
97
+ // translated to this component's internal "wrapper"/"input" naming below.
98
+ const callerClasses = restRecord.classes as
99
+ | Partial<Record<string, string>>
100
+ | undefined;
101
+ const mergedClassNames = mergeClassNames(
102
+ {
103
+ wrapper: styles.root, // The nested Input internal relative wrapper bounding box
104
+ input: styles.input,
105
+ section: styles.section,
106
+ },
107
+ callerClasses && {
108
+ wrapper: callerClasses.root,
109
+ input: callerClasses.input,
110
+ },
111
+ );
108
112
 
109
113
  const wrapperClass = className
110
114
  ? `${styles.layoutOverride} ${className}`
@@ -4,6 +4,7 @@ import MuiTimeline, {
4
4
  } from "@mui/lab/Timeline";
5
5
  import {
6
6
  filterStylingProps,
7
+ mergeClassNames,
7
8
  type RecursicaOverStyled,
8
9
  } from "../../utils/filterStylingProps";
9
10
  import styles from "./Timeline.module.css";
@@ -51,6 +52,13 @@ const TimelineInner = React.forwardRef<HTMLUListElement, TimelineProps>(
51
52
  ) {
52
53
  const sanitizedProps = filterStylingProps(rest, overStyled);
53
54
 
55
+ const mergedClassNames = mergeClassNames(
56
+ { root: styles.root },
57
+ (sanitizedProps as Record<string, unknown>).classes as
58
+ | Partial<Record<string, string>>
59
+ | undefined,
60
+ );
61
+
54
62
  const items = React.Children.toArray(children);
55
63
  const decoratedItems = items.map((item, index) =>
56
64
  React.isValidElement(item)
@@ -68,7 +76,7 @@ const TimelineInner = React.forwardRef<HTMLUListElement, TimelineProps>(
68
76
  MuiTimelineProps,
69
77
  "color" | "radius" | "bulletSize" | "lineWidth"
70
78
  >)}
71
- classes={{ root: styles.root }}
79
+ classes={mergedClassNames}
72
80
  >
73
81
  {decoratedItems}
74
82
  </MuiTimeline>
@@ -69,12 +69,12 @@ export const TimelineItem = React.forwardRef<HTMLLIElement, TimelineItemProps>(
69
69
  return (
70
70
  <MuiTimelineItem
71
71
  ref={ref}
72
- data-variant={bulletVariant}
73
- data-active={__active || undefined}
74
72
  {...(restSanitizedProps as unknown as Omit<
75
73
  MuiTimelineItemProps,
76
74
  "radius" | "color" | "lineVariant" | "title"
77
75
  >)}
76
+ data-variant={bulletVariant}
77
+ data-active={__active || undefined}
78
78
  className={
79
79
  userClassName ? `${styles.item} ${userClassName}` : styles.item
80
80
  }
@@ -7,6 +7,7 @@ import {
7
7
  } from "@mui/material";
8
8
  import {
9
9
  filterStylingProps,
10
+ mergeClassNames,
10
11
  type RecursicaOverStyled,
11
12
  } from "../../utils/filterStylingProps";
12
13
  import styles from "./Toast.module.css";
@@ -63,29 +64,19 @@ export const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
63
64
  ) {
64
65
  const sanitizedProps = filterStylingProps(rest, overStyled);
65
66
 
66
- const mergedClassNames: Partial<Record<string, string>> = {
67
- root: styles.root,
68
- message: styles.body,
69
- icon: styles.icon,
70
- action: styles.closeButton,
71
- };
72
-
73
- const classNamesProp = (sanitizedProps as Record<string, unknown>)
74
- .classNames;
75
- if (
76
- classNamesProp &&
77
- typeof classNamesProp === "object" &&
78
- !Array.isArray(classNamesProp)
79
- ) {
80
- const o = classNamesProp as Record<string, string>;
81
- Object.keys(o).forEach((key) => {
82
- if (mergedClassNames[key]) {
83
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
84
- } else {
85
- mergedClassNames[key] = o[key];
86
- }
87
- });
88
- }
67
+ // Note MUI's actual prop is "classes", not "classNames" (that's Mantine's naming) — this
68
+ // used to read the wrong key, silently no-op-ing any caller-supplied classes.
69
+ const mergedClassNames = mergeClassNames(
70
+ {
71
+ root: styles.root,
72
+ message: styles.body,
73
+ icon: styles.icon,
74
+ action: styles.closeButton,
75
+ },
76
+ (sanitizedProps as Record<string, unknown>).classes as
77
+ | Partial<Record<string, string>>
78
+ | undefined,
79
+ );
89
80
 
90
81
  const handleClose = () => {
91
82
  if (onClose) onClose();
@@ -4,6 +4,7 @@ import {
4
4
  } from "@mui/material";
5
5
  import {
6
6
  filterStylingProps,
7
+ mergeClassNames,
7
8
  type RecursicaOverStyled,
8
9
  } from "../../utils/filterStylingProps";
9
10
  import styles from "./Tooltip.module.css";
@@ -48,32 +49,18 @@ const TooltipBase = function Tooltip({
48
49
  const { title, ...restProps } = rest;
49
50
  const sanitizedProps = filterStylingProps(restProps, overStyled);
50
51
 
51
- // Bind CSS module classes to Mui's internal classNames API
52
- const mergedClassNames: Partial<Record<string, string>> = {
53
- tooltip: styles.tooltip,
54
- arrow: styles.arrow,
55
- };
56
-
57
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
58
- if (
59
- classNamesProp &&
60
- typeof classNamesProp === "object" &&
61
- !Array.isArray(classNamesProp)
62
- ) {
63
- const o = classNamesProp as Record<string, string>;
64
- Object.keys(o).forEach((key) => {
65
- if (mergedClassNames[key]) {
66
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
67
- } else {
68
- mergedClassNames[key] = o[key];
69
- }
70
- });
71
- }
72
-
73
- const withArrow = (sanitizedProps as Record<string, unknown>).withArrow as
74
- | boolean
75
- | undefined;
76
- const resolvedWithArrow = withBeak ?? withArrow;
52
+ // Bind CSS module classes to Mui's internal classNames API. Note MUI's actual prop is
53
+ // "classes", not "classNames" (that's Mantine's naming) — this used to read the wrong key,
54
+ // silently no-op-ing any caller-supplied classes.
55
+ const mergedClassNames = mergeClassNames(
56
+ {
57
+ tooltip: styles.tooltip,
58
+ arrow: styles.arrow,
59
+ },
60
+ (sanitizedProps as Record<string, unknown>).classes as
61
+ | Partial<Record<string, string>>
62
+ | undefined,
63
+ );
77
64
 
78
65
  return (
79
66
  <MuiTooltip
@@ -81,7 +68,7 @@ const TooltipBase = function Tooltip({
81
68
  title={label || title || ""}
82
69
  open={opened}
83
70
  placement="top" /* Recursica default; Mui defaults to "bottom" */
84
- arrow={resolvedWithArrow}
71
+ arrow={withBeak}
85
72
  classes={mergedClassNames}
86
73
  />
87
74
  );
@@ -346,10 +346,10 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>(
346
346
  readOnlyNativeProps={props}
347
347
  activeComponent={
348
348
  <div
349
+ {...(sanitizedProps as Record<string, unknown>)}
349
350
  className={styles.root}
350
351
  data-disabled={disabled ? "true" : undefined}
351
352
  data-error={error ? "true" : undefined}
352
- {...(sanitizedProps as Record<string, unknown>)}
353
353
  >
354
354
  <div className={styles.panes}>
355
355
  {renderPane(
@@ -58,6 +58,10 @@ export {
58
58
  type BlockedStylingKeys,
59
59
  type ForbiddenStyles,
60
60
  type WithRecursicaSpacing,
61
+ omitUnsupportedProps,
62
+ withCallerOverride,
63
+ mergeClassNames,
64
+ mergeStyles,
61
65
  } from "@recursica/adapter-common";
62
66
 
63
67
  const LAYOUT_PROPS = new Set([