@recursica/mui-adapter 0.27.0 → 0.28.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.
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/borderux/recursica.git",
14
14
  "directory": "packages/mui-adapter"
15
15
  },
16
- "version": "0.27.0",
16
+ "version": "0.28.0",
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
@@ -3,18 +3,21 @@
3
3
  */
4
4
 
5
5
  .root {
6
- /* HARDCODE: Basic structural flex layout for breadcrumbs */
7
- display: flex;
8
- align-items: center;
9
-
10
6
  /* Base style resets */
11
7
  background-color: transparent;
12
8
  color: inherit;
13
9
 
14
10
  /* Apply global breadcrumb properties */
15
11
  padding: var(--recursica_ui-kit_components_breadcrumb_properties_padding);
12
+ }
13
+
14
+ /* MUI renders items/separators inside the <ol>, not directly under <nav> (.root) — the
15
+ gap must live here to land uniformly between item/separator/item, matching Mantine. */
16
+ .ol {
17
+ /* HARDCODE: Basic structural flex layout for breadcrumbs */
18
+ display: flex;
19
+ align-items: center;
16
20
 
17
- /* Mantine's breadcrumbs uses gap on the root or pseudo classes, but standard gap is safest since flex is used */
18
21
  gap: var(--recursica_ui-kit_components_breadcrumb_properties_item-gap);
19
22
  }
20
23
 
@@ -23,4 +26,6 @@
23
26
  display: flex;
24
27
  align-items: center;
25
28
  justify-content: center;
29
+ /* Neutralize MUI's built-in 8px marginLeft/marginRight so .ol's gap alone controls spacing */
30
+ margin: 0;
26
31
  }
@@ -56,12 +56,6 @@ export const Default: Story = {
56
56
  },
57
57
  };
58
58
 
59
- export const StaticExample: Story = {
60
- args: {
61
- items: ["Store", "Electronics", "Computers", "Laptops"],
62
- },
63
- };
64
-
65
59
  export const CustomSeparator: Story = {
66
60
  args: {
67
61
  items: ["Root", "Branch", "Leaf"],
@@ -28,6 +28,7 @@ export const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>(
28
28
  const mergedClassNames = mergeClassNames(
29
29
  {
30
30
  root: styles.root,
31
+ ol: styles.ol,
31
32
  separator: styles.separator,
32
33
  },
33
34
  (sanitizedProps as Record<string, unknown>).classes as
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Chevron rendered in the right section alongside the clear button (item 3a). Only used for that
5
+ * combined case — MUI's own default `IconComponent` (`ArrowDropDown`) is left untouched everywhere
6
+ * else, so this is a hand-rolled copy of the exact same MUI glyph path, not a new icon design.
7
+ */
8
+ export function ChevronIcon(props: React.SVGProps<SVGSVGElement>) {
9
+ return (
10
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden {...props}>
11
+ <path d="M7 10l5 5 5-5z" />
12
+ </svg>
13
+ );
14
+ }
15
+
16
+ /** Clear ("x") icon for the clearable Dropdown's clear button (item 3a). */
17
+ export function ClearIcon(props: React.SVGProps<SVGSVGElement>) {
18
+ return (
19
+ <svg
20
+ viewBox="0 0 24 24"
21
+ fill="none"
22
+ stroke="currentColor"
23
+ strokeWidth="2"
24
+ strokeLinecap="round"
25
+ strokeLinejoin="round"
26
+ aria-hidden
27
+ {...props}
28
+ >
29
+ <line x1="18" y1="6" x2="6" y2="18" />
30
+ <line x1="6" y1="6" x2="18" y2="18" />
31
+ </svg>
32
+ );
33
+ }
@@ -59,20 +59,28 @@
59
59
  box-sizing: border-box;
60
60
  margin: 0;
61
61
 
62
- /* Box Geometry */
63
- min-height: var(--recursica_ui-kit_components_dropdown_properties_min-height);
62
+ /* Box Geometry
63
+ `!important` throughout: MUI's own `.MuiSelect-select`/`.MuiOutlinedInput-input` styled rules
64
+ (e.g. `minHeight: 1.4375em`, and outlined-variant default padding) are equal-specificity
65
+ single-class selectors injected by emotion at runtime, after this CSS module's own stylesheet
66
+ — on that tie they win by source order, which is what silently shrank the control below the
67
+ min-height token (measured 42.8px vs Mantine's 48px) even though the token was already wired
68
+ here. Same root cause as the notchedOutline double-border fix below. */
69
+ min-height: var(
70
+ --recursica_ui-kit_components_dropdown_properties_min-height
71
+ ) !important;
64
72
  padding-top: var(
65
73
  --recursica_ui-kit_components_dropdown_properties_vertical-padding
66
- );
74
+ ) !important;
67
75
  padding-bottom: var(
68
76
  --recursica_ui-kit_components_dropdown_properties_vertical-padding
69
- );
77
+ ) !important;
70
78
  padding-left: var(
71
79
  --recursica_ui-kit_components_dropdown_properties_horizontal-padding
72
- );
80
+ ) !important;
73
81
  padding-right: var(
74
82
  --recursica_ui-kit_components_dropdown_properties_horizontal-padding
75
- );
83
+ ) !important;
76
84
  border-radius: var(
77
85
  --recursica_ui-kit_components_dropdown_properties_border-radius
78
86
  );
