@recursica/mantine-adapter 0.42.1 → 0.43.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 (57) hide show
  1. package/CHANGELOG.md +11 -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.js +3547 -3292
  6. package/dist/mantine-adapter.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/Accordion/Accordion.tsx +32 -33
  9. package/src/components/AssistiveElement/AssistiveElement.tsx +1 -1
  10. package/src/components/AutoComplete/AutoComplete.tsx +26 -38
  11. package/src/components/Avatar/Avatar.tsx +22 -23
  12. package/src/components/Badge/Badge.tsx +12 -18
  13. package/src/components/Breadcrumb/Breadcrumb.tsx +10 -18
  14. package/src/components/Button/Button.tsx +31 -30
  15. package/src/components/Card/Card.tsx +48 -27
  16. package/src/components/Checkbox/Checkbox.tsx +29 -42
  17. package/src/components/Checkbox/CheckboxGroup.tsx +12 -5
  18. package/src/components/Chip/Chip.tsx +34 -27
  19. package/src/components/DatePicker/DatePicker.tsx +46 -47
  20. package/src/components/Dropdown/BareDropdown.tsx +25 -13
  21. package/src/components/Dropdown/Dropdown.tsx +25 -38
  22. package/src/components/FileInput/FileInput.tsx +1 -1
  23. package/src/components/FileUpload/FileUpload.tsx +1 -1
  24. package/src/components/HoverCard/HoverCard.tsx +18 -34
  25. package/src/components/Label/Label.tsx +20 -21
  26. package/src/components/Link/Link.tsx +6 -14
  27. package/src/components/Loader/Loader.tsx +8 -16
  28. package/src/components/Menu/Menu.tsx +55 -48
  29. package/src/components/Modal/Modal.tsx +134 -96
  30. package/src/components/NumberInput/NumberInput.tsx +14 -7
  31. package/src/components/Pagination/Pagination.tsx +25 -34
  32. package/src/components/Panel/Panel.tsx +19 -26
  33. package/src/components/Popover/Popover.tsx +19 -31
  34. package/src/components/Radio/Radio.tsx +32 -43
  35. package/src/components/Radio/RadioGroup.tsx +11 -4
  36. package/src/components/ReadOnlyField/ReadOnlyField.tsx +1 -1
  37. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +1 -1
  38. package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +1 -1
  39. package/src/components/SegmentedControl/SegmentedControl.tsx +29 -33
  40. package/src/components/Slider/Slider.tsx +30 -43
  41. package/src/components/Stepper/Stepper.tsx +20 -13
  42. package/src/components/Switch/Switch.tsx +45 -45
  43. package/src/components/Switch/SwitchGroup.tsx +11 -4
  44. package/src/components/Table/Table.tsx +1 -1
  45. package/src/components/Tabs/Tabs.tsx +13 -6
  46. package/src/components/Text/Text.tsx +1 -1
  47. package/src/components/TextArea/TextArea.tsx +21 -25
  48. package/src/components/TextField/TextField.tsx +23 -30
  49. package/src/components/TimePicker/TimePicker.tsx +26 -12
  50. package/src/components/Timeline/Timeline.tsx +7 -21
  51. package/src/components/Timeline/TimelineItem.tsx +14 -26
  52. package/src/components/Title/Title.tsx +12 -2
  53. package/src/components/Toast/Toast.tsx +32 -31
  54. package/src/components/Tooltip/Tooltip.tsx +18 -34
  55. package/src/components/TransferList/TransferList.tsx +1 -1
  56. package/src/components/Tree/Tree.tsx +8 -8
  57. package/src/utils/filterStylingProps.ts +4 -0
@@ -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
  />
@@ -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
  };