@recursica/mantine-adapter 0.42.1 → 0.44.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 (65) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.d.ts +93 -15
  3. package/dist/mantine-adapter.cjs +2 -2
  4. package/dist/mantine-adapter.cjs.map +1 -1
  5. package/dist/mantine-adapter.css +1 -1
  6. package/dist/mantine-adapter.js +3534 -3266
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/Accordion/Accordion.tsx +32 -33
  10. package/src/components/AssistiveElement/AssistiveElement.tsx +1 -1
  11. package/src/components/AutoComplete/AutoComplete.tsx +26 -38
  12. package/src/components/Avatar/Avatar.tsx +22 -23
  13. package/src/components/Badge/Badge.tsx +12 -18
  14. package/src/components/Breadcrumb/Breadcrumb.module.css +2 -0
  15. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +0 -6
  16. package/src/components/Breadcrumb/Breadcrumb.tsx +25 -17
  17. package/src/components/Button/Button.tsx +31 -30
  18. package/src/components/Card/Card.tsx +48 -27
  19. package/src/components/Checkbox/Checkbox.tsx +29 -42
  20. package/src/components/Checkbox/CheckboxGroup.tsx +12 -5
  21. package/src/components/Chip/Chip.tsx +34 -27
  22. package/src/components/DatePicker/DatePicker.tsx +46 -47
  23. package/src/components/Dropdown/BareDropdown.tsx +25 -13
  24. package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +2 -0
  25. package/src/components/Dropdown/Dropdown.module.css +61 -4
  26. package/src/components/Dropdown/Dropdown.stories.tsx +21 -0
  27. package/src/components/Dropdown/Dropdown.tsx +41 -37
  28. package/src/components/Dropdown/USAGE.md +1 -0
  29. package/src/components/FileInput/FileInput.tsx +1 -1
  30. package/src/components/FileUpload/FileUpload.tsx +1 -1
  31. package/src/components/HoverCard/HoverCard.tsx +18 -34
  32. package/src/components/Label/Label.tsx +20 -21
  33. package/src/components/Link/Link.tsx +6 -14
  34. package/src/components/Loader/Loader.tsx +8 -16
  35. package/src/components/Menu/Menu.stories.tsx +9 -1
  36. package/src/components/Menu/Menu.tsx +55 -48
  37. package/src/components/Modal/Modal.tsx +134 -96
  38. package/src/components/NumberInput/NumberInput.tsx +14 -7
  39. package/src/components/Pagination/Pagination.tsx +25 -34
  40. package/src/components/Panel/Panel.tsx +19 -26
  41. package/src/components/Popover/Popover.tsx +19 -31
  42. package/src/components/Radio/Radio.tsx +32 -43
  43. package/src/components/Radio/RadioGroup.tsx +11 -4
  44. package/src/components/ReadOnlyField/ReadOnlyField.tsx +1 -1
  45. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +1 -1
  46. package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +1 -1
  47. package/src/components/SegmentedControl/SegmentedControl.tsx +29 -33
  48. package/src/components/Slider/Slider.tsx +30 -43
  49. package/src/components/Stepper/Stepper.tsx +20 -13
  50. package/src/components/Switch/Switch.tsx +45 -45
  51. package/src/components/Switch/SwitchGroup.tsx +11 -4
  52. package/src/components/Table/Table.tsx +1 -1
  53. package/src/components/Tabs/Tabs.tsx +13 -6
  54. package/src/components/Text/Text.tsx +1 -1
  55. package/src/components/TextArea/TextArea.tsx +21 -25
  56. package/src/components/TextField/TextField.tsx +23 -30
  57. package/src/components/TimePicker/TimePicker.tsx +26 -12
  58. package/src/components/Timeline/Timeline.tsx +7 -21
  59. package/src/components/Timeline/TimelineItem.tsx +14 -26
  60. package/src/components/Title/Title.tsx +12 -2
  61. package/src/components/Toast/Toast.tsx +32 -31
  62. package/src/components/Tooltip/Tooltip.tsx +18 -34
  63. package/src/components/TransferList/TransferList.tsx +1 -1
  64. package/src/components/Tree/Tree.tsx +8 -8
  65. package/src/utils/filterStylingProps.ts +4 -0
