@recursica/mui-adapter 0.25.0 → 0.27.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 (78) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.d.ts +105 -19
  3. package/dist/mui-adapter.cjs +77 -77
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +29867 -25282
  7. package/dist/mui-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/Autocomplete/Autocomplete.module.css +0 -8
  12. package/src/components/Autocomplete/Autocomplete.tsx +46 -34
  13. package/src/components/Avatar/Avatar.module.css +0 -8
  14. package/src/components/Avatar/Avatar.tsx +11 -21
  15. package/src/components/Badge/Badge.tsx +9 -13
  16. package/src/components/Breadcrumb/Breadcrumb.tsx +12 -18
  17. package/src/components/Button/Button.module.css +0 -13
  18. package/src/components/Button/Button.tsx +17 -11
  19. package/src/components/Card/Card.tsx +9 -19
  20. package/src/components/Checkbox/Checkbox.tsx +22 -26
  21. package/src/components/Checkbox/CheckboxGroup.tsx +3 -3
  22. package/src/components/Chip/Chip.module.css +0 -27
  23. package/src/components/Chip/Chip.tsx +12 -19
  24. package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +29 -0
  25. package/src/components/DatePicker/DatePicker.icons.tsx +35 -0
  26. package/src/components/DatePicker/DatePicker.module.css +451 -42
  27. package/src/components/DatePicker/DatePicker.stories.tsx +23 -25
  28. package/src/components/DatePicker/DatePicker.tsx +229 -47
  29. package/src/components/DatePicker/USAGE.md +14 -1
  30. package/src/components/Dropdown/BareDropdown.tsx +14 -8
  31. package/src/components/Dropdown/Dropdown.module.css +0 -7
  32. package/src/components/Dropdown/Dropdown.tsx +13 -6
  33. package/src/components/FileInput/FileInput.module.css +0 -21
  34. package/src/components/FileInput/FileInput.tsx +1 -1
  35. package/src/components/FileUpload/FileUpload.module.css +0 -11
  36. package/src/components/FileUpload/FileUpload.tsx +1 -1
  37. package/src/components/FormControlLayout/FormControlLayout.tsx +1 -1
  38. package/src/components/FormControlWrapper/FormControlWrapper.tsx +1 -1
  39. package/src/components/HoverCard/HoverCard.module.css +1 -7
  40. package/src/components/HoverCard/HoverCard.tsx +13 -21
  41. package/src/components/Label/Label.module.css +0 -6
  42. package/src/components/Link/Link.module.css +0 -13
  43. package/src/components/Menu/Menu.module.css +0 -5
  44. package/src/components/Menu/Menu.tsx +14 -6
  45. package/src/components/Modal/Modal.module.css +0 -11
  46. package/src/components/Modal/Modal.tsx +12 -6
  47. package/src/components/NumberInput/NumberInput.module.css +0 -7
  48. package/src/components/NumberInput/NumberInput.tsx +30 -15
  49. package/src/components/Pagination/Pagination.module.css +0 -81
  50. package/src/components/Pagination/Pagination.tsx +5 -15
  51. package/src/components/Panel/Panel.tsx +17 -25
  52. package/src/components/Popover/Popover.module.css +0 -5
  53. package/src/components/Popover/Popover.tsx +10 -18
  54. package/src/components/Radio/Radio.tsx +21 -26
  55. package/src/components/Radio/RadioGroup.tsx +3 -4
  56. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +1 -1
  57. package/src/components/SegmentedControl/SegmentedControl.module.css +0 -8
  58. package/src/components/SegmentedControl/SegmentedControl.tsx +20 -25
  59. package/src/components/Slider/Slider.tsx +40 -43
  60. package/src/components/Stepper/Stepper.tsx +34 -14
  61. package/src/components/Switch/Switch.tsx +33 -49
  62. package/src/components/Switch/SwitchGroup.tsx +3 -3
  63. package/src/components/Tabs/Tabs.tsx +12 -4
  64. package/src/components/TextArea/TextArea.module.css +0 -7
  65. package/src/components/TextArea/TextArea.tsx +30 -7
  66. package/src/components/TextField/TextField.module.css +0 -8
  67. package/src/components/TextField/TextField.tsx +30 -26
  68. package/src/components/TimePicker/TimePicker.module.css +0 -16
  69. package/src/components/Timeline/Timeline.module.css +0 -6
  70. package/src/components/Timeline/Timeline.tsx +9 -1
  71. package/src/components/Timeline/TimelineItem.tsx +2 -2
  72. package/src/components/Toast/Toast.module.css +0 -7
  73. package/src/components/Toast/Toast.tsx +14 -23
  74. package/src/components/Tooltip/Tooltip.module.css +0 -9
  75. package/src/components/Tooltip/Tooltip.tsx +14 -27
  76. package/src/components/TransferList/TransferList.module.css +0 -8
  77. package/src/components/TransferList/TransferList.tsx +1 -1
  78. package/src/utils/filterStylingProps.ts +4 -0
