@recursica/mui-adapter 0.24.0 → 0.25.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 +26 -0
- package/dist/index.d.ts +166 -9
- package/dist/mui-adapter.cjs +69 -69
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +8868 -8066
- package/dist/mui-adapter.js.map +1 -1
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/components/AssistiveElement/AssistiveElement.tsx +1 -1
- package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Button/Button.module.css +6 -5
- package/src/components/Checkbox/Checkbox.tsx +4 -1
- package/src/components/FileInput/FileInput.tsx +6 -0
- package/src/components/Popover/IMPLEMENTATION_NOTES.md +22 -0
- package/src/components/Popover/Popover.module.css +123 -0
- package/src/components/Popover/Popover.stories.tsx +133 -0
- package/src/components/Popover/Popover.tsx +275 -0
- package/src/components/Popover/USAGE.md +69 -0
- package/src/components/Popover/index.ts +1 -0
- package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +6 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +33 -4
- package/src/components/SegmentedControl/SegmentedControl.tsx +18 -4
- package/src/components/Slider/IMPLEMENTATION_NOTES.md +29 -0
- package/src/components/Slider/Slider.module.css +80 -17
- package/src/components/Slider/Slider.stories.tsx +1 -1
- package/src/components/Slider/Slider.tsx +36 -1
- package/src/components/Stepper/IMPLEMENTATION_NOTES.md +54 -0
- package/src/components/Stepper/Stepper.module.css +139 -106
- package/src/components/Stepper/Stepper.tsx +76 -10
- package/src/components/Stepper/USAGE.md +4 -0
- package/src/components/Tabs/IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Tabs/Tabs.module.css +107 -24
- package/src/components/Tabs/Tabs.tsx +1 -0
- package/src/components/TextArea/TextArea.module.css +20 -4
- package/src/components/TextArea/TextArea.tsx +12 -23
- package/src/components/Timeline/IMPLEMENTATION_NOTES.md +24 -3
- package/src/components/Timeline/Timeline.module.css +56 -68
- package/src/components/Timeline/Timeline.tsx +23 -27
- package/src/components/Timeline/TimelineItem.tsx +38 -35
- package/src/components/TransferList/TRANSFERLIST_IMPLEMENTATION_NOTES.md +140 -0
- package/src/components/TransferList/TransferList.module.css +179 -35
- package/src/components/TransferList/TransferList.stories.tsx +110 -6
- package/src/components/TransferList/TransferList.tsx +417 -8
- package/src/components/TransferList/USAGE.md +37 -6
- package/src/components/index.ts +1 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Popover - Usage Guide
|
|
2
|
+
|
|
3
|
+
This document describes how to integrate and use the `Popover` component in your projects using `@recursica/mui-adapter`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Import Reference
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Popover } from "@recursica/mui-adapter";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. Basic Example
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import React from "react";
|
|
19
|
+
import { Popover, Button, Text } from "@recursica/mui-adapter";
|
|
20
|
+
|
|
21
|
+
export default function Demo() {
|
|
22
|
+
return (
|
|
23
|
+
<Popover position="bottom" withBeak>
|
|
24
|
+
<Popover.Target>
|
|
25
|
+
<Button>Open Popover</Button>
|
|
26
|
+
</Popover.Target>
|
|
27
|
+
<Popover.Dropdown>
|
|
28
|
+
<Text size="rec-sm">This is the popover content.</Text>
|
|
29
|
+
</Popover.Dropdown>
|
|
30
|
+
</Popover>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. Design System Integration
|
|
38
|
+
|
|
39
|
+
All Recursica components in the `@recursica/mui-adapter` package adhere strictly to design system spacing, scaling, and behavior patterns.
|
|
40
|
+
|
|
41
|
+
> [!IMPORTANT]
|
|
42
|
+
>
|
|
43
|
+
> - **Anti-override protection**: Rogue style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
44
|
+
> - **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.
|
|
45
|
+
> - **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.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 4. Key Integration Features & Constraints
|
|
50
|
+
|
|
51
|
+
### Composition
|
|
52
|
+
|
|
53
|
+
`Popover`, `Popover.Target`, and `Popover.Dropdown` are used together: `Popover.Target` wraps the trigger element and applies no styling of its own, while `Popover.Dropdown` renders the styled panel content. Both `Popover.Target` and `Popover.Dropdown` are required — omitting either throws.
|
|
54
|
+
|
|
55
|
+
### Open/close behavior
|
|
56
|
+
|
|
57
|
+
The dropdown opens when the user clicks the target and closes on an outside click, on Escape, or by clicking the target again. Use `opened`/`onChange` for controlled usage, or `defaultOpened` to set the initial uncontrolled state.
|
|
58
|
+
|
|
59
|
+
### Beak (Arrow)
|
|
60
|
+
|
|
61
|
+
The Recursica prop `withBeak` (defaulting to `true`) controls whether the pointer beak is shown.
|
|
62
|
+
|
|
63
|
+
### Position
|
|
64
|
+
|
|
65
|
+
`position` accepts the same 12 placement values as `HoverCard`/`Tooltip` (e.g. `"top"`, `"bottom-start"`, `"right-end"`) and defaults to `"top"`.
|
|
66
|
+
|
|
67
|
+
### Width
|
|
68
|
+
|
|
69
|
+
An optional `width` prop sets a fixed width on the dropdown panel.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Popover";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# SegmentedControl Implementation Notes
|
|
2
|
+
|
|
3
|
+
## Labels rendering uppercase (2026-08-19)
|
|
4
|
+
|
|
5
|
+
- **Root cause:** `--recursica_ui-kit_components_segmented-control-item_variants_selection-states_{unselected,selected}_properties_text_text-transform` resolves to `--recursica_tokens_font_cases_original`, which has no definition in `recursica_variables_scoped.css` (only `_lowercase`/`_titlecase`/`_uppercase` are defined there). The resulting `var()` on `.label` is invalid, and since `text-transform` is an inherited property, the invalid value falls back to the inherited value from `.control` (`.MuiToggleButton-root`) — which carries MUI's own `text-transform: uppercase` button default. Mantine's control has no such native uppercase default, so the same broken token never surfaced there.
|
|
6
|
+
- **Fix:** Reset `text-transform: none` on `.root .control` alongside the other MUI ToggleButton baseline resets (padding/border/etc.) already there, so nothing uppercase is left to inherit. Matches the existing `text-transform: none` MUI-baseline reset pattern in `Button.module.css`. Not a design-token value — it's a structural reset of MUI's own default, same category as the other hardcoded resets already exempted at the top of this file.
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
- border-style: solid; on container and indicator
|
|
3
3
|
- background-color: transparent; on label hover (overriding Mantine)
|
|
4
4
|
- Scope prefix .root to enforce Figma tokens over Mantine's inline calculation without using !important
|
|
5
|
+
- padding/border/border-radius/min-height/min-width: 0 and background-color: transparent on
|
|
6
|
+
.control (MUI's ToggleButton root) so its own baseline button box model does not stack on
|
|
7
|
+
top of .label's token-driven height/border/radius, mirroring Mantine's transparent .control wrapper
|
|
8
|
+
- text-transform: none on .control resets MUI's ToggleButton uppercase default (see comment
|
|
9
|
+
above that rule); needed because the item text-transform token has no valid scoped value
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
12
|
/* EXEMPTIONS:
|
|
8
13
|
- segmented-control-item_properties_item_border-radius is ignored because the item border-radius
|
|
9
14
|
is fully governed by the per-selection-state tokens (unselected/selected `properties_border-radius`,
|
|
10
|
-
already applied to `.label` and `.control.Mui-selected` below); this generic, state-agnostic radius
|
|
15
|
+
already applied to `.label` and `.control:global(.Mui-selected)` below); this generic, state-agnostic radius
|
|
11
16
|
token has no distinct consumption site without conflicting with those state-specific overrides.
|
|
12
17
|
The Mantine reference adapter exempts this same variable for the same reason. */
|
|
13
18
|
/* recursica-ignore: --recursica_ui-kit_components_segmented-control-item_properties_item_border-radius */
|
|
@@ -38,6 +43,28 @@
|
|
|
38
43
|
gap: var(--recursica_ui-kit_components_segmented-control_properties_item-gap);
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
/* MUI's ToggleButton root ships its own padding/border/border-radius/min-height/min-width and a
|
|
47
|
+
text-transform: uppercase button default; reset all of it so it doesn't stack on top of (or leak
|
|
48
|
+
through, via inheritance, into) .label's token-driven box model/typography below. The
|
|
49
|
+
text-transform reset matters because the item's text-transform token
|
|
50
|
+
(segmented-control-item_..._text_text-transform) currently has no valid scoped value to resolve
|
|
51
|
+
to, so without this reset .label's own `text-transform: var(...)` below is invalid and the
|
|
52
|
+
inherited MUI uppercase default would otherwise show through (Mantine has no such native
|
|
53
|
+
default, so it never surfaced there). */
|
|
54
|
+
.root .control {
|
|
55
|
+
padding: 0;
|
|
56
|
+
border: none;
|
|
57
|
+
border-radius: 0;
|
|
58
|
+
min-height: 0;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
background-color: transparent;
|
|
61
|
+
text-transform: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.root .control:hover {
|
|
65
|
+
background-color: transparent;
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
.root .label {
|
|
42
69
|
padding-left: var(
|
|
43
70
|
--recursica_ui-kit_components_segmented-control-item_properties_item_padding-horizontal
|
|
@@ -128,8 +155,10 @@
|
|
|
128
155
|
);
|
|
129
156
|
}
|
|
130
157
|
|
|
131
|
-
/* Indicator mapping (MUI ToggleButton applies .Mui-selected to the button instead of rendering a sliding indicator)
|
|
132
|
-
|
|
158
|
+
/* Indicator mapping (MUI ToggleButton applies .Mui-selected to the button instead of rendering a sliding indicator).
|
|
159
|
+
Mui-selected must be wrapped in :global() — otherwise CSS Modules locally hashes it and the
|
|
160
|
+
selector never matches MUI's actual global class (silently dropping the selected state). */
|
|
161
|
+
.root .control:global(.Mui-selected) {
|
|
133
162
|
background-color: var(
|
|
134
163
|
--recursica_ui-kit_components_segmented-control-item_variants_selection-states_selected_properties_colors_background-color
|
|
135
164
|
);
|
|
@@ -149,7 +178,7 @@
|
|
|
149
178
|
}
|
|
150
179
|
|
|
151
180
|
/* Selected label text color override */
|
|
152
|
-
.root .control.Mui-selected .label {
|
|
181
|
+
.root .control:global(.Mui-selected) .label {
|
|
153
182
|
color: var(
|
|
154
183
|
--recursica_ui-kit_components_segmented-control-item_variants_selection-states_selected_properties_colors_text-color
|
|
155
184
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
1
|
+
import { forwardRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
ToggleButtonGroup as MuiSegmentedControl,
|
|
4
4
|
ToggleButton,
|
|
@@ -85,12 +85,26 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
85
85
|
|
|
86
86
|
const stylingParams = useSegmentedControlClassNames(restRecord);
|
|
87
87
|
|
|
88
|
+
// MUI's ToggleButtonGroup is controlled-only and selects nothing when `value` is
|
|
89
|
+
// undefined; Mantine's SegmentedControl instead defaults to the first data item. Track an
|
|
90
|
+
// uncontrolled fallback so both adapters render the same default selection.
|
|
91
|
+
const firstValue = data.length
|
|
92
|
+
? typeof data[0] === "string"
|
|
93
|
+
? data[0]
|
|
94
|
+
: data[0].value
|
|
95
|
+
: undefined;
|
|
96
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<
|
|
97
|
+
string | undefined
|
|
98
|
+
>(value ?? firstValue);
|
|
99
|
+
const activeValue = value !== undefined ? value : uncontrolledValue;
|
|
100
|
+
|
|
88
101
|
const handleChange = (
|
|
89
102
|
_event: React.MouseEvent<HTMLElement>,
|
|
90
103
|
newValue: string | null,
|
|
91
104
|
) => {
|
|
92
|
-
if (newValue !== null
|
|
93
|
-
|
|
105
|
+
if (newValue !== null) {
|
|
106
|
+
setUncontrolledValue(newValue);
|
|
107
|
+
onChange?.(newValue);
|
|
94
108
|
}
|
|
95
109
|
};
|
|
96
110
|
|
|
@@ -107,7 +121,7 @@ const _SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
107
121
|
fullWidth={fullWidth}
|
|
108
122
|
data-orientation={orientation}
|
|
109
123
|
exclusive
|
|
110
|
-
value={
|
|
124
|
+
value={activeValue}
|
|
111
125
|
onChange={
|
|
112
126
|
handleChange as React.ComponentProps<
|
|
113
127
|
typeof MuiSegmentedControl
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Slider Implementation Notes
|
|
2
|
+
|
|
3
|
+
This document contains specific design decisions, architectural constraints, and hacks required to bridge the Recursica design system with MUI's underlying `Slider` primitive.
|
|
4
|
+
|
|
5
|
+
## 1. Pointer Clicks Falsely Trigger MUI's `Mui-focusVisible` Ring
|
|
6
|
+
|
|
7
|
+
**Symptom:** Clicking or dragging the thumb showed the keyboard focus ring, which Mantine never shows for a plain mouse interaction.
|
|
8
|
+
|
|
9
|
+
**Root cause:** MUI's `Slider` always programmatically re-focuses its hidden native `<input type="range">` on pointer-down (`focusThumb()` in `useSlider`). Because the focus target (`<input>`) differs from the element the pointer actually interacted with, and because a script-driven `.focus()` call is what browsers use to decide visibility, the native `:focus-visible` heuristic (which MUI's own `isFocusVisible` check relies on) resolves to `true` even for a plain click. Mantine's thumb is a plain `<div tabIndex>` that receives real native focus directly from the click, so the same heuristic correctly resolves to `false` there.
|
|
10
|
+
|
|
11
|
+
**Fix:** `Slider.tsx` tracks pointer-vs-keyboard itself (`onMouseDown` sets a ref, `onFocus` reads it to flag `suppressFocusRing`, `onKeyDown`/`onBlur` clear it — mirroring how real `:focus-visible` re-evaluates on a subsequent keypress). `Slider.module.css` only paints the ring when `[data-suppress-focus-ring="true"]` is absent from `.sliderContainer`.
|
|
12
|
+
|
|
13
|
+
## 2. Disabled Track Stayed Red
|
|
14
|
+
|
|
15
|
+
**Symptom:** Once `disabled` was wired through correctly, the filled track (`.sliderBar`) still rendered the active/red color instead of the disabled grey token.
|
|
16
|
+
|
|
17
|
+
**Root cause:** The base `.sliderBar` rule had `background-color: ... !important`, so the (non-`!important`) `[data-disabled="true"] .sliderBar` override could never win the cascade regardless of source order.
|
|
18
|
+
|
|
19
|
+
**Fix:** Removed the unneeded `!important` flags from the base `.sliderBar` rule — `injectFirst` already makes our CSS-module class beat MUI's native `.MuiSlider-track` via source order alone (same as `.sliderTrack`/`.sliderThumb`/`.sliderMark`, none of which need `!important`).
|
|
20
|
+
|
|
21
|
+
## 3. Assistive Text Rendered as `<div>` Instead of `<span>`
|
|
22
|
+
|
|
23
|
+
**Root cause:** `AssistiveElement.tsx` (mui-adapter) hardcoded a `<div>` around the children text; Mantine's equivalent uses a `<span>`. Not shared via `adapter-common` — each adapter has its own `AssistiveElement`.
|
|
24
|
+
|
|
25
|
+
**Fix:** Changed the inner text wrapper to a `<span>`. No CSS selector depended on the element type (`.text` is a class-only selector and remains a valid flex item as a span).
|
|
26
|
+
|
|
27
|
+
## 4. Mark Label Color
|
|
28
|
+
|
|
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.
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* HARDCODED VALUES:
|
|
3
3
|
* - display: flex; align-items: center; width: 100%; (Standard CSS flexbox layouts for bidirectional components)
|
|
4
4
|
* - flex-grow: 1; flex-shrink: 0; (Layout control structures)
|
|
5
|
-
* - transform: translateX(-50%); (
|
|
5
|
+
* - transform: translateX(-50%); (Step marks/mark labels positioning alignment offset, matching
|
|
6
|
+
* Mantine's own offset mechanism)
|
|
6
7
|
* - outline: none; border-style: solid; (Standard focus reset and border outlines)
|
|
7
8
|
* - Focus ring on thumb/input: the token schema no longer provides per-component focus
|
|
8
9
|
* colors for these (only `active` covers track/step-indicator-color). We apply the generic
|
|
9
10
|
* --recursica_brand_states_focus_* ring tokens (border-size/color/margin/blur) as a box-shadow
|
|
10
11
|
* ring instead of a color swap.
|
|
12
|
+
* - Focus ring on thumb is applied via MUI's own `Mui-focusVisible` class (not native
|
|
13
|
+
* `:focus`/`:focus-visible`, which never match the thumb span — see inline comment).
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
/* ==========================================
|
|
@@ -79,16 +82,14 @@
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
.sliderBar {
|
|
82
|
-
height: var(
|
|
83
|
-
--recursica_ui-kit_components_slider_properties_track-height
|
|
84
|
-
) !important;
|
|
85
|
+
height: var(--recursica_ui-kit_components_slider_properties_track-height);
|
|
85
86
|
border-radius: var(
|
|
86
87
|
--recursica_ui-kit_components_slider_properties_track-border-radius
|
|
87
|
-
)
|
|
88
|
+
);
|
|
88
89
|
background-color: var(
|
|
89
90
|
--recursica_ui-kit_components_slider_properties_colors_track-active
|
|
90
|
-
)
|
|
91
|
-
border: none
|
|
91
|
+
);
|
|
92
|
+
border: none;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
.sliderThumb {
|
|
@@ -116,8 +117,21 @@
|
|
|
116
117
|
);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
/*
|
|
121
|
+
* MUI toggles its own `Mui-focusVisible` class on the thumb (for both keyboard focus and
|
|
122
|
+
* mouse-driven dragging, since MUI's slider routes real DOM focus to a hidden native input,
|
|
123
|
+
* never the visible thumb span) and pairs it with a hardcoded `theme.palette.primary.main`
|
|
124
|
+
* box-shadow ring. Native `:focus`/`:focus-visible` never match the thumb span itself, so we
|
|
125
|
+
* target MUI's own class directly to replace its default-blue ring with the recursica ring.
|
|
126
|
+
*
|
|
127
|
+
* MUI also always programmatically re-focuses that hidden input on pointer interaction, which
|
|
128
|
+
* browsers' `:focus-visible` heuristic treats as keyboard-visible — unlike Mantine's plain div
|
|
129
|
+
* thumb, where a real click correctly resolves to a non-visible focus. `data-suppress-focus-ring`
|
|
130
|
+
* (set in Slider.tsx by tracking pointer-vs-keyboard ourselves) keeps the ring keyboard-only,
|
|
131
|
+
* matching Mantine.
|
|
132
|
+
*/
|
|
133
|
+
.sliderContainer:not([data-suppress-focus-ring="true"])
|
|
134
|
+
.sliderThumb:global(.Mui-focusVisible) {
|
|
121
135
|
outline: none;
|
|
122
136
|
box-shadow:
|
|
123
137
|
var(--recursica_ui-kit_components_slider_properties_thumb-elevation),
|
|
@@ -128,6 +142,14 @@
|
|
|
128
142
|
var(--recursica_brand_states_focus_color);
|
|
129
143
|
}
|
|
130
144
|
|
|
145
|
+
.sliderContainer[data-suppress-focus-ring="true"]
|
|
146
|
+
.sliderThumb:global(.Mui-focusVisible) {
|
|
147
|
+
outline: none;
|
|
148
|
+
box-shadow: var(
|
|
149
|
+
--recursica_ui-kit_components_slider_properties_thumb-elevation
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
131
153
|
.sliderMark {
|
|
132
154
|
box-sizing: border-box;
|
|
133
155
|
width: var(
|
|
@@ -154,6 +176,49 @@
|
|
|
154
176
|
);
|
|
155
177
|
}
|
|
156
178
|
|
|
179
|
+
/*
|
|
180
|
+
* MUI's default markLabel sits ~30-40px below the mark (`top: 30`, `top: 40` under
|
|
181
|
+
* `(pointer: coarse)`) and uses `theme.palette.text.secondary`. Mantine's equivalent label sits
|
|
182
|
+
* right under the mark (a half-step-indicator offset plus a small gap) and has no explicit color,
|
|
183
|
+
* inheriting the container's text color. Reusing the min-max-label typography tokens (the closest
|
|
184
|
+
* existing "small label near the track" token set) and inheriting color keeps this visually
|
|
185
|
+
* consistent with Mantine without a dedicated mark-label token.
|
|
186
|
+
*/
|
|
187
|
+
.sliderMarkLabel {
|
|
188
|
+
top: calc(
|
|
189
|
+
var(--recursica_ui-kit_components_slider_properties_step-indicator-width) /
|
|
190
|
+
2 +
|
|
191
|
+
var(--recursica_ui-kit_globals_form_properties_label-field-gap-vertical)
|
|
192
|
+
);
|
|
193
|
+
transform: translateX(-50%);
|
|
194
|
+
color: inherit;
|
|
195
|
+
|
|
196
|
+
font-family: var(
|
|
197
|
+
--recursica_ui-kit_components_slider_properties_min-max-label_font-family
|
|
198
|
+
);
|
|
199
|
+
font-size: var(
|
|
200
|
+
--recursica_ui-kit_components_slider_properties_min-max-label_font-size
|
|
201
|
+
);
|
|
202
|
+
font-style: var(
|
|
203
|
+
--recursica_ui-kit_components_slider_properties_min-max-label_font-style
|
|
204
|
+
);
|
|
205
|
+
font-weight: var(
|
|
206
|
+
--recursica_ui-kit_components_slider_properties_min-max-label_font-weight
|
|
207
|
+
);
|
|
208
|
+
letter-spacing: var(
|
|
209
|
+
--recursica_ui-kit_components_slider_properties_min-max-label_letter-spacing
|
|
210
|
+
);
|
|
211
|
+
line-height: var(
|
|
212
|
+
--recursica_ui-kit_components_slider_properties_min-max-label_line-height
|
|
213
|
+
);
|
|
214
|
+
text-decoration: var(
|
|
215
|
+
--recursica_ui-kit_components_slider_properties_min-max-label_text-decoration
|
|
216
|
+
);
|
|
217
|
+
text-transform: var(
|
|
218
|
+
--recursica_ui-kit_components_slider_properties_min-max-label_text-transform
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
157
222
|
.sliderTooltip {
|
|
158
223
|
background-color: var(--form-field-text-valued) !important;
|
|
159
224
|
color: var(--form-field-background) !important;
|
|
@@ -398,14 +463,12 @@
|
|
|
398
463
|
|
|
399
464
|
/* Focus states */
|
|
400
465
|
/* The token schema's `active` state (not a separate `focus` state) now covers track/step-indicator
|
|
401
|
-
color while focus is within the slider
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
466
|
+
color while focus is within the slider. Unlike Mantine (whose unfilled track is painted by an
|
|
467
|
+
internal `::before` layer that this rule never actually reaches), MUI's rail element is styled
|
|
468
|
+
directly by `.sliderTrack`, so applying the active-track color here visibly repaints the rail a
|
|
469
|
+
different color for the whole duration of a drag. There is no design requirement for the rail to
|
|
470
|
+
change color while dragging, so the track is intentionally left out of this rule; only the
|
|
471
|
+
step-indicator (mark) is included, matching the token schema's remaining active-state coverage. */
|
|
409
472
|
.sliderRoot:has(.sliderThumb:focus) .sliderMark,
|
|
410
473
|
.sliderRoot:focus-within .sliderMark {
|
|
411
474
|
background-color: var(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, useState, useEffect } from "react";
|
|
1
|
+
import React, { forwardRef, useState, useEffect, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Slider as MuiSlider,
|
|
4
4
|
type SliderProps as MuiSliderProps,
|
|
@@ -145,6 +145,36 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
145
145
|
delete restRecord["color"];
|
|
146
146
|
delete restRecord["wrapperProps"];
|
|
147
147
|
|
|
148
|
+
// MUI always programmatically re-focuses its hidden native input on pointer interaction,
|
|
149
|
+
// which browsers' `:focus-visible` heuristic treats as keyboard-visible — unlike Mantine's
|
|
150
|
+
// plain div thumb, where a real click correctly resolves to a non-visible focus. Track
|
|
151
|
+
// pointer-vs-keyboard ourselves so the focus ring only paints for genuine keyboard focus,
|
|
152
|
+
// matching Mantine (a later keypress while still focused reveals the ring, same as native).
|
|
153
|
+
const pointerDownRef = useRef(false);
|
|
154
|
+
const [suppressFocusRing, setSuppressFocusRing] = useState(false);
|
|
155
|
+
const externalOnMouseDown = (sanitizedProps as MuiSliderProps).onMouseDown;
|
|
156
|
+
const externalOnFocus = (sanitizedProps as MuiSliderProps).onFocus;
|
|
157
|
+
const externalOnBlur = (sanitizedProps as MuiSliderProps).onBlur;
|
|
158
|
+
const externalOnKeyDown = (sanitizedProps as MuiSliderProps).onKeyDown;
|
|
159
|
+
|
|
160
|
+
const handleThumbMouseDown = (e: React.MouseEvent<HTMLSpanElement>) => {
|
|
161
|
+
pointerDownRef.current = true;
|
|
162
|
+
externalOnMouseDown?.(e);
|
|
163
|
+
};
|
|
164
|
+
const handleThumbFocus = (e: React.FocusEvent<HTMLSpanElement>) => {
|
|
165
|
+
setSuppressFocusRing(pointerDownRef.current);
|
|
166
|
+
pointerDownRef.current = false;
|
|
167
|
+
externalOnFocus?.(e);
|
|
168
|
+
};
|
|
169
|
+
const handleThumbBlur = (e: React.FocusEvent<HTMLSpanElement>) => {
|
|
170
|
+
setSuppressFocusRing(false);
|
|
171
|
+
externalOnBlur?.(e);
|
|
172
|
+
};
|
|
173
|
+
const handleThumbKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
174
|
+
setSuppressFocusRing(false);
|
|
175
|
+
externalOnKeyDown?.(e);
|
|
176
|
+
};
|
|
177
|
+
|
|
148
178
|
// Securely map core native blocks down ensuring nested CSS modules map precisely
|
|
149
179
|
const mergedClassNames: Partial<Record<string, string>> = {
|
|
150
180
|
root: styles.sliderRoot,
|
|
@@ -226,6 +256,7 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
226
256
|
data-form-layout={formLayout}
|
|
227
257
|
data-disabled={disabled ? "true" : undefined}
|
|
228
258
|
data-error={error ? "true" : undefined}
|
|
259
|
+
data-suppress-focus-ring={suppressFocusRing ? "true" : undefined}
|
|
229
260
|
>
|
|
230
261
|
{leadingIcon}
|
|
231
262
|
|
|
@@ -240,6 +271,10 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
240
271
|
disabled={disabled}
|
|
241
272
|
value={resolvedValue}
|
|
242
273
|
onChange={handleValueChange}
|
|
274
|
+
onMouseDown={handleThumbMouseDown}
|
|
275
|
+
onFocus={handleThumbFocus}
|
|
276
|
+
onBlur={handleThumbBlur}
|
|
277
|
+
onKeyDown={handleThumbKeyDown}
|
|
243
278
|
onChangeCommitted={
|
|
244
279
|
onChangeEnd as unknown as (
|
|
245
280
|
event: Event | React.SyntheticEvent,
|
|
@@ -2,3 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
- **Compositional API Dropped:** Mantine manages stepper state and content via `<Stepper.Step>` and `<Stepper.Completed>`. MUI delegates content rendering to the developer and focuses purely on the stepper visual layout using `<Step>`, `<StepLabel>`, etc.
|
|
4
4
|
- **Monolithic API Adopted:** Following architectural review, we have abandoned the fabricated context wrappers for `mui-adapter`. We now natively export `Stepper`, `Step`, `StepLabel`, `StepButton`, and `StepConnector` wrapping their `@mui/material` counterparts. Developers are expected to manage the active step logic and content rendering outside the `Stepper` component, consistent with MUI patterns. Storybook tests have been updated to reflect this divergence while retaining core visual compatibility.
|
|
5
|
+
|
|
6
|
+
## Layout fixes (root-caused against mantine's visual reference)
|
|
7
|
+
|
|
8
|
+
Several parts of the original implementation used the correct-looking classes but wired
|
|
9
|
+
them to elements that never carry those classes at runtime, or copy-pasted comments/logic
|
|
10
|
+
from `mantine-adapter` that don't describe how MUI's Stepper actually works. Fixed:
|
|
11
|
+
|
|
12
|
+
- **Horizontal layout uses `alternativeLabel`:** MUI's own `Stepper` prop doc says
|
|
13
|
+
`alternativeLabel` positions the label under the icon — that's exactly Recursica's
|
|
14
|
+
horizontal layout, and it's also what makes `StepConnector`'s `alternativeLabel` variant
|
|
15
|
+
absolutely-position itself relative to each `Step`, centered on the icon. The adapter
|
|
16
|
+
previously never set this prop and instead tried to force column layout via CSS on
|
|
17
|
+
`Step`'s own root, which has no effect (`StepLabel`'s row-vs-column layout is driven
|
|
18
|
+
entirely by the `alternativeLabel` context flag, not by its parent's `flex-direction`).
|
|
19
|
+
Without it, labels rendered beside the icon (row layout) and the connector used the
|
|
20
|
+
non-`alternativeLabel` inline-flex positioning, both very visibly wrong.
|
|
21
|
+
- **Custom `StepIconComponent`:** MUI's default `StepIcon` draws its own self-contained
|
|
22
|
+
circle+check `CheckCircle` SVG (Material's `check_circle` glyph) whenever `completed` is
|
|
23
|
+
true, colored via `theme.palette.primary.main` (blue) — it never reads our tokens and its
|
|
24
|
+
SVG viewBox doesn't scale to `--stepper-indicator-size`. We now pass `RecursicaStepIcon`
|
|
25
|
+
(a plain span + our own inline check glyph, same convention as `Checkbox.tsx`'s `CheckIcon`)
|
|
26
|
+
so the token-driven circle (`.stepIconCircle`) is the only circle ever drawn, and the check
|
|
27
|
+
mark is sized via `--stepper-svg-size` and colored via the `completed-indicator-text` token.
|
|
28
|
+
- **`classes.labelContainer` was never mapped:** all of the `.stepBody` centering/max-width
|
|
29
|
+
CSS (copied verbatim from mantine, where `stepBody` is a real classNames key) was dead code
|
|
30
|
+
for MUI — `StepLabel` calls this slot `labelContainer`, not `stepBody`. Now mapped via
|
|
31
|
+
`classes={{ labelContainer: styles.stepBody }}`.
|
|
32
|
+
- **Vertical connecting line — real primitive doesn't fit, so it's drawn on `.stepIcon`:**
|
|
33
|
+
MUI's vertical `StepConnector` is a sibling with a fixed `minHeight: 24`; it can't stretch to
|
|
34
|
+
match a step's actual rendered height (e.g. a 2-line description), which is exactly why the
|
|
35
|
+
line floated disconnected from both circles. MUI's own answer to this is `StepContent`
|
|
36
|
+
(its `border-left` spans whatever height its content needs) — but Recursica intentionally
|
|
37
|
+
hides `.content` (steppers are structural-only, no per-step content in the DOM). Since
|
|
38
|
+
neither of MUI's two connecting-line primitives fits, the vertical connector is drawn as a
|
|
39
|
+
`.stepIcon::after` pseudo-element instead: `StepLabelRoot` gets `align-items: stretch` (a
|
|
40
|
+
real CSS stretch, not JS measurement) so `.stepIcon` (`iconContainer`) grows to match its
|
|
41
|
+
sibling label column's actual height, and the rail spans `top: indicator-size` down through
|
|
42
|
+
`bottom: calc(-1 * step-gap)` (the step's own padding-bottom, exactly reaching the next
|
|
43
|
+
step's icon). `Stepper.tsx` passes an empty `<></>` as the vertical `connector` since this
|
|
44
|
+
fully replaces its job. This requires zeroing MUI's own hardcoded
|
|
45
|
+
`StepLabelRoot` vertical `padding: 8px 0` (`.root.vertical .stepLabelRoot`) — leaving it in
|
|
46
|
+
place made the rail land 16px short of the next icon.
|
|
47
|
+
- **`:global()` was missing everywhere `.Mui-active`/`.Mui-completed`/`.MuiStepLabel-root` was
|
|
48
|
+
referenced:** in a CSS Module, a bare `.Mui-completed` selector gets scoped/hashed like any
|
|
49
|
+
other local class, so it silently never matches the real global class MUI stamps onto the
|
|
50
|
+
DOM (same pitfall documented in `Button.module.css`/`Switch.module.css` for `.Mui-disabled`/
|
|
51
|
+
`.Mui-checked`). This affected the label/description state-color rules (already present
|
|
52
|
+
before this fix) and the new separator/rail state-color rules — all now wrapped in
|
|
53
|
+
`:global(...)`. While fixing this, the description-color selectors were also corrected to
|
|
54
|
+
use `.stepBody:has(.stepLabel:global(.Mui-completed)) .stepDescription` — the prior
|
|
55
|
+
`:has(~ .Mui-completed)` could never match because `optional`/description renders _after_
|
|
56
|
+
the label in the DOM (not before), and `.MuiStepLabel-root.Mui-completed` could never match
|
|
57
|
+
because MUI only stamps the completed/active state class onto the `label`/`iconContainer`
|
|
58
|
+
slots, never onto `StepLabel`'s own root.
|