@recursica/mantine-adapter 0.48.1 → 0.49.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/mantine-adapter"
15
15
  },
16
- "version": "0.48.1",
16
+ "version": "0.49.0",
17
17
  "type": "module",
18
18
  "main": "./dist/mantine-adapter.cjs",
19
19
  "module": "./dist/mantine-adapter.js",
@@ -94,7 +94,7 @@
94
94
  "vitest": "^3.2.4"
95
95
  },
96
96
  "dependencies": {
97
- "@recursica/adapter-common": "^0.23.0",
97
+ "@recursica/adapter-common": "^0.24.0",
98
98
  "@recursica/official-release": "^2.8.0"
99
99
  },
100
100
  "peerDependencies": {
@@ -16,6 +16,22 @@ The Mantine `<Autocomplete>` dropdown menu and options are styled strictly using
16
16
 
17
17
  Focus, errors, and disabled visual states are enforced explicitly via the outer `<FormControlWrapper>` boundary emitting context down structurally (`[data-error]`, `[data-disabled]`) and evaluated efficiently against scoped nested selectors natively inside `AutoComplete.module.css`.
18
18
 
19
- ## Missing Active Option Color
19
+ ## Missing Selected Option Highlight
20
20
 
21
- Currently, there is no explicit JSON token for the background color of an active/hovered option in the AutoComplete dropdown. We temporarily map `.option:hover` and `.option[data-combobox-active]` to the `--recursica_ui-kit_components_autocomplete_variants_states_focus_properties_colors_background` variable. Because this variable maps to the base field background, the highlight is currently invisible. This will be updated once the correct token is added to the UI kit.
21
+ `.option:hover`/`.option[data-hovered="true"]` work hover has a real background tint. There's no
22
+ equivalent for "this option matches the current value" though, and it isn't fixable in CSS alone:
23
+ Mantine's `Autocomplete` never passes a `value` prop into its internal `OptionsDropdown` (only
24
+ `search`, for filtering — see `node_modules/@mantine/core/esm/components/Autocomplete/Autocomplete.mjs`),
25
+ so `OptionsDropdown`'s `checked` (and the `data-combobox-active` attribute it drives — the same one
26
+ `Dropdown`'s equivalent highlight below keys off) never gets set on any option, no matter what's
27
+ typed into the field. See `Dropdown.module.css`'s `.option[data-combobox-active="true"]` rule for
28
+ what a real fix would key off, if this ever gets solved upstream or by reimplementing option
29
+ rendering with our own value comparison.
30
+
31
+ ## Rich Option Content (`leadingIcon`/`supportingText`)
32
+
33
+ `data` items accept optional `leadingIcon`/`supportingText` fields, via the shared `RecursicaComboboxItem` type in `@recursica/adapter-common` (see `MANTINE_ADAPTER_RICH_OPTION_DATA.md` at the repo root) — the same type `Dropdown` and both `mui-adapter` components use. `AutoComplete.tsx` installs a default `renderOption` (`../../utils/renderRichOption.tsx`, shared with `Dropdown`) that reads these off Mantine's parsed `option`. `label` on the shared type is optional — Mantine's `getParsedComboboxData` only preserves an item's extra fields when it already has both `value` and `label`; an item with `value` only is rebuilt into a bare `{value, label: value, disabled}` object first, dropping `leadingIcon`/`supportingText`. `AutoComplete.tsx` runs `data` through adapter-common's `normalizeComboboxData` (backfills `label` from `value`) before handing it to Mantine, so the rich fields always survive regardless of whether the caller set `label` (`Dropdown.tsx` does the same now that its `label` is optional too). New CSS classes (`.optionContent`/`.optionIcon`/`.optionText`/`.optionSupportingText`) reuse the menu-item component's icon/supporting-text tokens, matching Dropdown's equivalent addition — no dedicated autocomplete-option tokens exist for either. The icon is a conditional child (only rendered when `leadingIcon` is set, not a hidden reserved slot), so label/supportingText shift left when there's no icon; `.optionContent`'s `align-items: center` keeps the label vertically centered when there's no `supportingText`.
34
+
35
+ ## `wrapItemText`
36
+
37
+ `label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`). Passing `wrapItemText` adds `.optionTextWrap` alongside `.optionText`, which re-enables wrapping (`white-space: normal; overflow-wrap: anywhere`) for both children — same later-cascade-wins mechanism (`.optionTextWrap > *` declared after `.optionText > *`, equal specificity) as the rest of this file's overrides. `renderRichOption`/`renderRichOptionContent` (shared util, both adapters) take `wrapItemText` as a third parameter and combine the two class names when it's true.
@@ -212,26 +212,31 @@
212
212
  color: var(
213
213
  --recursica_ui-kit_components_autocomplete_properties_colors_text-color
214
214
  );
