@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
@@ -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
 
@@ -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 "./Stepper.module.css";
@@ -30,28 +31,34 @@ const _Stepper = forwardRef<HTMLDivElement, StepperProps>(
30
31
  } = props;
31
32
 
32
33
  const sanitizedProps = filterStylingProps(rest, overStyled);
34
+ const restRecord = sanitizedProps as Record<string, unknown>;
35
+
36
+ const mergedClassNames = mergeClassNames(
37
+ {
38
+ steps: styles.steps,
39
+ step: styles.step,
40
+ stepIcon: styles.stepIcon,
41
+ stepCompletedIcon: styles.stepCompletedIcon,
42
+ stepBody: styles.stepBody,
43
+ stepLabel: styles.stepLabel,
44
+ stepDescription: styles.stepDescription,
45
+ separator: styles.separator,
46
+ verticalSeparator: styles.verticalSeparator,
47
+ content: styles.content,
48
+ },
49
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
50
+ );
33
51
 
34
52
  return (
35
53
  <MantineStepper
36
54
  ref={ref}
55
+ {...(sanitizedProps as MantineStepperProps)}
37
56
  orientation={orientation}
38
57
  className={`${styles.root} ${orientation === "horizontal" ? styles.horizontal : styles.vertical} ${size === "large" ? styles.large : styles.small} ${className || ""}`}
39
58
  style={style as React.CSSProperties}
40
59
  data-size={size}
41
60
  data-orientation={orientation}
42
- classNames={{
43
- steps: styles.steps,
44
- step: styles.step,
45
- stepIcon: styles.stepIcon,
46
- stepCompletedIcon: styles.stepCompletedIcon,
47
- stepBody: styles.stepBody,
48
- stepLabel: styles.stepLabel,
49
- stepDescription: styles.stepDescription,
50
- separator: styles.separator,
51
- verticalSeparator: styles.verticalSeparator,
52
- content: styles.content,
53
- }}
54
- {...(sanitizedProps as MantineStepperProps)}
61
+ classNames={mergedClassNames}
55
62
  />
56
63
  );
57
64
  },
