@recursica/mui-adapter 0.21.1 → 0.22.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/ARCHITECTURE.md +3 -0
- package/CHANGELOG.md +52 -0
- package/dist/index.d.ts +90 -35
- package/dist/mui-adapter.cjs +68 -68
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +5660 -5582
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +63 -0
- package/src/components/Accordion/Accordion.module.css +21 -2
- package/src/components/Accordion/Accordion.stories.tsx +38 -0
- package/src/components/Accordion/Accordion.tsx +27 -12
- package/src/components/AssistiveElement/ASSISTIVEELEMENT_IMPLEMENTATION_NOTES.md +59 -0
- package/src/components/AssistiveElement/AssistiveElement.module.css +8 -1
- package/src/components/AssistiveElement/AssistiveElement.tsx +17 -7
- package/src/components/Autocomplete/Autocomplete.tsx +18 -2
- package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +36 -0
- package/src/components/Avatar/Avatar.tsx +6 -9
- package/src/components/Badge/Badge.module.css +0 -12
- package/src/components/Badge/Badge.tsx +0 -4
- package/src/components/Card/Card.tsx +5 -5
- package/src/components/Checkbox/Checkbox.module.css +17 -21
- package/src/components/Checkbox/Checkbox.tsx +24 -18
- package/src/components/Chip/Chip.module.css +16 -7
- package/src/components/Chip/Chip.tsx +1 -1
- package/src/components/Flex/Flex.tsx +1 -1
- package/src/components/FormControlWrapper/FORMCONTROLWRAPPER_IMPLEMENTATION_NOTES.md +35 -0
- package/src/components/FormControlWrapper/FormControlWrapper.tsx +24 -8
- package/src/components/Grid/Grid.tsx +1 -1
- package/src/components/Group/Group.tsx +1 -1
- package/src/components/HoverCard/HoverCard.tsx +1 -1
- package/src/components/Label/Label.module.css +15 -2
- package/src/components/Label/Label.tsx +7 -6
- package/src/components/Menu/Menu.tsx +1 -1
- package/src/components/Modal/Modal.tsx +1 -1
- package/src/components/NumberInput/NumberInput.tsx +1 -1
- package/src/components/Pagination/Pagination.tsx +1 -1
- package/src/components/Panel/Panel.tsx +1 -1
- package/src/components/Radio/Radio.module.css +33 -38
- package/src/components/Radio/Radio.tsx +58 -44
- package/src/components/Radio/RadioGroup.tsx +5 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +0 -7
- package/src/components/SegmentedControl/SegmentedControl.tsx +4 -8
- package/src/components/Slider/Slider.tsx +1 -1
- package/src/components/Stack/Stack.tsx +1 -1
- package/src/components/Stepper/Stepper.tsx +3 -3
- package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +123 -0
- package/src/components/Switch/Switch.module.css +152 -89
- package/src/components/Switch/Switch.tsx +140 -33
- package/src/components/Switch/SwitchGroup.tsx +37 -9
- package/src/components/Table/Table.tsx +1 -1
- package/src/components/Tabs/IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Tabs/Tabs.module.css +120 -43
- package/src/components/Tabs/Tabs.stories.tsx +5 -2
- package/src/components/Tabs/Tabs.tsx +1 -1
- package/src/components/Tabs/USAGE.md +27 -11
- package/src/components/TextField/TextField.tsx +1 -1
- package/src/components/TimePicker/TimePicker.tsx +1 -1
- package/src/components/Timeline/Timeline.tsx +1 -1
- package/src/components/Timeline/TimelineItem.tsx +2 -2
- package/src/components/Toast/Toast.tsx +4 -4
- package/src/components/Tooltip/Tooltip.tsx +1 -1
- package/src/components/Tree/Tree.tsx +1 -1
- package/src/types/mantine.d.ts +0 -7
|
@@ -86,8 +86,14 @@
|
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/* Base explicit reset masking underlying framework defaults
|
|
89
|
+
/* Base explicit reset masking underlying framework defaults.
|
|
90
|
+
display: flex + centering: the checkmark icon renders as a nested child of this box
|
|
91
|
+
(see Checkbox.tsx — icon/checkedIcon are combined nodes, icon inside .input), so this
|
|
92
|
+
box has to center it itself rather than relying on a parent's centering. */
|
|
90
93
|
.input {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
91
97
|
cursor: pointer; /* HARDCODE: core interaction behavior */
|
|
92
98
|
appearance: none; /* HARDCODE: reset native widget */
|
|
93
99
|
box-sizing: border-box; /* HARDCODE: ensures size includes borders */
|
|
@@ -199,7 +205,14 @@
|
|
|
199
205
|
);
|
|
200
206
|
}
|
|
201
207
|
|
|
208
|
+
/* display: flex (not the browser default block) blockifies the native inline <label>
|
|
209
|
+
child per the CSS Display spec's flex-item blockification rule — matches Mantine's
|
|
210
|
+
own labelWrapper (inline-flex), whose flex context does the same to its label.
|
|
211
|
+
Without it, the label stays inline and its line box picks up extra half-leading
|
|
212
|
+
height, visually shifting it below vertical center against the checkbox. */
|
|
202
213
|
.labelWrapper {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
203
216
|
padding: 0; /* HARDCODE: structural layout reset */
|
|
204
217
|
margin: 0; /* HARDCODE: structural layout reset */
|
|
205
218
|
flex: 1;
|
|
@@ -250,26 +263,9 @@
|
|
|
250
263
|
);
|
|
251
264
|
}
|
|
252
265
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
font-size: 12px; /* HARDCODE: mantine default xs */
|
|
257
|
-
line-height: 1.2;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.error {
|
|
261
|
-
margin-top: 5px; /* HARDCODE: mantine default */
|
|
262
|
-
color: #fa5252; /* HARDCODE: mantine default error */
|
|
263
|
-
font-size: 12px; /* HARDCODE: mantine default xs */
|
|
264
|
-
line-height: 1.2;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
.description[data-disabled="true"],
|
|
268
|
-
.error[data-disabled="true"] {
|
|
269
|
-
opacity: var(
|
|
270
|
-
--recursica_ui-kit_components_checkbox-item_variants_states_disabled_properties_opacity
|
|
271
|
-
);
|
|
272
|
-
}
|
|
266
|
+
/* description/error text renders via AssistiveElement directly (see Checkbox.tsx) —
|
|
267
|
+
it owns its own tokenized styling. AssistiveElement has no disabled state and
|
|
268
|
+
must never dim. */
|
|
273
269
|
|
|
274
270
|
/* CheckboxGroup Layouts */
|
|
275
271
|
.groupRoot {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
FormControlLayout,
|
|
18
18
|
type FormControlLayoutProps,
|
|
19
19
|
} from "../FormControlLayout/FormControlLayout";
|
|
20
|
+
import { AssistiveElement } from "../AssistiveElement/AssistiveElement";
|
|
20
21
|
|
|
21
22
|
import styles from "./Checkbox.module.css";
|
|
22
23
|
|
|
@@ -96,6 +97,12 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
|
96
97
|
const isChecked = isGrouped
|
|
97
98
|
? (groupContext.value || []).includes(restRecord.value)
|
|
98
99
|
: !!(restRecord.checked ?? restRecord.defaultChecked);
|
|
100
|
+
// Only force controlled mode when we actually own the source of truth (grouped) or the
|
|
101
|
+
// caller explicitly passed `checked` (controlled). A plain `defaultChecked`/neither-prop
|
|
102
|
+
// usage must stay uncontrolled — otherwise `checked` gets pinned to a value that never
|
|
103
|
+
// updates, and MUI's native input silently snaps back after every click. Mirrors Switch's
|
|
104
|
+
// identical `isGrouped ? { checked: ... } : {}` pattern (Switch never had this bug).
|
|
105
|
+
const isControlled = isGrouped || restRecord.checked !== undefined;
|
|
99
106
|
|
|
100
107
|
if (readOnly && !!readOnlyComponent) {
|
|
101
108
|
const ReadOnlyComp = readOnlyComponent;
|
|
@@ -166,8 +173,9 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
|
166
173
|
classes={mergedClassNames}
|
|
167
174
|
disabled={readOnly || disabled || isGroupReadOnly}
|
|
168
175
|
{...(sanitizedProps as unknown as MuiCheckboxProps)}
|
|
169
|
-
checked
|
|
176
|
+
{...(isControlled ? { checked: isChecked as boolean } : {})}
|
|
170
177
|
onChange={handleChange}
|
|
178
|
+
disableRipple
|
|
171
179
|
sx={label ? { padding: 0 } : undefined}
|
|
172
180
|
icon={<div className={styles.input} />}
|
|
173
181
|
checkedIcon={
|
|
@@ -204,25 +212,23 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
|
204
212
|
>
|
|
205
213
|
{label as React.ReactNode}
|
|
206
214
|
</label>
|
|
207
|
-
{description
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
>
|
|
214
|
-
{description}
|
|
215
|
-
</div>
|
|
216
|
-
)}
|
|
217
|
-
{error && (
|
|
218
|
-
<div
|
|
219
|
-
className={styles.error}
|
|
220
|
-
data-disabled={
|
|
221
|
-
readOnly || disabled || isGroupReadOnly ? true : undefined
|
|
222
|
-
}
|
|
215
|
+
{/* description/error are mutually exclusive — error takes precedence */}
|
|
216
|
+
{error ? (
|
|
217
|
+
<AssistiveElement
|
|
218
|
+
assistiveVariant="error"
|
|
219
|
+
assistiveWithIcon={false}
|
|
223
220
|
>
|
|
224
221
|
{error}
|
|
225
|
-
</
|
|
222
|
+
</AssistiveElement>
|
|
223
|
+
) : (
|
|
224
|
+
description && (
|
|
225
|
+
<AssistiveElement
|
|
226
|
+
assistiveVariant="help"
|
|
227
|
+
assistiveWithIcon={false}
|
|
228
|
+
>
|
|
229
|
+
{description}
|
|
230
|
+
</AssistiveElement>
|
|
231
|
+
)
|
|
226
232
|
)}
|
|
227
233
|
</div>
|
|
228
234
|
</div>
|
|
@@ -128,8 +128,6 @@
|
|
|
128
128
|
background-color: var(--chip-bg);
|
|
129
129
|
border-color: var(--chip-border);
|
|
130
130
|
color: var(--chip-text);
|
|
131
|
-
min-width: var(--recursica_ui-kit_components_chip_properties_min-width);
|
|
132
|
-
max-width: var(--recursica_ui-kit_components_chip_properties_max-width);
|
|
133
131
|
box-shadow: var(--recursica_ui-kit_components_chip_properties_elevation);
|
|
134
132
|
|
|
135
133
|
transition: all 0.2s ease;
|
|
@@ -195,11 +193,12 @@
|
|
|
195
193
|
cursor: not-allowed;
|
|
196
194
|
}
|
|
197
195
|
|
|
198
|
-
/*
|
|
199
|
-
.
|
|
196
|
+
/* Selected-state check icon colors and size */
|
|
197
|
+
.checkIconWrapper.checkIconWrapper {
|
|
200
198
|
color: var(--chip-icon);
|
|
201
199
|
width: var(--recursica_ui-kit_components_chip_properties_icon-size);
|
|
202
200
|
height: var(--recursica_ui-kit_components_chip_properties_icon-size);
|
|
201
|
+
margin-left: 0;
|
|
203
202
|
margin-right: var(
|
|
204
203
|
--recursica_ui-kit_components_chip_properties_icon-text-gap
|
|
205
204
|
);
|
|
@@ -219,13 +218,21 @@
|
|
|
219
218
|
white-space: nowrap;
|
|
220
219
|
}
|
|
221
220
|
|
|
222
|
-
|
|
221
|
+
/* Doubled selector for specificity parity with MUI's own compound
|
|
222
|
+
`.MuiChip-root .MuiChip-icon` rule, which otherwise wins outright on a
|
|
223
|
+
plain single-class selector and leaks its default margin/color. */
|
|
224
|
+
.leadingIcon.leadingIcon {
|
|
223
225
|
display: inline-flex;
|
|
224
226
|
align-items: center;
|
|
225
227
|
justify-content: center;
|
|
226
228
|
color: var(--chip-icon);
|
|
227
229
|
width: var(--recursica_ui-kit_components_chip_properties_icon-size);
|
|
228
230
|
height: var(--recursica_ui-kit_components_chip_properties_icon-size);
|
|
231
|
+
/* Reset MUI's default icon margin, keep the icon-text gap */
|
|
232
|
+
margin-left: 0;
|
|
233
|
+
margin-right: var(
|
|
234
|
+
--recursica_ui-kit_components_chip_properties_icon-text-gap
|
|
235
|
+
);
|
|
229
236
|
}
|
|
230
237
|
|
|
231
238
|
.leadingIcon svg {
|
|
@@ -233,7 +240,9 @@
|
|
|
233
240
|
height: 100%;
|
|
234
241
|
}
|
|
235
242
|
|
|
236
|
-
|
|
243
|
+
/* Doubled selector for specificity parity with MUI's own compound
|
|
244
|
+
`.MuiChip-root .MuiChip-deleteIcon` rule (same reasoning as .leadingIcon). */
|
|
245
|
+
.removeIconWrapper.removeIconWrapper {
|
|
237
246
|
display: inline-flex;
|
|
238
247
|
align-items: center;
|
|
239
248
|
justify-content: center;
|
|
@@ -247,7 +256,7 @@
|
|
|
247
256
|
margin: 0;
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
.removeIconWrapper:focus-visible {
|
|
259
|
+
.removeIconWrapper.removeIconWrapper:focus-visible {
|
|
251
260
|
box-shadow: 0 0 0 2px var(--chip-border); /* HARDCODE: Focus indication */
|
|
252
261
|
}
|
|
253
262
|
|
|
@@ -109,7 +109,7 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
|
109
109
|
{...sanitizedProps}
|
|
110
110
|
icon={
|
|
111
111
|
checked ? (
|
|
112
|
-
<span className={styles.
|
|
112
|
+
<span className={styles.checkIconWrapper} aria-hidden>
|
|
113
113
|
<CheckIcon />
|
|
114
114
|
</span>
|
|
115
115
|
) : icon ? (
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# FormControlWrapper Implementation Notes
|
|
2
|
+
|
|
3
|
+
## Architectural Philosophy
|
|
4
|
+
|
|
5
|
+
Unlike the Mantine adapter (which tears out `Input.Wrapper` entirely), this adapter still
|
|
6
|
+
renders MUI's native `FormControl` as the root — MUI's own `disabled`/`error`/`focused`
|
|
7
|
+
cascading via `FormControlContext` to descendants is useful and kept. What's rebuilt on top is
|
|
8
|
+
label tracking, ARIA generation, and the help/error text block, mirroring the Mantine adapter's
|
|
9
|
+
own approach for consistency across frameworks.
|
|
10
|
+
|
|
11
|
+
### 1. Self-generated `id` fallback
|
|
12
|
+
|
|
13
|
+
MUI's `FormControl` doesn't generate or require an `id` itself — every ARIA relationship below
|
|
14
|
+
depends on having one. `React.useId()` provides a fallback (`recursica-fc-${generatedId}`)
|
|
15
|
+
whenever the caller doesn't pass their own `id`, matching the Mantine adapter, so the aria
|
|
16
|
+
wiring below fires by default instead of only when a caller happens to supply an `id`.
|
|
17
|
+
|
|
18
|
+
### 2. The `cloneElement` ARIA map
|
|
19
|
+
|
|
20
|
+
Same reasoning as the Mantine adapter: `aria-labelledby`/`aria-describedby`/`aria-errormessage`
|
|
21
|
+
are generated from the resolved `id` and injected onto the child field via `React.cloneElement`,
|
|
22
|
+
since there's no shared context gluing the field to this wrapper's label/assistive text.
|
|
23
|
+
|
|
24
|
+
- `aria-describedby` and `aria-errormessage` use **separate ids** (`${id}-assistive` /
|
|
25
|
+
`${id}-error`) rather than one shared id — help text and error text are never both visible
|
|
26
|
+
at once (error takes priority, same as Mantine), but keeping the ids distinct means each
|
|
27
|
+
attribute only ever points at content that's actually relevant to it.
|
|
28
|
+
- Neither is set if the child already carries its own explicit `aria-describedby`/
|
|
29
|
+
`aria-errormessage` — `cloneElement` never overwrites a caller's own value.
|
|
30
|
+
|
|
31
|
+
### 3. Strict `AssistiveElement` coupling
|
|
32
|
+
|
|
33
|
+
Same as Mantine: no generic MUI `FormHelperText` used directly here. `FormControlWrapper`
|
|
34
|
+
renders `<AssistiveElement>` itself, resolving to `"error"` or `"help"` depending on whether
|
|
35
|
+
`error` is set, and passing whichever id (`errorId`/`assistiveId`) matches.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useId } from "react";
|
|
2
2
|
import { FormControl, type FormControlProps } from "@mui/material";
|
|
3
3
|
import { filterStylingProps } from "../../utils/filterStylingProps";
|
|
4
4
|
import { Label } from "../Label/Label";
|
|
@@ -37,7 +37,7 @@ export const FormControlWrapper = React.forwardRef<
|
|
|
37
37
|
required,
|
|
38
38
|
disabled,
|
|
39
39
|
focused,
|
|
40
|
-
id,
|
|
40
|
+
id: userProvidedId,
|
|
41
41
|
|
|
42
42
|
formLayout = "stacked",
|
|
43
43
|
labelSize = "default",
|
|
@@ -57,19 +57,35 @@ export const FormControlWrapper = React.forwardRef<
|
|
|
57
57
|
|
|
58
58
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
59
59
|
|
|
60
|
+
// Generate a reliable ID when the caller doesn't provide one — matches the Mantine adapter,
|
|
61
|
+
// and is what makes the aria wiring below actually fire by default instead of only when a
|
|
62
|
+
// caller happens to pass their own `id`.
|
|
63
|
+
const generatedId = useId();
|
|
64
|
+
const id = userProvidedId || `recursica-fc-${generatedId}`;
|
|
65
|
+
|
|
60
66
|
// Map descriptions fallback exactly like Mantine
|
|
61
67
|
const finalAssistiveText = assistiveText || description || helperText;
|
|
62
68
|
|
|
63
|
-
// ARIA Bindings
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
// ARIA Bindings — separate ids for help text and error text (matching Mantine) so each can
|
|
70
|
+
// be referenced independently via `aria-describedby`/`aria-errormessage` rather than sharing
|
|
71
|
+
// one id for both.
|
|
72
|
+
const labelId = `${id}-label`;
|
|
73
|
+
const assistiveId = finalAssistiveText ? `${id}-assistive` : undefined;
|
|
74
|
+
const errorId = error ? `${id}-error` : undefined;
|
|
66
75
|
|
|
67
|
-
// Wrap children safely cloning aria hooks down natively
|
|
76
|
+
// Wrap children safely cloning aria hooks down natively. Never clobber a child's own
|
|
77
|
+
// explicit aria-describedby/aria-errormessage, matching Mantine's guard.
|
|
68
78
|
const mappedChildren = React.Children.map(children, (child) => {
|
|
69
79
|
if (React.isValidElement(child)) {
|
|
80
|
+
const childProps = child.props as Record<string, unknown>;
|
|
70
81
|
return React.cloneElement(child, {
|
|
71
82
|
"aria-labelledby": labelId,
|
|
72
|
-
"aria-describedby"
|
|
83
|
+
...(assistiveId && !childProps["aria-describedby"]
|
|
84
|
+
? { "aria-describedby": assistiveId }
|
|
85
|
+
: {}),
|
|
86
|
+
...(errorId && !childProps["aria-errormessage"]
|
|
87
|
+
? { "aria-errormessage": errorId }
|
|
88
|
+
: {}),
|
|
73
89
|
} as React.HTMLAttributes<HTMLElement>);
|
|
74
90
|
}
|
|
75
91
|
return child;
|
|
@@ -117,7 +133,7 @@ export const FormControlWrapper = React.forwardRef<
|
|
|
117
133
|
{/* Append native assistive block dynamically below the input */}
|
|
118
134
|
{(finalAssistiveText || error) && (
|
|
119
135
|
<AssistiveElement
|
|
120
|
-
id={
|
|
136
|
+
id={error ? errorId : assistiveId}
|
|
121
137
|
assistiveVariant={error ? "error" : "help"}
|
|
122
138
|
assistiveWithIcon={assistiveWithIcon}
|
|
123
139
|
>
|
|
@@ -152,11 +152,11 @@ const GridBase = forwardRef<HTMLDivElement, GridProps>(function Grid(
|
|
|
152
152
|
<GridContext.Provider value={{ grow }}>
|
|
153
153
|
<MuiGrid
|
|
154
154
|
ref={ref}
|
|
155
|
+
{...(safeProps as unknown as MuiGridProps)}
|
|
155
156
|
container
|
|
156
157
|
columns={columns}
|
|
157
158
|
spacing={resolvedGap}
|
|
158
159
|
className={styles.root}
|
|
159
|
-
{...(safeProps as unknown as MuiGridProps)}
|
|
160
160
|
style={{
|
|
161
161
|
justifyContent: justify,
|
|
162
162
|
alignItems: align,
|
|
@@ -46,6 +46,7 @@ export const Group = forwardRef<HTMLDivElement, GroupProps>(function Group(
|
|
|
46
46
|
return (
|
|
47
47
|
<MUIStack
|
|
48
48
|
ref={ref}
|
|
49
|
+
{...safeProps}
|
|
49
50
|
direction="row"
|
|
50
51
|
flexWrap={wrap || "wrap"}
|
|
51
52
|
justifyContent={justify}
|
|
@@ -55,7 +56,6 @@ export const Group = forwardRef<HTMLDivElement, GroupProps>(function Group(
|
|
|
55
56
|
...(resolvedRowGap ? { rowGap: resolvedRowGap } : {}),
|
|
56
57
|
...(resolvedColumnGap ? { columnGap: resolvedColumnGap } : {}),
|
|
57
58
|
}}
|
|
58
|
-
{...safeProps}
|
|
59
59
|
>
|
|
60
60
|
{children}
|
|
61
61
|
</MUIStack>
|
|
@@ -85,6 +85,7 @@ const HoverCardBase = function HoverCard({
|
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
87
|
<MuiTooltip
|
|
88
|
+
{...(sanitizedProps as unknown as Record<string, unknown>)}
|
|
88
89
|
title={dropdownNode}
|
|
89
90
|
placement={position as unknown as MuiTooltipProps["placement"]}
|
|
90
91
|
arrow={withBeak}
|
|
@@ -104,7 +105,6 @@ const HoverCardBase = function HoverCard({
|
|
|
104
105
|
},
|
|
105
106
|
}}
|
|
106
107
|
classes={mergedClassNames as unknown as MuiTooltipProps["classes"]}
|
|
107
|
-
{...(sanitizedProps as unknown as Record<string, unknown>)}
|
|
108
108
|
>
|
|
109
109
|
{/* Tooltip requires a single valid React element child that accepts a ref */}
|
|
110
110
|
{isValidElement(targetNode) ? targetNode : <span>{targetNode}</span>}
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
.innerLayout {
|
|
50
50
|
display: flex;
|
|
51
|
+
flex-wrap: wrap;
|
|
51
52
|
align-items: center;
|
|
52
53
|
gap: var(
|
|
53
54
|
--recursica_ui-kit_components_label_properties_label-optional-text-gap
|
|
@@ -74,8 +75,8 @@
|
|
|
74
75
|
display: none !important;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
/* MUI propagates .Mui-error to the label automatically on FormControl error.
|
|
78
|
-
We explicitly override this bleed to prevent the main label text from turning red,
|
|
78
|
+
/* MUI propagates .Mui-error to the label automatically on FormControl error.
|
|
79
|
+
We explicitly override this bleed to prevent the main label text from turning red,
|
|
79
80
|
while allowing the asterisk to take the error color! */
|
|
80
81
|
.root:global(.Mui-error) {
|
|
81
82
|
color: var(--recursica_ui-kit_components_label_properties_colors_text-color);
|
|
@@ -86,6 +87,15 @@
|
|
|
86
87
|
);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/* Same bleed, different trigger: MUI's FormControl propagates .Mui-focused to this label
|
|
91
|
+
whenever any descendant form control (e.g. a grouped Switch/Checkbox) receives focus,
|
|
92
|
+
turning the whole label MUI's primary blue via InputLabel's own default styling. No
|
|
93
|
+
Recursica design calls for a focus-reactive label color, so it's reset here — same fix as
|
|
94
|
+
the .Mui-error case above, just for the focused trigger instead of the error one. */
|
|
95
|
+
.root:global(.Mui-focused) {
|
|
96
|
+
color: var(--recursica_ui-kit_components_label_properties_colors_text-color);
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
.endNodes {
|
|
90
100
|
display: flex;
|
|
91
101
|
align-items: center;
|
|
@@ -95,6 +105,9 @@
|
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
.optionalText {
|
|
108
|
+
/* Forces the optional text onto its own row below the label text/end nodes,
|
|
109
|
+
mirroring mantine-adapter's flex-basis: 100% wrap behavior. */
|
|
110
|
+
flex-basis: 100%;
|
|
98
111
|
color: inherit;
|
|
99
112
|
font-family: var(
|
|
100
113
|
--recursica_ui-kit_components_label_properties_optional-text_font-family
|
|
@@ -46,9 +46,9 @@ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
|
46
46
|
return (
|
|
47
47
|
<InputLabel
|
|
48
48
|
ref={ref}
|
|
49
|
+
{...(sanitizedProps as InputLabelProps)}
|
|
49
50
|
shrink={true}
|
|
50
51
|
className={className ? `${styles.root} ${className}` : styles.root}
|
|
51
|
-
{...(sanitizedProps as InputLabelProps)}
|
|
52
52
|
>
|
|
53
53
|
<div className={styles.innerLayout}>
|
|
54
54
|
<span className={styles.textWrapper}>
|
|
@@ -60,11 +60,6 @@ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
|
60
60
|
{labelActionArea && (
|
|
61
61
|
<span className={styles.actionArea}>{labelActionArea}</span>
|
|
62
62
|
)}
|
|
63
|
-
{!required && labelOptionalText && !labelActionArea && (
|
|
64
|
-
<span className={styles.optionalText}>
|
|
65
|
-
{labelOptionalText === true ? "Optional" : labelOptionalText}
|
|
66
|
-
</span>
|
|
67
|
-
)}
|
|
68
63
|
{labelWithEditIcon && (
|
|
69
64
|
<button
|
|
70
65
|
type="button"
|
|
@@ -76,6 +71,12 @@ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
|
76
71
|
</button>
|
|
77
72
|
)}
|
|
78
73
|
</div>
|
|
74
|
+
|
|
75
|
+
{!required && labelOptionalText && !labelActionArea && (
|
|
76
|
+
<span className={styles.optionalText}>
|
|
77
|
+
{labelOptionalText === true ? "(optional)" : labelOptionalText}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
79
80
|
</div>
|
|
80
81
|
</InputLabel>
|
|
81
82
|
);
|
|
@@ -26,12 +26,12 @@ export const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(
|
|
|
26
26
|
return (
|
|
27
27
|
<MuiMenu
|
|
28
28
|
ref={ref}
|
|
29
|
+
{...(sanitizedProps as MuiMenuProps)}
|
|
29
30
|
className={`${styles.dropdown} ${className || ""}`}
|
|
30
31
|
classes={{
|
|
31
32
|
paper: styles.dropdown,
|
|
32
33
|
list: styles.dropdown,
|
|
33
34
|
}}
|
|
34
|
-
{...(sanitizedProps as MuiMenuProps)}
|
|
35
35
|
/>
|
|
36
36
|
);
|
|
37
37
|
});
|
|
@@ -77,11 +77,11 @@ const ModalRoot = forwardRef<HTMLDivElement, ModalProps>(function ModalRoot(
|
|
|
77
77
|
return (
|
|
78
78
|
<MuiDialog
|
|
79
79
|
ref={ref}
|
|
80
|
+
{...(sanitizedProps as MuiDialogProps)}
|
|
80
81
|
className={`${styles.root} ${className || ""}`}
|
|
81
82
|
classes={{
|
|
82
83
|
paper: styles.inner,
|
|
83
84
|
}}
|
|
84
|
-
{...(sanitizedProps as MuiDialogProps)}
|
|
85
85
|
open={
|
|
86
86
|
opened ||
|
|
87
87
|
((sanitizedProps as Record<string, unknown>).open as boolean) ||
|
|
@@ -137,6 +137,7 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
137
137
|
/* Naked Input execution safely decoupled from Mui's macro Input.Wrapper DOM hooks */
|
|
138
138
|
<MuiNumberInput
|
|
139
139
|
ref={ref}
|
|
140
|
+
{...(sanitizedProps as unknown as MuiNumberInputProps)}
|
|
140
141
|
classes={mergedClassNames}
|
|
141
142
|
disabled={disabled}
|
|
142
143
|
value={value}
|
|
@@ -154,7 +155,6 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
154
155
|
step,
|
|
155
156
|
...(restRecord.inputProps as Record<string, unknown>),
|
|
156
157
|
}}
|
|
157
|
-
{...(sanitizedProps as unknown as MuiNumberInputProps)}
|
|
158
158
|
/>
|
|
159
159
|
}
|
|
160
160
|
/>
|
|
@@ -63,13 +63,13 @@ const _Pagination = forwardRef<HTMLDivElement, PaginationProps>(
|
|
|
63
63
|
return (
|
|
64
64
|
<div ref={ref} className={stylingParams.className}>
|
|
65
65
|
<MuiPagination
|
|
66
|
+
{...(sanitizedProps as MuiPaginationProps)}
|
|
66
67
|
count={total}
|
|
67
68
|
showFirstButton={withEdges}
|
|
68
69
|
showLastButton={withEdges}
|
|
69
70
|
hidePrevButton={withControls === false ? true : undefined}
|
|
70
71
|
hideNextButton={withControls === false ? true : undefined}
|
|
71
72
|
classes={stylingParams.classNames}
|
|
72
|
-
{...(sanitizedProps as MuiPaginationProps)}
|
|
73
73
|
/>
|
|
74
74
|
</div>
|
|
75
75
|
);
|
|
@@ -124,7 +124,7 @@ export const PanelFooter = forwardRef<HTMLDivElement, PanelFooterProps>(
|
|
|
124
124
|
? `${styles.footer} ${classNameProp}`
|
|
125
125
|
: styles.footer;
|
|
126
126
|
|
|
127
|
-
return <div ref={ref} className={finalClassName}
|
|
127
|
+
return <div ref={ref} {...sanitizedProps} className={finalClassName} />;
|
|
128
128
|
},
|
|
129
129
|
);
|
|
130
130
|
PanelFooter.displayName = "PanelFooter";
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
.
|
|
73
|
+
.inner {
|
|
74
74
|
display: flex;
|
|
75
75
|
align-items: center;
|
|
76
76
|
justify-content: center;
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
--recursica_ui-kit_components_radio-button-item_properties_text_font-size
|
|
83
83
|
);
|
|
84
84
|
/* Mathematically syncs the physical input radio bounding block visually with the center of the text's first line.
|
|
85
|
-
NOTE: Since the Figma line-height token resolves to an 'em' length (e.g. 1.05em), we CANNOT multiply it by font-size in CSS.
|
|
85
|
+
NOTE: Since the Figma line-height token resolves to an 'em' length (e.g. 1.05em), we CANNOT multiply it by font-size in CSS.
|
|
86
86
|
Instead, it naturally evaluates to the correct height because we applied the font-size above. */
|
|
87
87
|
margin-top: calc(
|
|
88
88
|
(
|
|
@@ -95,10 +95,16 @@
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/* Base explicit reset masking underlying framework defaults
|
|
99
|
-
|
|
98
|
+
/* Base explicit reset masking underlying framework defaults.
|
|
99
|
+
MUI's native <input type="radio"> stays invisible (SwitchBase overlays it with
|
|
100
|
+
opacity: 0) — it is not the visual circle. The circle is drawn by our own
|
|
101
|
+
icon/checkedIcon node (see Radio.tsx), a sibling of the input, so it's styled
|
|
102
|
+
here directly rather than through MUI's `classes` prop (MUI has no "radio" slot). */
|
|
103
|
+
.radio {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
100
107
|
cursor: pointer; /* HARDCODE: core interaction behavior */
|
|
101
|
-
appearance: none; /* HARDCODE: reset native widget */
|
|
102
108
|
box-sizing: border-box; /* HARDCODE: ensures size includes borders */
|
|
103
109
|
width: var(--recursica_ui-kit_components_radio-button_properties_size);
|
|
104
110
|
height: var(--recursica_ui-kit_components_radio-button_properties_size);
|
|
@@ -121,7 +127,7 @@
|
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
/* Checked State */
|
|
124
|
-
.
|
|
130
|
+
.radioChecked {
|
|
125
131
|
background-color: var(
|
|
126
132
|
--recursica_ui-kit_components_radio-button_variants_selection-states_selected_properties_colors_background-color
|
|
127
133
|
);
|
|
@@ -130,11 +136,14 @@
|
|
|
130
136
|
);
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
/* Disabled State (colors now differ per selection state
|
|
134
|
-
|
|
139
|
+
/* Disabled State (colors now differ per selection state; MUI applies its `disabled`
|
|
140
|
+
classKey to the same root element as `root`, so `.root.disabled` targets the
|
|
141
|
+
actual disabled instance and cascades to the decorative icon div inside it —
|
|
142
|
+
mirrors Checkbox's split). */
|
|
143
|
+
.root.disabled .radio {
|
|
135
144
|
cursor: not-allowed; /* HARDCODE: disabled state default */
|
|
136
145
|
}
|
|
137
|
-
.root .radio:
|
|
146
|
+
.root.disabled .radio:not(.radioChecked) {
|
|
138
147
|
opacity: var(
|
|
139
148
|
--recursica_ui-kit_components_radio-button_variants_selection-states_unselected_variants_states_disabled_properties_opacity
|
|
140
149
|
);
|
|
@@ -145,7 +154,7 @@
|
|
|
145
154
|
--recursica_ui-kit_components_radio-button_variants_selection-states_unselected_variants_states_disabled_properties_colors_border-color
|
|
146
155
|
);
|
|
147
156
|
}
|
|
148
|
-
.root
|
|
157
|
+
.root.disabled .radioChecked {
|
|
149
158
|
opacity: var(
|
|
150
159
|
--recursica_ui-kit_components_radio-button_variants_selection-states_selected_variants_states_disabled_properties_opacity
|
|
151
160
|
);
|
|
@@ -157,29 +166,32 @@
|
|
|
157
166
|
);
|
|
158
167
|
}
|
|
159
168
|
|
|
160
|
-
.
|
|
161
|
-
position: absolute;
|
|
162
|
-
inset: 0;
|
|
163
|
-
margin: auto;
|
|
169
|
+
.icon {
|
|
164
170
|
pointer-events: none;
|
|
165
171
|
width: var(--recursica_ui-kit_components_radio-button_properties_icon-size);
|
|
166
172
|
height: var(--recursica_ui-kit_components_radio-button_properties_icon-size);
|
|
167
173
|
}
|
|
168
174
|
/* No icon-color token exists for the unselected state (no icon is visible then) */
|
|
169
|
-
.
|
|
170
|
-
.root .radio:checked + .icon {
|
|
175
|
+
.radioChecked .icon {
|
|
171
176
|
color: var(
|
|
172
177
|
--recursica_ui-kit_components_radio-button_variants_selection-states_selected_properties_colors_icon-color
|
|
173
178
|
);
|
|
174
179
|
}
|
|
175
|
-
.root
|
|
176
|
-
.root .radio:checked:disabled + .icon {
|
|
180
|
+
.root.disabled .radioChecked .icon {
|
|
177
181
|
color: var(
|
|
178
182
|
--recursica_ui-kit_components_radio-button_variants_selection-states_selected_variants_states_disabled_properties_colors_icon-color
|
|
179
183
|
);
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
.root .labelWrapper {
|
|
187
|
+
/* display: flex (not the browser default block) blockifies the native inline
|
|
188
|
+
<label> child per the CSS Display spec's flex-item blockification rule —
|
|
189
|
+
matches Mantine's own labelWrapper (inline-flex), whose flex context does
|
|
190
|
+
the same to its label. Without it, the label stays inline and its line box
|
|
191
|
+
picks up extra half-leading height, visually shifting it below vertical
|
|
192
|
+
center against the circle. */
|
|
193
|
+
display: flex;
|
|
194
|
+
flex-direction: column;
|
|
183
195
|
padding: 0; /* HARDCODE: structural layout reset */
|
|
184
196
|
margin: 0; /* HARDCODE: structural layout reset */
|
|
185
197
|
flex: 1;
|
|
@@ -230,23 +242,6 @@
|
|
|
230
242
|
);
|
|
231
243
|
}
|
|
232
244
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
font-size: 12px; /* HARDCODE: mantine default xs */
|
|
237
|
-
line-height: 1.2;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.root .error {
|
|
241
|
-
margin-top: 5px; /* HARDCODE: mantine default */
|
|
242
|
-
color: #fa5252; /* HARDCODE: mantine default error */
|
|
243
|
-
font-size: 12px; /* HARDCODE: mantine default xs */
|
|
244
|
-
line-height: 1.2;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.root .description[data-disabled="true"],
|
|
248
|
-
.root .error[data-disabled="true"] {
|
|
249
|
-
opacity: var(
|
|
250
|
-
--recursica_ui-kit_components_radio-button-item_variants_states_disabled_properties_opacity
|
|
251
|
-
);
|
|
252
|
-
}
|
|
245
|
+
/* description/error text renders via AssistiveElement directly (see Radio.tsx) —
|
|
246
|
+
it owns its own tokenized styling. AssistiveElement has no disabled state and
|
|
247
|
+
must never dim. */
|