@@ -9,6 +9,8 @@ import {
9
9
  } from "@mantine/core";
10
10
  import {
11
11
  filterStylingProps,
12
+ withCallerOverride,
13
+ mergeClassNames,
12
14
  type RecursicaOverStyled,
13
15
  } from "../../utils/filterStylingProps";
14
16
  import styles from "./Pagination.module.css";
@@ -42,25 +44,14 @@ function usePaginationClassNames(restRecord: Record<string, unknown>): {
42
44
  className: string;
43
45
  classNames: Partial<Record<string, string>>;
44
46
  } {
45
- const mergedClassNames: Partial<Record<string, string>> = {
46
- root: styles.root,
47
- control: styles.control,
48
- dots: styles.dots,
49
- };
50
-
51
- const classNamesProp = restRecord.classNames;
52
- if (
53
- classNamesProp &&
54
- typeof classNamesProp === "object" &&
55
- !Array.isArray(classNamesProp)
56
- ) {
57
- const o = classNamesProp as Partial<Record<string, string>>;
58
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
59
- mergedClassNames.control = o.control
60
- ? `${styles.control} ${o.control}`
61
- : styles.control;
62
- mergedClassNames.dots = o.dots ? `${styles.dots} ${o.dots}` : styles.dots;
63
- }
47
+ const mergedClassNames = mergeClassNames(
48
+ {
49
+ root: styles.root,
50
+ control: styles.control,
51
+ dots: styles.dots,
52
+ },
53
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
54
+ );
64
55
 
65
56
  const classNameProp = restRecord.className as string | undefined;
66
57
  const finalClass = classNameProp
@@ -80,9 +71,9 @@ const _PaginationRoot = forwardRef<HTMLDivElement, PaginationRootProps>(
80
71
  return (
81
72
  <MantinePagination.Root
82
73
  ref={ref}
74
+ {...(sanitizedProps as MantinePaginationRootProps)}
83
75
  className={stylingParams.className}
84
76
  classNames={stylingParams.classNames}
85
- {...(sanitizedProps as MantinePaginationRootProps)}
86
77
  />
87
78
  );
88
79
  },
@@ -97,13 +88,13 @@ const _PaginationNext = forwardRef<
97
88
  ref,
98
89
  ) {
99
90
  const sanitizedProps = filterStylingProps(rest, overStyled);
100
- const renderIcon = icon || (withLabel ? NextWithLabel : undefined);
91
+ const computedIcon = withLabel ? NextWithLabel : undefined;
101
92
  return (
102
93
  <MantinePagination.Next
103
94
  ref={ref}
104
- data-variant="text"
105
- icon={renderIcon as any}
106
95
  {...sanitizedProps}
96
+ data-variant="text"
97
+ icon={withCallerOverride(computedIcon, icon) as any}
107
98
  />
108
99
  );
109
100
  });
@@ -117,13 +108,13 @@ const _PaginationPrevious = forwardRef<
117
108
  ref,
118
109
  ) {
119
110
  const sanitizedProps = filterStylingProps(rest, overStyled);
120
- const renderIcon = icon || (withLabel ? PrevWithLabel : undefined);
111
+ const computedIcon = withLabel ? PrevWithLabel : undefined;
121
112
  return (
122
113
  <MantinePagination.Previous
123
114
  ref={ref}
124
- data-variant="text"
125
- icon={renderIcon as any}
126
115
  {...sanitizedProps}
116
+ data-variant="text"
117
+ icon={withCallerOverride(computedIcon, icon) as any}
127
118
  />
128
119
  );
129
120
  });
@@ -137,13 +128,13 @@ const _PaginationFirst = forwardRef<
137
128
  ref,