215
- padding: calc(
216
- var(
217
- --recursica_ui-kit_components_autocomplete_properties_vertical-padding
218
- ) *
219
- 0.75
215
+ /* Padding/inter-item gap reuse the menu-item component's tokens (same reasoning as the icon/
216
+ supporting-text tokens below) instead of an arbitrary scaled autocomplete padding — matches
217
+ Menu.module.css's `.item` padding + `margin-bottom` gap exactly. */
218
+ padding: var(
219
+ --recursica_ui-kit_components_menu-item_properties_vertical-padding
220
220
  )
221
- var(
222
- --recursica_ui-kit_components_autocomplete_properties_horizontal-padding
223
- );
221
+ var(--recursica_ui-kit_components_menu-item_properties_horizontal-padding);
222
+ margin-bottom: var(--recursica_ui-kit_components_menu_properties_item-gap);
224
223
  cursor: pointer;
225
224
  }
226
225
 
227
- /* No per-option selected/hovered tokens exist in the schema; use the generic overlay tint
228
- (same technique as Table/Dropdown) to highlight the active option. */
226
+ .option:last-of-type {
227
+ margin-bottom: 0;
228
+ }
229
+
230
+ /* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
231
+ as Table/Dropdown) to highlight a hovered-but-not-selected option. Mantine's own
232
+ `data-combobox-selected` here is its transient keyboard-navigation highlight (imperatively
233
+ set/cleared via arrow keys — see use-combobox.mjs's `selectOption`/`clearSelectedItem` — not
234
+ the persistently-chosen value), so it belongs in this hover-equivalent bucket, not the real
235
+ selected-value rule below. */
229
236
  .option[data-selected="true"],
230
237
  .option[data-selected],
231
238
  .option[data-combobox-selected="true"],
232
239
  .option[data-combobox-selected],
233
- .option[data-combobox-active="true"],
234
- .option[data-combobox-active],
235
240
  .option[data-hovered="true"],
236
241
  .option[data-hovered],
237
242
  .option:hover {
@@ -243,6 +248,106 @@
243
248
  );
244
249
  }
245
250
 
