@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
@@ -7,6 +7,8 @@ import {
7
7
  } from "@mantine/core";
8
8
  import {
9
9
  filterStylingProps,
10
+ omitUnsupportedProps,
11
+ mergeClassNames,
10
12
  type RecursicaOverStyled,
11
13
  } from "../../utils/filterStylingProps";
12
14
  import styles from "./Card.module.css";
@@ -32,11 +34,23 @@ export type CardProps = RecursicaOverStyled<
32
34
  /**
33
35
  * The root Card elevation box. It establishes the global background color, border radius, nested component gap, and outer shadow governed by the current `Layer` context.
34
36
  */
37
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
38
+ // through even if a caller forces them via plain JavaScript. `Card.module.css` unconditionally
39
+ // drives these from Recursica UI Kit tokens on `.root`; the native prop is never consulted.
40
+ const UNSUPPORTED_PROPS = [
41
+ "radius", // Border radius is token-driven (`--recursica_..._borders_border-radius`), not caller-set.
42
+ "withBorder", // Border is always applied via tokens on `.root`; not an optional caller toggle.
43
+ "padding", // Padding is token-driven (`--card-padding`), not Mantine's spacing-scale prop.
44
+ ] as const satisfies readonly (keyof MantineCardProps)[];
45
+
35
46
  const CardBase = forwardRef<HTMLDivElement, CardProps>(function Card(
36
47
  { overStyled = false, ...rest },
37
48
  ref,
38
49
  ) {
39
- const sanitizedProps = filterStylingProps(rest, overStyled);
50
+ const sanitizedProps = omitUnsupportedProps(
51
+ filterStylingProps(rest, overStyled),
52
+ UNSUPPORTED_PROPS,
53
+ );
40
54
 
41
55
  // Expose outer layout/flexbox properties on Card to allow structuring alongside siblings
42
56
  const safeCardLayoutKeys = [
@@ -56,25 +70,14 @@ const CardBase = forwardRef<HTMLDivElement, CardProps>(function Card(
56
70
  }
57
71
  });
58
72
 
59
- const mergedClassNames: Partial<Record<string, string>> = {
60
- root: styles.root,
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
- }
73
+ const mergedClassNames = mergeClassNames(
74
+ {
75
+ root: styles.root,
76
+ },
77
+ (sanitizedProps as Record<string, unknown>).classNames as
78
+ | Partial<Record<string, string>>
79
+ | undefined,
80
+ );
78
81
 
79
82
  const classNameProp = (sanitizedProps as Record<string, unknown>)
80
83
  .className as string | undefined;
@@ -82,9 +85,9 @@ const CardBase = forwardRef<HTMLDivElement, CardProps>(function Card(
82
85
  return (
83
86
  <MantineCard
84
87
  ref={ref}
88
+ {...(sanitizedProps as unknown as MantineCardProps)}
85
89
  className={classNameProp}
86
90
  classNames={mergedClassNames}
87
- {...(sanitizedProps as unknown as MantineCardProps)}
88
91
  />
89
92
  );
90
93
  });
@@ -106,10 +109,10 @@ export const CardSection = forwardRef<HTMLDivElement, CardSectionProps>(
106
109
  return (
107
110
  <MantineCard.Section
108
111
  ref={ref}
112
+ {...(sanitizedProps as unknown as MantineCardSectionProps)}
109
113
  className={
110
114
  classNameProp ? `${styles.section} ${classNameProp}` : styles.section
111
115
  }
112
- {...(sanitizedProps as unknown as MantineCardSectionProps)}
113
116
  />
114
117
  );
115
118
  },
@@ -122,19 +125,32 @@ export type CardHeaderProps = RecursicaOverStyled<RecursicaCardSectionProps>;
122
125
  /**
123
126
  * Replaces standard generic Mantine `<Card.Section>` wrappers by directly injecting the strict Recursica design tokens for header sizing, background drops, and intrinsic padding limits.
124
127
  */
128
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
129
+ // through even if a caller forces them via plain JavaScript. `CardHeaderProps` already omits all
130
+ // native Mantine section props from the public type, but that alone doesn't stop a forced-typed
131
+ // caller from smuggling one through `...rest`; `.header` in Card.module.css unconditionally
132
+ // drives background/padding/border from Recursica tokens, so these native toggles are meaningless.
133
+ const HEADER_FOOTER_UNSUPPORTED_PROPS = [
134
+ "withBorder", // The divider border is always applied via tokens on `.header`/`.footer`.
135
+ "inheritPadding", // Padding is fixed by the `.header`/`.footer` token-driven padding, not Mantine's default.
136
+ ] as const satisfies readonly (keyof MantineCardSectionProps)[];
137
+
125
138
  export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
126
139
  function CardHeader({ overStyled = false, ...rest }, ref) {
127
- const sanitizedProps = filterStylingProps(rest, overStyled);
140
+ const sanitizedProps = omitUnsupportedProps(
141
+ filterStylingProps(rest, overStyled),
142
+ HEADER_FOOTER_UNSUPPORTED_PROPS,
143
+ );
128
144
  const classNameProp = (sanitizedProps as Record<string, unknown>)
129
145
  .className as string | undefined;
130
146
 
131
147
  return (
132
148
  <MantineCard.Section
133
149
  ref={ref}
150
+ {...(sanitizedProps as unknown as MantineCardSectionProps)}
134
151
  className={
135
152
  classNameProp ? `${styles.header} ${classNameProp}` : styles.header
136
153
  }
137
- {...(sanitizedProps as unknown as MantineCardSectionProps)}
138
154
  />
139
155
  );
140
156
  },
@@ -149,17 +165,22 @@ export type CardFooterProps = RecursicaOverStyled<RecursicaCardSectionProps>;
149
165
  */
150
166
  export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
151
167
  function CardFooter({ overStyled = false, ...rest }, ref) {
152
- const sanitizedProps = filterStylingProps(rest, overStyled);
168
+ // Reuses HEADER_FOOTER_UNSUPPORTED_PROPS (declared above with CardHeader) — same rationale
169
+ // applies: `.footer` in Card.module.css unconditionally drives padding/border from tokens.
170
+ const sanitizedProps = omitUnsupportedProps(
171
+ filterStylingProps(rest, overStyled),
172
+ HEADER_FOOTER_UNSUPPORTED_PROPS,
173
+ );
153
174
  const classNameProp = (sanitizedProps as Record<string, unknown>)
154
175
  .className as string | undefined;
155
176
 
156
177
  return (
157
178
  <MantineCard.Section
158
179
  ref={ref}
180
+ {...(sanitizedProps as unknown as MantineCardSectionProps)}
159
181
  className={
160
182
  classNameProp ? `${styles.footer} ${classNameProp}` : styles.footer
161
183
  }
162
- {...(sanitizedProps as unknown as MantineCardSectionProps)}
163
184
  />
164
185
  );
165
186
  },
@@ -183,10 +204,10 @@ export const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
183
204
  return (
184
205
  <div
185
206
  ref={ref}
207
+ {...sanitizedProps}
186
208
  className={
187
209
  classNameProp ? `${styles.content} ${classNameProp}` : styles.content
188
210
  }
189
- {...sanitizedProps}
190
211
  />
191
212
  );
192
213
  },
@@ -7,6 +7,8 @@ import { type ReadOnlyControlProps } from "@recursica/adapter-common";
7
7
  import { CheckboxGroup } from "./CheckboxGroup";
8
8
  import {
9
9
  filterStylingProps,
10
+ omitUnsupportedProps,
11
+ mergeClassNames,
10
12
  type RecursicaOverStyled,
11
13
  } from "../../utils/filterStylingProps";
12
14
  import {
@@ -56,49 +58,34 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
56
58
  ...rest
57
59
  } = props;
58
60
  // Checkbox doesn't use Label onLabelEditClick natively since it isn't mapped inside FormControlWrapper intrinsically.
59
- const sanitizedProps = filterStylingProps(rest, overStyled);
61
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
62
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
63
+ const UNSUPPORTED_PROPS = [
64
+ "size", // Recursica controls sizing via Checkbox.module.css variables, not Mantine's native size scale.
65
+ "color", // Colors are token-driven via Checkbox.module.css; Mantine's native palette isn't exposed.
66
+ "radius", // Corner radius is fixed by Recursica tokens in Checkbox.module.css, not caller-configurable.
67
+ "variant", // Checkbox has a single Recursica-defined visual treatment; Mantine's variant isn't exposed.
68
+ "iconColor", // Icon color is token-driven via Checkbox.module.css, not a native Mantine override.
69
+ ] as const satisfies readonly (keyof MantineCheckboxProps)[];
70
+
71
+ const sanitizedProps = omitUnsupportedProps(
72
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
73
+ UNSUPPORTED_PROPS,
74
+ ) as Partial<typeof rest>;
60
75
  const restRecord = sanitizedProps as Record<string, unknown>;
61
76
 
62
- // Actively delete dimension bindings that bypass the abstraction
63
- delete restRecord["size"];
64
- delete restRecord["color"];
65
- delete restRecord["radius"];
66
- delete restRecord["variant"];
67
- delete restRecord["iconColor"];
68
-
69
- const mergedClassNames: Partial<Record<string, string>> = {
70
- root: styles.root,
71
- body: styles.body,
72
- inner: styles.inner,
73
- input: styles.input,
74
- icon: styles.icon,
75
- labelWrapper: styles.labelWrapper,
76
- label: styles.label,
77
- };
78
-
79
- const classNamesProp = restRecord.classNames;
80
- if (
81
- classNamesProp &&
82
- typeof classNamesProp === "object" &&
83
- !Array.isArray(classNamesProp)
84
- ) {
85
- const o = classNamesProp as Partial<Record<string, string>>;
86
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
87
- mergedClassNames.body = o.body ? `${styles.body} ${o.body}` : styles.body;
88
- mergedClassNames.inner = o.inner
89
- ? `${styles.inner} ${o.inner}`
90
- : styles.inner;
91
- mergedClassNames.input = o.input
92
- ? `${styles.input} ${o.input}`
93
- : styles.input;
94
- mergedClassNames.icon = o.icon ? `${styles.icon} ${o.icon}` : styles.icon;
95
- mergedClassNames.labelWrapper = o.labelWrapper
96
- ? `${styles.labelWrapper} ${o.labelWrapper}`
97
- : styles.labelWrapper;
98
- mergedClassNames.label = o.label
99
- ? `${styles.label} ${o.label}`
100
- : styles.label;
101
- }
77
+ const mergedClassNames = mergeClassNames(
78
+ {
79
+ root: styles.root,
80
+ body: styles.body,
81
+ inner: styles.inner,
82
+ input: styles.input,
83
+ icon: styles.icon,
84
+ labelWrapper: styles.labelWrapper,
85
+ label: styles.label,
86
+ },
87
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
88
+ );
102
89
 
103
90
  const classNameProp = restRecord.className as string | undefined;
104
91
  const finalClass = classNameProp
@@ -136,10 +123,10 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
136
123
  const checkboxNode = (
137
124
  <MantineCheckbox
138
125
  ref={ref}
126
+ {...(sanitizedProps as unknown as MantineCheckboxProps)}
139
127
  className={finalClass}
140
128
  classNames={mergedClassNames}
141
129
  disabled={readOnly || disabled}
142
- {...(sanitizedProps as unknown as MantineCheckboxProps)}
143
130
  />
144
131
  );
145
132
 
@@ -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";
@@ -62,11 +63,17 @@ export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
62
63
  defaultValue,
63
64
  ...rest
64
65
  } = props;
