@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
@@ -169,6 +169,39 @@
169
169
  );
170
170
  }
171
171
 
172
+ /* Mantine's own clear button (rendered via `clearButtonProps`, see Dropdown.tsx) is a bare
173
+ `CloseButton` by default — its own module CSS hardcodes a gray icon color and a circular gray
174
+ hover background, neither of which is Recursica-token-driven. The double class selector (same
175
+ specificity-boosting trick as Chip.module.css's `.root.root`) beats that default without
176
+ needing `!important`. Same icon-size/trailing-icon tokens as the rest of this section, same
177
+ focus-visible ring technique as Chip's own `.removeIcon` — the established inline icon-button
178
+ pattern elsewhere in this adapter. */
179
+ .clearButton.clearButton {
180
+ width: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
181
+ height: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
182
+ min-width: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
183
+ min-height: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
184
+ border-radius: 50%;
185
+ background: transparent;
186
+ color: var(
187
+ --recursica_ui-kit_components_dropdown_properties_colors_trailing-icon
188
+ );
189
+ outline: none;
190
+ }
191
+
192
+ .clearButton.clearButton:hover {
193
+ background: transparent;
194
+ }
195
+
196
+ .clearButton.clearButton:focus-visible {
197
+ box-shadow:
198
+ 0 0 0 var(--recursica_brand_states_focus_border-size)
199
+ var(--recursica_brand_states_focus_color),
200
+ 0 0 var(--recursica_brand_states_focus_blur)
201
+ var(--recursica_brand_states_focus_margin)
202
+ var(--recursica_brand_states_focus_color);
203
+ }
204
+
172
205
  /* -------------------------------------
173
206
  STATE CASCADE ARCHITECTURE
174
207
  -------------------------------------- */
@@ -254,6 +287,17 @@
254
287
  --recursica_ui-kit_components_dropdown_properties_border-radius
255
288
  );
256
289
  overflow: hidden;
290
+
291
+ /* The visible top/bottom gap before/after the option list was previously left to Mantine's own
292
+ untokenized `--combobox-padding` default (4px) — wire it to the same dropdown vertical-padding
293
+ token the closed control itself uses instead (mirrors the mui-adapter fix to the equivalent
294
+ gap there, which came from MUI's own untokenized 8px `.MuiList-padding` default). */
295
+ padding-top: var(
296
+ --recursica_ui-kit_components_dropdown_properties_vertical-padding
297
+ );
298
+ padding-bottom: var(
299
+ --recursica_ui-kit_components_dropdown_properties_vertical-padding
300
+ );
257
301
  }
258
302
 
259
303
  .option {
@@ -277,10 +321,8 @@
277
321
  cursor: pointer;
278
322
  }
279
323
 
280
- /* No per-option selected/hovered tokens exist in the schema; use the generic overlay tint
281
- (same technique as Table's row hover) to highlight the active option. */
282
- .option[data-selected="true"],
283
- .option[data-combobox-selected="true"],
324
+ /* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
325
+ as Table's row hover) to highlight a hovered-but-not-selected option. */
284
326
  .option[data-hovered="true"],
285
327
  .option:hover {
286
328
  background-color: color-mix(
@@ -290,3 +332,18 @@
290
332
  transparent
291
333
  );
292
334
  }
335
+
336
+ /* No dedicated dropdown-option "selected" token exists in the schema either, but the menu-item
337
+ component's selected-state colors are the closest real token family for the same concept (a
338
+ selected row in a list) — reused here rather than the neutral hover tint above, which is what
339
+ previously made the selected option read as grey instead of Recursica's brand color. Matches
340
+ the mui-adapter fix for the same gap. */
341
+ .option[data-selected="true"],
342
+ .option[data-combobox-selected="true"] {
343
+ background-color: var(
344
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_background-color
345
+ );
346
+ color: var(
347
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
348
+ );
349
+ }
@@ -65,6 +65,27 @@ export const SearchableClearable: Story = {
65
65
  },
66
66
  };
67
67
 
