@recursica/mantine-adapter 0.42.0 → 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 (86) hide show
  1. package/CHANGELOG.md +18 -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 +3546 -3250
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/GlobalExemptions.modules.css +0 -6
  10. package/src/components/Accordion/Accordion.module.css +0 -8
  11. package/src/components/Accordion/Accordion.tsx +32 -33
  12. package/src/components/AssistiveElement/AssistiveElement.tsx +1 -1
  13. package/src/components/AutoComplete/AutoComplete.module.css +0 -7
  14. package/src/components/AutoComplete/AutoComplete.tsx +26 -38
  15. package/src/components/Avatar/Avatar.module.css +0 -8
  16. package/src/components/Avatar/Avatar.tsx +22 -23
  17. package/src/components/Badge/Badge.tsx +12 -18
  18. package/src/components/Breadcrumb/Breadcrumb.tsx +10 -18
  19. package/src/components/Button/Button.tsx +31 -30
  20. package/src/components/Card/Card.tsx +48 -27
  21. package/src/components/Checkbox/Checkbox.tsx +29 -42
  22. package/src/components/Checkbox/CheckboxGroup.tsx +12 -5
  23. package/src/components/Chip/Chip.module.css +0 -16
  24. package/src/components/Chip/Chip.tsx +34 -27
  25. package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +14 -1
  26. package/src/components/DatePicker/DatePicker.icons.tsx +27 -0
  27. package/src/components/DatePicker/DatePicker.module.css +256 -7
  28. package/src/components/DatePicker/DatePicker.stories.tsx +0 -7
  29. package/src/components/DatePicker/DatePicker.tsx +68 -33
  30. package/src/components/DatePicker/USAGE.md +6 -2
  31. package/src/components/Dropdown/BareDropdown.tsx +25 -13
  32. package/src/components/Dropdown/Dropdown.module.css +0 -7
  33. package/src/components/Dropdown/Dropdown.tsx +25 -38
  34. package/src/components/FileInput/FileInput.module.css +0 -21
  35. package/src/components/FileInput/FileInput.tsx +1 -1
  36. package/src/components/FileUpload/FileUpload.module.css +0 -11
  37. package/src/components/FileUpload/FileUpload.tsx +1 -1
  38. package/src/components/HoverCard/HoverCard.module.css +1 -7
  39. package/src/components/HoverCard/HoverCard.tsx +18 -34
  40. package/src/components/Label/Label.module.css +0 -6
  41. package/src/components/Label/Label.tsx +20 -21
  42. package/src/components/Link/Link.module.css +1 -6
  43. package/src/components/Link/Link.tsx +6 -14
  44. package/src/components/Loader/Loader.tsx +8 -16
  45. package/src/components/Menu/Menu.module.css +0 -6
  46. package/src/components/Menu/Menu.tsx +55 -48
  47. package/src/components/Modal/Modal.module.css +0 -11
  48. package/src/components/Modal/Modal.tsx +134 -96
  49. package/src/components/NumberInput/NumberInput.module.css +0 -7
  50. package/src/components/NumberInput/NumberInput.tsx +14 -7
  51. package/src/components/Pagination/Pagination.module.css +0 -81
  52. package/src/components/Pagination/Pagination.tsx +25 -34
  53. package/src/components/Panel/Panel.tsx +19 -26
  54. package/src/components/Popover/Popover.tsx +19 -31
  55. package/src/components/Radio/Radio.tsx +32 -43
  56. package/src/components/Radio/RadioGroup.tsx +11 -4
  57. package/src/components/ReadOnlyField/ReadOnlyField.tsx +1 -1
  58. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +1 -1
  59. package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +1 -1
  60. package/src/components/SegmentedControl/SegmentedControl.module.css +0 -1
  61. package/src/components/SegmentedControl/SegmentedControl.tsx +29 -33
  62. package/src/components/Slider/Slider.tsx +30 -43
  63. package/src/components/Stepper/Stepper.tsx +20 -13
  64. package/src/components/Switch/Switch.tsx +45 -45
  65. package/src/components/Switch/SwitchGroup.tsx +11 -4
  66. package/src/components/Table/Table.tsx +1 -1
  67. package/src/components/Tabs/Tabs.tsx +13 -6
  68. package/src/components/Text/Text.tsx +1 -1
  69. package/src/components/TextArea/TextArea.module.css +0 -7
  70. package/src/components/TextArea/TextArea.tsx +21 -25
  71. package/src/components/TextField/TextField.module.css +0 -7
  72. package/src/components/TextField/TextField.tsx +23 -30
  73. package/src/components/TimePicker/TimePicker.module.css +0 -16
  74. package/src/components/TimePicker/TimePicker.tsx +26 -12
  75. package/src/components/Timeline/Timeline.module.css +0 -6
  76. package/src/components/Timeline/Timeline.tsx +7 -21
  77. package/src/components/Timeline/TimelineItem.tsx +14 -26
  78. package/src/components/Title/Title.tsx +12 -2
  79. package/src/components/Toast/Toast.module.css +0 -7
  80. package/src/components/Toast/Toast.tsx +32 -31
  81. package/src/components/Tooltip/Tooltip.module.css +0 -9
  82. package/src/components/Tooltip/Tooltip.tsx +18 -34
  83. package/src/components/TransferList/TransferList.module.css +0 -8
  84. package/src/components/TransferList/TransferList.tsx +1 -1
  85. package/src/components/Tree/Tree.tsx +8 -8
  86. package/src/utils/filterStylingProps.ts +4 -0
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/borderux/recursica.git",
14
14
  "directory": "packages/mantine-adapter"
