@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
@@ -14,6 +14,8 @@ import {
14
14
  } from "@mantine/core";
15
15
  import {
16
16
  filterStylingProps,
17
+ omitUnsupportedProps,
18
+ mergeClassNames,
17
19
  type RecursicaOverStyled,
18
20
  } from "../../utils/filterStylingProps";
19
21
  import styles from "./Menu.module.css";
@@ -46,38 +48,26 @@ export type MenuProps = RecursicaOverStyled<
46
48
 
47
49
  const MenuBase = function Menu({ overStyled = false, ...rest }: MenuProps) {
48
50
  const sanitizedProps = filterStylingProps(rest, overStyled);
51
+ const restRecord = sanitizedProps as Record<string, unknown>;
49
52
 
50
53
  // Bind CSS module classes to Mantine's internal classNames API
51
- const mergedClassNames: Partial<Record<string, string>> = {
52
- dropdown: styles.dropdown,
53
- item: styles.item,
54
- itemLabel: styles.itemLabel,
55
- itemSection: styles.itemSection,
56
- divider: styles.divider,
57
- label: styles.label,
58
- chevron: styles.chevron,
59
- };
60
-
61
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
62
- if (
63
- classNamesProp &&
64
- typeof classNamesProp === "object" &&
65
- !Array.isArray(classNamesProp)
66
- ) {
67
- const o = classNamesProp as Record<string, string>;
68
- Object.keys(o).forEach((key) => {
69
- if (mergedClassNames[key]) {
70
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
71
- } else {
72
- mergedClassNames[key] = o[key];
73
- }
74
- });
75
- }
54
+ const mergedClassNames = mergeClassNames(
55
+ {
56
+ dropdown: styles.dropdown,
57
+ item: styles.item,
58
+ itemLabel: styles.itemLabel,
59
+ itemSection: styles.itemSection,
60
+ divider: styles.divider,
61
+ label: styles.label,
62
+ chevron: styles.chevron,
63
+ },
64
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
65
+ );
76
66
 
77
67
  return (
78
68
  <MantineMenu
79
- classNames={mergedClassNames}
80
69
  {...(sanitizedProps as unknown as MantineMenuProps)}
70
+ classNames={mergedClassNames}
81
71
  />
82
72
  );
83
73
  };
@@ -91,10 +81,19 @@ MenuBase.displayName = "Menu";
91
81
  * Wrapper for the element that triggers the menu.
92
82
  * Requires a single child element that supports ref forwarding.
93
83
  */
94
- export type MenuTargetProps = MantineMenuTargetProps;
84
+ export type MenuTargetProps = RecursicaOverStyled<MantineMenuTargetProps>;
95
85
 