251
+ /* No dedicated autocomplete-option "selected" token exists in the schema either, but the
252
+ menu-item component's selected-state colors are the closest real token family for the same
253
+ concept (a selected row in a list) — same reuse Dropdown.module.css makes. Despite the name,
254
+ Mantine's `data-combobox-active` is the attribute it sets when an option's value matches the
255
+ current field value (see OptionsDropdown.mjs: `active: checked`) — the actual "this is the
256
+ selected item" flag, distinct from the transient `data-combobox-selected` keyboard-highlight
257
+ grouped with hover above. Keying this rule off `data-combobox-selected` instead (as it
258
+ previously did) is why the selected option only showed a highlight while arrow-key navigating,
259
+ and lost it as soon as the dropdown was reopened normally. */
260
+ .option[data-combobox-active="true"],
261
+ .option[data-combobox-active] {
262
+ background-color: var(
263
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_background-color
264
+ );
265
+ color: var(
266
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
267
+ );
268
+ }
269
+
270
+ /* Rich option content (leadingIcon/supportingText — see MANTINE_ADAPTER_RICH_OPTION_DATA.md). No
271
+ dedicated autocomplete-option icon or supporting-text token exists in the schema either; reuse
272
+ the menu-item component's tokens, same reasoning as Dropdown.module.css. */
273
+ .optionContent {
274
+ display: flex;
275
+ align-items: center;
276
+ gap: var(--recursica_ui-kit_components_menu-item_properties_icon-text-gap);
277
+ }
278
+
279
+ .optionIcon {
280
+ display: flex;
281
+ flex-shrink: 0;
282
+ width: var(
283
+ --recursica_ui-kit_components_menu-item_properties_icon-leading-size
284
+ );
285
+ height: var(
286
+ --recursica_ui-kit_components_menu-item_properties_icon-leading-size
287
+ );
288
+ color: var(
289
+ --recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_leading-icon-color
290
+ );
291
+ }
292
+
293
+ .optionIcon :global(svg) {
294
+ width: 100%;
295
+ height: 100%;
296
+ }
297
+
298
+ .optionText {
299
+ display: flex;
300
+ flex-direction: column;
301
+ min-width: 0;
302
+ gap: var(--recursica_ui-kit_components_menu-item_properties_text-gap);
303
+ }
304
+
305
+ /* Default: label/supportingText each truncate to a single line with an ellipsis. */
306
+ .optionText > * {
307
+ overflow: hidden;
308
+ text-overflow: ellipsis;
309
+ white-space: nowrap;
310
+ }
311
+
312
+ /* Applied alongside .optionText when `wrapItemText` is true — long values wrap onto additional
313
+ lines instead of overflowing the fixed-width dropdown. */
314
+ .optionTextWrap > * {
315
+ overflow: visible;
316
+ text-overflow: clip;
317
+ white-space: normal;
318
+ overflow-wrap: anywhere;
319
+ }
320
+
321
+ .optionSupportingText {
322
+ font-family: var(
323
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-family
324
+ );
325
+ font-size: var(
326
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-size
327
+ );
328
+ font-style: var(
329
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-style
330
+ );
331
+ font-weight: var(
332
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-weight
333
+ );
334
+ letter-spacing: var(
335
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_letter-spacing
336
+ );
337
+ line-height: var(
338
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_line-height
339
+ );
340
+ text-decoration: var(
341
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_text-decoration
342
+ );
343
+ text-transform: var(
344
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_text-transform
345
+ );
346
+ color: var(
347
+ --recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_supporting-text-color
348
+ );
349
+ }
350
+
246
351
  /* -------------------------------------
247
352
  STATE CASCADE ARCHITECTURE
248
353
  -------------------------------------- */
@@ -1,6 +1,8 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { AutoComplete } from "./AutoComplete";
3
3
  import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
4
+ import { renderRichOption } from "../../utils/renderRichOption";
5
+ import styles from "./AutoComplete.module.css";
4
6
 