65
- const sanitizedProps = filterStylingProps(rest, overStyled);
66
- const restRecord = sanitizedProps as Record<string, unknown>;
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 Checkbox.module.css variables, not Mantine's native size scale.
70
+ ] as const satisfies readonly (keyof MantineCheckboxGroupProps)[];
67
71
 
68
- // Delete prohibited sizing hooks from bypassing the variables
69
- delete restRecord["size"];
72
+ const sanitizedProps = omitUnsupportedProps(
73
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
74
+ UNSUPPORTED_PROPS,
75
+ ) as Partial<typeof rest>;
76
+ const restRecord = sanitizedProps as Record<string, unknown>;
70
77
 
71
78
  // Mantine's own `Checkbox.Group` always establishes a context whose `value` defaults to
72
79
  // `[]` (see @mantine/core's `useUncontrolled({ finalValue: [] })`), and its `Checkbox` spreads
@@ -123,8 +130,8 @@ export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
123
130
  ) : (
124
131
  <div
125
132
  ref={ref}
126
- role="group"
127
133
  {...(sanitizedProps as Record<string, unknown>)}
134
+ role="group"
128
135
  className={styles.groupRoot}
129
136
  data-layout={formLayout}
130
137
  >
@@ -5,6 +5,8 @@ import {
5
5
  } from "@mantine/core";
6
6
  import {
7
7
  filterStylingProps,
8
+ mergeClassNames,
9
+ withCallerOverride,
8
10
  type RecursicaOverStyled,
9
11
  } from "../../utils/filterStylingProps";
10
12
  import styles from "./Chip.module.css";
@@ -45,6 +47,9 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
45
47
  removeIconRef,
46
48
  children,
47
49
  overStyled = false,
50
+ wrapperProps,
51
+ tabIndex,
52
+ "aria-hidden": ariaHidden,
48
53
  ...rest
49
54
  },