96
- const MenuTarget = function MenuTarget(props: MenuTargetProps) {
97
- return <MantineMenu.Target {...props} />;
86
+ const MenuTarget = function MenuTarget({
87
+ overStyled = false,
88
+ ...rest
89
+ }: MenuTargetProps) {
90
+ const sanitizedProps = filterStylingProps(rest, overStyled);
91
+
92
+ return (
93
+ <MantineMenu.Target
94
+ {...(sanitizedProps as unknown as MantineMenuTargetProps)}
95
+ />
96
+ );
98
97
  };
99
98
  MenuTarget.displayName = "MenuTarget";
100
99
 
@@ -113,9 +112,9 @@ const MenuDropdown = forwardRef<HTMLDivElement, MenuDropdownProps>(
113
112
 
114
113
  return (
115
114
  <MantineMenu.Dropdown
115
+ {...(sanitizedProps as unknown as MantineMenuDropdownProps)}
116
116
  ref={ref}
117
117
  className={classNameProp}
118
- {...(sanitizedProps as unknown as MantineMenuDropdownProps)}
119
118
  />
120
119
  );
121
120
  },
@@ -129,8 +128,7 @@ MenuDropdown.displayName = "MenuDropdown";
129
128
  /**
130
129
  * An individual actionable item within the menu dropdown.
131
130
  *
132
- * **Note:** Mantine's `color` prop is stripped in strict mode to enforce
133
- * design token adherence. Use `overStyled={true}` if you need to bypass.
131
+ * **Note:** Mantine's `color` prop is stripped to enforce design token adherence.
134
132
  */
135
133
  export type MenuItemProps = RecursicaOverStyled<
136
134
  Omit<MantineMenuItemProps, "color">
@@ -138,21 +136,25 @@ export type MenuItemProps = RecursicaOverStyled<
138
136
 
139
137
  const _MenuItem = forwardRef<HTMLButtonElement, MenuItemProps>(
140
138
  function MenuItem({ overStyled = false, ...rest }, ref) {
141
- const sanitizedProps = filterStylingProps(rest, overStyled);
139
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
140
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
141
+ const UNSUPPORTED_PROPS = [
142
+ "color", // Colors are token-driven; the library's native palette isn't exposed.
143
+ ] as const satisfies readonly (keyof MantineMenuItemProps)[];
144
+
145
+ const sanitizedProps = omitUnsupportedProps(
146
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
147
+ UNSUPPORTED_PROPS,
148
+ );
142
149
  const restRecord = sanitizedProps as Record<string, unknown>;
143
150
 
144
- // Strip Mantine's `color` prop to enforce token-driven styling
145
- if (!overStyled) {
146
- delete restRecord["color"];
147
- }
148
-
149
151
  const classNameProp = restRecord.className as string | undefined;
150
152
 
151
153
  return (
152
154
  <MantineMenu.Item
155
+ {...(sanitizedProps as unknown as MantineMenuItemProps)}
153
156
  ref={ref}
154
157
  className={classNameProp}
155
- {...(sanitizedProps as unknown as MantineMenuItemProps)}
156
158
  />
157
159
  );
158
160
  },
@@ -185,9 +187,9 @@ const MenuDivider = forwardRef<HTMLHRElement, MenuDividerProps>(
185
187
 
186
188
  return (
187
189
  <MantineMenu.Divider
190
+ {...(sanitizedProps as unknown as MantineMenuDividerProps)}
188
191
  ref={ref}
189
192
  className={classNameProp}
190
- {...(sanitizedProps as unknown as MantineMenuDividerProps)}
191
193
  />
192
194
  );
193
195
  },
@@ -211,9 +213,9 @@ const MenuLabel = forwardRef<HTMLDivElement, MenuLabelProps>(function MenuLabel(
211
213
 
212
214
  return (
213
215
  <MantineMenu.Label
216
+ {...(sanitizedProps as unknown as MantineMenuLabelProps)}
214
217
  ref={ref}
215
218
  className={classNameProp}
216
- {...(sanitizedProps as unknown as MantineMenuLabelProps)}
217
219
  />
218
220
  );
219
221
  });
@@ -257,20 +259,25 @@ export type MenuSubItemProps = RecursicaOverStyled<
257
259
 
258
260
  const _MenuSubItem = forwardRef<HTMLButtonElement, MenuSubItemProps>(
259
261
  function MenuSubItem({ overStyled = false, ...rest }, ref) {
260
- const sanitizedProps = filterStylingProps(rest, overStyled);
262
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
263
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
264
+ const UNSUPPORTED_PROPS = [
265
+ "color", // Colors are token-driven; the library's native palette isn't exposed.
266
+ ] as const satisfies readonly (keyof MantineMenuItemProps)[];
267
+
268
+ const sanitizedProps = omitUnsupportedProps(
269
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
270
+ UNSUPPORTED_PROPS,
271
+ );
261
272
  const restRecord = sanitizedProps as Record<string, unknown>;
262
273
 
263
- if (!overStyled) {
264
- delete restRecord["color"];
265
- }
266
-
267
274
  const classNameProp = restRecord.className as string | undefined;
268
275
 
269
276
  return (
270
277
  <MantineMenu.Sub.Item
278
+ {...(sanitizedProps as unknown as MantineMenuItemProps)}
271
279
  ref={ref}
272
280
  className={classNameProp}
273
- {...(sanitizedProps as unknown as MantineMenuItemProps)}
274
281
  />
275
282
  );
276
283
  },
@@ -302,9 +309,9 @@ const MenuSubDropdown = forwardRef<HTMLDivElement, MenuSubDropdownProps>(
302
309
 
303
310
  return (
304
311
  <MantineMenu.Sub.Dropdown
312
+ {...(sanitizedProps as unknown as MantineMenuSubDropdownProps)}
305
313
  ref={ref}
306
314
  className={classNameProp}
307
- {...(sanitizedProps as unknown as MantineMenuSubDropdownProps)}
308
315
  />
309
316
  );
310
317
  },
@@ -8,9 +8,12 @@ import {
8
8
  type ModalHeaderProps as MantineModalHeaderProps,
9
9
  type ModalTitleProps as MantineModalTitleProps,
10
10
  type ModalCloseButtonProps as MantineModalCloseButtonProps,
11
+ type ModalBodyProps as MantineModalBodyProps,
11
12
  } from "@mantine/core";
12
13
  import {
13
14
  filterStylingProps,
15
+ omitUnsupportedProps,
16
+ mergeClassNames,
14
17
  type RecursicaOverStyled,
15
18
  } from "../../utils/filterStylingProps";
16
19
  import styles from "./Modal.module.css";
@@ -54,41 +57,40 @@ const ModalInner = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
54
57
  },
55
58
  ref,
56
59
  ) {
57
- const sanitizedProps = filterStylingProps(rest, overStyled);
58
-
59
- const mergedClassNames: Partial<Record<string, string>> = {
60
- root: styles.root,
61
- inner: styles.inner,
62
- content: styles.content,
63
- header: styles.header,
64
- title: styles.title,
65
- close: styles.close,
66
- };
67
-
68
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
69
- if (
70
- classNamesProp &&
71
- typeof classNamesProp === "object" &&
72
- !Array.isArray(classNamesProp)
73
- ) {
74
- const o = classNamesProp as Record<string, string>;
75
- Object.keys(o).forEach((key) => {
76
- if (mergedClassNames[key]) {
77
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
78
- } else {
79
- mergedClassNames[key] = o[key];
80
- }
81
- });
82
- }
60
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
61
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
62
+ const UNSUPPORTED_PROPS = [
63
+ "size", // Modal width/height are enforced via CSS module tokens (min/max-width, min/max-height), not Mantine's `size` scale.
64
+ "radius", // Modal corner radius is token-driven via `.content` in Modal.module.css, not Mantine's `radius` prop.
65
+ ] as const satisfies readonly (keyof MantineModalProps)[];
66
+
67
+ const sanitizedProps = omitUnsupportedProps(
68
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
69
+ UNSUPPORTED_PROPS,
70
+ ) as Partial<typeof rest>;
71
+
72
+ const mergedClassNames = mergeClassNames(
73
+ {
74
+ root: styles.root,
75
+ inner: styles.inner,
76
+ content: styles.content,
77
+ header: styles.header,
78
+ title: styles.title,
79
+ close: styles.close,
80
+ },
81
+ (sanitizedProps as Record<string, unknown>).classNames as
82
+ | Partial<Record<string, string>>
83
+ | undefined,
84
+ );
83
85
 
84
86
  return (
85
87
  <MantineModal.Root
86
88
  ref={ref}
87
- classNames={mergedClassNames}
88
89
  {...(sanitizedProps as unknown as Omit<
89
90
  MantineModalProps,
90
91
  "size" | "radius" | "shadow"
91
92
  >)}
93
+ classNames={mergedClassNames}
92
94
  >
93
95
  {withOverlay && <MantineModal.Overlay {...overlayProps} />}
94
96
  <MantineModal.Content>
@@ -130,81 +132,87 @@ const ModalFooter = React.forwardRef<HTMLDivElement, ModalFooterProps>(
130
132
  return (
131
133
  <div
132
134
  ref={ref}
133
- className={`${styles.footer} ${className || ""}`}
134
135
  {...sanitizedProps}
136
+ className={`${styles.footer} ${className || ""}`}
135
137
  />
136
138
  );
137
139
  },
138
140
  );
139
141
  ModalFooter.displayName = "Modal.Footer";
140
142
 
141
- const ModalBody = React.forwardRef<
142
- HTMLDivElement,
143
- React.ComponentPropsWithoutRef<typeof MantineModal.Body>
144
- >(function ModalBody({ className, onScroll, children, ...rest }, ref) {
145
- const internalRef = React.useRef<HTMLDivElement>(null);
146
- const [scrolledTop, setScrolledTop] = React.useState(false);
147
- const [scrolledBottom, setScrolledBottom] = React.useState(false);
148
-
149
- const checkScroll = React.useCallback(() => {
150
- if (internalRef.current) {
151
- const { scrollTop, scrollHeight, clientHeight } = internalRef.current;
152
- setScrolledTop(scrollTop > 0);
153
- setScrolledBottom(Math.ceil(scrollTop + clientHeight) < scrollHeight);
154
- }
155
- }, []);
156
-
157
- // Re-check scroll on mount and window resize
158
- React.useEffect(() => {
159
- checkScroll();
160
- window.addEventListener("resize", checkScroll);
161
- return () => window.removeEventListener("resize", checkScroll);
162
- }, [checkScroll, children]);
163
-
164
- const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
165
- checkScroll();
166
- onScroll?.(e);
167
- };
168
-
169
- // Intercept children to pull Modal.Footer out of the scrolling container
170
- let footer: React.ReactNode = null;
171
- const bodyChildren: React.ReactNode[] = [];
172
-
173
- React.Children.forEach(children, (child) => {
174
- if (
175
- React.isValidElement(child) &&
176
- (child.type === ModalFooter ||
177
- (child.type as React.ComponentType)?.displayName === "Modal.Footer")
178
- ) {
179
- footer = child;
180
- } else {
181
- bodyChildren.push(child);
182
- }
183
- });
143
+ export type ModalBodyProps = RecursicaOverStyled<MantineModalBodyProps>;
184
144
 
185
- return (
186
- <MantineModal.Body
187
- {...rest}
188
- ref={(node) => {
189
- if (typeof ref === "function") ref(node);
190
- else if (ref)
191
- (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
192
- }}
193
- className={`${styles.bodyWrapper} ${className || ""}`}
194
- >
195
- <div
196
- ref={internalRef}
197
- onScroll={handleScroll}
198
- data-scrolled-top={scrolledTop || undefined}
199
- data-scrolled-bottom={scrolledBottom || undefined}
200
- className={styles.scrollArea}
145
+ const ModalBody = React.forwardRef<HTMLDivElement, ModalBodyProps>(
146
+ function ModalBody(
147
+ { overStyled = false, className, onScroll, children, ...rest },
148
+ ref,
149
+ ) {
150
+ const sanitizedProps = filterStylingProps(rest, overStyled);
151
+ const internalRef = React.useRef<HTMLDivElement>(null);
152
+ const [scrolledTop, setScrolledTop] = React.useState(false);
153
+ const [scrolledBottom, setScrolledBottom] = React.useState(false);
154
+
155
+ const checkScroll = React.useCallback(() => {
156
+ if (internalRef.current) {
157
+ const { scrollTop, scrollHeight, clientHeight } = internalRef.current;
158
+ setScrolledTop(scrollTop > 0);
159
+ setScrolledBottom(Math.ceil(scrollTop + clientHeight) < scrollHeight);
160
+ }
161
+ }, []);
162
+
163
+ // Re-check scroll on mount and window resize
164
+ React.useEffect(() => {
165
+ checkScroll();
166
+ window.addEventListener("resize", checkScroll);
167
+ return () => window.removeEventListener("resize", checkScroll);
168
+ }, [checkScroll, children]);
169
+
170
+ const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
171
+ checkScroll();
172
+ onScroll?.(e);
173
+ };
174
+
175
+ // Intercept children to pull Modal.Footer out of the scrolling container
176
+ let footer: React.ReactNode = null;
177
+ const bodyChildren: React.ReactNode[] = [];
178
+
179
+ React.Children.forEach(children, (child) => {
180
+ if (
181
+ React.isValidElement(child) &&
182
+ (child.type === ModalFooter ||
183
+ (child.type as React.ComponentType)?.displayName === "Modal.Footer")
184
+ ) {
185
+ footer = child;
186
+ } else {
187
+ bodyChildren.push(child);
188
+ }
189
+ });
190
+
191
+ return (
192
+ <MantineModal.Body
193
+ {...sanitizedProps}
194
+ ref={(node) => {
195
+ if (typeof ref === "function") ref(node);
196
+ else if (ref)
197
+ (ref as React.MutableRefObject<HTMLDivElement | null>).current =
198
+ node;
199
+ }}
200
+ className={`${styles.bodyWrapper} ${className || ""}`}
201
201
  >
202
- {bodyChildren}
203
- </div>
204
- {footer}
205
- </MantineModal.Body>
206
- );
207
- });
202
+ <div
203
+ ref={internalRef}
204
+ onScroll={handleScroll}
205
+ data-scrolled-top={scrolledTop || undefined}
206
+ data-scrolled-bottom={scrolledBottom || undefined}
207
+ className={styles.scrollArea}
208
+ >
209
+ {bodyChildren}
210
+ </div>
211
+ {footer}
212
+ </MantineModal.Body>
213
+ );
214
+ },
215
+ );
208
216
  ModalBody.displayName = "Modal.Body";
209
217
 
210
218
  // ============================================================
@@ -213,9 +221,20 @@ ModalBody.displayName = "Modal.Body";
213
221
 
214
222
  export type ModalRootProps = RecursicaOverStyled<MantineModalRootProps>;
215
223
 
224
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
225
+ // through even if a caller forces them via plain JavaScript. `Modal.Root` is used directly for
226
+ // advanced composition, so it doesn't benefit from the top-level `Modal`'s `Omit<>` protection.
227
+ const MODAL_ROOT_UNSUPPORTED_PROPS = [
228
+ "size", // Modal width/height are enforced via CSS module tokens, not Mantine's `size` scale.
229
+ "radius", // Modal corner radius is token-driven via `.content` in Modal.module.css, not Mantine's `radius` prop.
230
+ ] as const satisfies readonly (keyof MantineModalRootProps)[];
231
+
216
232
  const ModalRoot = React.forwardRef<HTMLDivElement, ModalRootProps>(
217
233
  function ModalRoot({ overStyled = false, ...rest }, ref) {
218
- const sanitizedProps = filterStylingProps(rest, overStyled);
234
+ const sanitizedProps = omitUnsupportedProps(
235
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
236
+ MODAL_ROOT_UNSUPPORTED_PROPS,
237
+ ) as Partial<typeof rest>;
219
238
  return (
220
239
  <MantineModal.Root
221
240
  ref={ref}
@@ -243,9 +262,18 @@ ModalOverlay.displayName = "Modal.Overlay";
243
262
 
244
263
  export type ModalContentProps = RecursicaOverStyled<MantineModalContentProps>;
245
264
 
265
+ // Modal corner radius is token-driven via `.content` in Modal.module.css, not Mantine's
266
+ // native `radius` prop — deleted at runtime so it can't leak through via plain JavaScript.
267
+ const MODAL_CONTENT_UNSUPPORTED_PROPS = [
268
+ "radius",
269
+ ] as const satisfies readonly (keyof MantineModalContentProps)[];
270
+
246
271
  const ModalContent = React.forwardRef<HTMLDivElement, ModalContentProps>(
247
272
  function ModalContent({ overStyled = false, ...rest }, ref) {
248
- const sanitizedProps = filterStylingProps(rest, overStyled);
273
+ const sanitizedProps = omitUnsupportedProps(
274
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
275
+ MODAL_CONTENT_UNSUPPORTED_PROPS,
276
+ ) as Partial<typeof rest>;
249
277
  return (
250
278
  <MantineModal.Content
251
279
  ref={ref}
@@ -289,11 +317,21 @@ ModalTitle.displayName = "Modal.Title";
289
317
  export type ModalCloseButtonProps =
290
318
  RecursicaOverStyled<MantineModalCloseButtonProps>;
291
319
 
320
+ // The close button's dimensions and corner radius are enforced by Modal.module.css's `.close`
321
+ // (standard icon-button tokens), not Mantine's native `size`/`radius` scales.
322
+ const MODAL_CLOSE_BUTTON_UNSUPPORTED_PROPS = [
323
+ "size",
324
+ "radius",
325
+ ] as const satisfies readonly (keyof MantineModalCloseButtonProps)[];
326
+
292
327
  const ModalCloseButton = React.forwardRef<
293
328
  HTMLButtonElement,
294
329
  ModalCloseButtonProps
295
330
  >(function ModalCloseButton({ overStyled = false, ...rest }, ref) {
296
- const sanitizedProps = filterStylingProps(rest, overStyled);
331
+ const sanitizedProps = omitUnsupportedProps(
332
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
333
+ MODAL_CLOSE_BUTTON_UNSUPPORTED_PROPS,
334
+ ) as Partial<typeof rest>;
297
335
  return (
298
336
  <MantineModal.CloseButton
299
337
  ref={ref}
@@ -6,6 +6,7 @@ import {
6
6
  import { type ReadOnlyControlProps } from "@recursica/adapter-common";
7
7
  import {
8
8
  filterStylingProps,
9
+ omitUnsupportedProps,
9
10
  type RecursicaOverStyled,
10
11
  } from "../../utils/filterStylingProps";
11
12
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
@@ -35,6 +36,14 @@ export interface RecursicaNumberInputProps
35
36
 
36
37
  export type NumberInputProps = RecursicaOverStyled<RecursicaNumberInputProps>;
37
38
 
39
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
40
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
41
+ const UNSUPPORTED_PROPS = [
42
+ "size", // Recursica controls sizing via the `size` variant + design tokens, not raw dimensions.
43
+ "variant", // Colors/variants are token-driven; the library's native palette isn't exposed.
44
+ "radius", // Recursica does not expose the library's native corner-radius system.
45
+ ] as const satisfies readonly (keyof MantineNumberInputProps)[];
46
+
38
47
  export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
39
48
  function NumberInput(props, ref) {
40
49
  const {
@@ -67,14 +76,12 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
67
76
  ...rest
68
77
  } = props;
69
78
 
70
- const sanitizedProps = filterStylingProps(rest, overStyled);
79
+ const sanitizedProps = omitUnsupportedProps(
80
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
81
+ UNSUPPORTED_PROPS,
82
+ );
71
83
  const restRecord = sanitizedProps as Record<string, unknown>;
72
84
 
73
- // Delete prohibited sizing hooks from bypassing variables natively
74
- delete restRecord["size"];
75
- delete restRecord["variant"];
76
- delete restRecord["radius"];
77
-
78
85
  // Securely map core native blocks down ensuring nested CSS modules map precisely
79
86
  const mergedClassNames: Partial<Record<string, string>> = {
80
87
  wrapper: styles.root, // The nested Input internal relative wrapper bounding box
@@ -120,6 +127,7 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
120
127
  /* Naked Input execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks */
121
128
  <MantineNumberInput
122
129
  ref={ref}
130
+ {...(sanitizedProps as unknown as MantineNumberInputProps)}
123
131
  classNames={mergedClassNames}
124
132
  disabled={disabled}
125
133
  value={value}
@@ -139,7 +147,6 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
139
147
  "data-with-right-section":
140
148
  restRecord.rightSection || !hideControls ? "true" : undefined,
141
149
  }}
142
- {...(sanitizedProps as unknown as MantineNumberInputProps)}
143
150
  />
144
151
  }
145
152
  />