138
129
  ) {
139
130
  const sanitizedProps = filterStylingProps(rest, overStyled);
140
- const renderIcon = icon || (withLabel ? FirstWithLabel : undefined);
131
+ const computedIcon = withLabel ? FirstWithLabel : undefined;
141
132
  return (
142
133
  <MantinePagination.First
143
134
  ref={ref}
144
- data-variant="text"
145
- icon={renderIcon as any}
146
135
  {...sanitizedProps}
136
+ data-variant="text"
137
+ icon={withCallerOverride(computedIcon, icon) as any}
147
138
  />
148
139
  );
149
140
  });
@@ -157,13 +148,13 @@ const _PaginationLast = forwardRef<
157
148
  ref,
158
149
  ) {
159
150
  const sanitizedProps = filterStylingProps(rest, overStyled);
160
- const renderIcon = icon || (withLabel ? LastWithLabel : undefined);
151
+ const computedIcon = withLabel ? LastWithLabel : undefined;
161
152
  return (
162
153
  <MantinePagination.Last
163
154
  ref={ref}
164
- data-variant="text"
165
- icon={renderIcon as any}
166
155
  {...sanitizedProps}
156
+ data-variant="text"
157
+ icon={withCallerOverride(computedIcon, icon) as any}
167
158
  />
168
159
  );
169
160
  });
@@ -201,11 +192,11 @@ const _Pagination = forwardRef<HTMLDivElement, PaginationProps>(
201
192
  return (
202
193
  <MantinePagination
203
194
  ref={ref}
195
+ {...labelsIcons}
196
+ {...(sanitizedProps as MantinePaginationProps)}
204
197
  className={stylingParams.className}
205
198
  classNames={stylingParams.classNames}
206
199
  getControlProps={mergedGetControlProps}
207
- {...labelsIcons}
208
- {...(sanitizedProps as MantinePaginationProps)}
209
200
  />
210
201
  );
211
202
  },
@@ -12,6 +12,8 @@ import {
12
12
  } from "@mantine/core";
13
13
  import {
14
14
  filterStylingProps,
15
+ mergeClassNames,
16
+ withCallerOverride,
15
17
  type RecursicaOverStyled,
16
18
  } from "../../utils/filterStylingProps";
17
19
  import styles from "./Panel.module.css";
@@ -68,38 +70,29 @@ const PanelBase = function Panel({
68
70
  ...rest
69
71
  }: PanelProps) {
70
72
  const sanitizedProps = filterStylingProps(rest, overStyled);
73
+ const restRecord = sanitizedProps as Record<string, unknown>;
71
74
 
72
75
  // Bind CSS module classes to Mantine's internal classNames API
73
- const mergedClassNames: Partial<Record<string, string>> = {
74
- content: styles.content,
75
- header: styles.header,
76
- title: wrapHeaderText ? styles.titleTruncate : styles.title,
77
- body: styles.body,
78
- inner: styles.inner,
79
- };
80
-
81
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
82
- if (
83
- classNamesProp &&
84
- typeof classNamesProp === "object" &&
85
- !Array.isArray(classNamesProp)
86
- ) {
87
- const o = classNamesProp as Record<string, string>;
88
- Object.keys(o).forEach((key) => {
89
- if (mergedClassNames[key]) {
90
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
91
- } else {
92
- mergedClassNames[key] = o[key];
93
- }
94
- });
95
- }
76
+ const mergedClassNames = mergeClassNames(
77
+ {
78
+ content: styles.content,
79
+ header: styles.header,
80
+ title: wrapHeaderText ? styles.titleTruncate : styles.title,
81
+ body: styles.body,
82
+ inner: styles.inner,
83
+ },
84
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
85
+ );
96
86
 
97
87
  return (
98
88
  <MantineDrawer
89
+ {...(sanitizedProps as unknown as MantineDrawerProps)}
99
90
  position={placement} /* Recursica default: right; Mantine default: left */
100
91
  keepMounted={keepMounted}
101
- closeOnClickOutside={rest.closeOnClickOutside ?? Boolean(rest.opened)}
102
- {...(sanitizedProps as unknown as MantineDrawerProps)}
92
+ closeOnClickOutside={withCallerOverride(
93
+ Boolean(rest.opened),
94
+ rest.closeOnClickOutside,
95
+ )}
103
96
  classNames={mergedClassNames}
104
97
  />
105
98
  );
@@ -130,7 +123,7 @@ export const PanelFooter = forwardRef<HTMLDivElement, PanelFooterProps>(
130
123
  ? `${styles.footer} ${classNameProp}`
131
124
  : styles.footer;
132
125
 
133
- return <div ref={ref} className={finalClassName} {...sanitizedProps} />;
126
+ return <div ref={ref} {...sanitizedProps} className={finalClassName} />;
134
127
  },
135
128
  );