@@ -1,47 +1,229 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- import React from "react";
3
- import "./DatePicker.module.css";
4
- import { type RecursicaDatePickerProps } from "@recursica/adapter-common";
5
-
6
- export type DatePickerProps = Omit<
7
- React.HTMLAttributes<HTMLDivElement>,
8
- "value" | "onChange" | "defaultValue"
9
- > &
10
- RecursicaDatePickerProps & {
11
- disabled?: boolean;
12
- label?: React.ReactNode;
13
- assistiveText?: string;
14
- error?: React.ReactNode;
15
- required?: boolean;
16
- withAsterisk?: boolean;
17
- placeholder?: string;
18
- value?: Date | null;
19
- defaultValue?: Date | null;
20
- onChange?: (value: Date | null) => void;
21
- readOnly?: boolean;
22
- formLayout?: "stacked" | "side-by-side";
23
- leftSection?: React.ReactNode;
24
- labelWithEditIcon?: boolean;
25
- onLabelEditClick?: () => void;
26
- };
27
-
28
- export const DatePicker: React.FC<DatePickerProps> = ({
29
- disabled,
30
- label,
31
- assistiveText,
32
- error,
33
- required,
34
- withAsterisk,
35
- placeholder,
36
- value,
37
- defaultValue,
38
- onChange,
39
- readOnly,
40
- formLayout,
41
- leftSection,
42
- labelWithEditIcon,
43
- onLabelEditClick,
44
- ...props
45
- }) => {
46
- return <div {...props}>DatePicker</div>;
47
- };
1
+ import React, { forwardRef, useEffect, useState } from "react";
2
+ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
3
+ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
4
+ import {
5
+ DatePicker as MuiDatePicker,
6
+ type DatePickerProps as MuiDatePickerProps,
7
+ type DatePickerSlotProps,
8
+ type DatePickerSlots,
9
+ } from "@mui/x-date-pickers/DatePicker";
10
+ import dayjs, { type Dayjs } from "dayjs";
11
+ import { type ReadOnlyControlProps } from "@recursica/adapter-common";
12
+ import {
13
+ filterStylingProps,
14
+ type RecursicaOverStyled,
15
+ } from "../../utils/filterStylingProps";
16
+ import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
17
+ import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
18
+ import { CalendarIcon } from "./DatePicker.icons";
19
+ import styles from "./DatePicker.module.css";
20
+
21
+ import { type RecursicaDatePickerProps as BaseRecursicaDatePickerProps } from "@recursica/adapter-common";
22
+
23
+ // Default display/entry format (dayjs format string), matching the mantine-adapter's DatePicker.
24
+ const DATE_FORMAT = "MM/DD/YY";
25
+
26
+ function toDayjs(value: Date | null | undefined): Dayjs | null {
27
+ return value ? dayjs(value) : null;
28
+ }
29
+
30
+ export interface RecursicaDatePickerProps
31
+ extends Omit<
32
+ MuiDatePickerProps,
33
+ | "value"
34
+ | "defaultValue"
35
+ | "onChange"
36
+ | "minDate"
37
+ | "maxDate"
38
+ | "format"
39
+ | "slots"
40
+ | "slotProps"
41
+ >,
42
+ Pick<
43
+ RecursicaFormControlWrapperProps,
44
+ | "label"
45
+ | "error"
46
+ | "required"
47
+ | "id"
48
+ | "assistiveText"
49
+ | "assistiveWithIcon"
50
+ | "formLayout"
51
+ | "labelSize"
52
+ | "labelAlignment"
53
+ | "labelOptionalText"
54
+ | "labelWithEditIcon"
55
+ | "onLabelEditClick"
56
+ >,
57
+ ReadOnlyControlProps,
58
+ BaseRecursicaDatePickerProps {
59
+ /** Selected date, matching the mantine-adapter convention of a plain `Date` value. */
60
+ value?: Date | null;
61
+ /** Uncontrolled initial date. */
62
+ defaultValue?: Date | null;
63
+ /** Fires with the new date, or `null` if cleared. */
64
+ onChange?: (value: Date | null) => void;
65
+ minDate?: Date;
66
+ maxDate?: Date;
67
+ /** Overridable component slots — e.g. `slots={{ openPickerIcon: MyIcon }}` to replace the default calendar icon. */
68
+ slots?: DatePickerSlots;
69
+ slotProps?: DatePickerSlotProps;
70
+ /** Caller-provided inline style, passed through to the FormControlWrapper root. */
71
+ style?: React.CSSProperties;
72
+ }
73
+
74
+ export type DatePickerProps = RecursicaOverStyled<RecursicaDatePickerProps>;
75
+
76
+ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
77
+ function DatePicker(props, ref) {
78
+ const {
79
+ overStyled = false,
80
+ formLayout = "stacked",
81
+
82
+ // Label & Wrapper Maps
83
+ labelSize,
84
+ labelAlignment,
85
+ labelOptionalText,
86
+ labelWithEditIcon,
87
+ onLabelEditClick,
88
+
89
+ label,
90
+ assistiveText,
91
+ assistiveWithIcon,
92
+ error,
93
+ required,
94
+ id,
95
+ className,
96
+ style,
97
+ disabled,
98
+ readOnly,
99
+ readOnlyComponent,
100
+ emptyValueComponent,
101
+ value,
102
+ defaultValue,
103
+ onChange,
104
+ minDate,
105
+ maxDate,
106
+ slots,
107
+ slotProps,
108
+ ...rest
109
+ } = props;
110
+
111
+ const sanitizedProps = filterStylingProps(rest, overStyled);
112
+
113
+ // Internal Dayjs value: MUI X's DatePicker only speaks Dayjs (via AdapterDayjs); the public
114
+ // API stays plain `Date` to match the mantine-adapter convention.
115
+ const [internalValue, setInternalValue] = useState<Dayjs | null>(
116
+ () => toDayjs(value) ?? toDayjs(defaultValue) ?? null,
117
+ );
118
+
119
+ useEffect(() => {
120
+ if (value !== undefined) {
121
+ setInternalValue(toDayjs(value));
122
+ }
123
+ }, [value]);
124
+
125
+ const wrapperClass = className
126
+ ? `${styles.layoutOverride} ${className}`
127
+ : styles.layoutOverride;
128
+
129
+ const handleChange = (next: Dayjs | null) => {
130
+ setInternalValue(next);
131
+ onChange?.(next ? next.toDate() : null);
132
+ };
133
+
134
+ const readOnlyValue = (() => {
135
+ const source = value !== undefined ? value : defaultValue;
136
+ return toDayjs(source)?.format(DATE_FORMAT);
137
+ })();
138
+
139
+ // slotProps.field/desktopPaper are typed to also allow a `(ownerState) => props` function
140
+ // form; this adapter only ever merges plain objects (same assumption TextField/DatePicker's
141
+ // mantine counterpart make for consumer-supplied classNames), so narrow to that shape here.
142
+ const consumerFieldProps =
143
+ slotProps?.field && typeof slotProps.field === "object"
144
+ ? (slotProps.field as Record<string, unknown>)
145
+ : undefined;
146
+ const consumerDesktopPaperProps =
147
+ slotProps?.desktopPaper && typeof slotProps.desktopPaper === "object"
148
+ ? (slotProps.desktopPaper as Record<string, unknown>)
149
+ : undefined;
150
+
151
+ return (
152
+ <WithReadOnlyWrapper
153
+ ref={ref}
154
+ className={wrapperClass}
155
+ style={style}
156
+ controlMaxWidth={undefined}
157
+ controlMinWidth={undefined}
158
+ overStyled={overStyled as true}
159
+ formLayout={formLayout}
160
+ labelSize={labelSize}
161
+ labelAlignment={labelAlignment}
162
+ labelOptionalText={labelOptionalText}
163
+ labelWithEditIcon={labelWithEditIcon}
164
+ onLabelEditClick={onLabelEditClick}
165
+ label={label}
166
+ assistiveText={assistiveText}
167
+ assistiveWithIcon={assistiveWithIcon}
168
+ error={error}
169
+ required={required}
170
+ id={id}
171
+ readOnly={readOnly}
172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
173
+ readOnlyComponent={readOnlyComponent as any}
174
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
+ emptyValueComponent={(emptyValueComponent as any) || undefined}
176
+ readOnlyType="text"
177
+ readOnlyValue={readOnlyValue}
178
+ readOnlyNativeProps={props}
179
+ activeComponent={
180
+ /* Naked field execution safely decoupled from MUI X's own label/error macro handling,
181
+ same convention as TimePicker.tsx. Unlike TimePicker, the popup calendar IS used here
182
+ (and token-styled below) — see DATEPICKER_IMPLEMENTATION_NOTES.md. */
183
+ <div className={styles.root} data-error={error ? "true" : undefined}>
184
+ <LocalizationProvider dateAdapter={AdapterDayjs}>
185
+ <MuiDatePicker
186
+ {...(sanitizedProps as unknown as Partial<MuiDatePickerProps>)}
187
+ value={internalValue}
188
+ onChange={handleChange}
189
+ disabled={disabled}
190
+ format={DATE_FORMAT}
191
+ minDate={toDayjs(minDate) ?? undefined}
192
+ maxDate={toDayjs(maxDate) ?? undefined}
193
+ slots={{ openPickerIcon: CalendarIcon, ...slots }}
194
+ slotProps={
195
+ {
196
+ ...slotProps,
197
+ field: {
198
+ // Calendar icon opens the picker from the left side of the field, mirroring
199
+ // the mantine-adapter's default `leftSection`; overridable via
200
+ // `slots.openPickerIcon`. Only settable per-field, not at the picker's own
201
+ // top level (MUI X has no top-level `openPickerButtonPosition` prop).
202
+ openPickerButtonPosition: "start",
203
+ ...consumerFieldProps,
204
+ className: consumerFieldProps?.className
205
+ ? `${styles.field} ${consumerFieldProps.className as string}`
206
+ : styles.field,
207
+ },
208
+ // The popup surface is portaled to document.body, outside this component's own
209
+ // DOM subtree, so — same constraint mantine-adapter's `popoverProps.classNames`
210
+ // documents — it can only be reached by handing it a className directly, not a
211
+ // descendant CSS selector.
212
+ desktopPaper: {
213
+ ...consumerDesktopPaperProps,
214
+ className: consumerDesktopPaperProps?.className
215
+ ? `${styles.dropdown} ${consumerDesktopPaperProps.className as string}`
216
+ : styles.dropdown,
217
+ },
218
+ } as DatePickerSlotProps
219
+ }
220
+ />
221
+ </LocalizationProvider>
222
+ </div>
223
+ }
224
+ />
225
+ );
226
+ },
227
+ );
228
+
229
+ DatePicker.displayName = "DatePicker";
@@ -19,7 +19,7 @@ import React from "react";
19
19
  import { DatePicker } from "@recursica/mui-adapter";