68
+ export const WithLeadingIcon: Story = {
69
+ args: {
70
+ label: "Destination",
71
+ leftSection: (
72
+ <svg
73
+ width="24"
74
+ height="24"
75
+ viewBox="0 0 24 24"
76
+ fill="none"
77
+ stroke="currentColor"
78
+ strokeWidth="2"
79
+ strokeLinecap="round"
80
+ strokeLinejoin="round"
81
+ >
82
+ <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
83
+ <circle cx="12" cy="10" r="3"></circle>
84
+ </svg>
85
+ ),
86
+ },
87
+ };
88
+
68
89
  export const StaticError: Story = {
69
90
  args: {
70
91
  error: "You must choose a valid destination.",
@@ -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 RecursicaDropdownProps
28
30
 
29
31
  export type DropdownProps = RecursicaOverStyled<RecursicaDropdownProps>;
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 MantineSelectProps)[];
40
+
31
41
  export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
32
42
  function Dropdown(props, ref) {
33
43
  const {
@@ -58,48 +68,41 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
58
68
  value,
59
69
  defaultValue,
60
70
  data,
71
+ clearButtonProps,
61
72
  ...rest
62
73
  } = props;
63
- const sanitizedProps = filterStylingProps(rest, overStyled);
74
+ const sanitizedProps = omitUnsupportedProps(
75
+ filterStylingProps(rest, overStyled) as Record<string, unknown>,
76
+ UNSUPPORTED_PROPS,
77
+ );
64
78
  const restRecord = sanitizedProps as Record<string, unknown>;
65
79
 
66
- // Delete prohibited sizing hooks from bypassing variables natively
67
- delete restRecord["size"];
68
- delete restRecord["variant"];
69
- delete restRecord["radius"];
70
-
71
- // 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
- dropdown: styles.dropdown,
77
- option: styles.option,
80
+ // Mantine's own clear button (rendered when `clearable` + a value are both present) otherwise
81
+ // renders unstyled — bare `CloseButton` defaults, no Recursica icon-button treatment. Merge in
82
+ // our own class (see `.clearButton` in Dropdown.module.css) alongside anything the caller
83
+ // already passed, same merge shape as `mergeClassNames` but for a single `className` string
84
+ // rather than a per-slot classNames map.
85
+ const consumerClearButtonProps = clearButtonProps as
86
+ | Record<string, unknown>
87
+ | undefined;
88
+ const mergedClearButtonProps = {
89
+ ...consumerClearButtonProps,
90
+ className: consumerClearButtonProps?.className
91
+ ? `${styles.clearButton} ${consumerClearButtonProps.className as string}`
92
+ : styles.clearButton,
78
93
  };
79
94
 
80
- const classNamesProp = restRecord.classNames;
81
- if (
82
- classNamesProp &&
83
- typeof classNamesProp === "object" &&
84
- !Array.isArray(classNamesProp)
85
- ) {
86
- const o = classNamesProp as Partial<Record<string, string>>;
87
- mergedClassNames.wrapper = o.wrapper
88
- ? `${styles.root} ${o.wrapper}`
89
- : styles.root;
90
- mergedClassNames.input = o.input
91
- ? `${styles.input} ${o.input}`
92
- : styles.input;
93
- mergedClassNames.section = o.section
94
- ? `${styles.section} ${o.section}`
95
- : styles.section;
96
- mergedClassNames.dropdown = o.dropdown
97
- ? `${styles.dropdown} ${o.dropdown}`
98
- : styles.dropdown;
99
- mergedClassNames.option = o.option
100
- ? `${styles.option} ${o.option}`
101
- : styles.option;
102
- }
95
+ // Securely map core native blocks down ensuring nested CSS modules map precisely
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
+ dropdown: styles.dropdown,
102
+ option: styles.option,
103
+ },
104
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
105
+ );
103
106
 
104
107
  const injectedStyles = {
105
108
  ...((style as React.CSSProperties) || {}),
@@ -146,11 +149,13 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
146
149
  /* Naked Select execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks */
147
150
  <MantineSelect
148
151
  ref={ref}
152
+ {...(sanitizedProps as unknown as MantineSelectProps)}
149
153
  classNames={mergedClassNames}
150
154
  disabled={disabled}
151
155
  value={value}
152
156
  defaultValue={defaultValue}
153
157
  data={(data as unknown as MantineSelectProps["data"]) || []}
158
+ clearButtonProps={mergedClearButtonProps}
154
159
  label={undefined}
155
160
  description={undefined}
156
161
  error={undefined}
@@ -169,7 +174,6 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
169
174
  "data-error": error ? "true" : undefined,
170
175
  },