@@ -125,23 +133,47 @@
125
133
  color: inherit;
126
134
  }
127
135
 
136
+ /* MUI's outlined variant renders its own `<fieldset class="MuiOutlinedInput-notchedOutline">` as a
137
+ separate absolutely-positioned sibling of `.input` — independent of the border we already draw
138
+ on `.input` above. Left alone, it shows its own default/focused/error border (theme grays/blue/
139
+ red) UNDERNEATH ours, which is what produced the double border (bug: static-error story showing
140
+ a leftover dark border under the red one) and MUI's own blue focus ring leaking in alongside the
141
+ Recursica focus box-shadow. We own the border entirely via `.input`, so this native outline is
142
+ never needed — always neutralize it. */
143
+ .root :global(.MuiOutlinedInput-notchedOutline) {
144
+ border: none !important;
145
+ }
146
+
128
147
  /* Dynamic Padding Overrides */
129
148
  .root[data-with-left-section] .input {
130
- padding-left: var(--input-left-section-size);
149
+ padding-left: var(--input-left-section-size) !important;
131
150
  }
132
151
 
133
152
  .root[data-with-right-section] .input {
134
- padding-right: var(--input-right-section-size);
153
+ padding-right: var(--input-right-section-size) !important;
135
154
  }
136
155
 
137
- /* Flexible End-Caps (Icons, actions) */
156
+ /* Flexible End-Caps (Icons, actions)
157
+ Absolutely positioned within `.root` (already `position: relative`), NOT flex siblings of
158
+ `.input` in the row. MUI passes `startAdornment`/`endAdornment` as ordinary flex children next
159
+ to the select div, so as siblings they'd add their own width on top of `.input`'s already-100%-
160
+ width box instead of sharing it — the reserved padding above only carves out space *inside*
161
+ `.input`'s own bordered box, so a sibling rendered outside that box overflows past the visible
162
+ border (bug: leading icon / the clear+chevron pair sat outside the control's right edge).
163
+ Absolute positioning is Mantine's own technique for the equivalent `Input.Section` slot, which
164
+ is why the mantine-adapter never had this bug. */
138
165
  .section {
166
+ position: absolute;
167
+ top: 0;
168
+ bottom: 0;
139
169
  display: flex;
140
170
  align-items: center;
141
- height: 100%;
171
+ gap: var(--recursica_ui-kit_components_dropdown_properties_icon-text-gap);
172
+ pointer-events: none;
142
173
  }
143
174
 
