@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +68 -44
  3. package/dist/mui-adapter.cjs +54 -54
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +5600 -5600
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/components/Accordion/Accordion.tsx +6 -1
  10. package/src/components/Checkbox/CheckboxGroup.tsx +5 -3
  11. package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +3 -3
  12. package/src/components/Chip/Chip.module.css +5 -5
  13. package/src/components/Chip/Chip.stories.tsx +2 -2
  14. package/src/components/Chip/Chip.tsx +20 -19
  15. package/src/components/Chip/USAGE.md +4 -4
  16. package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +5 -3
  17. package/src/components/FileInput/FileInput.stories.tsx +3 -0
  18. package/src/components/FileInput/FileInput.tsx +11 -10
  19. package/src/components/FileInput/USAGE.md +1 -0
  20. package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +6 -6
  21. package/src/components/FileUpload/FileUpload.tsx +9 -9
  22. package/src/components/Radio/RadioGroup.stories.tsx +4 -4
  23. package/src/components/Radio/RadioGroup.tsx +1 -12
  24. package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +8 -0
  25. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +17 -28
  26. package/src/components/SegmentedControl/SegmentedControl.tsx +15 -17
  27. package/src/components/SegmentedControl/USAGE.md +13 -0
  28. package/src/components/Slider/IMPLEMENTATION_NOTES.md +10 -0
  29. package/src/components/Slider/Slider.stories.tsx +44 -0
  30. package/src/components/Slider/Slider.tsx +23 -4
  31. package/src/components/Slider/USAGE.md +6 -0
  32. 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
- // Props this component intentionally doesn't support — deleted at runtime so they can't leak
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
- _event: React.MouseEvent<HTMLElement>,
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
- const itemDisabled = typeof item === "string" ? false : item.disabled;
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}>{itemLabel}</div>
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, an optional leading icon, and an explicit read-only layout.
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}>{resolvedValue}</span>
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