@@ -1,4 +1,4 @@
1
- import { forwardRef } from "react";
1
+ import { forwardRef, type CSSProperties } from "react";
2
2
  import {
3
3
  Switch as MantineSwitch,
4
4
  type SwitchProps as MantineSwitchProps,
@@ -9,6 +9,10 @@ import { type ReadOnlyControlProps } from "@recursica/adapter-common";
9
9
 
10
10
  import {
11
11
  filterStylingProps,
12
+ omitUnsupportedProps,
13
+ withCallerOverride,
14
+ mergeClassNames,
15
+ mergeStyles,
12
16
  type RecursicaOverStyled,
13
17
  } from "../../utils/filterStylingProps";
14
18
  import {
@@ -59,50 +63,42 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
59
63
  controlMinWidth,
60
64
  ...rest
61
65
  } = props;
62
- const sanitizedProps = filterStylingProps(rest, overStyled);
66
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
67
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
68
+ const UNSUPPORTED_PROPS = [
69
+ "size", // Recursica controls sizing via Switch.module.css variables, not Mantine's native size scale.
70
+ "color", // Colors are token-driven via Switch.module.css; Mantine's native palette isn't exposed.
71
+ "radius", // Corner radius is fixed by Recursica tokens in Switch.module.css, not caller-configurable.
72
+ "variant", // Switch has a single Recursica-defined visual treatment; Mantine's variant isn't exposed.
73
+ ] as const satisfies readonly (keyof MantineSwitchProps)[];
74
+
75
+ const sanitizedProps = omitUnsupportedProps(
76
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
77
+ UNSUPPORTED_PROPS,
78
+ ) as Partial<typeof rest>;
63
79
  const restRecord = sanitizedProps as Record<string, unknown>;
64
80
 
65
- // Actively delete dimension bindings that bypass the abstraction
66
- delete restRecord["size"];
67
- delete restRecord["color"];
68
- delete restRecord["radius"];
69
- delete restRecord["variant"];
70
-
71
- const mergedClassNames: Partial<Record<string, string>> = {
72
- root: styles.root,
73
- body: styles.body,
74
- track: styles.track,
75
- thumb: styles.thumb,
76
- trackLabel: styles.trackLabel,
77
- labelWrapper: styles.labelWrapper,
78
- label: styles.label,
79
- };
80
-
81
- const classNamesProp = restRecord.classNames;
82
- if (
83
- classNamesProp &&
84
- typeof classNamesProp === "object" &&
85
- !Array.isArray(classNamesProp)
86
- ) {
87
- const o = classNamesProp as Partial<Record<string, string>>;
88
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
89
- mergedClassNames.body = o.body ? `${styles.body} ${o.body}` : styles.body;
90
- mergedClassNames.track = o.track
91
- ? `${styles.track} ${o.track}`
92
- : styles.track;
93
- mergedClassNames.thumb = o.thumb
94
- ? `${styles.thumb} ${o.thumb}`
95
- : styles.thumb;
96
- mergedClassNames.trackLabel = o.trackLabel
97
- ? `${styles.trackLabel} ${o.trackLabel}`
98
- : styles.trackLabel;
99
- mergedClassNames.labelWrapper = o.labelWrapper
100
- ? `${styles.labelWrapper} ${o.labelWrapper}`
101
- : styles.labelWrapper;
102
- mergedClassNames.label = o.label
103
- ? `${styles.label} ${o.label}`
104
- : styles.label;
105
- }
81
+ const mergedClassNames = mergeClassNames(
82
+ {
83
+ root: styles.root,
84
+ body: styles.body,
85
+ track: styles.track,
86
+ thumb: styles.thumb,
87
+ trackLabel: styles.trackLabel,
88
+ labelWrapper: styles.labelWrapper,
89
+ label: styles.label,
90
+ },
91
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
92
+ );
93
+
94
+ // Recursica has no default per-slot inline styles here (all default styling comes from
95
+ // Switch.module.css above) — this only exists so a caller-supplied `styles` prop (permitted
96
+ // once overStyled=true clears filterStylingProps) merges in per-slot instead of needing its
97
+ // own passthrough path.
98
+ const mergedStyles = mergeStyles(
99
+ {} as Record<string, CSSProperties | undefined>,
100
+ restRecord.styles as Partial<Record<string, CSSProperties>> | undefined,
101
+ );
106
102
 
107
103
  const classNameProp = restRecord.className as string | undefined;
108
104
  const finalClass = classNameProp
@@ -147,12 +143,16 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
147
143
  const switchNode = (
148
144
  <MantineSwitch
149
145
  ref={ref}
146
+ {...(sanitizedProps as unknown as MantineSwitchProps)}
150
147
  className={finalClass}
151
148
  classNames={mergedClassNames}
149
+ styles={mergedStyles}
152
150
  disabled={readOnly || disabled}
153
151
  data-disabled={readOnly || disabled || undefined}
154
- thumbIcon={thumbIcon ?? FinalThumbIcon}
155
- {...(sanitizedProps as unknown as MantineSwitchProps)}
152
+ thumbIcon={withCallerOverride<React.ReactNode>(
153
+ FinalThumbIcon,
154
+ thumbIcon,
155
+ )}
156
156
  />
157
157
  );
158
158
 
@@ -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 SwitchGroup = forwardRef<HTMLDivElement, SwitchGroupProps>(
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 Switch.module.css variables, not Mantine's native size scale.
69
+ ] as const satisfies readonly (keyof MantineSwitchGroupProps)[];
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
@@ -38,8 +38,8 @@ const TableBase = forwardRef<HTMLTableElement, TableProps>(function Table(
38
38
  return (
39
39
  <MantineTable
40
40
  ref={ref}
41
- className={finalClass}
42
41
  {...(sanitizedProps as unknown as MantineTableProps)}
42
+ className={finalClass}
43
43
  />
44
44
  );
45
45
  });
@@ -8,6 +8,7 @@ import {
8
8
  } from "@mantine/core";
9
9
  import {
10
10
  filterStylingProps,
11
+ mergeClassNames,
11
12
  type RecursicaOverStyled,
12
13
  } from "../../utils/filterStylingProps";
13
14
  import styles from "./Tabs.module.css";
@@ -30,21 +31,27 @@ const _Tabs = forwardRef<HTMLDivElement, TabsProps>(function Tabs(props, ref) {
30
31
  } = props;
31
32
 
32
33
  const sanitizedProps = filterStylingProps(rest, overStyled);
34
+ const restRecord = sanitizedProps as Record<string, unknown>;
35
+
36
+ const mergedClassNames = mergeClassNames(
37
+ {
38
+ list: styles.list,
39
+ tab: styles.tab,
40
+ panel: styles.panel,
41
+ },
42
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
43
+ );
33
44
 
34
45
  return (
35
46
  <MantineTabs
36
47
  ref={ref}
48
+ {...(sanitizedProps as MantineTabsProps)}
37
49
  variant={variant}
38
50
  orientation={orientation}
39
51
  className={`${styles.root} ${className || ""}`}
40
52
  data-variant={variant}
41
53
  data-orientation={orientation}
42
- classNames={{
43
- list: styles.list,
44
- tab: styles.tab,
45
- panel: styles.panel,
46
- }}
47
- {...(sanitizedProps as MantineTabsProps)}
54
+ classNames={mergedClassNames}
48
55
  />
49
56
  );
50
57
  });