144
175
  .section[data-position="left"] {
176
+ left: 0;
145
177
  justify-content: flex-start;
146
178
  padding-left: var(
147
179
  --recursica_ui-kit_components_dropdown_properties_horizontal-padding
@@ -149,10 +181,14 @@
149
181
  }
150
182
 
151
183
  .section[data-position="right"] {
184
+ right: 0;
152
185
  justify-content: flex-end;
153
186
  padding-right: var(
154
187
  --recursica_ui-kit_components_dropdown_properties_horizontal-padding
155
188
  );
189
+ /* Unlike the left section (decorative icon only), this one can hold the interactive clear
190
+ button, so it needs to opt back into pointer events itself. */
191
+ pointer-events: auto;
156
192
  }
157
193
 
158
194
  .section :global(svg) {
@@ -169,6 +205,42 @@
169
205
  );
170
206
  }
171
207
 
208
+ /* Clear ("x") button rendered inside the right section when `clearable` and a value is present.
209
+ Sized/colored via the same icon tokens as the rest of the right section (rather than any
210
+ default browser/library button chrome), same treatment as the mantine-adapter's equivalent
211
+ fix to its native clear button (see DROPDOWN_IMPLEMENTATION_NOTES.md). */
212
+ .clearButton {
213
+ display: inline-flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ flex-shrink: 0;
217
+ width: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
218
+ height: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
219
+ padding: 0;
220
+ border: none;
221
+ border-radius: 50%;
222
+ background: transparent;
223
+ color: var(
224
+ --recursica_ui-kit_components_dropdown_properties_colors_trailing-icon
225
+ );
226
+ cursor: pointer;
227
+ outline: none;
228
+ }
229
+
230
+ .clearButton svg {
231
+ width: 100%;
232
+ height: 100%;
233
+ }
234
+
235
+ .clearButton:focus-visible {
236
+ box-shadow:
237
+ 0 0 0 var(--recursica_brand_states_focus_border-size)
238
+ var(--recursica_brand_states_focus_color),
239
+ 0 0 var(--recursica_brand_states_focus_blur)
240
+ var(--recursica_brand_states_focus_margin)
241
+ var(--recursica_brand_states_focus_color);
242
+ }
243
+
172
244
  /* -------------------------------------
173
245
  STATE CASCADE ARCHITECTURE
174
246
  -------------------------------------- */
@@ -256,6 +328,21 @@
256
328
  overflow: hidden;
257
329
  }
258
330
 
331
+ /* The visible top/bottom gap inside the open menu, before/after the option list, is MUI's own
332
+ `.MuiList-padding` default (8px) on the inner `<ul>` — not driven by any Recursica token. Wire
333
+ it to the same dropdown vertical-padding token the closed control itself uses, matching the
334
+ mantine-adapter fix (its `.dropdown` had the same gap, driven by Mantine's own untokenized
335
+ 4px `--combobox-padding` default). `!important` needed to beat MUI's own emotion-generated
336
+ `.MuiList-padding` rule at equal selector specificity. */
337
+ .dropdown :global(.MuiMenu-list) {
338
+ padding-top: var(
339
+ --recursica_ui-kit_components_dropdown_properties_vertical-padding
340
+ ) !important;
341
+ padding-bottom: var(
342
+ --recursica_ui-kit_components_dropdown_properties_vertical-padding
343
+ ) !important;
344
+ }
345
+
259
346
  .option {
260
347
  font-family: var(
261
348
  --recursica_ui-kit_components_dropdown_properties_text_font-family
@@ -277,10 +364,11 @@
277
364
  cursor: pointer;
278
365
  }
279
366
 
280
- /* No per-option selected/hovered tokens exist in the schema; use the generic overlay tint
281
- (same technique as the Mantine reference implementation) to highlight the active option. */
282
- .option[data-selected="true"],
283
- .option[data-combobox-selected="true"],
367
+ /* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
368
+ as the Mantine reference implementation) to highlight a hovered-but-not-selected option.
369
+ `!important` is required here: MUI's own `.Mui-selected` (and `.Mui-selected:hover`) classes
370
+ carry their own default primary-blue tint at equal selector specificity, and without it that
371
+ default tint wins over this token-driven one. */
284
372
  .option[data-hovered="true"],
285
373
  .option:hover {
286
374
  background-color: color-mix(
@@ -288,5 +376,19 @@
288
376
  var(--recursica_brand_states_hover_color)
289
377
  calc(var(--recursica_brand_states_hover_opacity) * 100%),
290
378
  transparent
291
- );
379
+ ) !important;
380
+ }
381
+
382
+ /* No dedicated dropdown-option "selected" token exists in the schema either, but the menu-item
383
+ component's selected-state colors are the closest real token family for the same concept (a
384
+ selected row in a list) — reused here rather than the neutral hover tint above, which is what
385
+ previously made the selected option read as grey instead of Recursica's brand color. */
386
+ .option[data-selected="true"],
387
+ .option[data-combobox-selected="true"] {
388
+ background-color: var(
389
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_background-color
390
+ ) !important;
391
+ color: var(
392
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
393
+ ) !important;
292
394
  }