136
129
  PanelFooter.displayName = "PanelFooter";
@@ -6,6 +6,8 @@ import {
6
6
  } from "@mantine/core";
7
7
  import {
8
8
  filterStylingProps,
9
+ mergeClassNames,
10
+ withCallerOverride,
9
11
  type RecursicaOverStyled,
10
12
  } from "../../utils/filterStylingProps";
11
13
  import styles from "./Popover.module.css";
@@ -47,54 +49,40 @@ export type PopoverProps = Omit<
47
49
  const PopoverBase = function Popover({
48
50
  overStyled = false,
49
51
  withBeak = true,
52
+ position = "top", // Recursica default; Mantine defaults to "bottom"
50
53
  ...rest
51
54
  }: PopoverProps) {
52
55
  const sanitizedProps = filterStylingProps(rest, overStyled);
56
+ const restRecord = sanitizedProps as Record<string, unknown>;
53
57
 
54
58
  // Bind CSS module classes to Mantine's internal classNames API
55
- const mergedClassNames: Partial<Record<string, string>> = {
56
- dropdown: styles.dropdown,
57
- arrow: styles.arrow,
58
- };
59
-
60
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
61
- if (
62
- classNamesProp &&
63
- typeof classNamesProp === "object" &&
64
- !Array.isArray(classNamesProp)
65
- ) {
66
- const o = classNamesProp as Record<string, string>;
67
- Object.keys(o).forEach((key) => {
68
- if (mergedClassNames[key]) {
69
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
70
- } else {
71
- mergedClassNames[key] = o[key];
72
- }
73
- });
74
- }
59
+ const mergedClassNames = mergeClassNames(
60
+ { dropdown: styles.dropdown, arrow: styles.arrow },
61
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
62
+ );
75
63
 
76
64
  // arrowSize must be a JS number prop — Mantine uses it for inline width/height
77
65
  // and positioning offset (-arrowSize/2) calculations that cannot be CSS-driven.
78
66
  // Default to 16 to match the Recursica beak-size token (16px).
79
- const arrowSize =
80
- ((sanitizedProps as Record<string, unknown>).arrowSize as
81
- | number
82
- | undefined) ?? 16;
67
+ const arrowSize = withCallerOverride<number>(
68
+ 16,
69
+ restRecord.arrowSize as number | undefined,
70
+ );
83
71
 
84
72
  // Resolve withBeak (Recursica) vs withArrow (Mantine).
85
73
  // withBeak takes precedence when both are provided.
86
- const withArrow = (sanitizedProps as Record<string, unknown>).withArrow as
87
- | boolean
88
- | undefined;
89
- const resolvedWithArrow = withBeak ?? withArrow;
74
+ const resolvedWithArrow = withCallerOverride<boolean | undefined>(
75
+ withBeak,
76
+ restRecord.withArrow as boolean | undefined,
77
+ );
90
78
 
91
79
  return (
92
80
  <MantinePopover
93
- position="top" /* Recursica default; Mantine defaults to "bottom" */
81
+ {...(sanitizedProps as unknown as MantinePopoverProps)}
82
+ position={position}
94
83
  arrowSize={arrowSize}
95
84
  withArrow={resolvedWithArrow}
96
85
  classNames={mergedClassNames}
97
- {...(sanitizedProps as unknown as MantinePopoverProps)}
98
86
  />
99
87
  );
100
88
  };