@@ -31,8 +31,8 @@ const _Text = forwardRef<HTMLDivElement, TextProps>(function Text(
31
31
  return (
32
32
  <MantineText
33
33
  ref={ref}
34
- className={mergedClassName}
35
34
  {...(sanitizedProps as unknown as MantineTextProps)}
35
+ className={mergedClassName}
36
36
  />
37
37
  );
38
38
  });
@@ -1,10 +1,3 @@
1
- /* EXEMPTIONS:
2
- - border-size variables are ignored because a uniform 1px border is applied globally to prevent
3
- unexpected layout shift or flickering during focused, disabled, or error state transitions. */
4
- /* recursica-ignore: --recursica_ui-kit_components_textarea_properties_border-size */
5
- /* recursica-ignore: --recursica_ui-kit_components_textarea_variants_states_disabled_properties_border-size */
6
- /* recursica-ignore: --recursica_ui-kit_components_textarea_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 --textarea-control-{max,min}-width hooks consumed inline in TextArea.tsx, since
@@ -6,6 +6,8 @@ import {
6
6
  import { type ReadOnlyControlProps } from "@recursica/adapter-common";
7
7
  import {
8
8
  filterStylingProps,
9
+ omitUnsupportedProps,
10
+ mergeClassNames,
9
11
  type RecursicaOverStyled,
10
12
  } from "../../utils/filterStylingProps";
11
13
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
@@ -28,6 +30,14 @@ export interface RecursicaTextAreaProps
28
30
 
29
31
  export type TextAreaProps = RecursicaOverStyled<RecursicaTextAreaProps>;
30
32
 
33
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
34
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
35
+ const UNSUPPORTED_PROPS = [
36
+ "size", // Recursica controls sizing via the `size` variant + design tokens, not raw dimensions.
37
+ "variant", // Colors/variants are token-driven; the library's native palette isn't exposed.
38
+ "radius", // Recursica does not expose the library's native corner-radius system.
39
+ ] as const satisfies readonly (keyof MantineTextareaProps)[];
40
+
31
41
  export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
32
42
  function TextArea(props, ref) {
33
43
  const {
@@ -58,34 +68,20 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
58
68
  defaultValue,
59
69
  ...rest
60
70
  } = props;
61
- const sanitizedProps = filterStylingProps(rest, overStyled);
71
+ const sanitizedProps = omitUnsupportedProps(
72
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
73
+ UNSUPPORTED_PROPS,
74
+ );
62
75
  const restRecord = sanitizedProps as Record<string, unknown>;
63
76
 
64
- // Delete prohibited sizing hooks from bypassing variables natively
65
- delete restRecord["size"];
66
- delete restRecord["variant"];
67
- delete restRecord["radius"];
68
-
69
77
  // Securely map core native blocks down ensuring nested CSS modules map precisely
70
- const mergedClassNames: Partial<Record<string, string>> = {
71
- wrapper: styles.root, // The nested Input internal relative wrapper bounding box
72
- input: styles.input,
73
- };
74
-
75
- const classNamesProp = restRecord.classNames;
76
- if (
77
- classNamesProp &&
78
- typeof classNamesProp === "object" &&
79
- !Array.isArray(classNamesProp)
80
- ) {
81
- const o = classNamesProp as Partial<Record<string, string>>;
82
- mergedClassNames.wrapper = o.wrapper
83
- ? `${styles.root} ${o.wrapper}`
84
- : styles.root;
85
- mergedClassNames.input = o.input
86
- ? `${styles.input} ${o.input}`
87
- : styles.input;
88
- }
78
+ const mergedClassNames = mergeClassNames(
79
+ {
80
+ wrapper: styles.root, // The nested Input internal relative wrapper bounding box
81
+ input: styles.input,
82
+ },
83
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
84
+ );
89
85
 
90
86
  const wrapperClass = className
91
87
  ? `${styles.layoutOverride} ${className}`
@@ -1,10 +1,3 @@
1
- /* EXEMPTIONS:
2
- - border-size variables are ignored because a uniform 1px border is applied globally to prevent
3
- unexpected layout shift or flickering during focused, disabled, or error state transitions. */
4
- /* recursica-ignore: --recursica_ui-kit_components_text-field_properties_border-size */
5
- /* recursica-ignore: --recursica_ui-kit_components_text-field_variants_states_disabled_properties_border-size */
6
- /* recursica-ignore: --recursica_ui-kit_components_text-field_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 --text-field-control-{max,min}-width hooks consumed inline in TextField.tsx,
@@ -3,6 +3,8 @@ import { Input, type InputProps, type InputWrapperProps } from "@mantine/core";
3
3
  import { type ReadOnlyControlProps } from "@recursica/adapter-common";
4
4
  import {
5
5
  filterStylingProps,
6
+ omitUnsupportedProps,
7
+ mergeClassNames,
6
8
  type RecursicaOverStyled,
7
9
  } from "../../utils/filterStylingProps";
8
10
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
@@ -30,6 +32,14 @@ export interface RecursicaTextFieldProps
30
32
 
31
33
  export type TextFieldProps = RecursicaOverStyled<RecursicaTextFieldProps>;
32
34
 
35
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
36
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
37
+ const UNSUPPORTED_PROPS = [
38
+ "size", // Recursica controls sizing via the `size` variant + design tokens, not raw dimensions.
39
+ "variant", // Colors/variants are token-driven; the library's native palette isn't exposed.
40
+ "radius", // Recursica does not expose the library's native corner-radius system.
41
+ ] as const satisfies readonly (keyof InputProps)[];
42
+
33
43
  export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
34
44
  function TextField(props, ref) {
35
45
  const {
@@ -60,38 +70,21 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
60
70
  defaultValue,
61
71
  ...rest
62
72
  } = props;
63
- const sanitizedProps = filterStylingProps(rest, overStyled);
73
+ const sanitizedProps = omitUnsupportedProps(
74
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
75
+ UNSUPPORTED_PROPS,
76
+ );
64
77
  const restRecord = sanitizedProps as Record<string, unknown>;
65
78
 
66
- // Delete prohibited sizing hooks from bypassing variables natively
67
- delete restRecord["size"];
68
- delete restRecord["variant"];
69
- delete restRecord["radius"];
70
-
71
79
  // Securely map core native blocks down ensuring nested CSS modules map precisely
72
- const mergedClassNames: Partial<Record<string, string>> = {
73
- wrapper: styles.root, // The nested Input internal relative wrapper bounding box
74
- input: styles.input,
75
- section: styles.section,
76
- };
77
-
78
- const classNamesProp = restRecord.classNames;
79
- if (
80
- classNamesProp &&
81
- typeof classNamesProp === "object" &&
82
- !Array.isArray(classNamesProp)
83
- ) {
84
- const o = classNamesProp as Partial<Record<string, string>>;
85
- mergedClassNames.wrapper = o.wrapper
86
- ? `${styles.root} ${o.wrapper}`
87
- : styles.root;
88
- mergedClassNames.input = o.input
89
- ? `${styles.input} ${o.input}`
90
- : styles.input;
91
- mergedClassNames.section = o.section
92
- ? `${styles.section} ${o.section}`
93
- : styles.section;
94
- }
80
+ const mergedClassNames = mergeClassNames(
81
+ {
82
+ wrapper: styles.root, // The nested Input internal relative wrapper bounding box
83
+ input: styles.input,
84
+ section: styles.section,
85
+ },
86
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
87
+ );
95
88
 
96
89
  const wrapperClass = className
97
90
  ? `${styles.layoutOverride} ${className}`
@@ -127,6 +120,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
127
120
  /* Naked Input execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks */
128
121
  <Input
129
122
  ref={ref}
123
+ {...(sanitizedProps as unknown as InputProps)}
130
124
  classNames={mergedClassNames}
131
125
  disabled={disabled}
132
126
  value={value}
@@ -135,7 +129,6 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
135
129
  "data-disabled": disabled ? "true" : undefined,
136
130
  "data-error": error ? "true" : undefined,
137
131
  }}
138
- {...(sanitizedProps as unknown as InputProps)}
139
132
  />
140
133
  }
141
134
  />