@@ -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
+ startAdornment: (
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.",
@@ -1,7 +1,8 @@
1
- import React, { forwardRef, ReactNode } from "react";
1
+ import React, { forwardRef, ReactNode, useEffect, useState } from "react";
2
2
  import {
3
3
  Select as MuiSelect,
4
4
  SelectProps as MuiSelectProps,
5
+ SelectChangeEvent,
5
6
  MenuItem,
6
7
  } from "@mui/material";
7
8
  import { type ReadOnlyControlProps } from "@recursica/adapter-common";
@@ -12,10 +13,18 @@ import {
12
13
  } from "../../utils/filterStylingProps";
13
14
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
14
15
  import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
16
+ import { ChevronIcon, ClearIcon } from "./Dropdown.icons";
15
17
  import styles from "./Dropdown.module.css";
16
18
 
17
19
  import { type RecursicaDropdownProps as BaseRecursicaDropdownProps } from "@recursica/adapter-common";
18
20
 
21
+ // Swapped in for MUI's default `IconComponent` only when we're rendering our own combined
22
+ // clear+chevron `endAdornment` (see `showClear` below) — a stable reference so it isn't recreated
23
+ // every render.
24
+ function HiddenIcon() {
25
+ return null;
26
+ }
27
+
19
28
  export interface RecursicaDropdownProps
20
29
  extends Omit<
21
30
  MuiSelectProps,
@@ -28,9 +37,13 @@ export interface RecursicaDropdownProps
28
37
  | "ref"
29
38
  | "error"
30
39
  >,
40
+ // `onChange` is swept up by the `keyof React.HTMLAttributes<HTMLDivElement>` omission above
41
+ // (it's a standard DOM attribute name too) even though MUI's own `Select#onChange` has a
42
+ // completely different, non-DOM signature — add it back explicitly with MUI's real signature.
43
+ Pick<MuiSelectProps, "onChange">,
31
44
  Omit<
32
45
  RecursicaFormControlWrapperProps,
33
- "controlMaxWidth" | "controlMinWidth"
46
+ "controlMaxWidth" | "controlMinWidth" | "onChange"
34
47
  >,
35
48
  ReadOnlyControlProps,
36
49
  BaseRecursicaDropdownProps {}
@@ -67,11 +80,12 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
67
80
  emptyValueComponent,
68
81
  value,
69
82
  defaultValue,
83
+ onChange,
70
84
  data,
85
+ startAdornment,
71
86
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
72
87
  searchable, // Not natively supported by basic MUI Select, stubbed
73
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
74
- clearable, // Not natively supported by basic MUI Select, stubbed
88
+ clearable,
75
89
  ...rest
76
90
  } = props;
77
91
  // Props this component intentionally doesn't support — deleted at runtime so they can't leak
@@ -87,6 +101,47 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
87
101
  );
88
102
  const restRecord = sanitizedProps as Record<string, unknown>;
89
103
 
