@recursica/mui-adapter 0.21.1 → 0.23.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 (74) hide show
  1. package/ARCHITECTURE.md +3 -0
  2. package/CHANGELOG.md +75 -0
  3. package/dist/index.d.ts +247 -41
  4. package/dist/mui-adapter.cjs +78 -78
  5. package/dist/mui-adapter.cjs.map +1 -1
  6. package/dist/mui-adapter.css +1 -1
  7. package/dist/mui-adapter.js +7624 -7311
  8. package/dist/mui-adapter.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +63 -0
  11. package/src/components/Accordion/Accordion.module.css +21 -2
  12. package/src/components/Accordion/Accordion.stories.tsx +38 -0
  13. package/src/components/Accordion/Accordion.tsx +27 -12
  14. package/src/components/AssistiveElement/ASSISTIVEELEMENT_IMPLEMENTATION_NOTES.md +59 -0
  15. package/src/components/AssistiveElement/AssistiveElement.module.css +8 -1
  16. package/src/components/AssistiveElement/AssistiveElement.tsx +17 -7
  17. package/src/components/Autocomplete/Autocomplete.tsx +18 -2
  18. package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +36 -0
  19. package/src/components/Avatar/Avatar.tsx +6 -9
  20. package/src/components/Badge/Badge.module.css +0 -12
  21. package/src/components/Badge/Badge.tsx +0 -4
  22. package/src/components/Button/Button.module.css +13 -0
  23. package/src/components/Card/Card.tsx +5 -5
  24. package/src/components/Checkbox/Checkbox.module.css +17 -21
  25. package/src/components/Checkbox/Checkbox.tsx +24 -18
  26. package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +102 -0
  27. package/src/components/Chip/Chip.module.css +46 -11
  28. package/src/components/Chip/Chip.tsx +41 -5
  29. package/src/components/Chip/USAGE.md +19 -0
  30. package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +249 -0
  31. package/src/components/FileUpload/FileUpload.module.css +203 -38
  32. package/src/components/FileUpload/FileUpload.stories.tsx +348 -4
  33. package/src/components/FileUpload/FileUpload.tsx +350 -6
  34. package/src/components/FileUpload/USAGE.md +163 -5
  35. package/src/components/Flex/Flex.tsx +1 -1
  36. package/src/components/FormControlWrapper/FORMCONTROLWRAPPER_IMPLEMENTATION_NOTES.md +35 -0
  37. package/src/components/FormControlWrapper/FormControlWrapper.tsx +24 -8
  38. package/src/components/Grid/Grid.tsx +1 -1
  39. package/src/components/Group/Group.tsx +1 -1
  40. package/src/components/HoverCard/HoverCard.tsx +1 -1
  41. package/src/components/Label/Label.module.css +15 -2
  42. package/src/components/Label/Label.tsx +7 -6
  43. package/src/components/Menu/Menu.tsx +1 -1
  44. package/src/components/Modal/Modal.tsx +1 -1
  45. package/src/components/NumberInput/NumberInput.tsx +1 -1
  46. package/src/components/Pagination/Pagination.tsx +1 -1
  47. package/src/components/Panel/Panel.tsx +1 -1
  48. package/src/components/Radio/Radio.module.css +33 -38
  49. package/src/components/Radio/Radio.tsx +58 -44
  50. package/src/components/Radio/RadioGroup.tsx +5 -0
  51. package/src/components/SegmentedControl/SegmentedControl.module.css +0 -7
  52. package/src/components/SegmentedControl/SegmentedControl.tsx +4 -8
  53. package/src/components/Slider/Slider.tsx +1 -1
  54. package/src/components/Stack/Stack.tsx +1 -1
  55. package/src/components/Stepper/Stepper.tsx +3 -3
  56. package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +123 -0
  57. package/src/components/Switch/Switch.module.css +152 -89
  58. package/src/components/Switch/Switch.tsx +140 -33
  59. package/src/components/Switch/SwitchGroup.tsx +37 -9
  60. package/src/components/Table/Table.tsx +1 -1
  61. package/src/components/Tabs/IMPLEMENTATION_NOTES.md +10 -0
  62. package/src/components/Tabs/Tabs.module.css +120 -43
  63. package/src/components/Tabs/Tabs.stories.tsx +5 -2
  64. package/src/components/Tabs/Tabs.tsx +1 -1
  65. package/src/components/Tabs/USAGE.md +27 -11
  66. package/src/components/TextField/TextField.tsx +1 -1
  67. package/src/components/TimePicker/TimePicker.tsx +1 -1
  68. package/src/components/Timeline/Timeline.tsx +1 -1
  69. package/src/components/Timeline/TimelineItem.tsx +2 -2
  70. package/src/components/Toast/Toast.tsx +4 -4
  71. package/src/components/Tooltip/Tooltip.tsx +1 -1
  72. package/src/components/Tree/Tree.tsx +1 -1
  73. package/src/index.ts +3 -1
  74. package/src/types/mantine.d.ts +0 -7