15
15
  },
16
- "version": "0.42.0",
16
+ "version": "0.43.0",
17
17
  "type": "module",
18
18
  "main": "./dist/mantine-adapter.cjs",
19
19
  "module": "./dist/mantine-adapter.js",
@@ -5,9 +5,3 @@
5
5
  * recursica_variables_scoped.css but are not used by any specific component.
6
6
  * Isolating them here keeps individual component stylesheets clean and focused.
7
7
  */
8
-
9
- /* ==== GLOBAL ICON STYLE ====
10
- Global layout option specifying the overall SVG rendering style (solid/outline).
11
- Note to Forge Devs: style is not a CSS property or variable type and cannot be applied in standard CSS stylesheets.
12
- This variable is ignored in the UI adapters and should be reviewed by the Forge team. */
13
- /* recursica-ignore: --recursica_ui-kit_globals_icon_style */
@@ -1,11 +1,3 @@
1
- /* EXEMPTIONS:
2
- - accordion-header open-appearance background-color/border-color are ignored because Forge does
3
- not apply a visual swap on expand, even though the token schema defines "open" appearance
4
- colors distinct from "closed" (see .control below, which intentionally keeps a constant
5
- background/border across collapsed/expanded). */
6
- /* recursica-ignore: --recursica_ui-kit_components_accordion-header_variants_appearance_open_properties_colors_background-color */
7
- /* recursica-ignore: --recursica_ui-kit_components_accordion-header_variants_appearance_open_properties_colors_border-color */
8
-
9
1
  /* ==== ACCORDION CONTAINER ==== */