104
+ // MUI's basic `Select` has no built-in "clearable" concept (unlike Mantine's `Select`, which
105
+ // manages its own internal value so it can reset itself to `null` on demand) — the clear
106
+ // button needs somewhere to write an empty value to, so the value is lifted here rather than
107
+ // left for MUI's own internal uncontrolled state.
108
+ const [internalValue, setInternalValue] = useState<unknown>(
109
+ () => value ?? defaultValue ?? "",
110
+ );
111
+
112
+ useEffect(() => {
113
+ if (value !== undefined) setInternalValue(value);
114
+ }, [value]);
115
+
116
+ const handleChange = (
117
+ event: SelectChangeEvent<unknown>,
118
+ child: ReactNode,
119
+ ) => {
120
+ setInternalValue(event.target.value);
121
+ (
122
+ onChange as
123
+ | ((event: SelectChangeEvent<unknown>, child: ReactNode) => void)
124
+ | undefined
125
+ )?.(event, child);
126
+ };
127
+
128
+ const hasValue =
129
+ internalValue !== "" &&
130
+ internalValue !== null &&
131
+ internalValue !== undefined;
132
+ const showClear = !!clearable && hasValue && !disabled && !readOnly;
133
+
134
+ const handleClear = (event: React.MouseEvent) => {
135
+ event.preventDefault();
136
+ event.stopPropagation();
137
+ setInternalValue("");
138
+ (
139
+ onChange as
140
+ | ((event: SelectChangeEvent<unknown>, child: ReactNode) => void)
141
+ | undefined
142
+ )?.({ target: { value: "" } } as SelectChangeEvent<unknown>, null);
143
+ };
144
+
90
145
  const injectedStyles = {
91
146
  ...((style as React.CSSProperties) || {}),
92
147
  width: containerWidth || "100%",
@@ -105,12 +160,11 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
105
160
  key={`${item}-${index}`}
106
161
  value={item}
107
162
  className={styles.option}
163
+ disableRipple
108
164
  // Dropdown.module.css's own selected-state tint (`.option[data-selected="true"]`)
109
165
  // needs this explicitly — MUI's own `Mui-selected` class carries its default primary-
110
166
  // color tint instead, which is what shows through without it.
111
- data-selected={
112
- item === (value ?? defaultValue) ? "true" : undefined
113
- }
167
+ data-selected={item === internalValue ? "true" : undefined}
114
168
  >
115
169
  {item}
116
170
  </MenuItem>
@@ -122,9 +176,8 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
122
176
  value={item.value}
123
177
  disabled={item.disabled}
124
178
  className={styles.option}
125
- data-selected={
126
- item.value === (value ?? defaultValue) ? "true" : undefined
127
- }
179
+ disableRipple
180
+ data-selected={item.value === internalValue ? "true" : undefined}
128
181
  >
129
182
  {item.label}
130
183
  </MenuItem>
@@ -132,6 +185,26 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
132
185
  });
133
186
  };
134
187
 
188
+ const wrappedStartAdornment = startAdornment ? (
189
+ <span className={styles.section} data-position="left">
190
+ {startAdornment}
191
+ </span>
192
+ ) : undefined;
193
+
194
+ const wrappedEndAdornment = showClear ? (
195
+ <span className={styles.section} data-position="right">
196
+ <button
197
+ type="button"
198
+ className={styles.clearButton}
199
+ aria-label="Clear"
200
+ onClick={handleClear}
201
+ >
202
+ <ClearIcon />
203
+ </button>
204
+ <ChevronIcon />
205
+ </span>
206
+ ) : undefined;
207
+
135
208
  return (
136
209
  <WithReadOnlyWrapper
137
210
  className={wrapperClass}
@@ -168,8 +241,8 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
168
241
  ref={ref}
169
242
  {...(sanitizedProps as unknown as MuiSelectProps)}
170
243
  disabled={disabled}
171
- value={value}
172
- defaultValue={defaultValue}
244
+ value={internalValue}
245
+ onChange={handleChange}
173
246
  error={!!error}
174
247
  required={required}
175
248
  displayEmpty
@@ -178,6 +251,9 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
178
251
  select: styles.input,
179
252
  icon: styles.icon,
180
253
  }}