@@ -46,6 +46,7 @@ export const Group = forwardRef<HTMLDivElement, GroupProps>(function Group(
46
46
  return (
47
47
  <MUIStack
48
48
  ref={ref}
49
+ {...safeProps}
49
50
  direction="row"
50
51
  flexWrap={wrap || "wrap"}
51
52
  justifyContent={justify}
@@ -55,7 +56,6 @@ export const Group = forwardRef<HTMLDivElement, GroupProps>(function Group(
55
56
  ...(resolvedRowGap ? { rowGap: resolvedRowGap } : {}),
56
57
  ...(resolvedColumnGap ? { columnGap: resolvedColumnGap } : {}),
57
58
  }}
58
- {...safeProps}
59
59
  >
60
60
  {children}
61
61
  </MUIStack>
@@ -85,6 +85,7 @@ const HoverCardBase = function HoverCard({
85
85
 
86
86
  return (
87
87
  <MuiTooltip
88
+ {...(sanitizedProps as unknown as Record<string, unknown>)}
88
89
  title={dropdownNode}
89
90
  placement={position as unknown as MuiTooltipProps["placement"]}
90
91
  arrow={withBeak}
@@ -104,7 +105,6 @@ const HoverCardBase = function HoverCard({
104
105
  },
105
106
  }}
106
107
  classes={mergedClassNames as unknown as MuiTooltipProps["classes"]}
107
- {...(sanitizedProps as unknown as Record<string, unknown>)}
108
108
  >
109
109
  {/* Tooltip requires a single valid React element child that accepts a ref */}
110
110
  {isValidElement(targetNode) ? targetNode : <span>{targetNode}</span>}
@@ -48,6 +48,7 @@
48
48
 
49
49
  .innerLayout {
50
50
  display: flex;
51
+ flex-wrap: wrap;
51
52
  align-items: center;
52
53
  gap: var(
53
54
  --recursica_ui-kit_components_label_properties_label-optional-text-gap
@@ -74,8 +75,8 @@
74
75
  display: none !important;
75
76
  }
76
77
 
77
- /* MUI propagates .Mui-error to the label automatically on FormControl error.
78
- We explicitly override this bleed to prevent the main label text from turning red,
78
+ /* MUI propagates .Mui-error to the label automatically on FormControl error.
79
+ We explicitly override this bleed to prevent the main label text from turning red,
79
80
  while allowing the asterisk to take the error color! */
80
81
  .root:global(.Mui-error) {
81
82
  color: var(--recursica_ui-kit_components_label_properties_colors_text-color);
@@ -86,6 +87,15 @@
86
87
  );
87
88
  }
88
89
 
90
+ /* Same bleed, different trigger: MUI's FormControl propagates .Mui-focused to this label
91
+ whenever any descendant form control (e.g. a grouped Switch/Checkbox) receives focus,
92
+ turning the whole label MUI's primary blue via InputLabel's own default styling. No
93
+ Recursica design calls for a focus-reactive label color, so it's reset here — same fix as
94
+ the .Mui-error case above, just for the focused trigger instead of the error one. */
95
+ .root:global(.Mui-focused) {
96
+ color: var(--recursica_ui-kit_components_label_properties_colors_text-color);
97
+ }
98
+
89
99
  .endNodes {
90
100
  display: flex;
91
101
  align-items: center;
@@ -95,6 +105,9 @@
95
105
  }
96
106
 
97
107
  .optionalText {
108
+ /* Forces the optional text onto its own row below the label text/end nodes,
109
+ mirroring mantine-adapter's flex-basis: 100% wrap behavior. */
110
+ flex-basis: 100%;
98
111
  color: inherit;
99
112
  font-family: var(
100
113
  --recursica_ui-kit_components_label_properties_optional-text_font-family
@@ -46,9 +46,9 @@ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
46
46
  return (
47
47
  <InputLabel
48
48
  ref={ref}
49
+ {...(sanitizedProps as InputLabelProps)}
49
50
  shrink={true}
50
51
  className={className ? `${styles.root} ${className}` : styles.root}
51
- {...(sanitizedProps as InputLabelProps)}
52
52
  >
53
53
  <div className={styles.innerLayout}>
54
54
  <span className={styles.textWrapper}>
@@ -60,11 +60,6 @@ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
60
60
  {labelActionArea && (
61
61
  <span className={styles.actionArea}>{labelActionArea}</span>
62
62
  )}
63
- {!required && labelOptionalText && !labelActionArea && (
64
- <span className={styles.optionalText}>
65
- {labelOptionalText === true ? "Optional" : labelOptionalText}
66
- </span>
67
- )}
68
63
  {labelWithEditIcon && (
69
64
  <button
70
65
  type="button"
@@ -76,6 +71,12 @@ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
76
71
  </button>
77
72
  )}
78
73
  </div>
74
+
75
+ {!required && labelOptionalText && !labelActionArea && (
76
+ <span className={styles.optionalText}>
77
+ {labelOptionalText === true ? "(optional)" : labelOptionalText}
78
+ </span>
79
+ )}
79
80
  </div>
80
81
  </InputLabel>
81
82
  );
@@ -26,12 +26,12 @@ export const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(
26
26
  return (
27
27
  <MuiMenu
28
28
  ref={ref}
29
+ {...(sanitizedProps as MuiMenuProps)}
29
30
  className={`${styles.dropdown} ${className || ""}`}
30
31
  classes={{
31
32
  paper: styles.dropdown,
32
33
  list: styles.dropdown,
33
34
  }}
34
- {...(sanitizedProps as MuiMenuProps)}
35
35
  />
36
36
  );
37
37
  });
@@ -77,11 +77,11 @@ const ModalRoot = forwardRef<HTMLDivElement, ModalProps>(function ModalRoot(
77
77
  return (
78
78
  <MuiDialog
79
79
  ref={ref}
80
+ {...(sanitizedProps as MuiDialogProps)}
80
81
  className={`${styles.root} ${className || ""}`}
81
82
  classes={{
82
83
  paper: styles.inner,
83
84
  }}
84
- {...(sanitizedProps as MuiDialogProps)}
85
85
  open={
86
86
  opened ||
87
87
  ((sanitizedProps as Record<string, unknown>).open as boolean) ||
@@ -137,6 +137,7 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
137
137
  /* Naked Input execution safely decoupled from Mui's macro Input.Wrapper DOM hooks */
138
138
  <MuiNumberInput
139
139
  ref={ref}
140
+ {...(sanitizedProps as unknown as MuiNumberInputProps)}
140
141
  classes={mergedClassNames}
141
142
  disabled={disabled}
142
143
  value={value}
@@ -154,7 +155,6 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
154
155
  step,
155
156
  ...(restRecord.inputProps as Record<string, unknown>),
156
157
  }}
157
- {...(sanitizedProps as unknown as MuiNumberInputProps)}
158
158
  />
159
159
  }
160
160
  />
@@ -63,13 +63,13 @@ const _Pagination = forwardRef<HTMLDivElement, PaginationProps>(
63
63
  return (
64
64
  <div ref={ref} className={stylingParams.className}>
65
65
  <MuiPagination
66
+ {...(sanitizedProps as MuiPaginationProps)}
66
67
  count={total}
67
68
  showFirstButton={withEdges}
68
69
  showLastButton={withEdges}
69
70
  hidePrevButton={withControls === false ? true : undefined}
70
71
  hideNextButton={withControls === false ? true : undefined}
71
72
  classes={stylingParams.classNames}
72
- {...(sanitizedProps as MuiPaginationProps)}
73
73
  />
74
74
  </div>
75
75
  );
@@ -124,7 +124,7 @@ export const PanelFooter = forwardRef<HTMLDivElement, PanelFooterProps>(
124
124
  ? `${styles.footer} ${classNameProp}`
125
125
  : styles.footer;
126
126
 
127
- return <div ref={ref} className={finalClassName} {...sanitizedProps} />;
127
+ return <div ref={ref} {...sanitizedProps} className={finalClassName} />;
128
128
  },
129
129
  );
130
130
  PanelFooter.displayName = "PanelFooter";
@@ -70,7 +70,7 @@
70
70
  );
71
71
  }
72
72
 
73
- .root .inner {
73
+ .inner {
74
74
  display: flex;
75
75
  align-items: center;
76
76
  justify-content: center;
@@ -82,7 +82,7 @@
82
82
  --recursica_ui-kit_components_radio-button-item_properties_text_font-size
83
83
  );
84
84
  /* Mathematically syncs the physical input radio bounding block visually with the center of the text's first line.
85
- NOTE: Since the Figma line-height token resolves to an 'em' length (e.g. 1.05em), we CANNOT multiply it by font-size in CSS.
85
+ NOTE: Since the Figma line-height token resolves to an 'em' length (e.g. 1.05em), we CANNOT multiply it by font-size in CSS.
86
86
  Instead, it naturally evaluates to the correct height because we applied the font-size above. */
87
87
  margin-top: calc(
88
88
  (
@@ -95,10 +95,16 @@
95
95
  );
96
96
  }
97
97
 
98
- /* Base explicit reset masking underlying framework defaults */
99
- .root .radio {
98
+ /* Base explicit reset masking underlying framework defaults.
99
+ MUI's native <input type="radio"> stays invisible (SwitchBase overlays it with
100
+ opacity: 0) — it is not the visual circle. The circle is drawn by our own
101
+ icon/checkedIcon node (see Radio.tsx), a sibling of the input, so it's styled
102
+ here directly rather than through MUI's `classes` prop (MUI has no "radio" slot). */
103
+ .radio {
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
100
107
  cursor: pointer; /* HARDCODE: core interaction behavior */
101
- appearance: none; /* HARDCODE: reset native widget */
102
108
  box-sizing: border-box; /* HARDCODE: ensures size includes borders */
103
109
  width: var(--recursica_ui-kit_components_radio-button_properties_size);
104
110
  height: var(--recursica_ui-kit_components_radio-button_properties_size);
@@ -121,7 +127,7 @@
121
127
  }
122
128
 
123
129
  /* Checked State */
124
- .root .radio:checked {
130
+ .radioChecked {
125
131
  background-color: var(
126
132
  --recursica_ui-kit_components_radio-button_variants_selection-states_selected_properties_colors_background-color
127
133
  );
@@ -130,11 +136,14 @@
130
136
  );
131
137
  }
132
138
 
133
- /* Disabled State (colors now differ per selection state, so each combination is mapped explicitly) */
134
- .root .radio:disabled {
139
+ /* Disabled State (colors now differ per selection state; MUI applies its `disabled`
140
+ classKey to the same root element as `root`, so `.root.disabled` targets the
141
+ actual disabled instance and cascades to the decorative icon div inside it —
142
+ mirrors Checkbox's split). */
143
+ .root.disabled .radio {
135
144
  cursor: not-allowed; /* HARDCODE: disabled state default */
136
145
  }
137
- .root .radio:disabled:not(:checked) {
146
+ .root.disabled .radio:not(.radioChecked) {
138
147
  opacity: var(
139
148
  --recursica_ui-kit_components_radio-button_variants_selection-states_unselected_variants_states_disabled_properties_opacity
140
149
  );
@@ -145,7 +154,7 @@
145
154
  --recursica_ui-kit_components_radio-button_variants_selection-states_unselected_variants_states_disabled_properties_colors_border-color
146
155
  );
147
156
  }
148
- .root .radio:checked:disabled {
157
+ .root.disabled .radioChecked {
149
158
  opacity: var(
150
159
  --recursica_ui-kit_components_radio-button_variants_selection-states_selected_variants_states_disabled_properties_opacity
151
160
  );
@@ -157,29 +166,32 @@
157
166
  );
158
167
  }
159
168
 
160
- .root .icon {
161
- position: absolute;
162
- inset: 0;
163
- margin: auto;
169
+ .icon {
164
170
  pointer-events: none;
165
171
  width: var(--recursica_ui-kit_components_radio-button_properties_icon-size);
166
172
  height: var(--recursica_ui-kit_components_radio-button_properties_icon-size);
167
173
  }
168
174
  /* No icon-color token exists for the unselected state (no icon is visible then) */
169
- .root .radio:checked + .iconWrapper .icon,
170
- .root .radio:checked + .icon {
175
+ .radioChecked .icon {
171
176
  color: var(
172
177
  --recursica_ui-kit_components_radio-button_variants_selection-states_selected_properties_colors_icon-color
173
178
  );
174
179
  }
175
- .root .radio:checked:disabled + .iconWrapper .icon,
176
- .root .radio:checked:disabled + .icon {
180
+ .root.disabled .radioChecked .icon {
177
181
  color: var(
178
182
  --recursica_ui-kit_components_radio-button_variants_selection-states_selected_variants_states_disabled_properties_colors_icon-color
179
183
  );
180
184
  }
181
185
 
182
186
  .root .labelWrapper {
187
+ /* display: flex (not the browser default block) blockifies the native inline
188
+ <label> child per the CSS Display spec's flex-item blockification rule —
189
+ matches Mantine's own labelWrapper (inline-flex), whose flex context does
190
+ the same to its label. Without it, the label stays inline and its line box
191
+ picks up extra half-leading height, visually shifting it below vertical
192
+ center against the circle. */
193
+ display: flex;
194
+ flex-direction: column;
183
195
  padding: 0; /* HARDCODE: structural layout reset */
184
196
  margin: 0; /* HARDCODE: structural layout reset */
185
197
  flex: 1;
@@ -230,23 +242,6 @@
230
242
  );
231
243
  }
232
244
 
233
- .root .description {
234
- margin-top: 5px; /* HARDCODE: mantine default */
235
- color: #868e96; /* HARDCODE: mantine default dimmed */
236
- font-size: 12px; /* HARDCODE: mantine default xs */
237
- line-height: 1.2;
238
- }
239
-
240
- .root .error {
241
- margin-top: 5px; /* HARDCODE: mantine default */
242
- color: #fa5252; /* HARDCODE: mantine default error */
243
- font-size: 12px; /* HARDCODE: mantine default xs */
244
- line-height: 1.2;
245
- }
246
-
247
- .root .description[data-disabled="true"],
248
- .root .error[data-disabled="true"] {
249
- opacity: var(
250
- --recursica_ui-kit_components_radio-button-item_variants_states_disabled_properties_opacity
251
- );
252
- }
245
+ /* description/error text renders via AssistiveElement directly (see Radio.tsx) —
246
+ it owns its own tokenized styling. AssistiveElement has no disabled state and
247
+ must never dim. */
@@ -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
- body: styles.body,
84
- inner: styles.inner,
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 classNamesProp = restRecord.classNames;
98
+ const classesProp = restRecord.classes;
92
99
  if (
93
- classNamesProp &&
94
- typeof classNamesProp === "object" &&
95
- !Array.isArray(classNamesProp)
100
+ classesProp &&
101
+ typeof classesProp === "object" &&
102
+ !Array.isArray(classesProp)
96
103
  ) {
97
- const o = classNamesProp as Partial<Record<string, string>>;
104
+ const o = classesProp as Partial<Record<string, string>>;
98
105
  mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
99
- mergedClassNames.body = o.body ? `${styles.body} ${o.body}` : styles.body;
100
- mergedClassNames.inner = o.inner
101
- ? `${styles.inner} ${o.inner}`
102
- : styles.inner;
103
- mergedClassNames.radio = o.radio
104
- ? `${styles.radio} ${o.radio}`
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
- // We omit Mui's sizing/coloring so we rely strictly on variables from Radio.module.css
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
- icon={<RadioIcon />}
153
- checkedIcon={<RadioIcon className={styles.checked} />}
154
- className={finalClass}
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
- {...(sanitizedProps as unknown as MuiRadioProps)}
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
- <div
175
- className={styles.description}
176
- data-disabled={readOnly || disabled ? true : undefined}
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
- </div>
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
- radioNode
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
  },