@@ -133,8 +121,8 @@ const PopoverDropdown = function PopoverDropdown({
133
121
 
134
122
  return (
135
123
  <MantinePopover.Dropdown
136
- className={classNameProp}
137
124
  {...(sanitizedProps as unknown as MantinePopoverDropdownProps)}
125
+ className={classNameProp}
138
126
  />
139
127
  );
140
128
  };
@@ -7,6 +7,9 @@ import { type ReadOnlyControlProps } from "@recursica/adapter-common";
7
7
  import { RadioGroup } from "./RadioGroup";
8
8
  import {
9
9
  filterStylingProps,
10
+ omitUnsupportedProps,
11
+ withCallerOverride,
12
+ mergeClassNames,
10
13
  type RecursicaOverStyled,
11
14
  } from "../../utils/filterStylingProps";
12
15
  import {
@@ -64,55 +67,41 @@ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
64
67
  readOnly,
65
68
  readOnlyComponent,
66
69
  disabled,
70
+ icon,
67
71
  formLayout,
68
72
  labelSize,
69
73
  controlMaxWidth,
70
74
  controlMinWidth,
71
75
  ...rest
72
76
  } = props;
73
- const sanitizedProps = filterStylingProps(rest, overStyled);
77
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
78
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
79
+ const UNSUPPORTED_PROPS = [
80
+ "size", // Recursica controls sizing via Radio.module.css variables, not Mantine's native size scale.
81
+ "color", // Colors are token-driven via Radio.module.css; Mantine's native palette isn't exposed.
82
+ "radius", // Corner radius is fixed by Recursica tokens in Radio.module.css, not caller-configurable.
83
+ "variant", // Radio has a single Recursica-defined visual treatment; Mantine's variant isn't exposed.
84
+ "iconColor", // Icon color is token-driven via Radio.module.css, not a native Mantine override.
85
+ ] as const satisfies readonly (keyof MantineRadioProps)[];
86
+
87
+ const sanitizedProps = omitUnsupportedProps(
88
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
89
+ UNSUPPORTED_PROPS,
90
+ ) as Partial<typeof rest>;
74
91
  const restRecord = sanitizedProps as Record<string, unknown>;
75
92
 
76
- // Actively delete dimension bindings that bypass the abstraction
77
- delete restRecord["size"];
78
- delete restRecord["color"];
79
- delete restRecord["radius"];
80
- delete restRecord["variant"];
81
- delete restRecord["iconColor"];
82
-
83
- const mergedClassNames: Partial<Record<string, string>> = {
84
- root: styles.root,
85
- body: styles.body,
86
- inner: styles.inner,
87
- radio: styles.radio,
88
- icon: styles.icon,
89
- labelWrapper: styles.labelWrapper,
90
- label: styles.label,
91
- };
92
-
93
- const classNamesProp = restRecord.classNames;
94
- if (
95
- classNamesProp &&
96
- typeof classNamesProp === "object" &&
97
- !Array.isArray(classNamesProp)
98
- ) {
99
- const o = classNamesProp as Partial<Record<string, string>>;
100
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
101
- mergedClassNames.body = o.body ? `${styles.body} ${o.body}` : styles.body;
102
- mergedClassNames.inner = o.inner
103
- ? `${styles.inner} ${o.inner}`
104
- : styles.inner;
105
- mergedClassNames.radio = o.radio
106
- ? `${styles.radio} ${o.radio}`
107
- : styles.radio;
108
- mergedClassNames.icon = o.icon ? `${styles.icon} ${o.icon}` : styles.icon;
109
- mergedClassNames.labelWrapper = o.labelWrapper
110
- ? `${styles.labelWrapper} ${o.labelWrapper}`
111
- : styles.labelWrapper;
112
- mergedClassNames.label = o.label
113
- ? `${styles.label} ${o.label}`
114
- : styles.label;
115
- }
93
+ const mergedClassNames = mergeClassNames(
94
+ {
95
+ root: styles.root,
96
+ body: styles.body,
97
+ inner: styles.inner,
98
+ radio: styles.radio,
99
+ icon: styles.icon,
100
+ labelWrapper: styles.labelWrapper,
101
+ label: styles.label,
102
+ },
103
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
104
+ );
116
105
 
117
106
  const classNameProp = restRecord.className as string | undefined;
118
107
  const finalClass = classNameProp
@@ -150,11 +139,11 @@ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
150
139
  const radioNode = (
151
140
  <MantineRadio
152
141
  ref={ref}
153
- icon={RadioIcon}
142
+ {...(sanitizedProps as unknown as MantineRadioProps)}
143
+ icon={withCallerOverride<MantineRadioProps["icon"]>(RadioIcon, icon)}
154
144
  className={finalClass}
155
145
  classNames={mergedClassNames}
156
146
  disabled={readOnly || disabled}
157
- {...(sanitizedProps as unknown as MantineRadioProps)}
158
147
  />
159
148
  );
160
149
 
@@ -7,6 +7,7 @@ import {
7
7
  import { type ReadOnlyControlProps } from "@recursica/adapter-common";
8
8
  import {
9
9
  filterStylingProps,
10
+ omitUnsupportedProps,
10
11
  type RecursicaOverStyled,
11
12
  } from "../../utils/filterStylingProps";
12
13
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
@@ -61,11 +62,17 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
61
62
  defaultValue,
62
63
  ...rest
63
64
  } = props;
64
- const sanitizedProps = filterStylingProps(rest, overStyled);
65
- const restRecord = sanitizedProps as Record<string, unknown>;
65
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
66
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
67
+ const UNSUPPORTED_PROPS = [
68
+ "size", // Recursica controls sizing via Radio.module.css variables, not Mantine's native size scale.
69
+ ] as const satisfies readonly (keyof MantineRadioGroupProps)[];
66
70
 
67
- // Delete prohibited sizing hooks from bypassing the variables
68
- delete restRecord["size"];
71
+ const sanitizedProps = omitUnsupportedProps(
72
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
73
+ UNSUPPORTED_PROPS,
74
+ ) as Partial<typeof rest>;
75
+ const restRecord = sanitizedProps as Record<string, unknown>;
69
76
 
70
77
  return (
71
78
  <WithReadOnlyWrapper
@@ -100,10 +100,10 @@ export const ReadOnlyField = forwardRef<HTMLDivElement, ReadOnlyFieldProps>(
100
100
  return (
101
101
  <FormControlWrapper
102
102
  ref={ref}
103
+ {...(sanitizedProps as any)}
103
104
  overStyled={overStyled as true}
104
105
  className={wrapperClass}
105
106
  style={style}
106
- {...(sanitizedProps as any)}
107
107
  >
108
108
  {content}
109
109
  </FormControlWrapper>
@@ -27,12 +27,12 @@ export const ReadOnlyTextField: React.FC<ReadOnlyTextFieldProps> = ({
27
27
  return (
28
28
  <Box
29
29
  component="p"
30
+ {...sanitizedProps}
30
31
  className={
31
32
  classNameProp
32
33
  ? `${styles.textField} ${classNameProp}`
33
34
  : styles.textField
34
35
  }
35
- {...sanitizedProps}
36
36
  >
37
37
  {value != null
38
38
  ? React.isValidElement(value)
@@ -67,12 +67,12 @@ export const WithReadOnlyWrapper = forwardRef<
67
67
  return (
68
68
  <ReadOnlyField
69
69
  ref={ref}
70
+ {...wrapperProps}
70
71
  type={readOnlyType}
71
72
  value={readOnlyValue}
72
73
  emptyValueComponent={emptyValueComponent}
73
74
  onLabelEditClick={onLabelEditClick}
74
75
  overStyled={overStyled as true}
75
- {...wrapperProps}
76
76
  />
77
77
  );
78
78
  }
@@ -5,6 +5,8 @@ import {
5
5
  } from "@mantine/core";
6
6
  import {
7
7
  filterStylingProps,
8
+ omitUnsupportedProps,
9
+ mergeClassNames,
8
10
  type RecursicaOverStyled,
9
11
  } from "../../utils/filterStylingProps";
10
12
  import styles from "./SegmentedControl.module.css";
@@ -31,31 +33,15 @@ function useSegmentedControlClassNames(restRecord: Record<string, unknown>): {
31
33
  className: string;
32
34
  classNames: Partial<Record<string, string>>;
33
35
  } {
34
- const mergedClassNames: Partial<Record<string, string>> = {
35
- root: styles.root,
36
- control: styles.control,
37
- label: styles.label,
38
- indicator: styles.indicator,
39
- };
40
-
41
- const classNamesProp = restRecord.classNames;
42
- if (
43
- classNamesProp &&
44
- typeof classNamesProp === "object" &&
45
- !Array.isArray(classNamesProp)
46
- ) {
47
- const o = classNamesProp as Partial<Record<string, string>>;
48
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
49
- mergedClassNames.control = o.control
50
- ? `${styles.control} ${o.control}`
51
- : styles.control;
52
- mergedClassNames.label = o.label
53
- ? `${styles.label} ${o.label}`
54
- : styles.label;
55
- mergedClassNames.indicator = o.indicator
56
- ? `${styles.indicator} ${o.indicator}`
57
- : styles.indicator;
58
- }
36
+ const mergedClassNames = mergeClassNames(
37
+ {
38
+ root: styles.root,
39
+ control: styles.control,
40
+ label: styles.label,
41
+ indicator: styles.indicator,
42
+ },
43
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
44
+ );
59
45
 
60
46
  const classNameProp = restRecord.className as string | undefined;
61
47
  const finalClass = classNameProp
@@ -70,26 +56,36 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
70
56
  { overStyled = false, orientation = "horizontal", fullWidth, ...rest },
71
57
  ref,
72
58
  ) {
73
- const sanitizedProps = filterStylingProps(rest, overStyled);
74
- const restRecord = sanitizedProps as Record<string, unknown>;
59
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
60
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
61
+ const UNSUPPORTED_PROPS = [
62
+ // SegmentedControl only supports per-item disabling via the `data` array (each item may set
63
+ // its own `disabled`); a top-level `disabled` is intentionally unsupported (typed as `never`
64
+ // in RecursicaSegmentedControlProps) because Mantine's top-level `disabled` would disable
65
+ // the whole control uniformly instead of per-item.
66
+ "disabled",
67
+ ] as const satisfies readonly (keyof MantineSegmentedControlProps)[];
75
68
 
76
- // Explicitly prevent consumers from bypassing the disabled restriction
77
- delete restRecord["disabled"];
69
+ const sanitizedProps = omitUnsupportedProps(
70
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
71
+ UNSUPPORTED_PROPS,
72
+ ) as Partial<typeof rest>;
73
+ const restRecord = sanitizedProps as Record<string, unknown>;
78
74
 
79
75
  const stylingParams = useSegmentedControlClassNames(restRecord);
80
76
 
81
77
  return (
82
78
  <MantineSegmentedControl
83
79
  ref={ref}
80
+ {...(sanitizedProps as Omit<
81
+ MantineSegmentedControlProps,
82
+ "variant" | "size"
83
+ >)}
84
84
  className={stylingParams.className}
85
85
  classNames={stylingParams.classNames}
86
86
  orientation={orientation}
87
87
  fullWidth={fullWidth}
88
88
  data-orientation={orientation}
89
- {...(sanitizedProps as Omit<
90
- MantineSegmentedControlProps,
91
- "variant" | "size"
92
- >)}
93
89
  />
94
90
  );
95
91
  },
@@ -7,6 +7,8 @@ import {
7
7
  import { type ReadOnlyControlProps } from "@recursica/adapter-common";
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";
@@ -133,51 +135,36 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
133
135
  setInputValue(resolvedValue.toString());
134
136
  };
135
137
 
136
- const sanitizedProps = filterStylingProps(rest, overStyled);
137
- const restRecord = sanitizedProps as Record<string, unknown>;
138
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
139
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
140
+ const UNSUPPORTED_PROPS = [
141
+ "size", // Recursica controls sizing via Slider.module.css variables, not Mantine's native size scale.
142
+ "variant", // Slider has a single Recursica-defined visual treatment; Mantine's variant isn't exposed.
143
+ "radius", // Corner radius is fixed by Recursica tokens in Slider.module.css, not caller-configurable.
144
+ // Not currently a real key of Mantine's SliderProps (that's an Input.Wrapper-only prop) — kept
145
+ // as a defensive no-op delete since it predates this consolidation and the public prop type
146
+ // already `Omit`s it; Recursica's own FormControlLayout/WithReadOnlyWrapper own wrapper markup.
147
+ "wrapperProps",
148
+ ] as const satisfies readonly (keyof MantineSliderProps | "wrapperProps")[];
138
149
 
139
- // Delete prohibited sizing hooks from bypassing variables natively
140
- delete restRecord["size"];
141
- delete restRecord["variant"];
142
- delete restRecord["radius"];
143
- delete restRecord["wrapperProps"];
150
+ const sanitizedProps = omitUnsupportedProps(
151
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
152
+ UNSUPPORTED_PROPS,
153
+ ) as Partial<typeof rest>;
154
+ const restRecord = sanitizedProps as Record<string, unknown>;
144
155
 
145
156
  // Securely map core native blocks down ensuring nested CSS modules map precisely
146
- const mergedClassNames: Partial<Record<string, string>> = {
147
- root: styles.sliderRoot,
148
- track: styles.sliderTrack,
149
- bar: styles.sliderBar,
150
- thumb: styles.sliderThumb,
151
- mark: styles.sliderMark,
152
- markLabel: styles.sliderMarkLabel,
153
- };
154
-
155
- const classNamesProp = restRecord.classNames;
156
- if (
157
- classNamesProp &&
158
- typeof classNamesProp === "object" &&
159
- !Array.isArray(classNamesProp)
160
- ) {
161
- const o = classNamesProp as Partial<Record<string, string>>;
162
- mergedClassNames.root = o.root
163
- ? `${styles.sliderRoot} ${o.root}`
164
- : styles.sliderRoot;
165
- mergedClassNames.track = o.track
166
- ? `${styles.sliderTrack} ${o.track}`
167
- : styles.sliderTrack;
168
- mergedClassNames.bar = o.bar
169
- ? `${styles.sliderBar} ${o.bar}`
170
- : styles.sliderBar;
171
- mergedClassNames.thumb = o.thumb
172
- ? `${styles.sliderThumb} ${o.thumb}`
173
- : styles.sliderThumb;
174
- mergedClassNames.mark = o.mark
175
- ? `${styles.sliderMark} ${o.mark}`
176
- : styles.sliderMark;
177
- mergedClassNames.markLabel = o.markLabel
178
- ? `${styles.sliderMarkLabel} ${o.markLabel}`
179
- : styles.sliderMarkLabel;
180
- }
157
+ const mergedClassNames = mergeClassNames(
158
+ {
159
+ root: styles.sliderRoot,
160
+ track: styles.sliderTrack,
161
+ bar: styles.sliderBar,
162
+ thumb: styles.sliderThumb,
163
+ mark: styles.sliderMark,
164
+ markLabel: styles.sliderMarkLabel,
165
+ },
166
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
167
+ );
181
168
 
182
169
  const wrapperClass = className
183
170
  ? `${styles.layoutOverride} ${className}`
@@ -232,6 +219,7 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
232
219
 
233
220
  <div className={styles.sliderTrackWrapper}>
234
221
  <MantineSlider
222
+ {...(sanitizedProps as unknown as MantineSliderProps)}
235
223
  classNames={mergedClassNames}
236
224
  disabled={disabled}
237
225
  value={resolvedValue}
@@ -241,7 +229,6 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
241
229
  max={max}
242
230
  step={step}
243
231
  label={tooltipLabel}
244
- {...(sanitizedProps as unknown as MantineSliderProps)}
245
232
  />
246
233
  </div>
247
234