254
+ IconComponent={showClear ? HiddenIcon : undefined}
255
+ startAdornment={wrappedStartAdornment}
256
+ endAdornment={wrappedEndAdornment}
181
257
  MenuProps={{
182
258
  classes: { paper: styles.dropdown },
183
259
  }}
@@ -188,6 +264,8 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
188
264
  // inputProps, which meant the error border never actually appeared on the Dropdown.)
189
265
  data-disabled={disabled ? "true" : undefined}
190
266
  data-error={error ? "true" : undefined}
267
+ data-with-left-section={startAdornment ? "true" : undefined}
268
+ data-with-right-section={showClear ? "true" : undefined}
191
269
  inputProps={{
192
270
  "data-disabled": disabled ? "true" : undefined,
193
271
  "data-error": error ? "true" : undefined,
@@ -39,3 +39,10 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
39
39
  > - **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.
40
40
  > - **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.
41
41
  > - **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.
42
+
43
+ ---
44
+
45
+ ## 4. Notes
46
+
47
+ - Pass `startAdornment` 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, matching the mantine-adapter's `leftSection`/`clearable` behavior.
48
+ - `onChange` follows MUI's native `Select` signature: `(event: SelectChangeEvent, child: ReactNode) => void`.
@@ -5,7 +5,13 @@
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). */
8
+ behavior. We only override visual design tokens (colors, typography, spacing, borders).
9
+ - margin: 0 !important on .dropdown. Mui's Tooltip hardcodes a per-placement margin
10
+ (14px, reset to 0 only when arrow is enabled) on top of whatever offset the popper
11
+ "offset" modifier applies, so with withBeak=false the trigger-to-dropdown gap was
12
+ offset + 14px instead of just offset. There is no Recursica token for this spacing —
13
+ the gap is driven purely by the `offset` prop (a plain number, same as Mantine's
14
+ HoverCard), so we zero out Mui's extra margin to make offset the only source of gap. */
9
15
 
10
16
  /* ======================================
11
17
  DROPDOWN CONTAINER
@@ -72,14 +78,30 @@
72
78
  color: var(
73
79
  --recursica_ui-kit_components_hover-card-popover_properties_colors_content
74
80
  );
81
+
82
+ margin: 0 !important; /* HARDCODE: cancel Mui's built-in per-placement margin, see note above */
75
83
  }
76
84
 
77
85
  /* ======================================
78
86
  ARROW / BEAK
79
87
  ====================================== */
80
88
 
89
+ /* Mui's arrow is a rotated square (via ::before, background-color: currentColor)
90
+ sized in em units off font-size, not a bordered CSS triangle. `color` (not
91
+ border-color) is what actually fills it, which is why setting border-color here
92
+ previously had no visible effect and left it defaulting to the inherited text
93
+ color (solid black). Fill it with the same background-color token the card body
94
+ uses so it blends into the card exactly like Mantine's beak, and size it off the
95
+ beak-size token instead of Mui's default 1em/0.71em (font-size-relative) box. */
81
96
  .arrow {
82
- border-color: var(
83
- --recursica_ui-kit_components_hover-card-popover_properties_colors_border-color
97
+ color: var(
98
+ --recursica_ui-kit_components_hover-card-popover_properties_colors_background-color
99
+ );
100
+ width: var(
101
+ --recursica_ui-kit_components_hover-card-popover_properties_beak-size
84
102
  );
103
+ height: calc(
104
+ var(--recursica_ui-kit_components_hover-card-popover_properties_beak-size) *
105
+ 0.7071
106
+ ); /* = width / sqrt(2), same ratio Mui's default arrow uses */
85
107
  }
@@ -271,4 +271,7 @@
271
271
  position: relative;
272
272
  z-index: 1;
273
273
  color: inherit;
274
+ /* HARDCODE: prevent the flex row from shrinking the chevron below its intrinsic 14px size
275
+ when the row is tight, which otherwise reads as the submenu indicator being clipped. */
276
+ flex-shrink: 0;
274
277
  }