50
55
  ref,
@@ -52,26 +57,16 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
52
57
  const sanitizedProps = filterStylingProps(rest, overStyled);
53
58
  const restRecord = sanitizedProps as Record<string, unknown>;
54
59
 
55
- const mergedClassNames: Partial<Record<string, string>> = {
56
- root: styles.root,
57
- label: styles.label,
58
- input: styles.input,
59
- iconWrapper: styles.mantineIconWrapper,
60
- checkIcon: styles.checkIcon,
61
- };
62
-
63
- const classNamesProp = restRecord.classNames;
64
- if (
65
- classNamesProp &&
66
- typeof classNamesProp === "object" &&
67
- !Array.isArray(classNamesProp)
68
- ) {
69
- const o = classNamesProp as Partial<Record<string, string>>;
70
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
71
- mergedClassNames.label = o.label
72
- ? `${styles.label} ${o.label}`
73
- : styles.label;
74
- }
60
+ const mergedClassNames = mergeClassNames(
61
+ {
62
+ root: styles.root,
63
+ label: styles.label,
64
+ input: styles.input,
65
+ iconWrapper: styles.mantineIconWrapper,
66
+ checkIcon: styles.checkIcon,
67
+ },
68
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
69
+ );
75
70
 
76
71
  const classNameProp = restRecord.className as string | undefined;
