@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.
- package/CHANGELOG.md +18 -0
- package/dist/index.d.ts +42 -7
- package/dist/mantine-adapter.cjs +2 -2
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +1662 -1613
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.module.css +11 -0
- package/src/components/AutoComplete/AUTOCOMPLETE_IMPLEMENTATION_NOTES.md +18 -2
- package/src/components/AutoComplete/AutoComplete.module.css +127 -12
- package/src/components/AutoComplete/AutoComplete.stories.tsx +149 -0
- package/src/components/AutoComplete/AutoComplete.tsx +29 -1
- package/src/components/AutoComplete/USAGE.md +32 -2
- package/src/components/Button/Button.module.css +10 -0
- package/src/components/Card/Card.stories.tsx +2 -4
- package/src/components/Chip/Chip.module.css +9 -0
- package/src/components/DatePicker/DatePicker.module.css +16 -0
- package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +3 -0
- package/src/components/Dropdown/Dropdown.module.css +124 -6
- package/src/components/Dropdown/Dropdown.stories.tsx +149 -0
- package/src/components/Dropdown/Dropdown.tsx +30 -2
- package/src/components/Dropdown/USAGE.md +21 -0
- package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +4 -1
- package/src/components/FileInput/FileInput.module.css +17 -0
- package/src/components/FileUpload/FileUpload.module.css +6 -0
- package/src/components/Link/Link.module.css +5 -0
- package/src/components/Menu/Menu.module.css +6 -0
- package/src/components/NumberInput/NumberInput.module.css +8 -0
- package/src/components/Pagination/Pagination.module.css +7 -0
- package/src/components/Slider/Slider.module.css +8 -0
- package/src/components/Switch/Switch.module.css +8 -0
- package/src/components/Table/Table.module.css +6 -0
- package/src/components/Tabs/Tabs.module.css +7 -0
- package/src/components/TextArea/TextArea.module.css +8 -0
- package/src/components/TextField/TextField.module.css +8 -0
- package/src/components/TimePicker/TimePicker.module.css +8 -0
- package/src/components/Tree/Tree.module.css +11 -0
- 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.
|
|
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.
|
|
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
|
|
19
|
+
## Missing Selected Option Highlight
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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 {
|
|
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
|
-
###
|
|
48
|
+
### Rich option content: `leadingIcon` / `supportingText`
|
|
49
49
|
|
|
50
|
-
|
|
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).
|