@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
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Accordion – implementation notes
|
|
2
|
+
|
|
3
|
+
Decisions and design tweaks specific to the UI Kit's Accordion wrapped against `@mui/material`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Context-based container (no native MUI equivalent)
|
|
8
|
+
|
|
9
|
+
**Decision:** MUI has no group-level `Accordion` — its own `Accordion` component _is_ the
|
|
10
|
+
item, controlled individually via `expanded`/`onChange`. Recursica's container/group model
|
|
11
|
+
(single `value`, `multiple`, cascading `chevron`) is reimplemented with a React context
|
|
12
|
+
(`AccordionContext`) that each `AccordionItem`/`AccordionControl` reads from.
|
|
13
|
+
**Implementation:** `AccordionBase` owns the controlled/uncontrolled `value` state and hands
|
|
14
|
+
`{ value, onChange, chevron }` down via context; `AccordionItem` derives its own `expanded`
|
|
15
|
+
from that context instead of taking it as a prop.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Prop-contract Omits protect the context wiring
|
|
20
|
+
|
|
21
|
+
**Decision:** Because `AccordionItem`/`AccordionControl` are thin wrappers around real MUI
|
|
22
|
+
components (`MuiAccordion`/`MuiAccordionSummary`), their native `expanded`/`onChange`/
|
|
23
|
+
`expandIcon` props would otherwise collide with the values these wrappers compute from
|
|
24
|
+
context.
|
|
25
|
+
**Implementation:** `AccordionItemWrapperProps` omits `expanded`/`onChange`/`defaultExpanded`
|
|
26
|
+
from `MuiAccordionProps`; `AccordionControlWrapperProps` omits `expandIcon` from
|
|
27
|
+
`MuiAccordionSummaryProps`. Neither is exposed on `RecursicaAccordionItemProps`/
|
|
28
|
+
`RecursicaAccordionControlProps`, so passing them is a compile error, not a silent override.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 3. `disabled` is a formally supported, shared prop
|
|
33
|
+
|
|
34
|
+
**Decision:** `RecursicaAccordionItemProps.disabled` is shared as-is with MUI's native
|
|
35
|
+
`Accordion.disabled` — same name, same shape, no reshaping needed. MUI already blocks
|
|
36
|
+
click/keyboard interaction on the control internally once `disabled` is set (its `AccordionSummary`
|
|
37
|
+
renders as a real disabled `<button>`); the visual dim is Recursica's own, not MUI's default.
|
|
38
|
+
**Implementation:** `AccordionItem` destructures `disabled` explicitly (rather than letting it
|
|
39
|
+
ride through in `...rest`) so it can also drive `data-disabled` on the item root for the dim.
|
|
40
|
+
MUI's own default disabled treatment — a separate hardcoded background/opacity on both the
|
|
41
|
+
item root and the control (`.Mui-disabled`) — is neutralized in `Accordion.module.css` so the
|
|
42
|
+
token-driven `.item[data-disabled]` dim is the single source of truth.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 4. Chevron via `expandIcon` wrapper
|
|
47
|
+
|
|
48
|
+
**Decision:** MUI's expand indicator is set through `AccordionSummary.expandIcon`, not a
|
|
49
|
+
child. Recursica's container-level `chevron` (or the default arrow) is threaded through
|
|
50
|
+
context and rendered into that slot.
|
|
51
|
+
**Implementation:** `AccordionControl` resolves `ctx?.chevron ?? <ChevronIcon />` and passes
|
|
52
|
+
it as `expandIcon={<span className={styles.chevron}>{resolvedChevron}</span>}`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 5. Transparent background for `Layer` overrides
|
|
57
|
+
|
|
58
|
+
**Decision:** Same reasoning as the Mantine adapter's hover-fix note — MUI's `Paper`-derived
|
|
59
|
+
`Accordion` root ships its own background; Recursica needs it transparent so ambient `Layer`
|
|
60
|
+
tokens can apply.
|
|
61
|
+
**Implementation:** `AccordionItem` merges `backgroundColor: "transparent"` into `style`
|
|
62
|
+
ahead of any caller-supplied `style`, and forces `disableGutters`/`elevation={0}`/`square` to
|
|
63
|
+
strip MUI's own spacing/elevation/border-radius defaults.
|
|
@@ -168,11 +168,11 @@
|
|
|
168
168
|
margin: 0 !important;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
/*
|
|
171
|
+
/* MUI natively places hover states on the control node. We explicitly re-apply our own
|
|
172
172
|
constant background (MUI's native :hover CSS would otherwise show through) and use an
|
|
173
173
|
overlay ::after structure with the generic hover tokens for the actual hover tint; no per-item
|
|
174
174
|
hover-color/hover-opacity tokens exist anymore. */
|
|
175
|
-
.control:hover {
|
|
175
|
+
.control:hover:not(:disabled) {
|
|
176
176
|
background-color: var(
|
|
177
177
|
--recursica_ui-kit_components_accordion-header_variants_appearance_closed_properties_colors_background-color
|
|
178
178
|
);
|
|
@@ -192,6 +192,25 @@
|
|
|
192
192
|
opacity: var(--recursica_brand_states_hover_opacity);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/* MUI applies its own default disabled treatment on both the item root and the control
|
|
196
|
+
(`.Mui-disabled`, each with its own hardcoded background/opacity) — neutralized here so the
|
|
197
|
+
token-driven dim below is the single source of truth. */
|
|
198
|
+
.item:global(.Mui-disabled) {
|
|
199
|
+
background-color: transparent;
|
|
200
|
+
}
|
|
201
|
+
.control:global(.Mui-disabled) {
|
|
202
|
+
opacity: 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Dims the whole item — control and any currently-visible panel content alike — using the
|
|
206
|
+
generic disabled opacity token, same convention as every other disabled component in the
|
|
207
|
+
design system (no accordion-item-specific disabled token exists). MUI's own `disabled` prop
|
|
208
|
+
already blocks click/keyboard interaction on the control internally; this only handles the
|
|
209
|
+
visual dim. */
|
|
210
|
+
.item[data-disabled] {
|
|
211
|
+
opacity: var(--recursica_brand_states_disabled);
|
|
212
|
+
}
|
|
213
|
+
|
|
195
214
|
/* Label logic inside the Control wrapper */
|
|
196
215
|
.label {
|
|
197
216
|
flex: 1;
|
|
@@ -123,6 +123,44 @@ export const WithIcons: StoryObj<typeof Accordion> = {
|
|
|
123
123
|
},
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
+
export const Disabled: StoryObj<typeof Accordion> = {
|
|
127
|
+
render: () => {
|
|
128
|
+
return (
|
|
129
|
+
<Layer layer={0} style={{ padding: "24px" }}>
|
|
130
|
+
<Accordion defaultValue="expanded-disabled" chevron={<ChevronIcon />}>
|
|
131
|
+
<Accordion.Item value="expanded-disabled" disabled>
|
|
132
|
+
<Accordion.Control leftIcon={<SVGIcon />}>
|
|
133
|
+
Expanded and Disabled
|
|
134
|
+
</Accordion.Control>
|
|
135
|
+
<Accordion.Panel>
|
|
136
|
+
This item starts expanded so the panel content's dimming can be
|
|
137
|
+
verified alongside the control's, not just the collapsed header.
|
|
138
|
+
</Accordion.Panel>
|
|
139
|
+
</Accordion.Item>
|
|
140
|
+
|
|
141
|
+
<Accordion.Item value="collapsed-disabled" disabled>
|
|
142
|
+
<Accordion.Control leftIcon={<SVGIcon />}>
|
|
143
|
+
Collapsed and Disabled
|
|
144
|
+
</Accordion.Control>
|
|
145
|
+
<Accordion.Panel>
|
|
146
|
+
Clicking or tabbing to this control should have no effect.
|
|
147
|
+
</Accordion.Panel>
|
|
148
|
+
</Accordion.Item>
|
|
149
|
+
|
|
150
|
+
<Accordion.Item value="enabled">
|
|
151
|
+
<Accordion.Control leftIcon={<SVGIcon />}>
|
|
152
|
+
Enabled, for Comparison
|
|
153
|
+
</Accordion.Control>
|
|
154
|
+
<Accordion.Panel>
|
|
155
|
+
A normal, interactive item alongside the disabled ones above.
|
|
156
|
+
</Accordion.Panel>
|
|
157
|
+
</Accordion.Item>
|
|
158
|
+
</Accordion>
|
|
159
|
+
</Layer>
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
126
164
|
export const LayerOne: StoryObj<typeof Accordion> = {
|
|
127
165
|
render: () => {
|
|
128
166
|
return (
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type RecursicaAccordionProps,
|
|
18
18
|
type RecursicaAccordionItemProps,
|
|
19
19
|
type RecursicaAccordionControlProps,
|
|
20
|
+
type RecursicaAccordionPanelProps,
|
|
20
21
|
} from "@recursica/adapter-common";
|
|
21
22
|
|
|
22
23
|
const ChevronIcon = () => (
|
|
@@ -48,7 +49,7 @@ export type AccordionProps = RecursicaOverStyled<
|
|
|
48
49
|
const AccordionBase = forwardRef<HTMLDivElement, AccordionProps>(
|
|
49
50
|
function Accordion(
|
|
50
51
|
{
|
|
51
|
-
variant = "
|
|
52
|
+
variant = "default",
|
|
52
53
|
overStyled = false,
|
|
53
54
|
value,
|
|
54
55
|
defaultValue,
|
|
@@ -96,9 +97,9 @@ const AccordionBase = forwardRef<HTMLDivElement, AccordionProps>(
|
|
|
96
97
|
>
|
|
97
98
|
<div
|
|
98
99
|
ref={ref}
|
|
100
|
+
{...(sanitizedProps as React.HTMLAttributes<HTMLDivElement>)}
|
|
99
101
|
className={finalClass}
|
|
100
102
|
data-variant={variant}
|
|
101
|
-
{...(sanitizedProps as React.HTMLAttributes<HTMLDivElement>)}
|
|
102
103
|
>
|
|
103
104
|
{children}
|
|
104
105
|
</div>
|
|
@@ -109,7 +110,15 @@ const AccordionBase = forwardRef<HTMLDivElement, AccordionProps>(
|
|
|
109
110
|
AccordionBase.displayName = "Accordion";
|
|
110
111
|
|
|
111
112
|
export type AccordionItemWrapperProps = RecursicaOverStyled<
|
|
112
|
-
|
|
113
|
+
// `expanded`/`onChange` are computed internally from the group context — omitted so a
|
|
114
|
+
// caller can't silently detach this item from the controlled state. `defaultExpanded` has
|
|
115
|
+
// no meaning here since expansion is always driven by the group's `value`. `disabled` is
|
|
116
|
+
// shared as-is with `RecursicaAccordionItemProps.disabled` — same name, same shape, no
|
|
117
|
+
// reshaping needed; see ACCORDION_IMPLEMENTATION_NOTES.md.
|
|
118
|
+
Omit<
|
|
119
|
+
MuiAccordionProps,
|
|
120
|
+
"children" | "value" | "expanded" | "onChange" | "defaultExpanded"
|
|
121
|
+
> & {
|
|
113
122
|
children?: React.ReactNode;
|
|
114
123
|
value: string;
|
|
115
124
|
} & RecursicaAccordionItemProps
|
|
@@ -125,6 +134,7 @@ export const AccordionItem = forwardRef<
|
|
|
125
134
|
divider = true,
|
|
126
135
|
children,
|
|
127
136
|
value,
|
|
137
|
+
disabled = false,
|
|
128
138
|
overStyled = false,
|
|
129
139
|
style,
|
|
130
140
|
...rest
|
|
@@ -157,17 +167,19 @@ export const AccordionItem = forwardRef<
|
|
|
157
167
|
return (
|
|
158
168
|
<MuiAccordion
|
|
159
169
|
ref={ref}
|
|
170
|
+
{...(sanitizedProps as unknown as Omit<
|
|
171
|
+
MuiAccordionProps,
|
|
172
|
+
"expanded" | "onChange" | "defaultExpanded" | "disabled"
|
|
173
|
+
>)}
|
|
160
174
|
className={finalClass}
|
|
161
175
|
disableGutters
|
|
162
176
|
elevation={0}
|
|
163
177
|
square
|
|
164
178
|
expanded={isExpanded}
|
|
165
179
|
onChange={handleChange}
|
|
180
|
+
disabled={disabled}
|
|
181
|
+
data-disabled={disabled || undefined}
|
|
166
182
|
style={mergedStyle}
|
|
167
|
-
{...(sanitizedProps as unknown as Omit<
|
|
168
|
-
MuiAccordionProps,
|
|
169
|
-
"expanded" | "onChange"
|
|
170
|
-
>)}
|
|
171
183
|
>
|
|
172
184
|
{
|
|
173
185
|
(title ? (
|
|
@@ -187,7 +199,9 @@ AccordionItem.displayName = "AccordionItem";
|
|
|
187
199
|
|
|
188
200
|
// ==== ACCORDION CONTROL ====
|
|
189
201
|
export type AccordionControlWrapperProps = RecursicaOverStyled<
|
|
190
|
-
|
|
202
|
+
// `expandIcon` is resolved internally from the container's `chevron` (via context) —
|
|
203
|
+
// omitted so a caller can't silently override it with MUI's native chevron slot.
|
|
204
|
+
Omit<MuiAccordionSummaryProps, "expandIcon"> & RecursicaAccordionControlProps
|
|
191
205
|
>;
|
|
192
206
|
|
|
193
207
|
export const AccordionControl = forwardRef<
|
|
@@ -210,9 +224,9 @@ export const AccordionControl = forwardRef<
|
|
|
210
224
|
return (
|
|
211
225
|
<MuiAccordionSummary
|
|
212
226
|
ref={ref}
|
|
227
|
+
{...(sanitizedProps as unknown as MuiAccordionSummaryProps)}
|
|
213
228
|
className={finalClass}
|
|
214
229
|
expandIcon={<span className={styles.chevron}>{resolvedChevron}</span>}
|
|
215
|
-
{...(sanitizedProps as unknown as MuiAccordionSummaryProps)}
|
|
216
230
|
>
|
|
217
231
|
{leftIcon && (
|
|
218
232
|
<span className={styles.iconLeftWrapper} aria-hidden>
|
|
@@ -226,8 +240,9 @@ export const AccordionControl = forwardRef<
|
|
|
226
240
|
AccordionControl.displayName = "AccordionControl";
|
|
227
241
|
|
|
228
242
|
// ==== ACCORDION PANEL ====
|
|
229
|
-
export type AccordionPanelWrapperProps =
|
|
230
|
-
|
|
243
|
+
export type AccordionPanelWrapperProps = RecursicaOverStyled<
|
|
244
|
+
MuiAccordionDetailsProps & RecursicaAccordionPanelProps
|
|
245
|
+
>;
|
|
231
246
|
|
|
232
247
|
export const AccordionPanel = forwardRef<
|
|
233
248
|
HTMLDivElement,
|
|
@@ -243,8 +258,8 @@ export const AccordionPanel = forwardRef<
|
|
|
243
258
|
return (
|
|
244
259
|
<MuiAccordionDetails
|
|
245
260
|
ref={ref}
|
|
246
|
-
className={finalClass}
|
|
247
261
|
{...(sanitizedProps as unknown as MuiAccordionDetailsProps)}
|
|
262
|
+
className={finalClass}
|
|
248
263
|
>
|
|
249
264
|
<div className={styles.content}>{children}</div>
|
|
250
265
|
</MuiAccordionDetails>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# AssistiveElement – implementation notes
|
|
2
|
+
|
|
3
|
+
Decisions and design tweaks specific to the UI Kit's AssistiveElement wrapped against
|
|
4
|
+
`@mui/material`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Forced `component="div"` trades away `FormHelperText`'s default semantics
|
|
9
|
+
|
|
10
|
+
**Decision:** `FormHelperText` renders a `<p>` by default — a reasonable semantic choice for
|
|
11
|
+
standalone helper/error text. This adapter forces `component="div"` instead, purely so the
|
|
12
|
+
icon + text can sit side by side in a flex row; `<p>` can't contain the flex layout the same
|
|
13
|
+
way without other tradeoffs.
|
|
14
|
+
**Implementation:** `component="div"` and `error={isError}` (derived from `assistiveVariant`)
|
|
15
|
+
are both computed internally and omitted from `AssistiveElementProps` (`Omit<FormHelperTextProps,
|
|
16
|
+
"error" | "component">`) so a caller can't silently override either via spread.
|
|
17
|
+
`aria-describedby`/`aria-errormessage` association with a field is a different story: this
|
|
18
|
+
component has no reference to any sibling field, so it can't wire that itself. It's already
|
|
19
|
+
handled one layer up, in `FormControlWrapper` — which generates the ids, passes them down via
|
|
20
|
+
this component's own `id` prop, and clones `aria-describedby`/`aria-errormessage` onto the
|
|
21
|
+
field. Only a fully standalone `<AssistiveElement>` used outside `FormControlWrapper` still
|
|
22
|
+
needs that wiring done by hand. `role="alert"` for the error variant is handled here instead
|
|
23
|
+
(see §4) — that piece genuinely belongs at this component's own level.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. CSS specificity tie against MUI's own `.Mui-error` color
|
|
28
|
+
|
|
29
|
+
**Decision:** MUI's `FormHelperText` sets its own error color via `.MuiFormHelperText-root.Mui-error`
|
|
30
|
+
(two classes) whenever `error` is set. Our own `.root[data-variant="error"]` (one class + one
|
|
31
|
+
attribute) lands at the _same_ specificity — without help, whichever rule loads later in the
|
|
32
|
+
merged stylesheet wins, which is import-order luck rather than a deterministic override.
|
|
33
|
+
**Implementation:** `!important` on that one rule's `color` guarantees ours wins regardless of
|
|
34
|
+
load order (same reasoning as the focus-ring override in the Accordion adapter). Not needed for
|
|
35
|
+
the `help` variant — MUI's unconditional base `color` on `.root` alone already has lower
|
|
36
|
+
specificity than our attribute-qualified rule there.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. Native passthrough capabilities, left unblocked and undocumented
|
|
41
|
+
|
|
42
|
+
**Decision:** `FormHelperText`'s own `disabled`/`filled`/`focused`/`margin`/`required`/`variant`/
|
|
43
|
+
`classes`/`sx` all pass straight through today (none of them are omitted, and none are
|
|
44
|
+
computed internally so there's nothing for them to silently override). They're also not added
|
|
45
|
+
to `RecursicaAssistiveElementProps` — same "accidental passthrough" class as Accordion's
|
|
46
|
+
`disabled` was before it got formalized. Known asymmetry: MUI callers can reach for these
|
|
47
|
+
today, Mantine callers can't (Mantine's `AssistiveElement` wraps no native component at all).
|
|
48
|
+
Revisit if a real use case shows up for any of them.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 4. `role="alert"` defaults on for the error variant
|
|
53
|
+
|
|
54
|
+
**Decision:** Error text needs to be announced by assistive tech as it appears or changes;
|
|
55
|
+
static help text doesn't. Rather than requiring every integrator to remember `role="alert"`,
|
|
56
|
+
`assistiveVariant="error"` defaults it automatically.
|
|
57
|
+
**Implementation:** `role` is destructured out and resolved as `role ?? (isError ? "alert" :
|
|
58
|
+
undefined)` before rendering — an explicit caller-supplied `role` always wins over the default,
|
|
59
|
+
and `"help"` gets no default at all.
|
|
@@ -101,10 +101,17 @@
|
|
|
101
101
|
VARIANTS: Error
|
|
102
102
|
========================================= */
|
|
103
103
|
|
|
104
|
+
/* MUI's own `FormHelperText` sets its error color via `.MuiFormHelperText-root.Mui-error`
|
|
105
|
+
(two classes) whenever `error` is set — the exact same specificity as our own
|
|
106
|
+
`.root[data-variant="error"]` (one class + one attribute), so whichever rule loads later
|
|
107
|
+
in the merged stylesheet would otherwise win. `!important` makes ours win deterministically
|
|
108
|
+
regardless of CSS import order (same reasoning as the focus-ring override elsewhere in this
|
|
109
|
+
codebase). Not needed for the `help` variant above — MUI's unconditional base `color` on
|
|
110
|
+
`.root` alone has lower specificity than ours there already. */
|
|
104
111
|
.root[data-variant="error"] {
|
|
105
112
|
color: var(
|
|
106
113
|
--recursica_ui-kit_components_assistive-element_variants_types_error_properties_colors_text-color
|
|
107
|
-
);
|
|
114
|
+
) !important;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
.root[data-variant="error"] .icon {
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { FormHelperText, type FormHelperTextProps } from "@mui/material";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
filterStylingProps,
|
|
5
|
+
type RecursicaOverStyled,
|
|
6
|
+
} from "../../utils/filterStylingProps";
|
|
4
7
|
import styles from "./AssistiveElement.module.css";
|
|
5
8
|
|
|
6
9
|
import { type RecursicaAssistiveElementProps } from "@recursica/adapter-common";
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// `error`/`component` are computed internally (from `assistiveVariant`, and forced to "div"
|
|
12
|
+
// for layout) — omitted so a caller can't silently override either via spread.
|
|
13
|
+
export type AssistiveElementProps = RecursicaOverStyled<
|
|
14
|
+
Omit<FormHelperTextProps, "error" | "component"> &
|
|
15
|
+
RecursicaAssistiveElementProps
|
|
16
|
+
>;
|
|
13
17
|
|
|
14
18
|
export const AssistiveElement = React.forwardRef<
|
|
15
19
|
HTMLParagraphElement,
|
|
@@ -21,6 +25,7 @@ export const AssistiveElement = React.forwardRef<
|
|
|
21
25
|
overStyled = false,
|
|
22
26
|
className,
|
|
23
27
|
children,
|
|
28
|
+
role,
|
|
24
29
|
...rest
|
|
25
30
|
} = props;
|
|
26
31
|
|
|
@@ -61,15 +66,20 @@ export const AssistiveElement = React.forwardRef<
|
|
|
61
66
|
// Map to MUI's native error prop if it's explicitly an error state
|
|
62
67
|
const isError = assistiveVariant === "error";
|
|
63
68
|
|
|
69
|
+
// Announce error text as it appears/changes; a caller-supplied `role` always wins. No
|
|
70
|
+
// default for `help` — static descriptive text doesn't need a live region.
|
|
71
|
+
const resolvedRole = role ?? (isError ? "alert" : undefined);
|
|
72
|
+
|
|
64
73
|
// Force component = "div" so flex layouts execute correctly inside HelperText
|
|
65
74
|
return (
|
|
66
75
|
<FormHelperText
|
|
67
76
|
ref={ref}
|
|
77
|
+
{...(sanitizedProps as Omit<FormHelperTextProps, "error" | "component">)}
|
|
68
78
|
component="div"
|
|
69
79
|
error={isError}
|
|
80
|
+
role={resolvedRole}
|
|
70
81
|
data-variant={assistiveVariant}
|
|
71
82
|
className={className ? `${styles.root} ${className}` : styles.root}
|
|
72
|
-
{...(sanitizedProps as FormHelperTextProps)}
|
|
73
83
|
>
|
|
74
84
|
{assistiveWithIcon && (
|
|
75
85
|
<div className={styles.icon}>
|
|
@@ -134,7 +134,7 @@ export const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(
|
|
|
134
134
|
label={label}
|
|
135
135
|
assistiveText={assistiveText}
|
|
136
136
|
assistiveWithIcon={assistiveWithIcon}
|
|
137
|
-
error={
|
|
137
|
+
error={error}
|
|
138
138
|
required={required}
|
|
139
139
|
id={id}
|
|
140
140
|
disabled={disabled}
|
|
@@ -165,13 +165,29 @@ export const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(
|
|
|
165
165
|
}}
|
|
166
166
|
options={data || []}
|
|
167
167
|
renderInput={(params) => {
|
|
168
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
169
168
|
const { InputProps, ...restParams } = params;
|
|
170
169
|
return (
|
|
171
170
|
<MuiTextField
|
|
172
171
|
{...restParams}
|
|
173
172
|
placeholder={placeholder}
|
|
174
173
|
variant="standard"
|
|
174
|
+
InputProps={{
|
|
175
|
+
...InputProps,
|
|
176
|
+
startAdornment: leftSection ? (
|
|
177
|
+
<span className={styles.section} data-position="left">
|
|
178
|
+
{leftSection}
|
|
179
|
+
</span>
|
|
180
|
+
) : (
|
|
181
|
+
InputProps.startAdornment
|
|
182
|
+
),
|
|
183
|
+
endAdornment: rightSection ? (
|
|
184
|
+
<span className={styles.section} data-position="right">
|
|
185
|
+
{rightSection}
|
|
186
|
+
</span>
|
|
187
|
+
) : (
|
|
188
|
+
InputProps.endAdornment
|
|
189
|
+
),
|
|
190
|
+
}}
|
|
175
191
|
/>
|
|
176
192
|
);
|
|
177
193
|
}}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Avatar Component Implementation Notes
|
|
2
|
+
|
|
3
|
+
## Architecture Decisions
|
|
4
|
+
|
|
5
|
+
The `Avatar` component is an adapter over MUI's `Avatar`.
|
|
6
|
+
|
|
7
|
+
- We do not wrap `MuiAvatar` in any custom standard `div` elements, preserving DOM structure.
|
|
8
|
+
- All styles strictly pull from explicit `--recursica_ui-kit_components_avatar_*` CSS tokens.
|
|
9
|
+
|
|
10
|
+
## `data-style`/`data-variant`, not MUI's own classes
|
|
11
|
+
|
|
12
|
+
Same reasoning as the Mantine adapter: `data-style="image|icon|text"` is set based on which of
|
|
13
|
+
`src`/`icon`/`children` is present, and `data-variant` carries Recursica's own `variant` value
|
|
14
|
+
(`"solid"|"outline"|"ghost"`) — `Avatar.module.css` keys every visual treatment (background,
|
|
15
|
+
border, border-radius) off these two attributes, not off anything MUI's own classes do.
|
|
16
|
+
|
|
17
|
+
## `variant` — a name collision with a different meaning, not a shared concept
|
|
18
|
+
|
|
19
|
+
**Found while auditing this component (not in Forge's original report):** MUI's native
|
|
20
|
+
`Avatar.variant` means **shape** (`'circular'|'rounded'|'square'`) — unlike almost every other
|
|
21
|
+
MUI component (including Mantine's `Avatar.variant`, which really is the same color-treatment
|
|
22
|
+
concept Recursica's `variant` is), MUI repurposes this name for something else entirely.
|
|
23
|
+
|
|
24
|
+
The previous implementation mapped Recursica's `variant` (`solid|outline|ghost`) to strings
|
|
25
|
+
(`"filled"|"outline"|"transparent"`) and fed them into MUI's native `variant` prop via an
|
|
26
|
+
`as unknown as MuiAvatarProps["variant"]` cast — none of those strings are valid MUI shape
|
|
27
|
+
values, so it was silently a no-op on MUI's own shape class (visually masked only because
|
|
28
|
+
`Avatar.module.css` already forces border-radius per `data-style` unconditionally, regardless
|
|
29
|
+
of what shape class MUI resolves to).
|
|
30
|
+
|
|
31
|
+
**Fix:** stopped feeding Recursica's `variant` into MUI's native `variant` prop at all.
|
|
32
|
+
`<MuiAvatar>` is now given an explicit, real, valid `variant="circular"` — Recursica has no
|
|
33
|
+
shape concept of its own to expose here, and avatars are always circular by design, so this is
|
|
34
|
+
just being explicit about that instead of relying on MUI's own default. `Omit<MuiAvatarProps,
|
|
35
|
+
"variant">` in the type prevents a caller from reaching MUI's real shape prop under Recursica's
|
|
36
|
+
`variant` name and getting confused about what it does.
|
|
@@ -11,9 +11,12 @@ import styles from "./Avatar.module.css";
|
|
|
11
11
|
|
|
12
12
|
import { type RecursicaAvatarProps } from "@recursica/adapter-common";
|
|
13
13
|
|
|
14
|
+
// MUI's native `variant` is Avatar's *shape* ('circular'|'rounded'|'square'), not a color
|
|
15
|
+
// treatment like every other MUI component's `variant` — unlike Mantine's, where `variant`
|
|
16
|
+
// really is the same color-treatment concept ours is. Omitted so Recursica's own `variant`
|
|
17
|
+
// (a completely different concept) can't be confused with it; see AVATAR_IMPLEMENTATION_NOTES.md.
|
|
14
18
|
export type AvatarProps = RecursicaOverStyled<
|
|
15
|
-
Omit<MuiAvatarProps, "variant"
|
|
16
|
-
RecursicaAvatarProps
|
|
19
|
+
Omit<MuiAvatarProps, "variant"> & RecursicaAvatarProps
|
|
17
20
|
>;
|
|
18
21
|
|
|
19
22
|
const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
|
|
@@ -28,12 +31,6 @@ const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
|
|
|
28
31
|
},
|
|
29
32
|
ref,
|
|
30
33
|
) {
|
|
31
|
-
const mapVariant = {
|
|
32
|
-
solid: "filled",
|
|
33
|
-
outline: "outline",
|
|
34
|
-
ghost: "transparent",
|
|
35
|
-
} as const;
|
|
36
|
-
|
|
37
34
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
38
35
|
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
39
36
|
|
|
@@ -74,7 +71,7 @@ const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
|
|
|
74
71
|
ref={ref}
|
|
75
72
|
className={classNameProp}
|
|
76
73
|
classes={mergedClassNames}
|
|
77
|
-
variant=
|
|
74
|
+
variant="circular"
|
|
78
75
|
src={src}
|
|
79
76
|
data-variant={variant}
|
|
80
77
|
data-size={size}
|
|
@@ -113,15 +113,3 @@
|
|
|
113
113
|
--recursica_ui-kit_components_badge_variants_styles_warning_properties_colors_text-color
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
/* Target Mantine internal children if needed for typography constraints */
|
|
118
|
-
.root :global(.mantine-Badge-label),
|
|
119
|
-
.root :global(.mantine-Badge-section) {
|
|
120
|
-
/* HARDCODE: Prevent Mantine from overriding our typography setup inside inner wrappers */
|
|
121
|
-
font-size: inherit;
|
|
122
|
-
font-weight: inherit;
|
|
123
|
-
line-height: inherit;
|
|
124
|
-
font-family: inherit;
|
|
125
|
-
letter-spacing: inherit;
|
|
126
|
-
text-transform: inherit;
|
|
127
|
-
}
|
|
@@ -26,8 +26,6 @@ const _Badge = forwardRef<HTMLDivElement, BadgeProps>(function Badge(
|
|
|
26
26
|
|
|
27
27
|
const mergedClassNames: Partial<Record<string, string>> = {
|
|
28
28
|
root: styles.root,
|
|
29
|
-
section: styles.section,
|
|
30
|
-
label: styles.label,
|
|
31
29
|
};
|
|
32
30
|
|
|
33
31
|
const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
|
|
@@ -38,8 +36,6 @@ const _Badge = forwardRef<HTMLDivElement, BadgeProps>(function Badge(
|
|
|
38
36
|
) {
|
|
39
37
|
const o = classNamesProp as Partial<Record<string, string>>;
|
|
40
38
|
mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
|
|
41
|
-
mergedClassNames.section = o.section ?? styles.section;
|
|
42
|
-
mergedClassNames.label = o.label ?? styles.label;
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
const classNameProp = (sanitizedProps as Record<string, unknown>)
|
|
@@ -77,9 +77,9 @@ const CardBase = forwardRef<HTMLDivElement, CardProps>(function Card(
|
|
|
77
77
|
return (
|
|
78
78
|
<MuiCard
|
|
79
79
|
ref={ref}
|
|
80
|
+
{...(sanitizedProps as unknown as MuiCardProps)}
|
|
80
81
|
className={classNameProp}
|
|
81
82
|
classes={mergedClassNames}
|
|
82
|
-
{...(sanitizedProps as unknown as MuiCardProps)}
|
|
83
83
|
/>
|
|
84
84
|
);
|
|
85
85
|
});
|
|
@@ -100,10 +100,10 @@ export const CardSection = forwardRef<HTMLDivElement, CardSectionProps>(
|
|
|
100
100
|
return (
|
|
101
101
|
<div
|
|
102
102
|
ref={ref}
|
|
103
|
+
{...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
|
|
103
104
|
className={
|
|
104
105
|
classNameProp ? `${styles.section} ${classNameProp}` : styles.section
|
|
105
106
|
}
|
|
106
|
-
{...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
|
|
107
107
|
/>
|
|
108
108
|
);
|
|
109
109
|
},
|
|
@@ -125,10 +125,10 @@ export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
|
|
125
125
|
return (
|
|
126
126
|
<div
|
|
127
127
|
ref={ref}
|
|
128
|
+
{...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
|
|
128
129
|
className={
|
|
129
130
|
classNameProp ? `${styles.header} ${classNameProp}` : styles.header
|
|
130
131
|
}
|
|
131
|
-
{...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
|
|
132
132
|
/>
|
|
133
133
|
);
|
|
134
134
|
},
|
|
@@ -150,10 +150,10 @@ export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
|
|
|
150
150
|
return (
|
|
151
151
|
<div
|
|
152
152
|
ref={ref}
|
|
153
|
+
{...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
|
|
153
154
|
className={
|
|
154
155
|
classNameProp ? `${styles.footer} ${classNameProp}` : styles.footer
|
|
155
156
|
}
|
|
156
|
-
{...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
|
|
157
157
|
/>
|
|
158
158
|
);
|
|
159
159
|
},
|
|
@@ -177,10 +177,10 @@ export const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
|
|
|
177
177
|
return (
|
|
178
178
|
<div
|
|
179
179
|
ref={ref}
|
|
180
|
+
{...sanitizedProps}
|
|
180
181
|
className={
|
|
181
182
|
classNameProp ? `${styles.content} ${classNameProp}` : styles.content
|
|
182
183
|
}
|
|
183
|
-
{...sanitizedProps}
|
|
184
184
|
/>
|
|
185
185
|
);
|
|
186
186
|
},
|