77
72
  const finalClass = classNameProp
@@ -90,18 +85,30 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
90
85
  restRecord.onClick !== undefined ||
91
86
  restRecord.onChange !== undefined;
92
87
 
88
+ // Our own computed default for Mantine's `wrapperProps` — flags the internal error/interactive
89
+ // CSS hooks. This is a real Mantine `ChipProps` slot a caller could legitimately pass their own
90
+ // value for, so it must merge via `withCallerOverride`, not silently clobber theirs.
91
+ const computedWrapperProps = {
92
+ ...(dataError !== undefined ? { "data-error": "" } : {}),
93
+ ...(isInteractive ? { "data-interactive": "" } : {}),
94
+ } as NonNullable<MantineChipProps["wrapperProps"]>;
95
+
93
96
  return (
94
97
  <MantineChip
95
98
  ref={ref}
99
+ {...sanitizedProps}
96
100
  className={finalClass}
97
101
  classNames={mergedClassNames}
98
- wrapperProps={{
99
- ...(dataError !== undefined ? { "data-error": "" } : {}),
100
- ...(isInteractive ? { "data-interactive": "" } : {}),
101
- }}
102
- {...(isIconOnly ? { "data-icon-only": "" } : {})}
103
- {...(!isInteractive ? { tabIndex: -1, "aria-hidden": true } : {})}
104
- {...sanitizedProps}
102
+ wrapperProps={withCallerOverride(computedWrapperProps, wrapperProps)}
103
+ data-icon-only={isIconOnly ? "" : undefined}
104
+ tabIndex={withCallerOverride<number | undefined>(
105
+ isInteractive ? undefined : -1,
106
+ tabIndex,
107
+ )}
108
+ aria-hidden={withCallerOverride<boolean | "true" | "false" | undefined>(
109
+ isInteractive ? undefined : true,
110
+ ariaHidden,
111
+ )}
105
112
  >
106
113
  <span className={styles.innerWrapper}>
107
114
  {icon && (
@@ -3,6 +3,9 @@ import { DatePickerInput, type DatePickerInputProps } from "@mantine/dates";
3
3
  import { type ReadOnlyControlProps } from "@recursica/adapter-common";
4
4
  import {
5
5
  filterStylingProps,
6
+ omitUnsupportedProps,
7
+ withCallerOverride,
8
+ mergeClassNames,
6
9
  type RecursicaOverStyled,
7
10
  } from "../../utils/filterStylingProps";
8
11
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
@@ -39,6 +42,15 @@ export interface RecursicaDatePickerProps
39
42
 
40
43
  export type DatePickerProps = RecursicaOverStyled<RecursicaDatePickerProps>;
41
44
 
45
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
46
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
47
+ const UNSUPPORTED_PROPS = [
48
+ "size", // Recursica controls sizing via the `size` variant + design tokens, not raw dimensions.
49
+ "variant", // Colors/variants are token-driven; the library's native palette isn't exposed.
50
+ "radius", // Recursica does not expose the library's native corner-radius system.
51
+ "description", // Managed by FormControlWrapper via assistiveText.
52
+ ] as const satisfies readonly (keyof DatePickerInputProps)[];
53
+
42
54
  export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(
43
55
  function DatePicker(props, ref) {
44
56
  const {
@@ -67,48 +79,34 @@ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(
67
79
  emptyValueComponent,
68
80
  value,
69
81
  defaultValue,
82
+ highlightToday = true, // Default on so today is visually marked; consumers can still override.
83
+ valueFormat = "MM/DD/YY", // Default display format; consumers can still override.
84
+ placeholder = "MM / DD / YY", // Default placeholder; consumers can still override.
85
+ leftSection, // Default leading icon computed below; consumers can still override.
70
86
  ...rest
71
87
  } = props;
72
88
 
73
- const sanitizedProps = filterStylingProps(rest, overStyled);
89
+ const sanitizedProps = omitUnsupportedProps(
90
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
91
+ UNSUPPORTED_PROPS,
92
+ );
74
93
  const restRecord = sanitizedProps as Record<string, unknown>;
75
94
 
76
- // Delete prohibited sizing hooks from bypassing variables natively
77
- delete restRecord["size"];
78
- delete restRecord["variant"];
79
- delete restRecord["radius"];
80
- delete restRecord["description"]; // Managed by FormControlWrapper via assistiveText
81
-
82
95
  // Securely map core native blocks down ensuring nested CSS modules map precisely
83
- const mergedClassNames: Partial<Record<string, string>> = {
84
- wrapper: styles.root, // The nested Input internal relative wrapper bounding box
85
- input: styles.input,
86
- section: styles.section,
87
- day: styles.day,
88
- weekday: styles.weekday,
89
- calendarHeader: styles.calendarHeader,
90
- calendarHeaderControl: styles.calendarHeaderControl,
91
- calendarHeaderLevel: styles.calendarHeaderLevel,
92
- calendarHeaderControlIcon: styles.calendarHeaderControlIcon,
93
- };
94
-
95
- const classNamesProp = restRecord.classNames;
96
- if (
97
- classNamesProp &&
98
- typeof classNamesProp === "object" &&
99
- !Array.isArray(classNamesProp)
100
- ) {
101
- const o = classNamesProp as Partial<Record<string, string>>;
102
- mergedClassNames.wrapper = o.wrapper
103
- ? `${styles.root} ${o.wrapper}`
104
- : styles.root;
105
- mergedClassNames.input = o.input
106
- ? `${styles.input} ${o.input}`
107
- : styles.input;
108
- mergedClassNames.section = o.section
109
- ? `${styles.section} ${o.section}`
110
- : styles.section;
111
- }
96
+ const mergedClassNames = mergeClassNames(
97
+ {
98
+ wrapper: styles.root, // The nested Input internal relative wrapper bounding box
99
+ input: styles.input,
100
+ section: styles.section,
101
+ day: styles.day,
102
+ weekday: styles.weekday,
103
+ calendarHeader: styles.calendarHeader,
104
+ calendarHeaderControl: styles.calendarHeaderControl,
105
+ calendarHeaderLevel: styles.calendarHeaderLevel,
106
+ calendarHeaderControlIcon: styles.calendarHeaderControlIcon,
107
+ },
108
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
109
+ );
112
110
 
113
111
  // The calendar popover surface (.dropdown) is styled by Mantine's underlying `Popover`
114
112
  // component, not `DatePickerInput` itself — it only accepts classNames via `popoverProps`,
@@ -129,12 +127,10 @@ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(
129
127
  delete restRecord["popoverProps"];
130
128
  const mergedPopoverProps = {
131
129
  ...consumerPopoverProps,
132
- classNames: {
133
- ...consumerPopoverClassNames,
134
- dropdown: consumerPopoverClassNames?.dropdown
135
- ? `${styles.dropdown} ${consumerPopoverClassNames.dropdown}`
136
- : styles.dropdown,
137
- },
130
+ classNames: mergeClassNames(
131
+ { dropdown: styles.dropdown },
132
+ consumerPopoverClassNames,
133
+ ),
138
134
  };
139
135
 
140
136
  const wrapperClass = className
@@ -177,6 +173,7 @@ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(
177
173
  /* Naked Input execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks */
178
174
  <DatePickerInput
179
175
  ref={ref}
176
+ {...(sanitizedProps as unknown as DatePickerInputProps)}
180
177
  classNames={mergedClassNames}
181
178
  disabled={disabled}
182
179
  value={value}
@@ -185,16 +182,18 @@ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(
185
182
  description={undefined} // Disable Mantine's native description
186
183
  error={undefined} // Disable Mantine's native error text (handled by wrapper)
187
184
  withAsterisk={false} // Handled by wrapper
185
+ highlightToday={highlightToday}
186
+ valueFormat={valueFormat}
187
+ placeholder={placeholder}
188
+ leftSection={withCallerOverride<React.ReactNode>(
189
+ <CalendarIcon />,
190
+ leftSection,
191
+ )}
188
192
  wrapperProps={{
189
193
  "data-disabled": disabled ? "true" : undefined,
190
194
  "data-error": error ? "true" : undefined,
191
195
  }}
192
- highlightToday // Default on so today is visually marked; consumers can still override via rest
193
- valueFormat="MM/DD/YY" // Default display format; consumers can still override via rest
194
- placeholder="MM / DD / YY" // Default placeholder; consumers can still override via rest
195
- leftSection={<CalendarIcon />} // Default leading icon; consumers can still override via rest
196
196
  popoverProps={mergedPopoverProps}
197
- {...(sanitizedProps as unknown as DatePickerInputProps)}
198
197
  />
199
198
  }
200
199
  />
@@ -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 "./Dropdown.module.css";
@@ -37,29 +39,40 @@ export interface BareDropdownProps
37
39
 
38
40
  export type BareDropdownComponentProps = RecursicaOverStyled<BareDropdownProps>;
39
41
 
42
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
43
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
44
+ const UNSUPPORTED_PROPS = [
45
+ "size", // Recursica controls sizing via the `size` variant + design tokens, not raw dimensions.
46
+ "variant", // Colors/variants are token-driven; the library's native palette isn't exposed.
47
+ "radius", // Recursica does not expose the library's native corner-radius system.
48
+ ] as const satisfies readonly (keyof MantineSelectProps)[];
49
+
40
50
  export const BareDropdown = forwardRef<
41
51
  HTMLInputElement,
42
52
  BareDropdownComponentProps
43
53
  >(function BareDropdown(props, ref) {
44
54
  const { overStyled = false, className, disabled, error, ...rest } = props;
45
- const sanitizedProps = filterStylingProps(rest, overStyled);
55
+ const sanitizedProps = omitUnsupportedProps(
56
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
57
+ UNSUPPORTED_PROPS,
58
+ );
46
59
  const restRecord = sanitizedProps as Record<string, unknown>;
47
60
 
48
- delete restRecord["size"];
49
- delete restRecord["variant"];
50
- delete restRecord["radius"];
51
-
52
- const mergedClassNames: Partial<Record<string, string>> = {
53
- wrapper: className ? `${styles.root} ${className}` : styles.root,
54
- input: styles.input,
55
- section: styles.section,
56
- dropdown: styles.dropdown,
57
- option: styles.option,
58
- };
61
+ const mergedClassNames = mergeClassNames(
62
+ {
63
+ wrapper: className ? `${styles.root} ${className}` : styles.root,
64
+ input: styles.input,
65
+ section: styles.section,
66
+ dropdown: styles.dropdown,
67
+ option: styles.option,
68
+ },
69
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
70
+ );
59
71
 
60
72
  return (
61
73
  <MantineSelect
62
74
  ref={ref}
75
+ {...(sanitizedProps as unknown as MantineSelectProps)}
63
76
  classNames={mergedClassNames}
64
77
  disabled={disabled}
65
78
  label={undefined}
@@ -77,7 +90,6 @@ export const BareDropdown = forwardRef<
77
90
  "data-error": error ? "true" : undefined,
78
91
  },
79
92
  }}
80
- {...(sanitizedProps as unknown as MantineSelectProps)}
81
93
  />
82
94
  );
83
95
  });
@@ -5,3 +5,5 @@ The `Dropdown` component is mapped explicitly to Mantine's `<Select>` following
5
5
  1. **Naked Primitive Mapping:** Mantine's `Select` natively executes macro-label generation. To decouple it, we explicitly disable internal labels (`label={undefined}`) and inject it purely inside our generic `FormControlWrapper`.
6
6
  2. **Strict Dropdown Design Tokens:** The adapter implements strictly sandboxed styling utilizing only `--recursica_ui-kit_components_dropdown_...` variables. It explicitly does NOT inherit general `text-field` tokens despite geometric similarities, ensuring dropdown menus can be themed independently.
7
7
  3. **Dropdown Appendages:** To correctly map Mantine's detached Popover `.dropdown` and list `.option` items, we targeted focus and geometric bindings appending standard padding structures matched to the dropdown height overrides dynamically into our `Dropdown.module.css`.
8
+ 4. **Popup Vertical Padding:** The gap above/below the option list inside `.dropdown` is explicitly set from the same `--recursica_..._dropdown_properties_vertical-padding` token the closed control uses — it was previously left to Mantine's own untokenized `--combobox-padding` (4px) default.
9
+ 5. **Clear Button Styling:** Mantine's native clear button (`clearButtonProps`, shown when `clearable` + a value are both present) is a bare `CloseButton` with its own hardcoded gray icon/hover styling by default. `Dropdown.tsx` merges in a `.clearButton` class (Dropdown.module.css) that overrides it with the same icon-size/trailing-icon-color/focus-ring tokens as the rest of the right section, using the double-class-selector specificity trick (same as Chip.module.css's `.root.root`) to beat Mantine's own CSS module rule without `!important`.