20
20
 
21
21
  export default function Demo() {
22
- return <DatePicker label="Select Date" placeholder="Pick a date" />;
22
+ return <DatePicker label="Select Date" />;
23
23
  }
24
24
  ```
25
25
 
@@ -34,3 +34,16 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
34
34
  > - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
35
35
  > - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
36
36
  > - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
37
+
38
+ ---
39
+
40
+ ## 4. Key Integration Features & Constraints
41
+
42
+ ## Structural Constraints
43
+
44
+ 1. **Value Type**: `value`/`defaultValue`/`onChange` all speak plain `Date` objects (not MUI X's native `Dayjs`), matching the mantine-adapter's DatePicker convention — conversion to/from `Dayjs` happens internally.
45
+ 2. **Read-Only Rendering**: When rendered in read-only mode, the selected date is displayed formatted as `MM/DD/YY`. If you need custom date formatting, pass a `readOnlyComponent` prop to control how the value is rendered.
46
+ 3. **Calendar Popover Styling**: The UI Kit exports no calendar-specific tokens (surface, selected day, hover, today, in-range), so the calendar reuses the closest existing tokens — see `DATEPICKER_IMPLEMENTATION_NOTES.md` for the exact mapping, mirrored 1:1 from the mantine-adapter. The header's prev/next arrows and month-view toggle are styled as Recursica `text`-variant buttons.
47
+ 4. **Typing vs. Popover Entry**: Unlike the mantine-adapter (popover-only selection), MUI X's field supports typing a date directly into its masked `MM/DD/YY` segments, in addition to picking from the popup calendar.
48
+ 5. **Default Display/Entry Format**: `MM/DD/YY` (e.g. `08/19/26`), matching the mantine-adapter; pass your own `format` (a `dayjs` format string) via the standard MUI X `DatePicker` props to override.
49
+ 6. **Default Leading Icon**: A calendar icon opens the picker from the start of the field by default; pass `slots={{ openPickerIcon: MyIcon }}` to override it.
@@ -6,6 +6,7 @@ import {
6
6
  } from "@mui/material";
7
7
  import {
8
8
  filterStylingProps,
9
+ omitUnsupportedProps,
9
10
  type RecursicaOverStyled,
10
11
  } from "../../utils/filterStylingProps";
11
12
  import styles from "./Dropdown.module.css";
@@ -52,14 +53,19 @@ export const BareDropdown = forwardRef<
52
53
  error,
53
54
  ...rest
54
55
  } = props;
55
- const sanitizedProps = filterStylingProps(rest, overStyled);
56
- const restRecord = sanitizedProps as Record<string, unknown>;
56
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
57
+ // through even if a caller forces them via plain JavaScript, bypassing the Omit<> above.
58
+ const UNSUPPORTED_PROPS = [
59
+ "size", // Recursica controls sizing via design tokens, not MUI's native small/medium size
60
+ "variant", // Recursica styles the naked select directly; MUI's standard/filled/outlined unused
61
+ "classes", // Recursica computes its own classes object below; caller's would silently clobber it
62
+ ] as const satisfies readonly (keyof MuiSelectProps)[];
57
63
 
58
- delete restRecord["size"];
59
- delete restRecord["variant"];
60
- // className is merged explicitly below — don't let the spread further down silently overwrite
61
- // styles.root with just the caller's own class.
62
- delete restRecord["className"];
64
+ const sanitizedProps = omitUnsupportedProps(
65
+ filterStylingProps(rest, overStyled),
66
+ UNSUPPORTED_PROPS,
67
+ );
68
+ const restRecord = sanitizedProps as Record<string, unknown>;
63
69
 
64
70
  const mergedClassName = className
65
71
  ? `${styles.root} ${className}`
@@ -100,6 +106,7 @@ export const BareDropdown = forwardRef<
100
106
  return (
101
107
  <MuiSelect
102
108
  ref={ref}
109
+ {...(sanitizedProps as unknown as MuiSelectProps)}
103
110
  disabled={disabled}
104
111
  value={value}
105
112
  defaultValue={defaultValue}
@@ -125,7 +132,6 @@ export const BareDropdown = forwardRef<
125
132
  "data-error": error ? "true" : undefined,
126
133
  ...(restRecord.inputProps as Record<string, unknown>),
127
134
  }}
128
- {...(sanitizedProps as unknown as MuiSelectProps)}
129
135
  >
130
136
  {renderOptions()}
131
137
  </MuiSelect>
@@ -1,10 +1,3 @@
1
- /* EXEMPTIONS:
2
- - state-specific border-size variables are ignored because a uniform 1px border is applied globally to prevent
3
- unexpected layout shift or flickering during focus, disabled, or error state transitions. */
4
- /* recursica-ignore: --recursica_ui-kit_components_dropdown_properties_border-size */
5
- /* recursica-ignore: --recursica_ui-kit_components_dropdown_variants_states_disabled_properties_border-size */
6
- /* recursica-ignore: --recursica_ui-kit_components_dropdown_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 --dropdown-control-{max,min}-width hooks consumed inline in Dropdown.tsx, since
@@ -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";
@@ -73,12 +74,18 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
73
74
  clearable, // Not natively supported by basic MUI Select, stubbed
74
75
  ...rest
75
76
  } = props;
76
- const sanitizedProps = filterStylingProps(rest, overStyled);
77
- const restRecord = sanitizedProps as Record<string, unknown>;
77
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
78
+ // through even if a caller forces them via plain JavaScript, bypassing the Omit<> above.
79
+ const UNSUPPORTED_PROPS = [
80
+ "size", // Recursica controls sizing via design tokens, not MUI's native small/medium size
81
+ "variant", // Recursica styles the naked select directly; MUI's standard/filled/outlined unused
82
+ ] as const satisfies readonly (keyof MuiSelectProps)[];
78
83
 
79
- // Delete prohibited sizing hooks
80
- delete restRecord["size"];
81
- delete restRecord["variant"];
84
+ const sanitizedProps = omitUnsupportedProps(
85
+ filterStylingProps(rest, overStyled),
86
+ UNSUPPORTED_PROPS,
87
+ );
88
+ const restRecord = sanitizedProps as Record<string, unknown>;
82
89
 
83
90
  const injectedStyles = {
84
91
  ...((style as React.CSSProperties) || {}),
@@ -159,6 +166,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
159
166
  activeComponent={
160
167
  <MuiSelect
161
168
  ref={ref}
169
+ {...(sanitizedProps as unknown as MuiSelectProps)}
162
170
  disabled={disabled}
163
171
  value={value}
164
172
  defaultValue={defaultValue}
@@ -185,7 +193,6 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
185
193
  "data-error": error ? "true" : undefined,
186
194
  ...(restRecord.inputProps as Record<string, unknown>),
187
195
  }}
188
- {...(sanitizedProps as unknown as MuiSelectProps)}
189
196
  >
190
197
  {renderOptions()}
191
198
  </MuiSelect>
@@ -1,24 +1,3 @@
1
- /* EXEMPTIONS:
2
- - The disabled/error states' own border-size variables are ignored so the border never changes
3
- thickness across states and shifts the layout — same house policy as TextField/DatePicker.
4
- The flat, state-agnostic properties_border-size token below is applied uniformly instead.
5
- - There is no forge-defined focus state for file-input (no `states.focus` axis, unlike its
6
- disabled/error siblings) — the generic recursica_brand_states_focus_* ring is used instead,
7
- same fallback TextField/DatePicker use for the same reason.
8
- - properties_icon-text-gap doubles as the gap between chips: file-input has no dedicated
9
- chip-gap token (unlike file-upload's properties_item-gap).
10
- - The trailing clear-all affordance is now a real shared `Button` (icon-only, "text" variant)
11
- rather than a bespoke span, so it gets its own real button semantics/keyboard handling —
12
- same pattern as Tree's expand/collapse button. It renders through Button's own tokened
13
- color states (including disabled), so file-input's own trailing-icon color tokens — which
14
- have no "clear button is disabled/errored" equivalent state of their own anyway — are no
15
- longer consumed here. */
16
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_border-size */
17
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_border-size */
18
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_colors_trailing-icon */
19
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_trailing-icon */
20
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_trailing-icon */
21
-
22
1
  /* LAYOUT SPACING OVERRIDES:
23
2
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
24
3
  - Also sets the --file-input-control-{max,min}-width hooks consumed inline in FileInput.tsx,
@@ -308,6 +308,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
308
308
  >
309
309
  <div
310
310
  ref={ref}
311
+ {...restRecord}
311
312
  className={styles.root}
312
313
  role="button"
313
314
  aria-label={browseLabel}
@@ -323,7 +324,6 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
323
324
  onDragLeave={interactive ? handleDragLeave : undefined}
324
325
  onDragOver={interactive ? handleDragOver : undefined}
325
326
  onDrop={interactive ? handleDrop : undefined}
326
- {...restRecord}
327
327
  >
328
328
  <span className={styles.leadingIcon} aria-hidden>
329
329
  {icon ?? <UploadIcon />}
@@ -1,14 +1,3 @@
1
- /* EXEMPTIONS:
2
- - border-style has no usable var(...) form: the Figma-exported token's value is the literal
3
- string `"dashed"` (quotes included), which is not a valid CSS <line-style> keyword — the same
4
- class of "structural, not tokenized" property the canonical guide already treats border-style
5
- as (see .root's own HARDCODED VALUES note below and TimePicker/DatePicker's identical
6
- hardcoded `border-style: solid`). Hardcoded to the keyword `dashed` instead.
7
- - properties_icon-size has no equivalent: unlike TextField/DatePicker, file-upload has no
8
- dedicated icon-size token. Sized to visually match the Figma reference instead (see
9
- HARDCODED VALUES below). */
10
- /* recursica-ignore: --recursica_ui-kit_components_file-upload_properties_border-style */
11
-
12
1
  /* HARDCODED VALUES:
13
2
  - .root: display: flex; flex-direction: column; width: 100% — structural layout, not a design
14
3
  token concern (matches how other block-level components like Layer are laid out).
@@ -267,10 +267,10 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
267
267
  >
268
268
  <div
269
269
  ref={ref}
270
+ {...restRecord}
270
271
  className={styles.root}
271
272
  data-disabled={disabled ? "true" : undefined}
272
273
  data-error={effectiveError ? "true" : undefined}
273
- {...restRecord}
274
274
  >
275
275
  {!readOnly && (
276
276
  <div
@@ -32,6 +32,7 @@ export const FormControlLayout = React.forwardRef<
32
32
  return (
33
33
  <div
34
34
  ref={ref}
35
+ {...sanitizedProps}
35
36
  className={className ? `${styles.root} ${className}` : styles.root}
36
37
  data-form-layout={formLayout}
37
38
  style={
@@ -45,7 +46,6 @@ export const FormControlLayout = React.forwardRef<
45
46
  : {}),
46
47
  } as React.CSSProperties
47
48
  }
48
- {...sanitizedProps}
49
49
  >
50
50
  {/*
51
51
  The left section strictly enforces the Label boundary sizes
@@ -94,6 +94,7 @@ export const FormControlWrapper = React.forwardRef<
94
94
  return (
95
95
  <FormControl
96
96
  ref={ref}
97
+ {...(sanitizedProps as FormControlProps)}
97
98
  error={!!error}
98
99
  required={required}
99
100
  disabled={disabled}
@@ -103,7 +104,6 @@ export const FormControlWrapper = React.forwardRef<
103
104
  data-error={error ? "true" : undefined}
104
105
  data-disabled={disabled ? "true" : undefined}
105
106
  data-focused={focused ? "true" : undefined}
106
- {...(sanitizedProps as FormControlProps)}
107
107
  >
108
108
  <FormControlLayout
109
109
  formLayout={formLayout}
@@ -5,13 +5,7 @@
5
5
  and positioning (-arrowSize/2) calculations that cannot be CSS-driven. The default matches
6
6
  the Recursica beak-size token (16px). See COMPONENT_ISSUES.md for details.
7
7
  - All structural layout (display, position, overflow) is deferred to Mantine's native
8
- behavior. We only override visual design tokens (colors, typography, spacing, borders).
9
- /* EXEMPTIONS:
10
- - beak-size is ignored here because Mantine's popover arrows compute their sizing and positional offsets
11
- dynamically within the React rendering engine (requires inline pixel number rather than standard CSS variables).
12
- The beak size is checked in JS against a fixed value of 16px. If this variable changes, the TSX components
13
- need to be reviewed/re-compiled. */
14
- /* recursica-ignore: --recursica_ui-kit_components_hover-card-popover_properties_beak-size = 16px */
8
+ behavior. We only override visual design tokens (colors, typography, spacing, borders). */
15
9
 
16
10
  /* ======================================
17
11
  DROPDOWN CONTAINER
@@ -5,6 +5,7 @@ import {
5
5
  } from "@mui/material";
6
6
  import {
7
7
  filterStylingProps,
8
+ mergeClassNames,
8
9
  type RecursicaOverStyled,
9
10
  } from "../../utils/filterStylingProps";
10
11
  import styles from "./HoverCard.module.css";
@@ -35,27 +36,18 @@ const HoverCardBase = function HoverCard({
35
36
  overStyled,
36
37
  );
37
38
 
38
- // Bind CSS module classes to Mui's internal classNames API
39
- const mergedClassNames: Partial<Record<string, string>> = {
40
- tooltip: styles.dropdown,
41
- arrow: styles.arrow,
42
- };
43
-
44
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
45
- if (
46
- classNamesProp &&
47
- typeof classNamesProp === "object" &&
48
- !Array.isArray(classNamesProp)
49
- ) {
50
- const o = classNamesProp as Record<string, string>;
51
- Object.keys(o).forEach((key) => {
52
- if (mergedClassNames[key]) {
53
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
54
- } else {
55
- mergedClassNames[key] = o[key];
56
- }
57
- });
58
- }
39
+ // Bind CSS module classes to Mui's internal classNames API. Note MUI's actual prop is
40
+ // "classes", not "classNames" (that's Mantine's naming) — this used to read the wrong key,
41
+ // silently no-op-ing any caller-supplied classes.
42
+ const mergedClassNames = mergeClassNames(
43
+ {
44
+ tooltip: styles.dropdown,
45
+ arrow: styles.arrow,
46
+ },
47
+ (sanitizedProps as Record<string, unknown>).classes as
48
+ | Partial<Record<string, string>>
49
+ | undefined,
50
+ );
59
51
 
60
52
  // Find Target and Dropdown children
61
53
  let targetNode: React.ReactNode = null;
@@ -1,9 +1,3 @@
1
- /* EXEMPTIONS:
2
- - layouts_stacked sizes width variables are ignored because stacked labels naturally occupy 100% width block-level
3
- real estate in HTML rendering, making explicit width properties redundant. Mirrors the mantine-adapter reference. */
4
- /* recursica-ignore: --recursica_ui-kit_components_label_variants_layouts_stacked_variants_sizes_default_properties_width */
5
- /* recursica-ignore: --recursica_ui-kit_components_label_variants_layouts_stacked_variants_sizes_small_properties_width */
6
-
7
1
  .root {
8
2
  /* Prevent default MUI absolute positioning logic */
9
3
  position: relative;
@@ -1,8 +1,3 @@
1
- /* EXEMPTIONS:
2
- - text_text-transform is ignored because links naturally inherit casing directly from text
3
- children rather than requiring custom CSS transforms. Mirrors the mantine-adapter reference. */
4
- /* recursica-ignore: --recursica_ui-kit_components_link_properties_text_text-transform */
5
-
6
1
  /*
7
2
  HARDCODED VALUES:
8
3
  - display: inline-flex; (Base layout to align icon and text correctly)
@@ -79,11 +74,3 @@ HARDCODED VALUES:
79
74
  /* HARDCODE: allows text truncation if bounded */
80
75
  min-width: 0;
81
76
  }
82
-
83
- /* EXEMPTIONS:
84
- text_text-transform across multiple link states (default, hover, visited, visited-hover) is ignored
85
- because links naturally inherit casing directly from text children rather than requiring custom CSS transforms. */
86
- /* recursica-ignore: --recursica_ui-kit_components_link_variants_states_default_properties_text_text-transform */
87
- /* recursica-ignore: --recursica_ui-kit_components_link_variants_states_hover_properties_text_text-transform */
88
- /* recursica-ignore: --recursica_ui-kit_components_link_variants_states_visited_properties_text_text-transform */
89
- /* recursica-ignore: --recursica_ui-kit_components_link_variants_states_visited-hover_properties_text_text-transform */
@@ -1,8 +1,3 @@
1
- /* EXEMPTIONS:
2
- - colors_supporting-text-color (selected variant) is ignored because selected menu items do not render
3
- supporting secondary text, making the selected-state override for supporting text colors obsolete. */
4
- /* recursica-ignore: --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_supporting-text-color */
5
-
6
1
  /* HARDCODED VALUES:
7
2
  - border-style: solid. Structural rendering rule for the dropdown border (Mantine uses Paper which may not set border natively).
8
3
  - NOTE: overflow is intentionally NOT set on .dropdown. Mantine renders sub-menu dropdowns