@recursica/mantine-adapter 0.6.0 → 0.7.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/.storybook/commonArgTypes.ts +148 -0
- package/CHANGELOG.md +69 -0
- package/dist/mantine-adapter.cjs +1 -1
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +660 -586
- package/dist/mantine-adapter.js.map +1 -1
- package/dist/src/components/Checkbox/CheckboxGroup.d.ts +1 -1
- package/dist/src/components/Chip/Chip.d.ts +14 -2
- package/package.json +5 -2
- package/src/OverStyling.stories.tsx +174 -0
- package/src/Version.stories.tsx +56 -0
- package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +48 -0
- package/src/components/Accordion/Accordion.module.css +261 -0
- package/src/components/Accordion/Accordion.stories.tsx +145 -0
- package/src/components/Accordion/Accordion.tsx +200 -0
- package/src/components/Accordion/index.ts +1 -0
- package/src/components/AssistiveElement/AssistiveElement.module.css +111 -0
- package/src/components/AssistiveElement/AssistiveElement.stories.tsx +73 -0
- package/src/components/AssistiveElement/AssistiveElement.tsx +91 -0
- package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +24 -0
- package/src/components/Avatar/Avatar.module.css +340 -0
- package/src/components/Avatar/Avatar.stories.tsx +102 -0
- package/src/components/Avatar/Avatar.tsx +100 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/BADGE_IMPLEMENTATION_NOTES.md +18 -0
- package/src/components/Badge/Badge.module.css +124 -0
- package/src/components/Badge/Badge.stories.tsx +77 -0
- package/src/components/Badge/Badge.tsx +66 -0
- package/src/components/Breadcrumb/BREADCRUMB_IMPLEMENTATION_NOTES.md +14 -0
- package/src/components/Breadcrumb/Breadcrumb.module.css +26 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +77 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +64 -0
- package/src/components/Button/Button.module.css +275 -0
- package/src/components/Button/Button.stories.tsx +105 -0
- package/src/components/Button/Button.tsx +122 -0
- package/src/components/Button/IMPLEMENTATION_NOTES.md +45 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Card/CARD_IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Card/Card.module.css +60 -0
- package/src/components/Card/Card.stories.tsx +141 -0
- package/src/components/Card/Card.tsx +176 -0
- package/src/components/Checkbox/CHECKBOX_IMPLEMENTATION_NOTES.md +25 -0
- package/src/components/Checkbox/Checkbox.module.css +204 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +73 -0
- package/src/components/Checkbox/Checkbox.tsx +111 -0
- package/src/components/Checkbox/CheckboxGroup.stories.tsx +138 -0
- package/src/components/Checkbox/CheckboxGroup.tsx +140 -0
- package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +35 -0
- package/src/components/Chip/Chip.module.css +218 -0
- package/src/components/Chip/Chip.stories.tsx +115 -0
- package/src/components/Chip/Chip.tsx +143 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +22 -0
- package/src/components/DatePicker/DatePicker.tsx +7 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +22 -0
- package/src/components/Dropdown/Dropdown.tsx +7 -0
- package/src/components/FileInput/FileInput.stories.tsx +22 -0
- package/src/components/FileInput/FileInput.tsx +7 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +22 -0
- package/src/components/FileUpload/FileUpload.tsx +7 -0
- package/src/components/FormControlWrapper/FORMCONTROLWRAPPER_IMPLEMENTATION_NOTES.md +32 -0
- package/src/components/FormControlWrapper/FormControlWrapper.module.css +46 -0
- package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +116 -0
- package/src/components/FormControlWrapper/FormControlWrapper.tsx +170 -0
- package/src/components/HoverCard/HoverCard.stories.tsx +17 -0
- package/src/components/HoverCard/HoverCard.tsx +7 -0
- package/src/components/Label/LABEL_IMPLEMENTATION_NOTES.md +48 -0
- package/src/components/Label/Label.module.css +177 -0
- package/src/components/Label/Label.stories.tsx +123 -0
- package/src/components/Label/Label.tsx +132 -0
- package/src/components/Label/index.ts +1 -0
- package/src/components/Link/Link.stories.tsx +17 -0
- package/src/components/Link/Link.tsx +7 -0
- package/src/components/Loader/Loader.stories.tsx +17 -0
- package/src/components/Loader/Loader.tsx +7 -0
- package/src/components/Menu/Menu.stories.tsx +17 -0
- package/src/components/Menu/Menu.tsx +7 -0
- package/src/components/Modal/Modal.stories.tsx +17 -0
- package/src/components/Modal/Modal.tsx +7 -0
- package/src/components/NumberInput/NumberInput.stories.tsx +22 -0
- package/src/components/NumberInput/NumberInput.tsx +7 -0
- package/src/components/Pagination/Pagination.stories.tsx +17 -0
- package/src/components/Pagination/Pagination.tsx +7 -0
- package/src/components/Panel/Panel.stories.tsx +17 -0
- package/src/components/Panel/Panel.tsx +7 -0
- package/src/components/Popover/Popover.stories.tsx +17 -0
- package/src/components/Popover/Popover.tsx +7 -0
- package/src/components/Radio/Radio.stories.tsx +22 -0
- package/src/components/Radio/Radio.tsx +7 -0
- package/src/components/ReadOnlyField/READ_ONLY_FIELD_IMPLEMENTATION_NOTES.md +15 -0
- package/src/components/ReadOnlyField/ReadOnlyField.module.css +49 -0
- package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +120 -0
- package/src/components/ReadOnlyField/ReadOnlyField.tsx +75 -0
- package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +46 -0
- package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +85 -0
- package/src/components/ReadOnlyField/index.ts +3 -0
- package/src/components/Search/Search.stories.tsx +17 -0
- package/src/components/Search/Search.tsx +7 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +22 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +7 -0
- package/src/components/Slider/Slider.stories.tsx +22 -0
- package/src/components/Slider/Slider.tsx +7 -0
- package/src/components/Stepper/Stepper.stories.tsx +17 -0
- package/src/components/Stepper/Stepper.tsx +7 -0
- package/src/components/Switch/Switch.stories.tsx +22 -0
- package/src/components/Switch/Switch.tsx +7 -0
- package/src/components/Table/Table.stories.tsx +17 -0
- package/src/components/Table/Table.tsx +7 -0
- package/src/components/Tabs/Tabs.stories.tsx +17 -0
- package/src/components/Tabs/Tabs.tsx +7 -0
- package/src/components/Text/Text.stories.tsx +88 -0
- package/src/components/Text/Text.tsx +54 -0
- package/src/components/TextArea/TextArea.stories.tsx +22 -0
- package/src/components/TextArea/TextArea.tsx +7 -0
- package/src/components/TextField/TEXTFIELD_IMPLEMENTATION_NOTES.md +19 -0
- package/src/components/TextField/TextField.module.css +225 -0
- package/src/components/TextField/TextField.stories.tsx +162 -0
- package/src/components/TextField/TextField.tsx +138 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +22 -0
- package/src/components/TimePicker/TimePicker.tsx +7 -0
- package/src/components/Timeline/Timeline.stories.tsx +17 -0
- package/src/components/Timeline/Timeline.tsx +7 -0
- package/src/components/Title/Title.stories.tsx +63 -0
- package/src/components/Title/Title.tsx +45 -0
- package/src/components/Toast/Toast.stories.tsx +17 -0
- package/src/components/Toast/Toast.tsx +7 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +17 -0
- package/src/components/Tooltip/Tooltip.tsx +7 -0
- package/src/components/TransferList/TransferList.stories.tsx +17 -0
- package/src/components/TransferList/TransferList.tsx +7 -0
- package/src/components/index.ts +39 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +3 -0
- package/src/types/index.ts +9 -0
- package/src/types/mantine.d.ts +7 -0
- package/src/types/scss-modules.d.ts +9 -0
- package/src/utils/ColorSchemeWrapper.tsx +27 -0
- package/src/utils/copyToClipboard.ts +36 -0
- package/src/utils/filterStylingProps.ts +139 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Chip Implementation Notes
|
|
2
|
+
|
|
3
|
+
## Architecture decisions
|
|
4
|
+
|
|
5
|
+
### Mantine DOM Structure & Label Overrides
|
|
6
|
+
|
|
7
|
+
Mantine's `<Chip>` behaves like an input element (`radio` or `checkbox`). Under the hood, it renders:
|
|
8
|
+
|
|
9
|
+
1. `.mantine-Chip-root` (wrapper)
|
|
10
|
+
2. `input` (hidden visual structure)
|
|
11
|
+
3. `.mantine-Chip-label` (The actual visible button-like pill)
|
|
12
|
+
|
|
13
|
+
Because the `.label` is the primary visual surface and handles Mantine's built-in `:hover` and active states, we direct our Recursica styling natively to `.label`.
|
|
14
|
+
|
|
15
|
+
### Icon and Remove Implementations
|
|
16
|
+
|
|
17
|
+
The Recursica specifications for the Chip component define `--chip-icon` and `--chip-close` which map to leading and trailing states.
|
|
18
|
+
To achieve this without breaking Mantine's `Chip` input architecture, we wrapped the internal `children` using a standard `span` DOM strategy:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<span className={styles.innerWrapper}>
|
|
22
|
+
{icon}
|
|
23
|
+
<span className={styles.children}>{children}</span>
|
|
24
|
+
{onRemove}
|
|
25
|
+
</span>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Accessibility of Remove Action
|
|
29
|
+
|
|
30
|
+
Because the Chip fundamentally functions as a `<label>` linked to an `<input>`, placing a raw interactive element like `<button>` directly inside the standard Chip sub-tree violates nested interactive element ARIA constraints in strict validators.
|
|
31
|
+
To accommodate this, the visual "close" icon uses a `<span>` element configured with `role="button"` and `tabIndex={0}` to hook into standard keyboard activations without triggering generic nested `<form>` conflicts native to Mantine's baseline constraints.
|
|
32
|
+
|
|
33
|
+
### Removing Sizing Properties
|
|
34
|
+
|
|
35
|
+
During implementation, the parsed Figma design tokens natively exported specific height/padding vectors dynamically (e.g., `--recursica_ui-kit_components_chip_properties_icon-size`) rather than explicit string variants (`sm`, `md`, `lg`). Therefore, we omitted `size` conceptually from the `RecursicaChipProps` wrapper to lock down size evaluation natively against the active layer variables.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/* HARDCODED VALUES
|
|
2
|
+
border-style: solid; (Baseline reset)
|
|
3
|
+
box-sizing: border-box; (Baseline reset)
|
|
4
|
+
display: inline-flex; (Baseline reset)
|
|
5
|
+
align-items: center; (Baseline reset)
|
|
6
|
+
outline: none; (Accessible fallback)
|
|
7
|
+
padding: 0; (Override mantine default)
|
|
8
|
+
background: transparent; (Override mantine default)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.root {
|
|
12
|
+
/* Set preset layout variables to override mantine defaults safely */
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
|
|
16
|
+
/* Token properties */
|
|
17
|
+
--chip-height: max-content; /* Sizing is driven by padding and line height */
|
|
18
|
+
--chip-bg: var(
|
|
19
|
+
--recursica_ui-kit_components_chip_variants_styles_unselected_properties_colors_background
|
|
20
|
+
);
|
|
21
|
+
--chip-border: var(
|
|
22
|
+
--recursica_ui-kit_components_chip_variants_styles_unselected_properties_colors_border-color
|
|
23
|
+
);
|
|
24
|
+
--chip-text: var(
|
|
25
|
+
--recursica_ui-kit_components_chip_variants_styles_unselected_properties_colors_text
|
|
26
|
+
);
|
|
27
|
+
--chip-icon: var(
|
|
28
|
+
--recursica_ui-kit_components_chip_variants_styles_unselected_properties_colors_leading-icon-color
|
|
29
|
+
);
|
|
30
|
+
--chip-close: var(
|
|
31
|
+
--recursica_ui-kit_components_chip_variants_styles_unselected_properties_colors_close-icon-color
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
border-radius: var(
|
|
35
|
+
--recursica_ui-kit_components_chip_properties_border-radius
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* We target the mantine label directly because that is the visible container in Mantine's Chip */
|
|
40
|
+
.label {
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
|
|
46
|
+
/* Reset mantine styles */
|
|
47
|
+
background: transparent !important; /* HARDCODE: Mantine forces background styling on label */
|
|
48
|
+
border-style: solid;
|
|
49
|
+
border-width: var(--recursica_ui-kit_components_chip_properties_border-size);
|
|
50
|
+
padding: var(--recursica_ui-kit_components_chip_properties_vertical-padding)
|
|
51
|
+
var(--recursica_ui-kit_components_chip_properties_horizontal-padding) !important;
|
|
52
|
+
|
|
53
|
+
/* Typography */
|
|
54
|
+
font-family: var(
|
|
55
|
+
--recursica_ui-kit_components_chip_properties_text_font-family
|
|
56
|
+
);
|
|
57
|
+
font-size: var(--recursica_ui-kit_components_chip_properties_text_font-size);
|
|
58
|
+
font-weight: var(
|
|
59
|
+
--recursica_ui-kit_components_chip_properties_text_font-weight
|
|
60
|
+
);
|
|
61
|
+
letter-spacing: var(
|
|
62
|
+
--recursica_ui-kit_components_chip_properties_text_letter-spacing
|
|
63
|
+
);
|
|
64
|
+
line-height: var(
|
|
65
|
+
--recursica_ui-kit_components_chip_properties_text_line-height
|
|
66
|
+
);
|
|
67
|
+
text-decoration: var(
|
|
68
|
+
--recursica_ui-kit_components_chip_properties_text_text-decoration
|
|
69
|
+
);
|
|
70
|
+
text-transform: var(
|
|
71
|
+
--recursica_ui-kit_components_chip_properties_text_text-transform
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/* Token evaluation */
|
|
75
|
+
background-color: var(--chip-bg) !important;
|
|
76
|
+
border-color: var(--chip-border) !important;
|
|
77
|
+
color: var(--chip-text) !important;
|
|
78
|
+
min-width: var(--recursica_ui-kit_components_chip_properties_min-width);
|
|
79
|
+
max-width: var(--recursica_ui-kit_components_chip_properties_max-width);
|
|
80
|
+
box-shadow: var(
|
|
81
|
+
--recursica_ui-kit_components_chip_properties_elevation,
|
|
82
|
+
none
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
transition: all 0.2s ease;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Default (Unselected) hover/active interactions (Mantine overrides) */
|
|
89
|
+
.label[data-checked] {
|
|
90
|
+
--chip-bg: var(
|
|
91
|
+
--recursica_ui-kit_components_chip_variants_styles_selected_properties_colors_background
|
|
92
|
+
);
|
|
93
|
+
--chip-border: var(
|
|
94
|
+
--recursica_ui-kit_components_chip_variants_styles_selected_properties_colors_border-color
|
|
95
|
+
);
|
|
96
|
+
--chip-text: var(
|
|
97
|
+
--recursica_ui-kit_components_chip_variants_styles_selected_properties_colors_text
|
|
98
|
+
);
|
|
99
|
+
--chip-icon: var(
|
|
100
|
+
--recursica_ui-kit_components_chip_variants_styles_selected_properties_colors_selected-icon-color
|
|
101
|
+
);
|
|
102
|
+
--chip-close: var(
|
|
103
|
+
--recursica_ui-kit_components_chip_variants_styles_selected_properties_colors_close-icon-color
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.root[data-error] .label {
|
|
108
|
+
--chip-bg: var(
|
|
109
|
+
--recursica_ui-kit_components_chip_variants_styles_error_properties_colors_background
|
|
110
|
+
);
|
|
111
|
+
--chip-border: var(
|
|
112
|
+
--recursica_ui-kit_components_chip_variants_styles_error_properties_colors_border-color
|
|
113
|
+
);
|
|
114
|
+
--chip-text: var(
|
|
115
|
+
--recursica_ui-kit_components_chip_variants_styles_error_properties_colors_text
|
|
116
|
+
);
|
|
117
|
+
--chip-icon: var(
|
|
118
|
+
--recursica_ui-kit_components_chip_variants_styles_error_properties_colors_icon
|
|
119
|
+
);
|
|
120
|
+
--chip-close: var(
|
|
121
|
+
--recursica_ui-kit_components_chip_variants_styles_error_properties_colors_close-icon-color
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.root[data-error] .label[data-checked] {
|
|
126
|
+
--chip-bg: var(
|
|
127
|
+
--recursica_ui-kit_components_chip_variants_styles_error-selected_properties_colors_background
|
|
128
|
+
);
|
|
129
|
+
--chip-border: var(
|
|
130
|
+
--recursica_ui-kit_components_chip_variants_styles_error-selected_properties_colors_border-color
|
|
131
|
+
);
|
|
132
|
+
--chip-text: var(
|
|
133
|
+
--recursica_ui-kit_components_chip_variants_styles_error-selected_properties_colors_text
|
|
134
|
+
);
|
|
135
|
+
--chip-icon: var(
|
|
136
|
+
--recursica_ui-kit_components_chip_variants_styles_error-selected_properties_colors_selected-icon-color
|
|
137
|
+
);
|
|
138
|
+
--chip-close: var(
|
|
139
|
+
--recursica_ui-kit_components_chip_variants_styles_error-selected_properties_colors_close-icon-color
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.root[data-disabled] .label {
|
|
144
|
+
opacity: var(--recursica_brand_states_disabled, 0.5);
|
|
145
|
+
cursor: not-allowed;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Override Mantine's built-in check icon colors and size if we keep it */
|
|
149
|
+
.mantineIconWrapper {
|
|
150
|
+
color: var(--chip-icon) !important;
|
|
151
|
+
width: var(
|
|
152
|
+
--recursica_ui-kit_components_chip_properties_icon-size
|
|
153
|
+
) !important;
|
|
154
|
+
height: var(
|
|
155
|
+
--recursica_ui-kit_components_chip_properties_icon-size
|
|
156
|
+
) !important;
|
|
157
|
+
margin-right: var(
|
|
158
|
+
--recursica_ui-kit_components_chip_properties_icon-text-gap
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.innerWrapper {
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: var(--recursica_ui-kit_components_chip_properties_icon-text-gap);
|
|
166
|
+
width: 100%;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.children {
|
|
170
|
+
flex-grow: 1;
|
|
171
|
+
text-overflow: ellipsis;
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
white-space: nowrap;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.leadingIcon {
|
|
177
|
+
display: inline-flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
color: var(--chip-icon);
|
|
181
|
+
width: var(--recursica_ui-kit_components_chip_properties_icon-size);
|
|
182
|
+
height: var(--recursica_ui-kit_components_chip_properties_icon-size);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.leadingIcon svg {
|
|
186
|
+
width: 100%;
|
|
187
|
+
height: 100%;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.removeIcon {
|
|
191
|
+
display: inline-flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: center;
|
|
194
|
+
color: var(--chip-close);
|
|
195
|
+
width: var(--recursica_ui-kit_components_chip_properties_close-icon-size);
|
|
196
|
+
height: var(--recursica_ui-kit_components_chip_properties_close-icon-size);
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
border-radius: 50%; /* HARDCODE: Make focus ring rounded */
|
|
199
|
+
outline: none;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.removeIcon:focus-visible {
|
|
203
|
+
box-shadow: 0 0 0 2px var(--chip-border); /* HARDCODE: Focus indication */
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.removeIcon svg {
|
|
207
|
+
width: 100%;
|
|
208
|
+
height: 100%;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.root[data-icon-only] .label {
|
|
212
|
+
padding-left: var(
|
|
213
|
+
--recursica_ui-kit_components_chip_properties_vertical-padding
|
|
214
|
+
) !important;
|
|
215
|
+
padding-right: var(
|
|
216
|
+
--recursica_ui-kit_components_chip_properties_vertical-padding
|
|
217
|
+
) !important;
|
|
218
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Chip } from "./Chip";
|
|
4
|
+
|
|
5
|
+
type ChipStoryProps = React.ComponentProps<typeof Chip>;
|
|
6
|
+
|
|
7
|
+
const meta: Meta<ChipStoryProps> = {
|
|
8
|
+
title: "UI-Kit/Chip",
|
|
9
|
+
component: Chip,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
"The Chip component is used to represent interactive selections, descriptive tags, or dynamic filters natively bounded to Recursica variables. It can be used as a toggleable input, can render a custom leading `icon`, and handles close constraints automatically via the `onRemove` property.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
argTypes: {
|
|
20
|
+
error: {
|
|
21
|
+
control: "boolean",
|
|
22
|
+
description: "Applies the error state styling dynamically.",
|
|
23
|
+
},
|
|
24
|
+
disabled: {
|
|
25
|
+
control: "boolean",
|
|
26
|
+
description: "Applies disabled token states.",
|
|
27
|
+
},
|
|
28
|
+
checked: {
|
|
29
|
+
control: "boolean",
|
|
30
|
+
description: "Forces the visual selected state.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
|
|
37
|
+
type Story = StoryObj<ChipStoryProps>;
|
|
38
|
+
|
|
39
|
+
// The Default story exposing native properties for the playground.
|
|
40
|
+
export const Default: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
children: "Default Chip",
|
|
43
|
+
error: false,
|
|
44
|
+
disabled: false,
|
|
45
|
+
checked: false,
|
|
46
|
+
},
|
|
47
|
+
render: (args: ChipStoryProps) => <Chip {...args} />,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Static Stories for Regression Snapshots
|
|
51
|
+
export const Unselected: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
children: "Unselected",
|
|
54
|
+
checked: false,
|
|
55
|
+
},
|
|
56
|
+
render: (args: ChipStoryProps) => <Chip {...args} />,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const Selected: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
children: "Selected",
|
|
62
|
+
checked: true,
|
|
63
|
+
},
|
|
64
|
+
render: (args: ChipStoryProps) => <Chip {...args} onChange={() => {}} />,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const ErrorState: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
children: "Error",
|
|
70
|
+
error: true,
|
|
71
|
+
checked: false,
|
|
72
|
+
},
|
|
73
|
+
render: (args: ChipStoryProps) => <Chip {...args} />,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const ErrorSelected: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
children: "Error Selected",
|
|
79
|
+
error: true,
|
|
80
|
+
checked: true,
|
|
81
|
+
},
|
|
82
|
+
render: (args: ChipStoryProps) => <Chip {...args} onChange={() => {}} />,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Removable: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
children: "Dismissible",
|
|
88
|
+
checked: false,
|
|
89
|
+
onRemove: () => console.log("Removal Action Triggered"),
|
|
90
|
+
},
|
|
91
|
+
render: (args: ChipStoryProps) => <Chip {...args} />,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const WithLeadingIcon: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
children: "Leading Icon",
|
|
97
|
+
checked: false,
|
|
98
|
+
icon: (
|
|
99
|
+
<svg
|
|
100
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
101
|
+
viewBox="0 0 24 24"
|
|
102
|
+
fill="none"
|
|
103
|
+
stroke="currentColor"
|
|
104
|
+
strokeWidth="2"
|
|
105
|
+
strokeLinecap="round"
|
|
106
|
+
strokeLinejoin="round"
|
|
107
|
+
>
|
|
108
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
109
|
+
<path d="M12 8v4"></path>
|
|
110
|
+
<path d="M12 16h.01"></path>
|
|
111
|
+
</svg>
|
|
112
|
+
),
|
|
113
|
+
},
|
|
114
|
+
render: (args: ChipStoryProps) => <Chip {...args} />,
|
|
115
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Chip as MantineChip,
|
|
4
|
+
type ChipProps as MantineChipProps,
|
|
5
|
+
} from "@mantine/core";
|
|
6
|
+
import {
|
|
7
|
+
filterStylingProps,
|
|
8
|
+
type RecursicaOverStyled,
|
|
9
|
+
} from "../../utils/filterStylingProps";
|
|
10
|
+
import styles from "./Chip.module.css";
|
|
11
|
+
|
|
12
|
+
export interface RecursicaChipProps {
|
|
13
|
+
/** Enables the error state styling */
|
|
14
|
+
error?: boolean;
|
|
15
|
+
|
|
16
|
+
/** Leading icon content */
|
|
17
|
+
icon?: React.ReactNode;
|
|
18
|
+
|
|
19
|
+
/** Called when the remove (X) icon is clicked. If provided, the remove icon will be displayed. */
|
|
20
|
+
onRemove?: (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => void;
|
|
21
|
+
|
|
22
|
+
/** Screen reader label for the remove button. Defaults to 'Remove' */
|
|
23
|
+
removeLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ChipProps = RecursicaOverStyled<
|
|
27
|
+
Omit<MantineChipProps, "variant" | "size" | "color" | "radius"> &
|
|
28
|
+
RecursicaChipProps
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
function CloseIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
|
32
|
+
return (
|
|
33
|
+
<svg
|
|
34
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
35
|
+
width="24"
|
|
36
|
+
height="24"
|
|
37
|
+
viewBox="0 0 24 24"
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke="currentColor"
|
|
40
|
+
strokeWidth="2"
|
|
41
|
+
strokeLinecap="round"
|
|
42
|
+
strokeLinejoin="round"
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
46
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
47
|
+
</svg>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
52
|
+
{
|
|
53
|
+
error = false,
|
|
54
|
+
icon,
|
|
55
|
+
onRemove,
|
|
56
|
+
removeLabel = "Remove",
|
|
57
|
+
children,
|
|
58
|
+
overStyled = false,
|
|
59
|
+
...rest
|
|
60
|
+
},
|
|
61
|
+
ref,
|
|
62
|
+
) {
|
|
63
|
+
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
64
|
+
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
65
|
+
|
|
66
|
+
const mergedClassNames: Partial<Record<string, string>> = {
|
|
67
|
+
root: styles.root,
|
|
68
|
+
label: styles.label,
|
|
69
|
+
input: styles.input,
|
|
70
|
+
iconWrapper: styles.mantineIconWrapper,
|
|
71
|
+
checkIcon: styles.checkIcon,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const classNamesProp = restRecord.classNames;
|
|
75
|
+
if (
|
|
76
|
+
classNamesProp &&
|
|
77
|
+
typeof classNamesProp === "object" &&
|
|
78
|
+
!Array.isArray(classNamesProp)
|
|
79
|
+
) {
|
|
80
|
+
const o = classNamesProp as Partial<Record<string, string>>;
|
|
81
|
+
mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
|
|
82
|
+
mergedClassNames.label = o.label
|
|
83
|
+
? `${styles.label} ${o.label}`
|
|
84
|
+
: styles.label;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const classNameProp = restRecord.className as string | undefined;
|
|
88
|
+
const finalClass = classNameProp
|
|
89
|
+
? `${styles.root} ${classNameProp}`
|
|
90
|
+
: styles.root;
|
|
91
|
+
|
|
92
|
+
// Determine state
|
|
93
|
+
const dataError = error ? "" : undefined;
|
|
94
|
+
const isIconOnly = !children && (!!icon || !!onRemove);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<MantineChip
|
|
98
|
+
ref={ref}
|
|
99
|
+
className={finalClass}
|
|
100
|
+
classNames={mergedClassNames}
|
|
101
|
+
{...(dataError !== undefined ? { "data-error": "" } : {})}
|
|
102
|
+
{...(isIconOnly ? { "data-icon-only": "" } : {})}
|
|
103
|
+
{...sanitizedProps}
|
|
104
|
+
>
|
|
105
|
+
<span className={styles.innerWrapper}>
|
|
106
|
+
{icon && (
|
|
107
|
+
<span className={styles.leadingIcon} aria-hidden>
|
|
108
|
+
{icon}
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
<span className={styles.children}>{children}</span>
|
|
113
|
+
|
|
114
|
+
{onRemove && (
|
|
115
|
+
<span
|
|
116
|
+
role="button"
|
|
117
|
+
className={styles.removeIcon}
|
|
118
|
+
onClick={(e) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
onRemove(e);
|
|
122
|
+
}}
|
|
123
|
+
aria-label={removeLabel}
|
|
124
|
+
onKeyDown={(e) => {
|
|
125
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
onRemove(
|
|
129
|
+
e as unknown as React.MouseEvent<HTMLSpanElement, MouseEvent>,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
tabIndex={0}
|
|
134
|
+
>
|
|
135
|
+
<CloseIcon />
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</span>
|
|
139
|
+
</MantineChip>
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
Chip.displayName = "Chip";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { DatePicker } from "./DatePicker";
|
|
3
|
+
import { ComingSoon } from "@recursica/storybook-template";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof DatePicker> = {
|
|
6
|
+
title: "UI-Kit/🚧 DatePicker",
|
|
7
|
+
component: DatePicker,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
disabled: {
|
|
11
|
+
control: "boolean",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof DatePicker>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
render: () => <ComingSoon componentName="DatePicker" />,
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Dropdown } from "./Dropdown";
|
|
3
|
+
import { ComingSoon } from "@recursica/storybook-template";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Dropdown> = {
|
|
6
|
+
title: "UI-Kit/🚧 Dropdown",
|
|
7
|
+
component: Dropdown,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
disabled: {
|
|
11
|
+
control: "boolean",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof Dropdown>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
render: () => <ComingSoon componentName="Dropdown" />,
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { FileInput } from "./FileInput";
|
|
3
|
+
import { ComingSoon } from "@recursica/storybook-template";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof FileInput> = {
|
|
6
|
+
title: "UI-Kit/🚧 FileInput",
|
|
7
|
+
component: FileInput,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
disabled: {
|
|
11
|
+
control: "boolean",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof FileInput>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
render: () => <ComingSoon componentName="FileInput" />,
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { FileUpload } from "./FileUpload";
|
|
3
|
+
import { ComingSoon } from "@recursica/storybook-template";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof FileUpload> = {
|
|
6
|
+
title: "UI-Kit/🚧 FileUpload",
|
|
7
|
+
component: FileUpload,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
disabled: {
|
|
11
|
+
control: "boolean",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof FileUpload>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
render: () => <ComingSoon componentName="FileUpload" />,
|
|
22
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# FormControlWrapper Implementation Notes
|
|
2
|
+
|
|
3
|
+
## Architectural Philosophy
|
|
4
|
+
|
|
5
|
+
The `FormControlWrapper` is the ultimate structural replacement for Mantine's built-in `Input.Wrapper`. By abandoning Mantine's opinionated wrappers entirely across the design system, we centralize all label tracking, error rendering, ARIA generation, and grid layouts natively inside this single component.
|
|
6
|
+
|
|
7
|
+
### 1. Bypassing `Input.Wrapper`
|
|
8
|
+
|
|
9
|
+
Under the hood of Mantine, elements like `TextInput` heavily rely on `Input.Wrapper`. We actively discourage their use. The central tenet of Recursica Forms is to strip the UI primitive back to its "naked" form (e.g. `<Input />`, `<Checkbox />`) and encapsulate it manually inside `<FormControlWrapper>`.
|
|
10
|
+
|
|
11
|
+
- **Why?** It enforces complete layout mastery. It natively enables `formLayout="side-by-side"` and completely disables Mantine's margin collisions without requiring messy CSS hacks.
|
|
12
|
+
|
|
13
|
+
### 2. The `cloneElement` ARIA Map
|
|
14
|
+
|
|
15
|
+
Because we tore out Mantine's `InputContext` provider (which natively glued error strings to `<input>` tags using React Context), we explicitly utilize `React.cloneElement` on the nested children inside this wrapper.
|
|
16
|
+
|
|
17
|
+
- `aria-describedby` and `aria-errormessage` are dynamically generated using `React.useId()` and physically injected back onto the provided child node. Screen readers rely strictly on this mapping to announce the assistive fields correctly.
|
|
18
|
+
|
|
19
|
+
### 3. Strict `AssistiveElement` Coupling
|
|
20
|
+
|
|
21
|
+
We completely abandoned generic `<Input.Description>` tags. The `FormControlWrapper` directly renders `<AssistiveElement>` primitives, parsing them seamlessly mapping them to `"error"` or `"help"` variants automatically depending on the component's internal state machine.
|
|
22
|
+
|
|
23
|
+
### 4. Dynamic Geometric Variable Payloads
|
|
24
|
+
|
|
25
|
+
Because `FormControlWrapper` acts as an agnostic grid box encompassing raw primitives (like `TextField`, `Select`), it initially stretches `100%` across horizontal bounds.
|
|
26
|
+
|
|
27
|
+
- **The Bug:** If a child `TextField` carries its own hardcoded `max-width` token, it stops expanding early, but the wrapper and `<Label>` keep expanding, causing right-aligned labels to aggressively float past the field to the screen's edge dynamically.
|
|
28
|
+
- **The Variable Payload Resolution:** Instead of destroying grids with `width: fit-content` arrays, primitive components are required to pass their local `max-width` tokens UP to the wrapper explicitly via React `style`:
|
|
29
|
+
```tsx
|
|
30
|
+
<FormControlWrapper style={{ "--form-control-max-width": "var(--...)" }}>
|
|
31
|
+
```
|
|
32
|
+
The wrapper natively respects `max-width: var(--form-control-max-width, 100%)`. This structurally unifies the bounding caps so right-aligned labels flawlessly snap tightly to the explicit boundary edge of the specific primitive it is wrapping.
|