@recursica/mui-adapter 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/index.d.ts +68 -44
- package/dist/mui-adapter.cjs +54 -54
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +5600 -5600
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.tsx +6 -1
- package/src/components/Checkbox/CheckboxGroup.tsx +5 -3
- package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +3 -3
- package/src/components/Chip/Chip.module.css +5 -5
- package/src/components/Chip/Chip.stories.tsx +2 -2
- package/src/components/Chip/Chip.tsx +20 -19
- package/src/components/Chip/USAGE.md +4 -4
- package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +5 -3
- package/src/components/FileInput/FileInput.stories.tsx +3 -0
- package/src/components/FileInput/FileInput.tsx +11 -10
- package/src/components/FileInput/USAGE.md +1 -0
- package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +6 -6
- package/src/components/FileUpload/FileUpload.tsx +9 -9
- package/src/components/Radio/RadioGroup.stories.tsx +4 -4
- package/src/components/Radio/RadioGroup.tsx +1 -12
- package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +8 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +17 -28
- package/src/components/SegmentedControl/SegmentedControl.tsx +15 -17
- package/src/components/SegmentedControl/USAGE.md +13 -0
- package/src/components/Slider/IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Slider/Slider.stories.tsx +44 -0
- package/src/components/Slider/Slider.tsx +23 -4
- package/src/components/Slider/USAGE.md +6 -0
- package/src/components/Switch/SwitchGroup.tsx +5 -1
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
} from "@mui/material";
|
|
7
7
|
import {
|
|
8
8
|
filterStylingProps,
|
|
9
|
-
omitUnsupportedProps,
|
|
10
9
|
mergeClassNames,
|
|
11
10
|
type RecursicaOverStyled,
|
|
12
11
|
} from "../../utils/filterStylingProps";
|
|
@@ -23,9 +22,7 @@ export type SegmentedControlProps = RecursicaOverStyled<
|
|
|
23
22
|
| "color"
|
|
24
23
|
| "classNames"
|
|
25
24
|
| "className"
|
|
26
|
-
| "disabled"
|
|
27
25
|
| "value"
|
|
28
|
-
| "onChange"
|
|
29
26
|
> & {
|
|
30
27
|
className?: string;
|
|
31
28
|
classNames?: Partial<Record<string, string>>;
|
|
@@ -61,21 +58,13 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
61
58
|
fullWidth,
|
|
62
59
|
data = [],
|
|
63
60
|
value,
|
|
61
|
+
disabled,
|
|
64
62
|
onChange,
|
|
65
63
|
...rest
|
|
66
64
|
},
|
|
67
65
|
ref,
|
|
68
66
|
) {
|
|
69
|
-
|
|
70
|
-
// through even if a caller forces them via plain JavaScript, bypassing the Omit<> above.
|
|
71
|
-
const UNSUPPORTED_PROPS = [
|
|
72
|
-
"disabled", // Recursica controls per-item disabled state via `data.disabled`, not the group
|
|
73
|
-
] as const satisfies readonly (keyof MuiSegmentedControlProps)[];
|
|
74
|
-
|
|
75
|
-
const sanitizedProps = omitUnsupportedProps(
|
|
76
|
-
filterStylingProps(rest, overStyled),
|
|
77
|
-
UNSUPPORTED_PROPS,
|
|
78
|
-
);
|
|
67
|
+
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
79
68
|
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
80
69
|
|
|
81
70
|
const stylingParams = useSegmentedControlClassNames(restRecord);
|
|
@@ -94,12 +83,12 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
94
83
|
const activeValue = value !== undefined ? value : uncontrolledValue;
|
|
95
84
|
|
|
96
85
|
const handleChange = (
|
|
97
|
-
|
|
86
|
+
event: React.MouseEvent<HTMLElement>,
|
|
98
87
|
newValue: string | null,
|
|
99
88
|
) => {
|
|
100
89
|
if (newValue !== null) {
|
|
101
90
|
setUncontrolledValue(newValue);
|
|
102
|
-
onChange?.(newValue);
|
|
91
|
+
onChange?.(event, newValue);
|
|
103
92
|
}
|
|
104
93
|
};
|
|
105
94
|
|
|
@@ -116,6 +105,7 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
116
105
|
fullWidth={fullWidth}
|
|
117
106
|
data-orientation={orientation}
|
|
118
107
|
exclusive
|
|
108
|
+
disabled={disabled}
|
|
119
109
|
value={activeValue}
|
|
120
110
|
onChange={
|
|
121
111
|
handleChange as React.ComponentProps<
|
|
@@ -126,7 +116,12 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
126
116
|
{data.map((item) => {
|
|
127
117
|
const itemValue = typeof item === "string" ? item : item.value;
|
|
128
118
|
const itemLabel = typeof item === "string" ? item : item.label;
|
|
129
|
-
|
|
119
|
+
// `undefined` (not `false`) for items with no per-item override, so the group-level
|
|
120
|
+
// `disabled` above can cascade through MUI's ToggleButtonGroupContext instead of being
|
|
121
|
+
// masked by an explicit `false` on every ToggleButton.
|
|
122
|
+
const itemDisabled =
|
|
123
|
+
typeof item === "string" ? undefined : item.disabled;
|
|
124
|
+
const itemIcon = typeof item === "string" ? undefined : item.icon;
|
|
130
125
|
|
|
131
126
|
return (
|
|
132
127
|
<ToggleButton
|
|
@@ -135,7 +130,10 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
135
130
|
disabled={itemDisabled}
|
|
136
131
|
className={stylingParams.classNames.control}
|
|
137
132
|
>
|
|
138
|
-
<div className={stylingParams.classNames.label}>
|
|
133
|
+
<div className={stylingParams.classNames.label}>
|
|
134
|
+
{itemIcon}
|
|
135
|
+
{itemLabel}
|
|
136
|
+
</div>
|
|
139
137
|
</ToggleButton>
|
|
140
138
|
);
|
|
141
139
|
})}
|
|
@@ -23,6 +23,17 @@ export default function Demo() {
|
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Each `data` item may also be an object with an optional `icon`, rendered ahead of the label:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
<SegmentedControl
|
|
30
|
+
data={[
|
|
31
|
+
{ value: "daily", label: "Daily", icon: <CheckIcon /> },
|
|
32
|
+
{ value: "weekly", label: "Weekly" },
|
|
33
|
+
]}
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
26
37
|
---
|
|
27
38
|
|
|
28
39
|
## 3. Design System Integration
|
|
@@ -34,3 +45,5 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
|
|
|
34
45
|
> - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
35
46
|
> - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
|
|
36
47
|
> - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
|
|
48
|
+
|
|
49
|
+
A top-level `disabled` disables every item at once; an individual item can still be disabled on its own via `data[].disabled`.
|
|
@@ -27,3 +27,13 @@ This document contains specific design decisions, architectural constraints, and
|
|
|
27
27
|
## 4. Mark Label Color
|
|
28
28
|
|
|
29
29
|
**Root cause:** MUI's `.sliderMarkLabel` already inherited the container text color using the min-max-label typography tokens. Mantine's equivalent class (`styles.sliderMarkLabel`) was referenced in `Slider.tsx`'s `classNames` map but was never defined in Mantine's `Slider.module.css`, so Mantine silently fell back to its own default theme grey instead of any recursica token. Fixed in `mantine-adapter` by adding the missing `.sliderMarkLabel` rule (same tokens/inherit-color approach as MUI) rather than copying Mantine's undefined behavior into MUI.
|
|
30
|
+
|
|
31
|
+
## 5. Formatted Current Value, Label Overrides, Trailing Icon
|
|
32
|
+
|
|
33
|
+
**Symptom:** `.currentValue` always rendered the raw numeric value, even when `tooltipLabel` was a formatter function (already used for MUI's own `valueLabelFormat`) — a caller mapping values onto custom text got the formatted tooltip while dragging but the raw number next to the track otherwise. Same bug as `mantine-adapter`.
|
|
34
|
+
|
|
35
|
+
**Fix:** `.currentValue` now runs `resolvedValue` through `tooltipLabel` when it's a function, reusing the same formatter passed to `valueLabelFormat`. Added `minLabel`/`maxLabel` (new `adapter-common` props) to override the `.minMaxGuide` text at either end of the track, and `trailingIcon` (new `adapter-common` prop) to render a second icon opposite the existing `icon`, reusing the same `.iconWrapper` styling.
|
|
36
|
+
|
|
37
|
+
## 6. No Dual-Thumb / Range Support
|
|
38
|
+
|
|
39
|
+
**Decision:** Requested (MUI's `Slider` already accepts `number[]` for `value`/`onChange` and renders multiple thumbs natively), declined — no current use case needs it. `Slider` stays single-thumb only; `value`/`onChange` remain typed as `number` and arrays continue to be collapsed to `value[0]`.
|
|
@@ -146,6 +146,50 @@ export const WithMarks: Story = {
|
|
|
146
146
|
},
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
+
export const WithIconsAndLabels: Story = {
|
|
150
|
+
args: {
|
|
151
|
+
label: "Volume",
|
|
152
|
+
assistiveText:
|
|
153
|
+
"Icons flank the track; min/max labels replace the raw bounds.",
|
|
154
|
+
defaultValue: 60,
|
|
155
|
+
minLabel: "Quiet",
|
|
156
|
+
maxLabel: "Loud",
|
|
157
|
+
tooltipLabel: (value: number) => `${value}%`,
|
|
158
|
+
icon: (
|
|
159
|
+
<svg
|
|
160
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
161
|
+
width="16"
|
|
162
|
+
height="16"
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
fill="none"
|
|
165
|
+
stroke="currentColor"
|
|
166
|
+
strokeWidth="2"
|
|
167
|
+
strokeLinecap="round"
|
|
168
|
+
strokeLinejoin="round"
|
|
169
|
+
>
|
|
170
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
|
171
|
+
</svg>
|
|
172
|
+
),
|
|
173
|
+
trailingIcon: (
|
|
174
|
+
<svg
|
|
175
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
176
|
+
width="16"
|
|
177
|
+
height="16"
|
|
178
|
+
viewBox="0 0 24 24"
|
|
179
|
+
fill="none"
|
|
180
|
+
stroke="currentColor"
|
|
181
|
+
strokeWidth="2"
|
|
182
|
+
strokeLinecap="round"
|
|
183
|
+
strokeLinejoin="round"
|
|
184
|
+
>
|
|
185
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
|
186
|
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
|
187
|
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
|
|
188
|
+
</svg>
|
|
189
|
+
),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
149
193
|
export const FormLayouts: Story = {
|
|
150
194
|
render: () => (
|
|
151
195
|
<div
|
|
@@ -48,7 +48,8 @@ const SliderReadOnlyValue: React.FC<{ value: number }> = ({ value }) => {
|
|
|
48
48
|
* Recursica Slider component wrapping Mui's Slider.
|
|
49
49
|
*
|
|
50
50
|
* Implements a bidirectional text input field next to the slider track, responsive layouts,
|
|
51
|
-
* custom typography-bound min/max labels
|
|
51
|
+
* custom typography-bound min/max labels (optionally overridden via `minLabel`/`maxLabel`),
|
|
52
|
+
* optional leading/trailing icons, and an explicit read-only layout.
|
|
52
53
|
*/
|
|
53
54
|
export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
54
55
|
function Slider(props, ref) {
|
|
@@ -82,10 +83,13 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
82
83
|
value,
|
|
83
84
|
defaultValue,
|
|
84
85
|
icon,
|
|
86
|
+
trailingIcon,
|
|
85
87
|
showInput = false,
|
|
86
88
|
showMinMaxLabels = true,
|
|
87
89
|
min = 0,
|
|
88
90
|
max = 100,
|
|
91
|
+
minLabel,
|
|
92
|
+
maxLabel,
|
|
89
93
|
step = 1,
|
|
90
94
|
onChange,
|
|
91
95
|
onChangeEnd,
|
|
@@ -220,6 +224,19 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
220
224
|
</span>
|
|
221
225
|
) : null;
|
|
222
226
|
|
|
227
|
+
const trailingIconEl = trailingIcon ? (
|
|
228
|
+
<span className={styles.iconWrapper} aria-hidden>
|
|
229
|
+
{trailingIcon}
|
|
230
|
+
</span>
|
|
231
|
+
) : null;
|
|
232
|
+
|
|
233
|
+
// Duplicates the raw numeric value next to the track by default; when `tooltipLabel` is a
|
|
234
|
+
// formatter, reuse it here too so both displays agree instead of one showing raw numbers.
|
|
235
|
+
const displayValue =
|
|
236
|
+
typeof tooltipLabel === "function"
|
|
237
|
+
? tooltipLabel(resolvedValue)
|
|
238
|
+
: resolvedValue;
|
|
239
|
+
|
|
223
240
|
return (
|
|
224
241
|
<WithReadOnlyWrapper
|
|
225
242
|
ref={ref}
|
|
@@ -258,7 +275,7 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
258
275
|
{leadingIcon}
|
|
259
276
|
|
|
260
277
|
{showMinMaxLabels && (
|
|
261
|
-
<span className={styles.minMaxGuide}>{min}</span>
|
|
278
|
+
<span className={styles.minMaxGuide}>{minLabel ?? min}</span>
|
|
262
279
|
)}
|
|
263
280
|
|
|
264
281
|
<div className={styles.sliderTrackWrapper}>
|
|
@@ -295,10 +312,10 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
295
312
|
|
|
296
313
|
<div className={styles.rightGuideContainer}>
|
|
297
314
|
{!showInput && (
|
|
298
|
-
<span className={styles.currentValue}>{
|
|
315
|
+
<span className={styles.currentValue}>{displayValue}</span>
|
|
299
316
|
)}
|
|
300
317
|
{showMinMaxLabels && (
|
|
301
|
-
<span className={styles.minMaxGuide}>{max}</span>
|
|
318
|
+
<span className={styles.minMaxGuide}>{maxLabel ?? max}</span>
|
|
302
319
|
)}
|
|
303
320
|
</div>
|
|
304
321
|
|
|
@@ -316,6 +333,8 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
316
333
|
data-error={error ? "true" : undefined}
|
|
317
334
|
/>
|
|
318
335
|
)}
|
|
336
|
+
|
|
337
|
+
{trailingIconEl}
|
|
319
338
|
</div>
|
|
320
339
|
}
|
|
321
340
|
/>
|
|
@@ -34,3 +34,9 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
|
|
|
34
34
|
> - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
35
35
|
> - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
|
|
36
36
|
> - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 4. Key Integration Features & Constraints
|
|
41
|
+
|
|
42
|
+
The `label` prop is passed through to the surrounding form label rather than MUI's dragging tooltip; use `tooltipLabel` to set the label shown while dragging. When `showInput` is enabled, a numeric text input is rendered alongside the track and stays in sync with the slider's value. Set `showMinMaxLabels` to `false` to hide the min/max guides shown at either end of the track. Otherwise, the current value is displayed near the track instead — pass `tooltipLabel` as a formatter function (`(value) => ReactNode`) and that same formatter is reused for this display, instead of always showing the raw number. `minLabel`/`maxLabel` override the text shown at either end of the track (defaults to the numeric `min`/`max`). `icon` renders a leading icon next to the track; `trailingIcon` renders one on the opposite side.
|
|
@@ -43,7 +43,11 @@ export interface RecursicaSwitchGroupProps
|
|
|
43
43
|
| "defaultValue"
|
|
44
44
|
>,
|
|
45
45
|
ReadOnlyControlProps,
|
|
46
|
-
BaseRecursicaSwitchGroupProps {
|
|
46
|
+
BaseRecursicaSwitchGroupProps {
|
|
47
|
+
// MUI has no native switch-group concept to match (its own `FormGroup` is layout-only, no
|
|
48
|
+
// value/onChange) — this signature is Recursica's own, same as TransferList/Accordion/CheckboxGroup.
|
|
49
|
+
onChange?: (value: string[]) => void;
|
|
50
|
+
}
|
|
47
51
|
|
|
48
52
|
export type SwitchGroupProps = RecursicaOverStyled<RecursicaSwitchGroupProps>;
|
|
49
53
|
|