171
176
  }}
172
- {...(sanitizedProps as unknown as MantineSelectProps)}
173
177
  />
174
178
  }
175
179
  />
@@ -45,3 +45,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
45
45
  ## 4. Notes
46
46
 
47
47
  - `Dropdown` is styled independently from `TextField`; even though they look similar, they are themed using separate design tokens.
48
+ - Pass `leftSection` for a leading icon, and `clearable` (with a value present) to show a clear button — both render using the dropdown's own icon-color tokens.
@@ -309,6 +309,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
309
309
  >
310
310
  <div
311
311
  ref={ref}
312
+ {...restRecord}
312
313
  className={styles.root}
313
314
  role="button"
314
315
  aria-label={browseLabel}
@@ -324,7 +325,6 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
324
325
  onDragLeave={interactive ? handleDragLeave : undefined}
325
326
  onDragOver={interactive ? handleDragOver : undefined}
326
327
  onDrop={interactive ? handleDrop : undefined}
327
- {...restRecord}
328
328
  >
329
329
  <span className={styles.leadingIcon} aria-hidden>
330
330
  {icon ?? <UploadIcon />}
@@ -268,10 +268,10 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
268
268
  >
269
269
  <div
270
270
  ref={ref}
271
+ {...restRecord}
271
272
  className={styles.root}
272
273
  data-disabled={disabled ? "true" : undefined}
273
274
  data-error={effectiveError ? "true" : undefined}
274
- {...restRecord}
275
275
  >
