lkd-web-kit 0.10.2 → 0.10.4
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/.agents/skills/mantine-combobox/SKILL.md +73 -0
- package/.agents/skills/mantine-combobox/references/api.md +199 -0
- package/.agents/skills/mantine-combobox/references/patterns.md +279 -0
- package/.agents/skills/mantine-custom-components/SKILL.md +112 -0
- package/.agents/skills/mantine-custom-components/references/api.md +407 -0
- package/.agents/skills/mantine-custom-components/references/patterns.md +431 -0
- package/.agents/skills/publish-lkd-web-kit/SKILL.md +165 -0
- package/.agents/skills/publish-lkd-web-kit/agents/openai.yaml +4 -0
- package/.agents/skills/publish-lkd-web-kit/references/npm-trusted-publishing.md +82 -0
- package/.agents/skills/update-lkd-dependencies/SKILL.md +88 -0
- package/.agents/skills/update-lkd-dependencies/agents/openai.yaml +4 -0
- package/.agents/skills/update-lkd-dependencies/scripts/collect-npm-metadata.mjs +73 -0
- package/dist/index.cjs +2 -0
- package/dist/index.js +2 -1
- package/dist/mantine/breakpoints-with-px.cjs +3 -2
- package/dist/mantine/breakpoints-with-px.d.ts +2 -1
- package/dist/mantine/breakpoints-with-px.d.ts.map +1 -1
- package/dist/mantine/breakpoints-with-px.js +3 -2
- package/dist/mantine/create-lkd-theme.cjs +7 -0
- package/dist/mantine/create-lkd-theme.d.ts +3 -0
- package/dist/mantine/create-lkd-theme.d.ts.map +1 -0
- package/dist/mantine/create-lkd-theme.js +6 -0
- package/dist/mantine/index.d.ts +1 -0
- package/dist/mantine/index.d.ts.map +1 -1
- package/dist/mantine/my-default-theme.cjs +14 -0
- package/dist/mantine/my-default-theme.d.ts.map +1 -1
- package/dist/mantine/my-default-theme.js +15 -1
- package/package.json +3 -3
- package/scripts/{install-codex-skills.mjs → install-agent-skills.mjs} +1 -1
- package/codex/skills/create-modal-component/SKILL.md +0 -168
- package/codex/skills/create-modal-component/agents/openai.yaml +0 -4
- package/codex/skills/create-svg-icon/SKILL.md +0 -41
- package/codex/skills/create-svg-icon/agents/openai.yaml +0 -3
- package/codex/skills/create-table-component/SKILL.md +0 -189
- package/codex/skills/create-table-component/agents/openai.yaml +0 -3
- package/codex/skills/rhf-lkd-forms/SKILL.md +0 -186
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mantine-combobox
|
|
3
|
+
description: >
|
|
4
|
+
Build custom dropdown/select/autocomplete/multiselect components using Mantine's Combobox
|
|
5
|
+
primitives. Use this skill when: (1) creating a new custom select-like component with
|
|
6
|
+
Combobox primitives, (2) building a searchable dropdown, (3) implementing a multi-select
|
|
7
|
+
or tags input variant, (4) customizing option rendering, (5) adding custom filtering logic,
|
|
8
|
+
or (6) any task involving useCombobox, Combobox.Target, Combobox.Option, or Combobox.Dropdown.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Mantine Combobox Skill
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
`Combobox` provides low-level primitives for building any select-like UI. The built-in
|
|
16
|
+
`Select`, `Autocomplete`, and `TagsInput` components are all built on top of it.
|
|
17
|
+
|
|
18
|
+
## Core Workflow
|
|
19
|
+
|
|
20
|
+
### 1. Create the store
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
const combobox = useCombobox({
|
|
24
|
+
onDropdownClose: () => combobox.resetSelectedOption(),
|
|
25
|
+
onDropdownOpen: () => combobox.selectFirstOption(),
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Render structure
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<Combobox store={combobox} onOptionSubmit={handleSubmit}>
|
|
33
|
+
<Combobox.Target>
|
|
34
|
+
<InputBase
|
|
35
|
+
component="button"
|
|
36
|
+
pointer
|
|
37
|
+
rightSection={<Combobox.Chevron />}
|
|
38
|
+
onClick={() => combobox.toggleDropdown()}
|
|
39
|
+
>
|
|
40
|
+
{value || <Input.Placeholder>Pick value</Input.Placeholder>}
|
|
41
|
+
</InputBase>
|
|
42
|
+
</Combobox.Target>
|
|
43
|
+
<Combobox.Dropdown>
|
|
44
|
+
<Combobox.Options>
|
|
45
|
+
{options.map((item) => (
|
|
46
|
+
<Combobox.Option value={item} key={item}>{item}</Combobox.Option>
|
|
47
|
+
))}
|
|
48
|
+
</Combobox.Options>
|
|
49
|
+
</Combobox.Dropdown>
|
|
50
|
+
</Combobox>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Handle submit
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const handleSubmit = (val: string) => {
|
|
57
|
+
setValue(val);
|
|
58
|
+
combobox.closeDropdown();
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Target Types
|
|
63
|
+
|
|
64
|
+
| Scenario | Use |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Button trigger (no text input) | `<Combobox.Target targetType="button">` |
|
|
67
|
+
| Input trigger | `<Combobox.Target>` (default) |
|
|
68
|
+
| Pills + separate input (multi-select) | `<Combobox.DropdownTarget>` + `<Combobox.EventsTarget>` |
|
|
69
|
+
|
|
70
|
+
## References
|
|
71
|
+
|
|
72
|
+
- **[`references/api.md`](references/api.md)** — Full API: `useCombobox` options and store, all sub-component props, CSS variables, Styles API selectors
|
|
73
|
+
- **[`references/patterns.md`](references/patterns.md)** — Code examples: searchable select, multi-select with pills, groups, custom rendering, clear button, form integration
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Combobox API Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [useCombobox hook](#usecombobox-hook)
|
|
5
|
+
- [Combobox (root)](#combobox-root)
|
|
6
|
+
- [Sub-components](#sub-components)
|
|
7
|
+
- [CSS variables & Styles API](#css-variables--styles-api)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## useCombobox hook
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
const combobox = useCombobox(options?: UseComboboxOptions);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Options:**
|
|
18
|
+
```ts
|
|
19
|
+
interface UseComboboxOptions {
|
|
20
|
+
defaultOpened?: boolean;
|
|
21
|
+
opened?: boolean;
|
|
22
|
+
onOpenedChange?: (opened: boolean) => void;
|
|
23
|
+
onDropdownClose?: (eventSource: 'keyboard' | 'mouse' | 'unknown') => void;
|
|
24
|
+
onDropdownOpen?: (eventSource: 'keyboard' | 'mouse' | 'unknown') => void;
|
|
25
|
+
loop?: boolean; // Default: true — keyboard nav wraps at boundaries
|
|
26
|
+
scrollBehavior?: ScrollBehavior; // Default: 'instant'
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Returned store:**
|
|
31
|
+
```ts
|
|
32
|
+
interface ComboboxStore {
|
|
33
|
+
// Dropdown state
|
|
34
|
+
dropdownOpened: boolean;
|
|
35
|
+
openDropdown(eventSource?: 'keyboard' | 'mouse' | 'unknown'): void;
|
|
36
|
+
closeDropdown(eventSource?: 'keyboard' | 'mouse' | 'unknown'): void;
|
|
37
|
+
toggleDropdown(eventSource?: 'keyboard' | 'mouse' | 'unknown'): void;
|
|
38
|
+
|
|
39
|
+
// Option keyboard navigation
|
|
40
|
+
selectActiveOption(): string | null;
|
|
41
|
+
selectFirstOption(): string | null;
|
|
42
|
+
selectNextOption(): string | null;
|
|
43
|
+
selectPreviousOption(): string | null;
|
|
44
|
+
resetSelectedOption(): void;
|
|
45
|
+
clickSelectedOption(): void;
|
|
46
|
+
updateSelectedOptionIndex(
|
|
47
|
+
target?: 'active' | 'selected' | number,
|
|
48
|
+
options?: { scrollIntoView?: boolean }
|
|
49
|
+
): void;
|
|
50
|
+
|
|
51
|
+
// Programmatic focus
|
|
52
|
+
focusSearchInput(): void;
|
|
53
|
+
focusTarget(): void;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Combobox (root)
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<Combobox
|
|
63
|
+
store={combobox} // required — ComboboxStore from useCombobox()
|
|
64
|
+
onOptionSubmit={fn} // (value: string, optionProps) => void
|
|
65
|
+
size="sm" // MantineSize | string, default: 'sm'
|
|
66
|
+
dropdownPadding={4} // number, default: 4
|
|
67
|
+
resetSelectionOnOptionHover // boolean
|
|
68
|
+
readOnly // boolean — disables all interactions
|
|
69
|
+
// + all Popover props (position, offset, width, withinPortal, etc.)
|
|
70
|
+
/>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Sub-components
|
|
76
|
+
|
|
77
|
+
### Combobox.Target
|
|
78
|
+
```tsx
|
|
79
|
+
<Combobox.Target
|
|
80
|
+
targetType="input" // 'button' | 'input', default: 'input'
|
|
81
|
+
withKeyboardNavigation // boolean, default: true
|
|
82
|
+
withAriaAttributes // boolean, default: true
|
|
83
|
+
withExpandedAttribute // boolean, default: false
|
|
84
|
+
autoComplete="off" // string
|
|
85
|
+
>
|
|
86
|
+
{/* single child — the trigger element */}
|
|
87
|
+
</Combobox.Target>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use `targetType="button"` when the trigger is a button (Space key toggles dropdown).
|
|
91
|
+
|
|
92
|
+
### Combobox.DropdownTarget
|
|
93
|
+
Marks the element used for dropdown positioning when separate from the keyboard events target. Used together with `Combobox.EventsTarget` in multi-select/pills patterns.
|
|
94
|
+
|
|
95
|
+
### Combobox.EventsTarget
|
|
96
|
+
Receives keyboard events for dropdown navigation. Used alongside `Combobox.DropdownTarget` when the typing input is nested inside the trigger (e.g. inside `PillsInput`).
|
|
97
|
+
|
|
98
|
+
### Combobox.Dropdown
|
|
99
|
+
```tsx
|
|
100
|
+
<Combobox.Dropdown hidden={false}>
|
|
101
|
+
{/* dropdown content */}
|
|
102
|
+
</Combobox.Dropdown>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use `hidden` to hide without unmounting (preserves scroll position).
|
|
106
|
+
|
|
107
|
+
### Combobox.Options
|
|
108
|
+
```tsx
|
|
109
|
+
<Combobox.Options labelledBy="some-label-id">
|
|
110
|
+
{/* Combobox.Option or Combobox.Group elements */}
|
|
111
|
+
</Combobox.Options>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Combobox.Option
|
|
115
|
+
```tsx
|
|
116
|
+
<Combobox.Option
|
|
117
|
+
value="react" // string | number, required
|
|
118
|
+
active={false} // visually marks as selected; used by selectActiveOption()
|
|
119
|
+
disabled={false}
|
|
120
|
+
>
|
|
121
|
+
React
|
|
122
|
+
</Combobox.Option>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Combobox.Search
|
|
126
|
+
Built-in search input, wired to keyboard navigation. Renders sticky at top of dropdown.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<Combobox.Search
|
|
130
|
+
value={search}
|
|
131
|
+
onChange={(e) => setSearch(e.currentTarget.value)}
|
|
132
|
+
placeholder="Search..."
|
|
133
|
+
withAriaAttributes // boolean, default: true
|
|
134
|
+
withKeyboardNavigation // boolean, default: true
|
|
135
|
+
// + all Input props
|
|
136
|
+
/>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Combobox.Empty
|
|
140
|
+
```tsx
|
|
141
|
+
<Combobox.Empty>Nothing found</Combobox.Empty>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Combobox.Group
|
|
145
|
+
```tsx
|
|
146
|
+
<Combobox.Group label="Frontend">
|
|
147
|
+
<Combobox.Option value="react">React</Combobox.Option>
|
|
148
|
+
</Combobox.Group>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Combobox.Header / Combobox.Footer
|
|
152
|
+
```tsx
|
|
153
|
+
<Combobox.Header>Custom header</Combobox.Header>
|
|
154
|
+
<Combobox.Footer>Custom footer</Combobox.Footer>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Combobox.Chevron
|
|
158
|
+
```tsx
|
|
159
|
+
<Combobox.Chevron size="sm" error={error} color="blue" />
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Combobox.ClearButton
|
|
163
|
+
```tsx
|
|
164
|
+
<Combobox.ClearButton onClear={() => setValue(null)} />
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Combobox.HiddenInput
|
|
168
|
+
```tsx
|
|
169
|
+
<Combobox.HiddenInput
|
|
170
|
+
value={value} // string | number | (string | number)[] | null
|
|
171
|
+
valuesDivider="," // string, default: ','
|
|
172
|
+
name="myField"
|
|
173
|
+
form="myForm"
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## CSS variables & Styles API
|
|
180
|
+
|
|
181
|
+
### CSS variables (set on root element)
|
|
182
|
+
| Variable | Description |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `--combobox-option-fz` | Option font size (driven by `size` prop) |
|
|
185
|
+
| `--combobox-option-padding` | Option padding (driven by `size` prop) |
|
|
186
|
+
| `--combobox-padding` | Dropdown padding (driven by `dropdownPadding`, default 4px) |
|
|
187
|
+
|
|
188
|
+
### Styles API selectors
|
|
189
|
+
| Selector | Element |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `dropdown` | Dropdown container |
|
|
192
|
+
| `options` | Options listbox |
|
|
193
|
+
| `option` | Individual option |
|
|
194
|
+
| `search` | Search input |
|
|
195
|
+
| `empty` | Nothing found message |
|
|
196
|
+
| `header` | Dropdown header |
|
|
197
|
+
| `footer` | Dropdown footer |
|
|
198
|
+
| `group` | Group container |
|
|
199
|
+
| `groupLabel` | Group label text |
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Combobox Implementation Patterns
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Basic select (button trigger)](#basic-select-button-trigger)
|
|
5
|
+
- [Searchable select (input trigger)](#searchable-select-input-trigger)
|
|
6
|
+
- [Multi-select with pills](#multi-select-with-pills)
|
|
7
|
+
- [Options with groups](#options-with-groups)
|
|
8
|
+
- [Custom option rendering](#custom-option-rendering)
|
|
9
|
+
- [Clear button](#clear-button)
|
|
10
|
+
- [Form integration (hidden input)](#form-integration-hidden-input)
|
|
11
|
+
- [Nothing found message](#nothing-found-message)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Basic select (button trigger)
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
function CustomSelect({ data }: { data: string[] }) {
|
|
19
|
+
const [value, setValue] = useState<string | null>(null);
|
|
20
|
+
|
|
21
|
+
const combobox = useCombobox({
|
|
22
|
+
onDropdownClose: () => combobox.resetSelectedOption(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const options = data.map((item) => (
|
|
26
|
+
<Combobox.Option value={item} key={item} active={item === value}>
|
|
27
|
+
{item}
|
|
28
|
+
</Combobox.Option>
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Combobox
|
|
33
|
+
store={combobox}
|
|
34
|
+
onOptionSubmit={(val) => {
|
|
35
|
+
setValue(val);
|
|
36
|
+
combobox.closeDropdown();
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<Combobox.Target targetType="button">
|
|
40
|
+
<InputBase
|
|
41
|
+
component="button"
|
|
42
|
+
type="button"
|
|
43
|
+
pointer
|
|
44
|
+
rightSection={<Combobox.Chevron />}
|
|
45
|
+
rightSectionPointerEvents="none"
|
|
46
|
+
onClick={() => combobox.toggleDropdown()}
|
|
47
|
+
>
|
|
48
|
+
{value || <Input.Placeholder>Pick value</Input.Placeholder>}
|
|
49
|
+
</InputBase>
|
|
50
|
+
</Combobox.Target>
|
|
51
|
+
|
|
52
|
+
<Combobox.Dropdown>
|
|
53
|
+
<Combobox.Options>{options}</Combobox.Options>
|
|
54
|
+
</Combobox.Dropdown>
|
|
55
|
+
</Combobox>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Searchable select (input trigger)
|
|
63
|
+
|
|
64
|
+
Input acts as both trigger and search field. Filter options based on the typed value.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
function SearchableSelect({ data }: { data: string[] }) {
|
|
68
|
+
const [value, setValue] = useState<string | null>(null);
|
|
69
|
+
const [search, setSearch] = useState('');
|
|
70
|
+
|
|
71
|
+
const combobox = useCombobox({
|
|
72
|
+
onDropdownClose: () => combobox.resetSelectedOption(),
|
|
73
|
+
onDropdownOpen: () => combobox.selectFirstOption(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const shouldFilterOptions = data.every((item) => item !== search);
|
|
77
|
+
const filtered = shouldFilterOptions
|
|
78
|
+
? data.filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))
|
|
79
|
+
: data;
|
|
80
|
+
|
|
81
|
+
const options = filtered.length > 0
|
|
82
|
+
? filtered.map((item) => (
|
|
83
|
+
<Combobox.Option value={item} key={item}>{item}</Combobox.Option>
|
|
84
|
+
))
|
|
85
|
+
: <Combobox.Empty>Nothing found</Combobox.Empty>;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Combobox
|
|
89
|
+
store={combobox}
|
|
90
|
+
onOptionSubmit={(val) => {
|
|
91
|
+
setValue(val);
|
|
92
|
+
setSearch(val);
|
|
93
|
+
combobox.closeDropdown();
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<Combobox.Target>
|
|
97
|
+
<InputBase
|
|
98
|
+
rightSection={<Combobox.Chevron />}
|
|
99
|
+
value={search}
|
|
100
|
+
onChange={(e) => {
|
|
101
|
+
combobox.openDropdown();
|
|
102
|
+
combobox.updateSelectedOptionIndex();
|
|
103
|
+
setSearch(e.currentTarget.value);
|
|
104
|
+
}}
|
|
105
|
+
onClick={() => combobox.openDropdown()}
|
|
106
|
+
onFocus={() => combobox.openDropdown()}
|
|
107
|
+
onBlur={() => {
|
|
108
|
+
combobox.closeDropdown();
|
|
109
|
+
setSearch(value || '');
|
|
110
|
+
}}
|
|
111
|
+
placeholder="Search value"
|
|
112
|
+
rightSectionPointerEvents="none"
|
|
113
|
+
/>
|
|
114
|
+
</Combobox.Target>
|
|
115
|
+
|
|
116
|
+
<Combobox.Dropdown>
|
|
117
|
+
<Combobox.Options>{options}</Combobox.Options>
|
|
118
|
+
</Combobox.Dropdown>
|
|
119
|
+
</Combobox>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Multi-select with pills
|
|
127
|
+
|
|
128
|
+
Use `Combobox.DropdownTarget` for the outer container and `Combobox.EventsTarget` around the text input inside it.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
function MultiSelect({ data }: { data: string[] }) {
|
|
132
|
+
const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption() });
|
|
133
|
+
const [search, setSearch] = useState('');
|
|
134
|
+
const [value, setValue] = useState<string[]>([]);
|
|
135
|
+
|
|
136
|
+
const handleValueSelect = (val: string) =>
|
|
137
|
+
setValue((current) =>
|
|
138
|
+
current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const handleValueRemove = (val: string) =>
|
|
142
|
+
setValue((current) => current.filter((v) => v !== val));
|
|
143
|
+
|
|
144
|
+
const options = data
|
|
145
|
+
.filter((item) => item.toLowerCase().includes(search.trim().toLowerCase()))
|
|
146
|
+
.map((item) => (
|
|
147
|
+
<Combobox.Option value={item} key={item} active={value.includes(item)}>
|
|
148
|
+
<Group gap="sm">
|
|
149
|
+
{value.includes(item) ? <CheckIcon size={12} /> : null}
|
|
150
|
+
<span>{item}</span>
|
|
151
|
+
</Group>
|
|
152
|
+
</Combobox.Option>
|
|
153
|
+
));
|
|
154
|
+
|
|
155
|
+
const pills = value.map((item) => (
|
|
156
|
+
<Pill key={item} withRemoveButton onRemove={() => handleValueRemove(item)}>
|
|
157
|
+
{item}
|
|
158
|
+
</Pill>
|
|
159
|
+
));
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Combobox store={combobox} onOptionSubmit={handleValueSelect}>
|
|
163
|
+
<Combobox.DropdownTarget>
|
|
164
|
+
<PillsInput onClick={() => combobox.openDropdown()}>
|
|
165
|
+
<Pill.Group>
|
|
166
|
+
{pills}
|
|
167
|
+
<Combobox.EventsTarget>
|
|
168
|
+
<PillsInput.Field
|
|
169
|
+
onFocus={() => combobox.openDropdown()}
|
|
170
|
+
onBlur={() => combobox.closeDropdown()}
|
|
171
|
+
value={search}
|
|
172
|
+
placeholder="Search values"
|
|
173
|
+
onChange={(e) => {
|
|
174
|
+
combobox.updateSelectedOptionIndex();
|
|
175
|
+
setSearch(e.currentTarget.value);
|
|
176
|
+
}}
|
|
177
|
+
onKeyDown={(e) => {
|
|
178
|
+
if (e.key === 'Backspace' && search.length === 0) {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
handleValueRemove(value[value.length - 1]);
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
</Combobox.EventsTarget>
|
|
185
|
+
</Pill.Group>
|
|
186
|
+
</PillsInput>
|
|
187
|
+
</Combobox.DropdownTarget>
|
|
188
|
+
|
|
189
|
+
<Combobox.Dropdown>
|
|
190
|
+
<Combobox.Options>
|
|
191
|
+
{options.length > 0 ? options : <Combobox.Empty>Nothing found</Combobox.Empty>}
|
|
192
|
+
</Combobox.Options>
|
|
193
|
+
</Combobox.Dropdown>
|
|
194
|
+
</Combobox>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Options with groups
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const groups = data.map((group) => (
|
|
205
|
+
<Combobox.Group label={group.group} key={group.group}>
|
|
206
|
+
{group.items.map((item) => (
|
|
207
|
+
<Combobox.Option value={item.value} key={item.value}>
|
|
208
|
+
{item.label}
|
|
209
|
+
</Combobox.Option>
|
|
210
|
+
))}
|
|
211
|
+
</Combobox.Group>
|
|
212
|
+
));
|
|
213
|
+
|
|
214
|
+
// Inside Combobox.Dropdown:
|
|
215
|
+
<Combobox.Options>{groups}</Combobox.Options>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Custom option rendering
|
|
221
|
+
|
|
222
|
+
Render any content inside `Combobox.Option`:
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
const options = data.map((item) => (
|
|
226
|
+
<Combobox.Option value={item.value} key={item.value}>
|
|
227
|
+
<Group>
|
|
228
|
+
<Avatar src={item.avatar} size="sm" />
|
|
229
|
+
<div>
|
|
230
|
+
<Text size="sm">{item.label}</Text>
|
|
231
|
+
<Text size="xs" c="dimmed">{item.email}</Text>
|
|
232
|
+
</div>
|
|
233
|
+
</Group>
|
|
234
|
+
</Combobox.Option>
|
|
235
|
+
));
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Clear button
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
const rightSection = value ? (
|
|
244
|
+
<Combobox.ClearButton onClear={() => { setValue(null); setSearch(''); }} />
|
|
245
|
+
) : (
|
|
246
|
+
<Combobox.Chevron />
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
<InputBase
|
|
250
|
+
rightSection={rightSection}
|
|
251
|
+
rightSectionPointerEvents={value ? 'all' : 'none'}
|
|
252
|
+
/>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Form integration (hidden input)
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
// Single value
|
|
261
|
+
<Combobox.HiddenInput value={value} name="framework" />
|
|
262
|
+
|
|
263
|
+
// Multiple values (joined by comma by default)
|
|
264
|
+
<Combobox.HiddenInput value={selectedValues} name="frameworks" valuesDivider="," />
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Nothing found message
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
<Combobox.Dropdown>
|
|
273
|
+
<Combobox.Options>
|
|
274
|
+
{filteredOptions.length > 0
|
|
275
|
+
? filteredOptions
|
|
276
|
+
: <Combobox.Empty>Nothing found for "{search}"</Combobox.Empty>}
|
|
277
|
+
</Combobox.Options>
|
|
278
|
+
</Combobox.Dropdown>
|
|
279
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mantine-custom-components
|
|
3
|
+
description: >
|
|
4
|
+
Build custom components that integrate with Mantine's theming, Styles API, and core features.
|
|
5
|
+
Use this skill when: (1) creating a new component using factory(), polymorphicFactory(), or
|
|
6
|
+
genericFactory(), (2) adding Styles API support (classNames, styles, vars, unstyled), (3)
|
|
7
|
+
implementing CSS variables via createVarsResolver, (4) building compound components with
|
|
8
|
+
sub-components and shared context, (5) registering a component with MantineProvider via
|
|
9
|
+
Component.extend(), or (6) any task involving Factory, useProps, useStyles, BoxProps,
|
|
10
|
+
StylesApiProps, or ElementProps in @mantine/core.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Mantine Custom Components Skill
|
|
14
|
+
|
|
15
|
+
## Component template
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import {
|
|
19
|
+
Box, BoxProps, createVarsResolver, ElementProps,
|
|
20
|
+
factory, Factory, getRadius, MantineRadius,
|
|
21
|
+
StylesApiProps, useProps, useStyles,
|
|
22
|
+
} from '@mantine/core';
|
|
23
|
+
import classes from './MyComponent.module.css';
|
|
24
|
+
|
|
25
|
+
export type MyComponentStylesNames = 'root' | 'inner';
|
|
26
|
+
export type MyComponentVariant = 'filled' | 'outline';
|
|
27
|
+
export type MyComponentCssVariables = { root: '--my-radius' };
|
|
28
|
+
|
|
29
|
+
export interface MyComponentProps
|
|
30
|
+
extends BoxProps, StylesApiProps<MyComponentFactory>, ElementProps<'div'> {
|
|
31
|
+
radius?: MantineRadius;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type MyComponentFactory = Factory<{
|
|
35
|
+
props: MyComponentProps;
|
|
36
|
+
ref: HTMLDivElement;
|
|
37
|
+
stylesNames: MyComponentStylesNames;
|
|
38
|
+
vars: MyComponentCssVariables;
|
|
39
|
+
variant: MyComponentVariant;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
const defaultProps = { radius: 'md' } satisfies Partial<MyComponentProps>;
|
|
43
|
+
|
|
44
|
+
const varsResolver = createVarsResolver<MyComponentFactory>((_theme, { radius }) => ({
|
|
45
|
+
root: { '--my-radius': getRadius(radius) },
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
export const MyComponent = factory<MyComponentFactory>((_props) => {
|
|
49
|
+
const props = useProps('MyComponent', defaultProps, _props);
|
|
50
|
+
const { classNames, className, style, styles, unstyled, vars, attributes, radius, ...others } = props;
|
|
51
|
+
|
|
52
|
+
const getStyles = useStyles<MyComponentFactory>({
|
|
53
|
+
name: 'MyComponent', classes, props,
|
|
54
|
+
className, style, classNames, styles, unstyled, vars, attributes, varsResolver,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return <Box {...getStyles('root')} {...others} />;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
MyComponent.displayName = '@mantine/core/MyComponent';
|
|
61
|
+
MyComponent.classes = classes;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Factory variant — which to use
|
|
65
|
+
|
|
66
|
+
| Scenario | Factory function | Type |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Standard component | `factory()` | `Factory<{}>` |
|
|
69
|
+
| Supports `component` prop (polymorphic) | `polymorphicFactory()` | `PolymorphicFactory<{}>` — add `defaultComponent` and `defaultRef` |
|
|
70
|
+
| Props change based on a generic (e.g. `multiple`) | `genericFactory()` | `Factory<{ signature: ... }>` |
|
|
71
|
+
|
|
72
|
+
Use `polymorphicFactory` sparingly — it adds TypeScript overhead and slows IDE autocomplete.
|
|
73
|
+
|
|
74
|
+
## Factory type fields
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
Factory<{
|
|
78
|
+
props: MyComponentProps; // required
|
|
79
|
+
ref: HTMLDivElement; // element type for the forwarded ref
|
|
80
|
+
stylesNames: 'root' | 'inner'; // union of Styles API selectors
|
|
81
|
+
vars: { root: '--my-var' }; // CSS variable map per selector
|
|
82
|
+
variant: 'filled' | 'outline'; // accepted variant strings
|
|
83
|
+
staticComponents: { // sub-components (compound pattern)
|
|
84
|
+
Item: typeof MyComponentItem;
|
|
85
|
+
};
|
|
86
|
+
compound?: boolean; // true = sub-component; disables theme classNames/styles/vars
|
|
87
|
+
ctx?: MyContextType; // passed to styles/vars resolvers as third arg
|
|
88
|
+
signature?: (...) => JSX.Element; // only for genericFactory
|
|
89
|
+
}>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Theme integration
|
|
93
|
+
|
|
94
|
+
Users and the theme can override defaults via `Component.extend()`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const theme = createTheme({
|
|
98
|
+
components: {
|
|
99
|
+
MyComponent: MyComponent.extend({
|
|
100
|
+
defaultProps: { radius: 'xl' },
|
|
101
|
+
classNames: { root: 'my-root' },
|
|
102
|
+
styles: { root: { color: 'red' } },
|
|
103
|
+
vars: (_theme, props) => ({ root: { '--my-radius': getRadius(props.radius) } }),
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## References
|
|
110
|
+
|
|
111
|
+
- **[`references/api.md`](references/api.md)** — All imports: `factory`, `useProps`, `useStyles`, `createVarsResolver`, `createSafeContext`, `StylesApiProps`, `CompoundStylesApiProps`, `BoxProps`, `ElementProps`, theme helpers (`getSize`, `getRadius`, etc.)
|
|
112
|
+
- **[`references/patterns.md`](references/patterns.md)** — Full examples: compound components with context, polymorphic component, generic component, theme integration
|