@recursica/mantine-adapter 0.48.0 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.d.ts +42 -7
  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 +1662 -1613
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/components/Accordion/Accordion.module.css +11 -0
  10. package/src/components/AutoComplete/AUTOCOMPLETE_IMPLEMENTATION_NOTES.md +18 -2
  11. package/src/components/AutoComplete/AutoComplete.module.css +127 -12
  12. package/src/components/AutoComplete/AutoComplete.stories.tsx +149 -0
  13. package/src/components/AutoComplete/AutoComplete.tsx +29 -1
  14. package/src/components/AutoComplete/USAGE.md +32 -2
  15. package/src/components/Button/Button.module.css +10 -0
  16. package/src/components/Card/Card.stories.tsx +2 -4
  17. package/src/components/Chip/Chip.module.css +9 -0
  18. package/src/components/DatePicker/DatePicker.module.css +16 -0
  19. package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +3 -0
  20. package/src/components/Dropdown/Dropdown.module.css +124 -6
  21. package/src/components/Dropdown/Dropdown.stories.tsx +149 -0
  22. package/src/components/Dropdown/Dropdown.tsx +30 -2
  23. package/src/components/Dropdown/USAGE.md +21 -0
  24. package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +4 -1
  25. package/src/components/FileInput/FileInput.module.css +17 -0
  26. package/src/components/FileUpload/FileUpload.module.css +6 -0
  27. package/src/components/Link/Link.module.css +5 -0
  28. package/src/components/Menu/Menu.module.css +6 -0
  29. package/src/components/NumberInput/NumberInput.module.css +8 -0
  30. package/src/components/Pagination/Pagination.module.css +7 -0
  31. package/src/components/Slider/Slider.module.css +8 -0
  32. package/src/components/Switch/Switch.module.css +8 -0
  33. package/src/components/Table/Table.module.css +6 -0
  34. package/src/components/Tabs/Tabs.module.css +7 -0
  35. package/src/components/TextArea/TextArea.module.css +8 -0
  36. package/src/components/TextField/TextField.module.css +8 -0
  37. package/src/components/TimePicker/TimePicker.module.css +8 -0
  38. package/src/components/Tree/Tree.module.css +11 -0
  39. package/src/utils/renderRichOption.tsx +65 -0
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.0",
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": {
@@ -1,3 +1,14 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_disabled
4
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
5
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
6
+ * recursica-allow-brand: --recursica_brand_states_focus_color
7
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
8
+ * recursica-allow-brand: --recursica_brand_states_hover_color
9
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
10
+ */
11
+
1
12
  /* ==== ACCORDION CONTAINER ==== */
2
13
  .root {
3
14
  box-sizing: border-box;
@@ -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.
@@ -1,3 +1,13 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ * recursica-allow-brand: --recursica_brand_states_hover_color
8
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
9
+ */
10
+
1
11
  /* LAYOUT SPACING OVERRIDES:
2
12
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
3
13
  - Also sets the --autocomplete-control-{max,min}-width hooks consumed inline in AutoComplete.tsx,
@@ -202,26 +212,31 @@
202
212
  color: var(
203
213
  --recursica_ui-kit_components_autocomplete_properties_colors_text-color
204
214
  );
205
- padding: calc(
206
- var(
207
- --recursica_ui-kit_components_autocomplete_properties_vertical-padding
208
- ) *
209
- 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
210
220
  )
211
- var(
212
- --recursica_ui-kit_components_autocomplete_properties_horizontal-padding
213
- );
221
+ var(--recursica_ui-kit_components_menu-item_properties_horizontal-padding);
222
+ margin-bottom: var(--recursica_ui-kit_components_menu_properties_item-gap);
214
223
  cursor: pointer;
215
224
  }
216
225
 
217
- /* No per-option selected/hovered tokens exist in the schema; use the generic overlay tint
218
- (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. */
219
236
  .option[data-selected="true"],
220
237
  .option[data-selected],
221
238
  .option[data-combobox-selected="true"],
222
239
  .option[data-combobox-selected],
223
- .option[data-combobox-active="true"],
224
- .option[data-combobox-active],
225
240
  .option[data-hovered="true"],
226
241
  .option[data-hovered],
227
242
  .option:hover {
@@ -233,6 +248,106 @@
233
248
  );
234
249
  }
235
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
+
236
351
  /* -------------------------------------
237
352
  STATE CASCADE ARCHITECTURE
238
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.
@@ -1,3 +1,13 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ * recursica-allow-brand: --recursica_brand_states_hover_color
8
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
9
+ */
10
+
1
11
  /**
2
12
  * IMPLEMENTATION NOTES:
3
13
  *
@@ -37,8 +37,7 @@ export const Default: Story = {
37
37
  <div
38
38
  style={{
39
39
  padding: "48px",
40
- backgroundColor:
41
- "var(--recursica_brand_palettes_neutral_050_color_tone)",
40
+ backgroundColor: "#e9ecef",
42
41
  }}
43
42
  >
44
43
  <Layer layer={0}>
@@ -73,8 +72,7 @@ export const HeaderlessAndFooterless: Story = {
73
72
  <div
74
73
  style={{
75
74
  padding: "48px",
76
- backgroundColor:
77
- "var(--recursica_brand_palettes_neutral_050_color_tone)",
75
+ backgroundColor: "#e9ecef",
78
76
  }}
79
77
  >
80
78
  <Layer layer={0}>
@@ -1,3 +1,12 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_disabled
4
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
5
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
6
+ * recursica-allow-brand: --recursica_brand_states_focus_color
7
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
8
+ */
9
+
1
10
  /* HARDCODED VALUES
2
11
  border-style: solid; (Baseline reset)
3
12
  box-sizing: border-box; (Baseline reset)
@@ -1,3 +1,19 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * No ui-kit_components_date-picker radius token exists for the calendar day cells; reuses the brand default border-radius.
3
+ * recursica-allow-brand: --recursica_brand_dimensions_border-radii_default
4
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
5
+ * recursica-allow-brand: --recursica_brand_states_disabled
6
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
7
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
8
+ * recursica-allow-brand: --recursica_brand_states_focus_color
9
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
10
+ * recursica-allow-brand: --recursica_brand_states_hover_color
11
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
12
+ * No ui-kit_components_date-picker in-range token exists; the generic overlay tint fills the range-selection band (see inline comment above .day[data-in-range]).
13
+ * recursica-allow-brand: --recursica_brand_states_overlay_color
14
+ * recursica-allow-brand: --recursica_brand_states_overlay_opacity
15
+ */
16
+
1
17
  /* LAYOUT SPACING OVERRIDES:
2
18
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens. */
3
19
  .layoutOverride {
@@ -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).