10
2
  .root {
11
3
  box-sizing: border-box;
@@ -8,6 +8,8 @@ import {
8
8
  } from "@mantine/core";
9
9
  import {
10
10
  filterStylingProps,
11
+ omitUnsupportedProps,
12
+ mergeClassNames,
11
13
  type RecursicaOverStyled,
12
14
  } from "../../utils/filterStylingProps";
13
15
  import styles from "./Accordion.module.css";
@@ -47,44 +49,31 @@ const AccordionBase = function Accordion({
47
49
  : variant;
48
50
 
49
51
  const sanitizedProps = filterStylingProps(rest, overStyled);
52
+ const restRecord = sanitizedProps as Record<string, unknown>;
50
53
 
51
54
  // Bind all deep CSS module references natively into the global class mapping schema
52
- const mergedClassNames: Partial<Record<string, string>> = {
53
- root: styles.root,
54
- item: styles.item,
55
- control: styles.control,
56
- label: styles.label,
57
- chevron: styles.chevron,
58
- icon: styles.icon,
59
- panel: styles.panel,
60
- content: styles.content,
61
- };
62
-
63
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
64
- if (
65
- classNamesProp &&
66
- typeof classNamesProp === "object" &&
67
- !Array.isArray(classNamesProp)
68
- ) {
69
- const o = classNamesProp as Record<string, string>;
70
- Object.keys(o).forEach((key) => {
71
- if (mergedClassNames[key]) {
72
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
73
- } else {
74
- mergedClassNames[key] = o[key];
75
- }
76
- });
77
- }
55
+ const mergedClassNames = mergeClassNames(
56
+ {
57
+ root: styles.root,
58
+ item: styles.item,
59
+ control: styles.control,
60
+ label: styles.label,
61
+ chevron: styles.chevron,
62
+ icon: styles.icon,
63
+ panel: styles.panel,
64
+ content: styles.content,
65
+ },
66
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
67
+ );
78
68
 
79
- const classNameProp = (sanitizedProps as Record<string, unknown>)
80
- .className as string | undefined;
69
+ const classNameProp = restRecord.className as string | undefined;
81
70
 
82
71
  return (
83
72
  <MantineAccordion
73
+ {...(sanitizedProps as unknown as MantineAccordionProps)}
84
74
  variant={resolvedVariant as MantineAccordionProps["variant"]}
85
75
  className={classNameProp}
86
76
  classNames={mergedClassNames}
87
- {...(sanitizedProps as unknown as MantineAccordionProps)}
88
77
  />
89
78
  );
90
79
  };
@@ -128,9 +117,9 @@ export const AccordionItem = forwardRef<
128
117
  return (
129
118
  <MantineAccordion.Item
130
119
  ref={ref}
120
+ {...(sanitizedProps as unknown as AccordionItemProps)}
131
121
  className={finalClass}
132
122
  data-disabled={disabled || undefined}
133
- {...(sanitizedProps as unknown as AccordionItemProps)}
134
123
  >
135
124
  {title ? (
136
125
  <>
@@ -155,6 +144,13 @@ export type AccordionControlWrapperProps = RecursicaOverStyled<
155
144
  Omit<AccordionControlProps, "icon"> & RecursicaAccordionControlProps
156
145
  >;
157
146
 
147
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
148
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
149
+ const UNSUPPORTED_PROPS = [
150
+ "icon", // Mantine's native icon slot is fully computed here from `leftIcon`; the `Omit<>` on
151
+ // AccordionControlWrapperProps only stops a well-typed caller, this is the runtime backstop.
152
+ ] as const satisfies readonly (keyof AccordionControlProps)[];
153
+
158
154
  export const AccordionControl = forwardRef<
159
155
  HTMLButtonElement,
160
156
  AccordionControlWrapperProps
@@ -162,13 +158,17 @@ export const AccordionControl = forwardRef<
162
158
  { leftIcon, children, overStyled = false, ...rest },
163
159
  ref,
164
160
  ) {
165
- const sanitizedProps = filterStylingProps(rest, overStyled);
161
+ const sanitizedProps = omitUnsupportedProps(
162
+ filterStylingProps(rest, overStyled),
163
+ UNSUPPORTED_PROPS,
164
+ );
166
165
  const classNameProp = (sanitizedProps as Record<string, unknown>)
167
166
  .className as string | undefined;
168
167
 
169
168
  return (
170
169
  <MantineAccordion.Control
171
170
  ref={ref}
171
+ {...(sanitizedProps as unknown as AccordionControlProps)}
172
172
  className={classNameProp}
173
173
  icon={
174
174
  leftIcon ? (
@@ -177,7 +177,6 @@ export const AccordionControl = forwardRef<
177
177
  </span>
178
178
  ) : undefined
179
179
  }
180
- {...(sanitizedProps as unknown as AccordionControlProps)}
181
180
  >
182
181
  {children}
183
182
  </MantineAccordion.Control>
@@ -201,8 +200,8 @@ export const AccordionPanel = forwardRef<
201
200
  return (
202
201
  <MantineAccordion.Panel
203
202
  ref={ref}
204
- className={classNameProp}
205
203
  {...(sanitizedProps as unknown as AccordionPanelProps)}
204
+ className={classNameProp}
206
205
  />
207
206
  );
208
207
  });
@@ -74,11 +74,11 @@ export const AssistiveElement = forwardRef<
74
74
  return (
75
75
  <div
76
76
  ref={ref}
77
+ {...restRecord} // Spread standard HTML attributes (like id, aria-*) natively.
77
78
  className={finalClass}
78
79
  data-variant={assistiveVariant}
79
80
  role={resolvedRole}
80
81
  style={restRecord.style as React.CSSProperties}
81
- {...restRecord} // Spread standard HTML attributes (like id, aria-*) natively.
82
82
  >
83
83
  {assistiveWithIcon && (
84
84
  <span className={styles.iconWrapper}>
@@ -1,10 +1,3 @@
1
- /* EXEMPTIONS:
2
- - border-size variables are ignored because the input box uses a uniform static boundary to prevent
3
- unexpected layout shifts or flickering when switching between focused, disabled, or error states. */
4
- /* recursica-ignore: --recursica_ui-kit_components_autocomplete_properties_border-size */
5
- /* recursica-ignore: --recursica_ui-kit_components_autocomplete_variants_states_disabled_properties_border-size */
6
- /* recursica-ignore: --recursica_ui-kit_components_autocomplete_variants_states_error_properties_border-size */
7
-
8
1
  /* LAYOUT SPACING OVERRIDES:
9
2
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
10
3
  - Also sets the --autocomplete-control-{max,min}-width hooks consumed inline in AutoComplete.tsx,
@@ -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";
@@ -42,6 +44,14 @@ export interface RecursicaAutoCompleteProps
42
44
 
43
45
  export type AutoCompleteProps = RecursicaOverStyled<RecursicaAutoCompleteProps>;
44
46
 
47
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
48
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
49
+ const UNSUPPORTED_PROPS = [
50
+ "size", // Recursica controls sizing via the `size` variant + design tokens, not raw dimensions.
51
+ "variant", // Colors/variants are token-driven; the library's native palette isn't exposed.
52
+ "radius", // Recursica does not expose the library's native corner-radius system.
53
+ ] as const satisfies readonly (keyof MantineAutocompleteProps)[];
54
+
45
55
  export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
46
56
  function AutoComplete(props, ref) {
47
57
  const {
@@ -72,46 +82,24 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
72
82
  defaultValue,
73
83
  ...rest
74
84
  } = props;
75
- const sanitizedProps = filterStylingProps(rest, overStyled);
76
- const restRecord = sanitizedProps as Record<string, unknown>;
77
85
 
78
- // Delete prohibited sizing hooks from bypassing variables natively
79
- delete restRecord["size"];
80
- delete restRecord["variant"];
81
- delete restRecord["radius"];
86
+ const sanitizedProps = omitUnsupportedProps(
87
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
88
+ UNSUPPORTED_PROPS,
89
+ );
90
+ const restRecord = sanitizedProps as Record<string, unknown>;
82
91
 
83
92
  // Securely map core native blocks down ensuring nested CSS modules map precisely
84
- const mergedClassNames: Partial<Record<string, string>> = {
85
- wrapper: styles.root, // The nested Input internal relative wrapper bounding box
86
- input: styles.input,
87
- section: styles.section,
88
- dropdown: styles.dropdown,
89
- option: styles.option,
90
- };
91
-
92
- const classNamesProp = restRecord.classNames;
93
- if (
94
- classNamesProp &&
95
- typeof classNamesProp === "object" &&
96
- !Array.isArray(classNamesProp)
97
- ) {
98
- const o = classNamesProp as Partial<Record<string, string>>;
99
- mergedClassNames.wrapper = o.wrapper
100
- ? `${styles.root} ${o.wrapper}`
101
- : styles.root;
102
- mergedClassNames.input = o.input
103
- ? `${styles.input} ${o.input}`
104
- : styles.input;
105
- mergedClassNames.section = o.section
106
- ? `${styles.section} ${o.section}`
107
- : styles.section;
108
- mergedClassNames.dropdown = o.dropdown
109
- ? `${styles.dropdown} ${o.dropdown}`
110
- : styles.dropdown;
111
- mergedClassNames.option = o.option
112
- ? `${styles.option} ${o.option}`
113
- : styles.option;
114
- }
93
+ const mergedClassNames = mergeClassNames(
94
+ {
95
+ wrapper: styles.root, // The nested Input internal relative wrapper bounding box
96
+ input: styles.input,
97
+ section: styles.section,
98
+ dropdown: styles.dropdown,
99
+ option: styles.option,
100
+ },
101
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
102
+ );
115
103
 
116
104
  const wrapperClass = className
117
105
  ? `${styles.layoutOverride} ${className}`
@@ -147,12 +135,12 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
147
135
  /* Naked Input execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks */
148
136
  <MantineAutocomplete
149
137
  ref={ref}
138
+ {...(sanitizedProps as unknown as MantineAutocompleteProps)}
150
139
  classNames={mergedClassNames}
151
140
  disabled={disabled}
152
141
  value={value as string | undefined}
153
142
  defaultValue={defaultValue as string | undefined}
154
143
  error={!!error}
155
- {...(sanitizedProps as unknown as MantineAutocompleteProps)}
156
144
  />
157
145
  }
158
146
  />
@@ -7,14 +7,6 @@ HARDCODED VALUES:
7
7
  - Image has no explicit size tokens other than root size, assume it scales 100%
8
8
  */
9
9
 
10
- /* EXEMPTIONS:
11
- The generic size variables below are defined in Figma but unused.
12
- We explicitly style the avatar's dimensions using the dedicated width and height tokens
13
- (e.g., small_properties_width and small_properties_height) for precise component layout. */
14
- /* recursica-ignore: --recursica_ui-kit_components_avatar_variants_sizes_default_properties_size */
15
- /* recursica-ignore: --recursica_ui-kit_components_avatar_variants_sizes_large_properties_size */
16
- /* recursica-ignore: --recursica_ui-kit_components_avatar_variants_sizes_small_properties_size */
17
-
18
10
  .root {
19
11
  box-sizing: border-box;
20
12
  margin: 0;
@@ -6,6 +6,8 @@ import {
6
6
  } from "@mantine/core";
7
7
  import {
8
8
  filterStylingProps,
9
+ omitUnsupportedProps,
10
+ mergeClassNames,
9
11
  type RecursicaOverStyled,
10
12
  } from "../../utils/filterStylingProps";
11
13
  import styles from "./Avatar.module.css";
@@ -41,7 +43,17 @@ const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
41
43
  large: "lg",
42
44
  } as const;
43
45
 
44
- const sanitizedProps = filterStylingProps(rest, overStyled);
46
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
47
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
48
+ const UNSUPPORTED_PROPS = [
49
+ "color", // Colors are token-driven via `data-variant`; Mantine's native palette isn't exposed.
50
+ "radius", // Avatar corner radius is controlled by design tokens, not a raw radius prop.
51
+ ] as const satisfies readonly (keyof MantineAvatarProps)[];
52
+
53
+ const sanitizedProps = omitUnsupportedProps(
54
+ filterStylingProps(rest, overStyled),
55
+ UNSUPPORTED_PROPS,
56
+ );
45
57
  const restRecord = sanitizedProps as Record<string, unknown>;
46
58
 
47
59
  // Determine semantic style based on provided props
@@ -52,33 +64,21 @@ const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
52
64
  computedStyle = "icon";
53
65
  }
54
66
 
55
- const mergedClassNames: Partial<Record<string, string>> = {
56
- root: styles.root,
57
- image: styles.image,
58
- placeholder: styles.placeholder,
59
- };
60
-
61
- const classNamesProp = restRecord.classNames;
62
- if (
63
- classNamesProp &&
64
- typeof classNamesProp === "object" &&
65
- !Array.isArray(classNamesProp)
66
- ) {
67
- const o = classNamesProp as Partial<Record<string, string>>;
68
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
69
- mergedClassNames.image = o.image
70
- ? `${styles.image} ${o.image}`
71
- : styles.image;
72
- mergedClassNames.placeholder = o.placeholder
73
- ? `${styles.placeholder} ${o.placeholder}`
74
- : styles.placeholder;
75
- }
67
+ const mergedClassNames = mergeClassNames(
68
+ {
69
+ root: styles.root,
70
+ image: styles.image,
71
+ placeholder: styles.placeholder,
72
+ },
73
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
74
+ );
76
75
 
77
76
  const classNameProp = restRecord.className as string | undefined;
78
77
 
79
78
  return (
80
79
  <MantineAvatar
81
80
  ref={ref}
81
+ {...sanitizedProps}
82
82
  className={classNameProp}
83
83
  classNames={mergedClassNames}
84
84
  variant={mapVariant[variant]}
@@ -87,7 +87,6 @@ const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
87
87
  data-variant={variant}
88
88
  data-size={size}
89
89
  data-style={computedStyle}
90
- {...sanitizedProps}
91
90
  >
92
91
  {icon != null ? (
93
92
  <span className={styles.iconWrapper} aria-hidden>
@@ -6,6 +6,7 @@ import {
6
6
  } from "@mantine/core";
7
7
  import {
8
8
  filterStylingProps,
9
+ mergeClassNames,
9
10
  type RecursicaOverStyled,
10
11
  } from "../../utils/filterStylingProps";
11
12
  import styles from "./Badge.module.css";
@@ -25,23 +26,16 @@ const _Badge = forwardRef<HTMLDivElement, BadgeProps>(function Badge(
25
26
  // External layout props like margins are safely preserved.
26
27
  const sanitizedProps = filterStylingProps(rest, overStyled);
27
28
 
28
- const mergedClassNames: Partial<Record<string, string>> = {
29
- root: styles.root,
30
- section: styles.section,
31
- label: styles.label,
32
- };
33
-
34
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
35
- if (
36
- classNamesProp &&
37
- typeof classNamesProp === "object" &&
38
- !Array.isArray(classNamesProp)
39
- ) {
40
- const o = classNamesProp as Partial<Record<string, string>>;
41
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
42
- mergedClassNames.section = o.section ?? styles.section;
43
- mergedClassNames.label = o.label ?? styles.label;
44
- }
29
+ const mergedClassNames = mergeClassNames(
30
+ {
31
+ root: styles.root,
32
+ section: styles.section,
33
+ label: styles.label,
34
+ },
35
+ (sanitizedProps as Record<string, unknown>).classNames as
36
+ | Partial<Record<string, string>>
37
+ | undefined,
38
+ );
45
39
 
46
40
  const classNameProp = (sanitizedProps as Record<string, unknown>)
47
41
  .className as string | undefined;
@@ -52,9 +46,9 @@ const _Badge = forwardRef<HTMLDivElement, BadgeProps>(function Badge(
52
46
  return (
53
47
  <MantineBadge
54
48
  ref={ref}
49
+ {...sanitizedProps}
55
50
  variant="filled" // We manage visual style purely through our CSS variables mapping
56
51
  data-variant={variant}
57
- {...sanitizedProps}
58
52
  className={finalClass}
59
53
  classNames={mergedClassNames}
60
54
  />
@@ -5,6 +5,7 @@ import {
5
5
  } from "@mantine/core";
6
6
  import {
7
7
  filterStylingProps,
8
+ mergeClassNames,
8
9
  type RecursicaOverStyled,
9
10
  } from "../../utils/filterStylingProps";
10
11
  import styles from "./Breadcrumb.module.css";
@@ -24,24 +25,15 @@ export const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>(
24
25
  overStyled,
25
26
  );
26
27
 
27
- const mergedClassNames: Partial<Record<string, string>> = {
28
- root: styles.root,
29
- separator: styles.separator,
30
- };
31
-
32
- const classNamesProp = (sanitizedProps as Record<string, unknown>)
33
- .classNames;
34
- if (
35
- classNamesProp &&
36
- typeof classNamesProp === "object" &&
37
- !Array.isArray(classNamesProp)
38
- ) {
39
- const o = classNamesProp as Partial<Record<string, string>>;
40
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
41
- mergedClassNames.separator = o.separator
42
- ? `${styles.separator} ${o.separator}`
43
- : styles.separator;
44
- }
28
+ const mergedClassNames = mergeClassNames(
29
+ {
30
+ root: styles.root,
31
+ separator: styles.separator,
32
+ },
33
+ (sanitizedProps as Record<string, unknown>).classNames as
34
+ | Partial<Record<string, string>>
35
+ | undefined,
36
+ );
45
37
 
46
38
  const classNameProp = (sanitizedProps as Record<string, unknown>)
47
39
  .className as string | undefined;
@@ -7,6 +7,9 @@ import {
7
7
  } from "@mantine/core";
8
8
  import {
9
9
  filterStylingProps,
10
+ omitUnsupportedProps,
11
+ withCallerOverride,
12
+ mergeClassNames,
10
13
  type RecursicaOverStyled,
11
14
  } from "../../utils/filterStylingProps";
12
15
  import styles from "./Button.module.css";
@@ -49,11 +52,17 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
49
52
  small: "sm",
50
53
  } as const;
51
54
 
52
- const sanitizedProps = filterStylingProps(rest, overStyled);
53
- const restRecord = sanitizedProps as Record<string, unknown>;
55
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
56
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
57
+ const UNSUPPORTED_PROPS = [
58
+ "fullWidth", // Recursica Button width comes from layout/token constraints, not Mantine's boolean fullWidth toggle.
59
+ ] as const satisfies readonly (keyof MantineButtonProps)[];
54
60
 
55
- // Explicitly delete blocked semantic expansion dimension props
56
- delete restRecord["fullWidth"];
61
+ const sanitizedProps = omitUnsupportedProps(
62
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
63
+ UNSUPPORTED_PROPS,
64
+ ) as Partial<typeof rest>;
65
+ const restRecord = sanitizedProps as Record<string, unknown>;
57
66
 
58
67
  const hasLeftSection = !!icon || !!restRecord["leftSection"];
59
68
  const hasRightSection = !!restRecord["rightSection"];
@@ -78,41 +87,33 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
78
87
  );
79
88
  }
80
89
 
81
- const mergedClassNames: Partial<Record<string, string>> = {
82
- root: styles.root,
83
- section: styles.section,
84
- label: styles.label,
85
- loader: styles.loader,
86
- };
87
-
88
- const classNamesProp = restRecord.classNames;
89
- if (
90
- classNamesProp &&
91
- typeof classNamesProp === "object" &&
92
- !Array.isArray(classNamesProp)
93
- ) {
94
- const o = classNamesProp as Partial<Record<string, string>>;
95
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
96
- mergedClassNames.section = o.section ?? styles.section;
97
- mergedClassNames.label = o.label ?? styles.label;
98
- }
90
+ const mergedClassNames = mergeClassNames(
91
+ {
92
+ root: styles.root,
93
+ section: styles.section,
94
+ label: styles.label,
95
+ loader: styles.loader,
96
+ },
97
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
98
+ );
99
99
 
100
100
  const classNameProp = restRecord.className as string | undefined;
101
101
  const finalClass = classNameProp
102
102
  ? `${styles.root} ${classNameProp}`
103
103
  : styles.root;
104
- // className is merged explicitly above — don't let the {...sanitizedProps} spread below
105
- // silently overwrite finalClass with just the caller's own class (same bug class as
106
- // mantine-adapter's Dropdown/BareDropdown.tsx had). Masked here today since `classNames.root`
107
- // (a separate prop, unaffected) also carries styles.root — but a real latent bug regardless.
108
- delete restRecord["className"];
104
+ // Note: `finalClass` is safe from being clobbered by `{...sanitizedProps}` below purely
105
+ // because of spread order — `className` (like `classNames`, `variant`, `size`, `leftSection`,
106
+ // and the `data-*` attributes) is spread first, then re-asserted after, so our computed value
107
+ // always wins. See the canonical guide's §3.3 for why ordering (not deletion) is the fix here.
109
108
 
110
109
  const userLoaderProps = restRecord.loaderProps as
111
110
  | Record<string, unknown>
112
111
  | undefined;
113
112
 
114
- const resolvedLoaderSize =
115
- loaderSize ?? (size === "small" ? "small" : "default");
113
+ const resolvedLoaderSize = withCallerOverride(
114
+ size === "small" ? "small" : "default",
115
+ loaderSize,
116
+ );
116
117
 
117
118
  let mergedLoaderProps = userLoaderProps;
118
119
  if (useRecursicaLoader) {
@@ -125,6 +126,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
125
126
  return (
126
127
  <MantineButton
127
128
  ref={ref}
129
+ {...sanitizedProps}
128
130
  className={finalClass}
129
131
  classNames={mergedClassNames}
130
132
  variant={mapVariant[variant]}
@@ -140,7 +142,6 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
140
142
  data-variant={variant}
141
143
  data-size={size}
142
144
  data-content={contentType}
143
- {...sanitizedProps}
144
145
  disabled={!!restRecord.disabled || !!restRecord.loading}
145
146
  >
146
147
  <span className={styles.labelText}>{children}</span>