5
7
  const meta: Meta<typeof AutoComplete> = {
6
8
  title: "UI-Kit/AutoComplete",
@@ -55,6 +57,11 @@ Always structure horizontal architectures via the generic \`formLayout\` paramet
55
57
  description:
56
58
  "Toggles structural read-only data presentation explicitly blocking standard component bindings.",
57
59
  },
60
+ wrapItemText: {
61
+ control: "boolean",
62
+ description:
63
+ "Wraps option label/supportingText onto additional lines instead of truncating with an ellipsis.",
64
+ },
58
65
  },
59
66
  };
60
67
 
@@ -144,6 +151,148 @@ export const WithTrailingIcon: Story = {
144
151
  },
145
152
  };
146
153
 
154
+ const UserIcon = (
155
+ <svg
156
+ width="16"
157
+ height="16"
158
+ viewBox="0 0 24 24"
159
+ fill="none"
160
+ stroke="currentColor"
161
+ strokeWidth="2"
162
+ strokeLinecap="round"
163
+ strokeLinejoin="round"
164
+ >
165
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
166
+ <circle cx="12" cy="7" r="4"></circle>
167
+ </svg>
168
+ );
169
+
170
+ export const WithRichOptions: Story = {
171
+ args: {
172
+ label: "Assignee",
173
+ placeholder: "Search team members...",
174
+ data: [
175
+ {
176
+ value: "jdoe",
177
+ label: "Jane Doe",
178
+ leadingIcon: UserIcon,
179
+ supportingText: "jane.doe@example.com",
180
+ },
181
+ {
182
+ value: "asmith",
183
+ label: "Alex Smith",
184
+ leadingIcon: UserIcon,
185
+ supportingText: "alex.smith@example.com",
186
+ },
187
+ { value: "unassigned", label: "Unassigned" },
188
+ ],
189
+ assistiveText:
190
+ "Each option can show a leading icon and supporting text — see MANTINE_ADAPTER_RICH_OPTION_DATA.md.",
191
+ },
192
+ };
193
+
194
+ export const WithRichOptionsWrapped: Story = {
195
+ args: {
196
+ label: "Assignee",
197
+ placeholder: "Search team members...",
198
+ wrapItemText: true,
199
+ data: [
200
+ {
201
+ value: "jdoe",
202
+ label: "Jane Doe, Senior Staff Engineer, Platform Infrastructure",
203
+ leadingIcon: UserIcon,
204
+ supportingText:
205
+ "jane.doe@example.com — Platform Infrastructure team, on-call rotation lead",
206
+ },
207
+ { value: "unassigned", label: "Unassigned" },
208
+ ],
209
+ assistiveText:
210
+ "wrapItemText=true — long label/supportingText wrap instead of truncating.",
211
+ },
212
+ };
213
+
214
+ const optionRowPreviewClassNames = {
215
+ optionContent: styles.optionContent,
216
+ optionIcon: styles.optionIcon,
217
+ optionText: styles.optionText,
218
+ optionTextWrap: styles.optionTextWrap,
219
+ optionSupportingText: styles.optionSupportingText,
220
+ };
221
+
222
+ const OPTION_ROW_PREVIEW_ITEMS = [
223
+ {
224
+ value: "icon-and-supporting",
225
+ label: "Jane Doe",
226
+ leadingIcon: UserIcon,
227
+ supportingText: "jane.doe@example.com",
228
+ },
229
+ {
230
+ value: "no-icon",
231
+ label: "Alex Smith",
232
+ supportingText:
233
+ "No leadingIcon — label/supportingText shift left, no reserved icon space",
234
+ },
235
+ {
236
+ value: "no-supporting-text",
237
+ label: "Taylor Rivera",
238
+ leadingIcon: UserIcon,
239
+ },
240
+ {
241
+ value: "plain",
242
+ label: "Plain option — no leadingIcon, no supportingText",
243
+ },
244
+ {
245
+ value: "long-text",
246
+ label:
247
+ "A very long option label that, with wrapItemText, wraps onto a second line instead of overflowing the fixed-width dropdown — otherwise it truncates with an ellipsis",
248
+ leadingIcon: UserIcon,
249
+ supportingText:
250
+ "A similarly long supporting text string, to confirm the same wrap-or-truncate behavior applies to it too",
251
+ },
252
+ ];
253
+
254
+ // Renders the option row content directly — outside the floating/portal menu — inside a
255
+ // container sized to AutoComplete's own max-width token. Spacing between rows, icon/
256
+ // supportingText presence-or-absence alignment, and long-text wrapping/truncation are all much
257
+ // easier to inspect this way than by opening the real (portal-rendered) combobox dropdown. See
258
+ // MANTINE_ADAPTER_RICH_OPTION_DATA.md.
259
+ const renderOptionRowPreview = (wrapItemText: boolean) => (
260
+ <div
261
+ className={styles.dropdown}
262
+ style={{
263
+ width:
264
+ "var(--recursica_ui-kit_components_autocomplete_variants_layouts_stacked_properties_max-width)",
265
+ }}
266
+ >
267
+ {OPTION_ROW_PREVIEW_ITEMS.map((item) => (
268
+ <div key={item.value} className={styles.option}>
269
+ {renderRichOption(
270
+ { option: item },
271
+ optionRowPreviewClassNames,
272
+ wrapItemText,
273
+ )}
274
+ </div>
275
+ ))}
276
+ </div>
277
+ );
278
+
279
+ // Default: `wrapItemText` is false — label/supportingText truncate to a single line with an
280
+ // ellipsis instead of wrapping.
281
+ export const RichOptionRowPreview: Story = {
282
+ parameters: {
283
+ controls: { disable: true },
284
+ },
285
+ render: () => renderOptionRowPreview(false),
286
+ };
287
+
288
+ // `wrapItemText: true` — label/supportingText wrap onto additional lines instead of truncating.
289
+ export const RichOptionRowPreviewWrapped: Story = {
290
+ parameters: {
291
+ controls: { disable: true },
292
+ },
293
+ render: () => renderOptionRowPreview(true),
294
+ };
295
+
147
296
  export const Disabled: Story = {
148
297
  args: {
149
298
  label: "Disabled Deployment Node",
@@ -4,7 +4,10 @@ import {
4
4
  type AutocompleteProps as MantineAutocompleteProps,
5
5
  type InputWrapperProps,
6
6
  } from "@mantine/core";
7
- import { type ReadOnlyControlProps } from "@recursica/adapter-common";
7
+ import {
8
+ type ReadOnlyControlProps,
9
+ normalizeComboboxData,
10
+ } from "@recursica/adapter-common";
8
11
  import {
9
12
  filterStylingProps,
10
13
  omitUnsupportedProps,
@@ -13,6 +16,7 @@ import {
13
16
  } from "../../utils/filterStylingProps";
14
17
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
15
18
  import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
19
+ import { renderRichOption } from "../../utils/renderRichOption";
16
20
  import styles from "./AutoComplete.module.css";
17
21
 
18
22
  import { type RecursicaAutocompleteProps as BaseRecursicaAutocompleteProps } from "@recursica/adapter-common";
@@ -80,6 +84,9 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
80
84
  emptyValueComponent,
81
85
  value,
82
86
  defaultValue,
87
+ data,
88
+ renderOption,
89
+ wrapItemText = false,
83
90
  ...rest
84
91
  } = props;
85
92
 
@@ -89,6 +96,13 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
89
96
  );
90
97
  const restRecord = sanitizedProps as Record<string, unknown>;
91
98
 
99
+ // Mantine's own data parser only preserves extra fields (`leadingIcon`/`supportingText`) when
100
+ // an item already has both `value` and `label` — an item with `value` only is rebuilt into a
101
+ // bare `{value, label: value, disabled}` object, silently dropping them (see
102
+ // get-parsed-combobox-data.mjs). `normalizeComboboxData` backfills `label` so the rich fields
103
+ // always survive regardless of whether the caller set it.
104
+ const normalizedData = normalizeComboboxData(data);
105
+
92
106
  // Securely map core native blocks down ensuring nested CSS modules map precisely
93
107
  const mergedClassNames = mergeClassNames(
94
108
  {
@@ -101,6 +115,14 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
101
115
  restRecord.classNames as Partial<Record<string, string>> | undefined,
102
116
  );
103
117
 
118
+ const optionClassNames = {
119
+ optionContent: styles.optionContent,
120
+ optionIcon: styles.optionIcon,
121
+ optionText: styles.optionText,
122
+ optionTextWrap: styles.optionTextWrap,
123
+ optionSupportingText: styles.optionSupportingText,
124
+ };
125
+
104
126
  const wrapperClass = className
105
127
  ? `${styles.layoutOverride} ${className}`
106
128
  : styles.layoutOverride;
@@ -141,6 +163,12 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
141
163
  value={value as string | undefined}
142
164
  defaultValue={defaultValue as string | undefined}
143
165
  error={!!error}
166
+ data={normalizedData as unknown as MantineAutocompleteProps["data"]}
167
+ renderOption={
168
+ renderOption ??
169
+ ((input) =>
170
+ renderRichOption(input, optionClassNames, wrapItemText))
171
+ }
144
172
  />
145
173
  }
146
174
  />
@@ -45,6 +45,36 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
45
45
 
46
46
  ## 4. Key Integration Features & Constraints
47
47
 
48
- ### Known Limitation: Active Option Highlight
48
+ ### Rich option content: `leadingIcon` / `supportingText`
49
49
 
50
- The background highlight for a hovered or active option in the dropdown is not currently visible. This will be addressed in a future update.
50
+ `data` items can carry an icon and a secondary line of text, rendered inside each option row:
51
+
52
+ ```tsx
53
+ <AutoComplete
54
+ label="Assignee"
55
+ data={[
56
+ {
57
+ value: "jdoe",
58
+ label: "Jane Doe",
59
+ leadingIcon: <UserIcon />,
60
+ supportingText: "jane.doe@example.com",
61
+ },
62
+ { value: "asmith", label: "Alex Smith" },
63
+ ]}
64
+ />
65
+ ```
66
+
67
+ - `label` is optional, same as always — items with `value` only still work, falling back to
68
+ `value` as the displayed/matched text.
69
+ - Pass your own `renderOption` to opt out of this default rendering entirely for a given
70
+ component instance.
71
+ - By default `label`/`supportingText` truncate to a single line with an ellipsis. Set
72
+ `wrapItemText` to wrap them onto additional lines instead:
73
+
74
+ ```tsx
75
+ <AutoComplete data={data} wrapItemText />
76
+ ```
77
+
78
+ ### Known Limitation: Selected Option Highlight
79
+
80
+ Hover works (see `.option:hover`/`.option[data-hovered="true"]`), but there's no distinct "this option matches the current value" background — unlike `Dropdown`, which has one. This isn't a styling gap: Mantine's `Autocomplete` never passes a `value` prop into its internal `OptionsDropdown` (only `search`, for filtering — see `Autocomplete.mjs`), so `checked`/`data-combobox-active` (the attribute that drives `Dropdown`'s equivalent highlight) never gets set on any option, regardless of what's typed into the field. There's no other DOM signal to key a CSS rule off. Would need either an upstream Mantine change or reimplementing option rendering ourselves with a value comparison.
@@ -7,3 +7,6 @@ The `Dropdown` component is mapped explicitly to Mantine's `<Select>` following
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
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
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`.
10
+ 6. **Rich Option Content (`leadingIcon`/`supportingText`):** `data` items now accept optional `leadingIcon`/`supportingText` fields, via the shared `RecursicaComboboxItem` type in `@recursica/adapter-common` (see `MANTINE_ADAPTER_RICH_OPTION_DATA.md` at the repo root) — the same type `AutoComplete` and both `mui-adapter` components use, so it's declared once and not redeclared per component/adapter. `label` on that shared type is optional, falling back to `value` (matches Mantine's own runtime default for both `Select` and `Autocomplete`). `Dropdown.tsx` installs a default `renderOption` (`../../utils/renderRichOption.tsx`, shared with `AutoComplete`) that reads `leadingIcon`/`supportingText` straight off Mantine's own parsed `option` — no separate lookup-by-value map is needed, since Mantine's `getParsedComboboxData` passes extra fields on an item through untouched whenever the item already has both `value` and `label`. Because `label` is now optional, `data` is first run through adapter-common's `normalizeComboboxData` (backfilling `label` from `value`) before being handed to Mantine, so items missing `label` don't have their `leadingIcon`/`supportingText` silently dropped by that same parser (see `AutoComplete.tsx`'s identical, longer-standing use of this — `Dropdown`'s `label` used to be required, which is why it didn't need this before). A caller-supplied `renderOption` always wins over the default. New CSS classes (`.optionContent`/`.optionIcon`/`.optionText`/`.optionSupportingText`) reuse the menu-item component's icon/supporting-text tokens — no dedicated dropdown-option tokens exist for either, same reasoning as the `.option[data-selected]` reuse above. The icon is only rendered when `leadingIcon` is present (a conditional child, not a hidden reserved slot), so label/supportingText shift left when there's no icon; `.optionContent`'s `align-items: center` keeps the label vertically centered when there's no `supportingText` to stack under it.
11
+ 7. **`wrapItemText`:** `label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`). Passing `wrapItemText` adds `.optionTextWrap` alongside `.optionText`, re-enabling wrapping (`white-space: normal; overflow-wrap: anywhere`) for both children — later-cascade-wins, equal specificity, same mechanism as this file's other CSS overrides. `renderRichOption`/`renderRichOptionContent` take `wrapItemText` as a third parameter and combine the two class names when it's true.
12
+ 8. **Selected Option Highlight (bug fix):** The `.option[data-selected="true"], .option[data-combobox-active="true"]` background/text-color rule (and the matching `.optionIcon`/`.optionSupportingText` color rules) used to key off `data-combobox-selected` instead of `data-combobox-active`. Despite the name, `data-combobox-active` is what Mantine sets when an option's value matches the current field value (`OptionsDropdown.mjs`: `active: checked`, consumed by `ComboboxOption.mjs`'s `mod` map); `data-combobox-selected` is an unrelated, transient keyboard-navigation highlight that Mantine sets/clears imperatively via `element.setAttribute` outside React (`use-combobox.mjs`'s `selectOption`/`clearSelectedItem`), not tied to the chosen value at all. The bug: the real selected option only showed the brand background while being arrow-key-navigated, and lost it entirely on a normal open/close (matches Playwright verification — `data-combobox-active="true"` is present and gets the token background after a plain click-select-reopen, with no keyboard involved).
@@ -323,14 +323,21 @@
323
323
  color: var(
324
324
  --recursica_ui-kit_components_dropdown_properties_colors_text-color
325
325
  );
326
- padding: calc(
327
- var(--recursica_ui-kit_components_dropdown_properties_vertical-padding) *
328
- 0.75
326
+ /* Padding/inter-item gap reuse the menu-item component's tokens (same reasoning as the icon/
327
+ supporting-text tokens below) instead of an arbitrary scaled dropdown padding — matches
328
+ Menu.module.css's `.item` padding + `margin-bottom` gap exactly. */
329
+ padding: var(
330
+ --recursica_ui-kit_components_menu-item_properties_vertical-padding
329
331
  )
330
- var(--recursica_ui-kit_components_dropdown_properties_horizontal-padding);
332
+ var(--recursica_ui-kit_components_menu-item_properties_horizontal-padding);
333
+ margin-bottom: var(--recursica_ui-kit_components_menu_properties_item-gap);
331
334
  cursor: pointer;
332
335
  }
333
336
 
337
+ .option:last-of-type {
338
+ margin-bottom: 0;
339
+ }
340
+
334
341
  /* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
335
342
  as Table's row hover) to highlight a hovered-but-not-selected option. */
336
343
  .option[data-hovered="true"],
@@ -347,9 +354,15 @@
347
354
  component's selected-state colors are the closest real token family for the same concept (a
348
355
  selected row in a list) — reused here rather than the neutral hover tint above, which is what
349
356
  previously made the selected option read as grey instead of Recursica's brand color. Matches
350
- the mui-adapter fix for the same gap. */
357
+ the mui-adapter fix for the same gap. Despite the name, Mantine's `data-combobox-active` — not
358
+ `data-combobox-selected` — is the attribute it sets when an option's value matches the current
359
+ field value (see OptionsDropdown.mjs: `active: checked`); `data-combobox-selected` is an
360
+ unrelated transient keyboard-navigation highlight (imperatively set/cleared via arrow keys —
361
+ use-combobox.mjs's `selectOption`/`clearSelectedItem`). Keying this off `data-combobox-selected`
362
+ meant the real selected option only showed the brand color while arrow-key navigating, and lost
363
+ it as soon as the dropdown was reopened normally. */
351
364
  .option[data-selected="true"],
352
- .option[data-combobox-selected="true"] {
365
+ .option[data-combobox-active="true"] {
353
366
  background-color: var(
354
367
  --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_background-color
355
368
  );
@@ -357,3 +370,98 @@
357
370
  --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
358
371
  );
359
372
  }
373
+
374
+ /* Rich option content (leadingIcon/supportingText — see MANTINE_ADAPTER_RICH_OPTION_DATA.md). No
375
+ dedicated dropdown-option icon or supporting-text token exists in the schema either; reuse the
376
+ menu-item component's tokens for the same reason the selected-state colors above do. */
377
+ .optionContent {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: var(--recursica_ui-kit_components_menu-item_properties_icon-text-gap);
381
+ }
382
+
383
+ .optionIcon {
384
+ display: flex;
385
+ flex-shrink: 0;
386
+ width: var(
387
+ --recursica_ui-kit_components_menu-item_properties_icon-leading-size
388
+ );
389
+ height: var(
390
+ --recursica_ui-kit_components_menu-item_properties_icon-leading-size
391
+ );
392
+ color: var(
393
+ --recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_leading-icon-color
394
+ );
395
+ }
396
+
397
+ .optionIcon :global(svg) {
398
+ width: 100%;
399
+ height: 100%;
400
+ }
401
+
402
+ .optionText {
403
+ display: flex;
404
+ flex-direction: column;
405
+ min-width: 0;
406
+ gap: var(--recursica_ui-kit_components_menu-item_properties_text-gap);
407
+ }
408
+
409
+ /* Default: label/supportingText each truncate to a single line with an ellipsis. */
410
+ .optionText > * {
411
+ overflow: hidden;
412
+ text-overflow: ellipsis;
413
+ white-space: nowrap;
414
+ }
415
+
416
+ /* Applied alongside .optionText when `wrapItemText` is true — long values wrap onto additional
417
+ lines instead of overflowing the fixed-width dropdown. */
418
+ .optionTextWrap > * {
419
+ overflow: visible;
420
+ text-overflow: clip;
421
+ white-space: normal;
422
+ overflow-wrap: anywhere;
423
+ }
424
+
425
+ .optionSupportingText {
426
+ font-family: var(
427
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-family
428
+ );
429
+ font-size: var(
430
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-size
431
+ );
432
+ font-style: var(
433
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-style
434
+ );
435
+ font-weight: var(
436
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-weight
437
+ );
438
+ letter-spacing: var(
439
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_letter-spacing
440
+ );
441
+ line-height: var(
442
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_line-height
443
+ );
444
+ text-decoration: var(
445
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_text-decoration
446
+ );
447
+ text-transform: var(
448
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_text-transform
449
+ );
450
+ color: var(
451
+ --recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_supporting-text-color
452
+ );
453
+ }
454
+
455
+ .option[data-selected="true"] .optionIcon,
456
+ .option[data-combobox-active="true"] .optionIcon {
457
+ color: var(
458
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_leading-icon-color
459
+ );
460
+ }
461
+
462
+ .option[data-selected="true"] .optionSupportingText,
463
+ .option[data-combobox-active="true"] .optionSupportingText {
464
+ color: var(
465
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_supporting-text-color
466
+ );
467
+ }