276
276
  {!readOnly && (
277
277
  <div
@@ -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 "./HoverCard.module.css";
@@ -42,54 +43,37 @@ export type HoverCardProps = RecursicaOverStyled<
42
43
  const HoverCardBase = function HoverCard({
43
44
  overStyled = false,
44
45
  withBeak = true,
46
+ position = "top", // Recursica default; Mantine defaults to "bottom". Consumers can still override.
47
+ // arrowSize must be a JS number prop — Mantine uses it for inline width/height
48
+ // and positioning offset (-arrowSize/2) calculations that cannot be CSS-driven.
49
+ // Default to 16 to match the Recursica beak-size token (16px). Consumers can still override.
50
+ arrowSize = 16,
51
+ withArrow,
45
52
  ...rest
46
53
  }: HoverCardProps) {
47
54
  const sanitizedProps = filterStylingProps(rest, overStyled);
55
+ const restRecord = sanitizedProps as Record<string, unknown>;
48
56
 
49
57
  // Bind CSS module classes to Mantine's internal classNames API
50
- const mergedClassNames: Partial<Record<string, string>> = {
51
- dropdown: styles.dropdown,
52
- arrow: styles.arrow,
53
- };
54
-
55
- const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
56
- if (
57
- classNamesProp &&
58
- typeof classNamesProp === "object" &&
59
- !Array.isArray(classNamesProp)
60
- ) {
61
- const o = classNamesProp as Record<string, string>;
62
- Object.keys(o).forEach((key) => {
63
- if (mergedClassNames[key]) {
64
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
65
- } else {
66
- mergedClassNames[key] = o[key];
67
- }
68
- });
69
- }
70
-
71
- // arrowSize must be a JS number prop — Mantine uses it for inline width/height
72
- // and positioning offset (-arrowSize/2) calculations that cannot be CSS-driven.
73
- // Default to 16 to match the Recursica beak-size token (16px).
74
- const arrowSize =
75
- ((sanitizedProps as Record<string, unknown>).arrowSize as
76
- | number
77
- | undefined) ?? 16;
58
+ const mergedClassNames = mergeClassNames(
59
+ {
60
+ dropdown: styles.dropdown,
61
+ arrow: styles.arrow,
62
+ },
63
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
64
+ );
78
65
 
79
66
  // Resolve withBeak (Recursica) vs withArrow (Mantine).
80
67
  // withBeak takes precedence when both are provided.
81
- const withArrow = (sanitizedProps as Record<string, unknown>).withArrow as
82
- | boolean
83
- | undefined;
84
68
  const resolvedWithArrow = withBeak ?? withArrow;
85
69
 
86
70
  return (
87
71
  <MantineHoverCard
88
- position="top" /* Recursica default; Mantine defaults to "bottom" */
72
+ {...(sanitizedProps as unknown as MantineHoverCardProps)}
73
+ position={position}
89
74
  arrowSize={arrowSize}
90
75
  withArrow={resolvedWithArrow}
91
76
  classNames={mergedClassNames}
92
- {...(sanitizedProps as unknown as MantineHoverCardProps)}
93
77
  />
94
78
  );
95
79
  };
@@ -128,8 +112,8 @@ const HoverCardDropdown = function HoverCardDropdown({
128
112
 
129
113
  return (
130
114
  <MantineHoverCard.Dropdown
131
- className={classNameProp}
132
115
  {...(sanitizedProps as unknown as MantineHoverCardDropdownProps)}
116
+ className={classNameProp}
133
117
  />
134
118
  );
135
119
  };
@@ -2,6 +2,8 @@ import React, { forwardRef } from "react";
2
2
  import { Input, type InputLabelProps } from "@mantine/core";
3
3
  import {
4
4
  filterStylingProps,
5
+ omitUnsupportedProps,
6
+ mergeClassNames,
5
7
  type RecursicaOverStyled,
6
8
  } from "../../utils/filterStylingProps";
7
9
  import styles from "./Label.module.css";
@@ -35,28 +37,25 @@ export const Label = forwardRef<HTMLLabelElement, LabelProps>(function Label(
35
37
  else if (labelOptionalText) resolvedOptionalText = labelOptionalText;
36
38
  }
37
39
 
38
- const sanitizedProps = filterStylingProps(rest, overStyled);
39
- const restRecord = sanitizedProps as Record<string, unknown>;
40
+ // Props this component intentionally doesn't support — deleted at runtime so they can't leak
41
+ // through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
42
+ const UNSUPPORTED_PROPS = [
43
+ "size", // Recursica controls label sizing via `labelSize` + design tokens, not Mantine's native `size`.
44
+ ] as const satisfies readonly (keyof InputLabelProps)[];
40
45
 
41
- const mergedClassNames: Partial<Record<string, string>> = {
42
- label: styles.root,
43
- required: styles.required,
44
- };
46
+ const sanitizedProps = omitUnsupportedProps(
47
+ filterStylingProps(rest, overStyled),
48
+ UNSUPPORTED_PROPS,
49
+ );
50
+ const restRecord = sanitizedProps as Record<string, unknown>;
45
51
 
46
- const classNamesProp = restRecord.classNames;
47
- if (
48
- classNamesProp &&
49
- typeof classNamesProp === "object" &&
50
- !Array.isArray(classNamesProp)
51
- ) {
52
- const o = classNamesProp as Partial<Record<string, string>>;
53
- mergedClassNames.label = o.label
54
- ? `${styles.root} ${o.label}`
55
- : styles.root;
56
- mergedClassNames.required = o.required
57
- ? `${styles.required} ${o.required}`
58
- : styles.required;
59
- }
52
+ const mergedClassNames = mergeClassNames(
53
+ {
54
+ label: styles.root,
55
+ required: styles.required,
56
+ },
57
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
58
+ );
60
59
 
61
60
  const classNameProp = restRecord.className as string | undefined;
62
61
  const finalClass = classNameProp
@@ -66,12 +65,12 @@ export const Label = forwardRef<HTMLLabelElement, LabelProps>(function Label(
66
65
  return (
67
66
  <Input.Label
68
67
  ref={ref}
68
+ {...sanitizedProps}
69
69
  className={finalClass}
70
70
  classNames={mergedClassNames}
71
71
  data-size={labelSize}
72
72
  data-alignment={resolvedAlignment}
73
73
  required={required && !labelWithEditIcon}
74
- {...sanitizedProps}
75
74
  >
76
75
  <span className={styles.labelText}>{children}</span>
77
76
  {resolvedOptionalText && (
@@ -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 "./Link.module.css";
@@ -23,19 +24,10 @@ const _Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
23
24
  const sanitizedProps = filterStylingProps(rest, overStyled);
24
25
  const restRecord = sanitizedProps as Record<string, unknown>;
25
26
 
26
- const mergedClassNames: Partial<Record<string, string>> = {
27
- root: styles.root,
28
- };
29
-
30
- const classNamesProp = restRecord.classNames;
31
- if (
32
- classNamesProp &&
33
- typeof classNamesProp === "object" &&
34
- !Array.isArray(classNamesProp)
35
- ) {
36
- const o = classNamesProp as Partial<Record<string, string>>;
37
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
38
- }
27
+ const mergedClassNames = mergeClassNames(
28
+ { root: styles.root },
29
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
30
+ );
39
31
 
40
32
  const classNameProp = restRecord.className as string | undefined;
41
33
  const finalClass = classNameProp
@@ -45,11 +37,11 @@ const _Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
45
37
  return (
46
38
  <MantineAnchor
47
39
  ref={ref}
40
+ {...(sanitizedProps as Record<string, unknown>)}
48
41
  className={finalClass}
49
42
  classNames={mergedClassNames}
50
43
  underline="never"
51
44
  {...(icon ? { "data-has-icon": "" } : {})}
52
- {...(sanitizedProps as Record<string, unknown>)}
53
45
  >
54
46
  {icon && (
55
47
  <span className={styles.iconWrapper} aria-hidden>
@@ -5,6 +5,7 @@ import {
5
5
  } from "@mantine/core";
6
6
  import {
7
7
  filterStylingProps,
8
+ mergeClassNames,
8
9
  type RecursicaOverStyled,
9
10
  } from "../../utils/filterStylingProps";
10
11
  import styles from "./Loader.module.css";
@@ -33,23 +34,14 @@ export const Loader = forwardRef<HTMLSpanElement, LoaderProps>(function Loader(
33
34
  // Strip all visual override injections unless developer has specifically opted into overStyling.
34
35
  // External layout props like margins are safely preserved.
35
36
  const sanitizedProps = filterStylingProps(rest, overStyled);
37
+ const restRecord = sanitizedProps as Record<string, unknown>;
36
38
 
37
- const mergedClassNames: Partial<Record<string, string>> = {
38
- root: styles.root,
39
- };
40
-
41
- const classNamesProp = (sanitizedProps as Record<string, unknown>).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
- }
39
+ const mergedClassNames = mergeClassNames(
40
+ { root: styles.root },
41
+ restRecord.classNames as Partial<Record<string, string>> | undefined,
42
+ );
50
43
 
51
- const classNameProp = (sanitizedProps as Record<string, unknown>)
52
- .className as string | undefined;
44
+ const classNameProp = restRecord.className as string | undefined;
53
45
  const finalClass = classNameProp
54
46
  ? `${styles.root} ${classNameProp}`
55
47
  : styles.root;
@@ -57,10 +49,10 @@ export const Loader = forwardRef<HTMLSpanElement, LoaderProps>(function Loader(
57
49
  return (
58
50
  <MantineLoader
59
51
  ref={ref}
52
+ {...sanitizedProps}
60
53
  type={variant}
61
54
  data-variant={variant}
62
55
  data-size={resolvedSize}
63
- {...sanitizedProps}
64
56
  className={finalClass}
65
57
  classNames={mergedClassNames}
66
58
  />
@@ -199,7 +199,9 @@ export const Default: Story = {
199
199
  position: "bottom-start",
200
200
  withArrow: false,
201
201
  offset: 5,
202
- opened: undefined,
202
+ // Rendered open by default so this story is diffable against the MUI adapter
203
+ // without an interaction step.
204
+ opened: true,
203
205
  },
204
206
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
205
207
  render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
@@ -230,6 +232,9 @@ export const Default: Story = {
230
232
  export const WithDisabledItems: Story = {
231
233
  args: {
232
234
  position: "bottom-start",
235
+ // Rendered open by default so this story is diffable against the MUI adapter
236
+ // without an interaction step.
237
+ opened: true,
233
238
  },
234
239
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
235
240
  render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
@@ -258,6 +263,9 @@ export const WithSubmenus: Story = {
258
263
  args: {
259
264
  position: "bottom-start",
260
265
  width: 200,
266
+ // Rendered open by default so this story is diffable against the MUI adapter
267
+ // without an interaction step.
268
+ opened: true,
261
269
  },
262
270
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
263
271
  render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {