@recursica/mui-adapter 0.31.0 → 0.32.1
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 +18 -0
- package/dist/index.d.ts +11 -3
- package/dist/mui-adapter.cjs +50 -50
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +3717 -3659
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.module.css +7 -0
- package/src/components/Autocomplete/Autocomplete.module.css +10 -0
- package/src/components/Button/Button.module.css +10 -0
- package/src/components/Card/Card.stories.tsx +2 -4
- package/src/components/Chip/Chip.module.css +9 -0
- package/src/components/DatePicker/DatePicker.module.css +13 -0
- package/src/components/Dropdown/Dropdown.module.css +10 -0
- package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +4 -1
- package/src/components/FileInput/FileInput.module.css +17 -0
- package/src/components/FileUpload/FileUpload.module.css +6 -0
- package/src/components/Link/Link.module.css +5 -0
- package/src/components/Menu/Menu.module.css +6 -0
- package/src/components/NumberInput/NumberInput.module.css +8 -0
- package/src/components/Pagination/Pagination.module.css +7 -0
- package/src/components/Slider/IMPLEMENTATION_NOTES.md +18 -2
- package/src/components/Slider/Slider.module.css +20 -3
- package/src/components/Slider/Slider.stories.tsx +65 -0
- package/src/components/Slider/Slider.tsx +130 -40
- package/src/components/Slider/USAGE.md +15 -1
- package/src/components/Switch/Switch.module.css +8 -0
- package/src/components/Tabs/Tabs.module.css +9 -0
- package/src/components/TextArea/TextArea.module.css +8 -0
- package/src/components/TextField/TextField.module.css +8 -0
- package/src/components/TimePicker/TimePicker.module.css +8 -0
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +3 -0
- package/src/components/Tree/Tree.module.css +11 -0
- package/src/components/Tree/Tree.tsx +7 -1
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/borderux/recursica.git",
|
|
14
14
|
"directory": "packages/mui-adapter"
|
|
15
15
|
},
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.32.1",
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"vitest": "^3.2.4"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@recursica/adapter-common": "^0.
|
|
105
|
+
"@recursica/adapter-common": "^0.23.0",
|
|
106
106
|
"@recursica/official-release": "^2.8.0",
|
|
107
107
|
"dayjs": "^1.11.21"
|
|
108
108
|
},
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_disabled
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
/* ==== ACCORDION CONTAINER ==== */
|
|
2
9
|
.root {
|
|
3
10
|
box-sizing: border-box;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
8
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
/* LAYOUT SPACING OVERRIDES:
|
|
2
12
|
- Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
|
|
3
13
|
- Also sets the --autocomplete-control-{max,min}-width hooks consumed inline in Autocomplete.tsx,
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
8
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
/* Baseline and Reset */
|
|
2
12
|
.root {
|
|
3
13
|
box-sizing: border-box;
|
|
@@ -37,8 +37,7 @@ export const Default: Story = {
|
|
|
37
37
|
<div
|
|
38
38
|
style={{
|
|
39
39
|
padding: "48px",
|
|
40
|
-
backgroundColor:
|
|
41
|
-
"var(--recursica_brand_palettes_neutral_050_color_tone)",
|
|
40
|
+
backgroundColor: "#e9ecef",
|
|
42
41
|
}}
|
|
43
42
|
>
|
|
44
43
|
<Layer layer={0}>
|
|
@@ -73,8 +72,7 @@ export const HeaderlessAndFooterless: Story = {
|
|
|
73
72
|
<div
|
|
74
73
|
style={{
|
|
75
74
|
padding: "48px",
|
|
76
|
-
backgroundColor:
|
|
77
|
-
"var(--recursica_brand_palettes_neutral_050_color_tone)",
|
|
75
|
+
backgroundColor: "#e9ecef",
|
|
78
76
|
}}
|
|
79
77
|
>
|
|
80
78
|
<Layer layer={0}>
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_disabled
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
/* HARDCODED VALUES
|
|
2
11
|
border-style: solid; (Baseline reset)
|
|
3
12
|
box-sizing: border-box; (Baseline reset)
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* No ui-kit_components_date-picker radius token exists for the calendar day cells; reuses the brand default border-radius.
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_dimensions_border-radii_default
|
|
4
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_disabled
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
8
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
9
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
10
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
11
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
12
|
+
*/
|
|
13
|
+
|
|
1
14
|
/* HARDCODED VALUES:
|
|
2
15
|
- border-style: solid. Native structural rendering rule.
|
|
3
16
|
- outline: none. Bypassing browser focus rings to rely strictly on Recursica focus states natively.
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
8
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
/* LAYOUT SPACING OVERRIDES:
|
|
2
12
|
- Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
|
|
3
13
|
- Also sets the --dropdown-control-{max,min}-width hooks consumed inline in Dropdown.tsx, since
|
|
@@ -136,7 +136,10 @@ roving group, _then_ the trailing clear button as its own stop.
|
|
|
136
136
|
wraps onto multiple lines below the dropzone — `FileInput` is a fixed single-line, `min-height`d
|
|
137
137
|
control, so wrapping would grow it vertically. Enough chips to overflow the control's own width
|
|
138
138
|
scroll horizontally within it instead (mouse wheel/trackpad or a native scrollbar), same tradeoff
|
|
139
|
-
already made for `.value`'s ellipsis truncation.
|
|
139
|
+
already made for `.value`'s ellipsis truncation. The native scrollbar itself is hidden
|
|
140
|
+
(`scrollbar-width: none`, `-ms-overflow-style: none`, `::-webkit-scrollbar { display: none }`) —
|
|
141
|
+
the control's `min-height` isn't tall enough to host a visible horizontal scrollbar without
|
|
142
|
+
clipping the chips, and scrolling (wheel/trackpad/drag) still works with it hidden.
|
|
140
143
|
|
|
141
144
|
## Read-only vs disabled
|
|
142
145
|
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
8
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
/* LAYOUT SPACING OVERRIDES:
|
|
2
12
|
- Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
|
|
3
13
|
- Also sets the --file-input-control-{max,min}-width hooks consumed inline in FileInput.tsx,
|
|
@@ -178,6 +188,13 @@
|
|
|
178
188
|
overflow-x: auto;
|
|
179
189
|
overflow-y: hidden;
|
|
180
190
|
gap: var(--recursica_ui-kit_components_file-input_properties_icon-text-gap);
|
|
191
|
+
/* HARDCODE: scrollbar stays functional (still scrolls via wheel/trackpad/drag) but hidden —
|
|
192
|
+
the control isn't tall enough to host a visible scrollbar without clipping the chips. */
|
|
193
|
+
scrollbar-width: none; /* Firefox */
|
|
194
|
+
-ms-overflow-style: none; /* legacy Edge */
|
|
195
|
+
}
|
|
196
|
+
.chipRow::-webkit-scrollbar {
|
|
197
|
+
display: none; /* Chrome, Safari */
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
.chipWrapper {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/* HARDCODED VALUES:
|
|
2
8
|
- .root: display: flex; flex-direction: column; width: 100% — structural layout, not a design
|
|
3
9
|
token concern (matches how other block-level components like Layer are laid out).
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Only source for link hover-underline; no per-component token exists (see IMPLEMENTATION_NOTES.md).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_link_decoration
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
/*
|
|
2
7
|
HARDCODED VALUES:
|
|
3
8
|
- display: inline-flex; (Base layout to align icon and text correctly)
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/* HARDCODED VALUES:
|
|
2
8
|
- border-style: solid. Structural rendering rule for the dropdown border (Mantine uses Paper which may not set border natively).
|
|
3
9
|
- NOTE: overflow is intentionally NOT set on .dropdown. Mantine renders sub-menu dropdowns
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/* LAYOUT SPACING OVERRIDES:
|
|
2
10
|
- Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
|
|
3
11
|
- Also sets the --number-input-control-{max,min}-width hooks consumed inline in NumberInput.tsx,
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_disabled
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
.root {
|
|
2
9
|
display: flex;
|
|
3
10
|
align-items: center;
|
|
@@ -34,6 +34,22 @@ This document contains specific design decisions, architectural constraints, and
|
|
|
34
34
|
|
|
35
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
36
|
|
|
37
|
-
## 6.
|
|
37
|
+
## 6. Dual-Thumb / Range Support
|
|
38
38
|
|
|
39
|
-
**Decision:**
|
|
39
|
+
**Decision:** Previously requested and declined (see git history for this section) as no use case needed it; reopened with a full spec and implemented. `value`/`defaultValue`/`onChange`/`onChangeEnd` are now typed `number | [number, number]` in `@recursica/adapter-common`'s `RecursicaSliderProps`.
|
|
40
|
+
**Implementation:**
|
|
41
|
+
|
|
42
|
+
- MUI's `Slider` already accepts `number[]` for `value`/`onChange` and renders two thumbs natively, so unlike `mantine-adapter` (which must swap to a separate `RangeSlider` component) this stays the same `<MuiSlider>` — arrays are no longer collapsed to `value[0]`.
|
|
43
|
+
- Internal state (`internalValue`, `resolvedValue`, `inputValue`) generalized from `number` to `number | [number, number]`; range-mode input handlers (`handleLowerInputChange`/`handleUpperInputChange`) clamp each thumb against the other's current value (not the shared `min`/`max`) so the two inputs can't cross.
|
|
44
|
+
- `SliderReadOnlyValue` and the floating `.currentValue` display both render `"lower – upper"` for a range value, running each side through `tooltipLabel` independently (`valueLabelFormat` already handled this per-thumb natively, unchanged).
|
|
45
|
+
- DOM order in range mode (input → leading icon → min label → track → max label → trailing icon → input) mirrors Forge's own Material/Carbon range layouts — see §7 below.
|
|
46
|
+
|
|
47
|
+
## 7. Trailing Icon Order Relative to the Input Field
|
|
48
|
+
|
|
49
|
+
**Decision:** `trailingIcon` previously rendered after the numeric input (`... max label, input, trailing icon`), reversed from Forge's Material/Carbon kits, which always render the trailing icon directly after the max label and before the input.
|
|
50
|
+
**Implementation:** Moved `{trailingIconEl}` before the `showInput` input block in the single-value layout; the range layout was built with this order from the start (input → icon → min label → track → max label → trailing icon → input).
|
|
51
|
+
|
|
52
|
+
## 8. Mark Vertically Off-Center
|
|
53
|
+
|
|
54
|
+
**Decision:** MUI's own mark is `top: 50%; transform: translate(-1px, -50%)` — the `-1px` assumes MUI's built-in 2px dot, the `-50%` is real vertical centering. `.sliderMark` overrode `transform` to `translateX(-50%)` (horizontal-only, meant to mirror the mantine-adapter) without noticing it dropped MUI's vertical `-50%`, leaving the dot hanging below the track's midpoint instead of centered.
|
|
55
|
+
**Implementation:** Changed `.sliderMark`'s transform to `translate(-50%, -50%)` — keeps MUI's vertical centering and swaps the horizontal term to properly center our (non-2px) `step-indicator-width` instead of MUI's hardcoded 1px.
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/*
|
|
2
10
|
* HARDCODED VALUES:
|
|
3
11
|
* - display: flex; align-items: center; width: 100%; (Standard CSS flexbox layouts for bidirectional components)
|
|
4
12
|
* - flex-grow: 1; flex-shrink: 0; (Layout control structures)
|
|
5
|
-
* - transform:
|
|
6
|
-
*
|
|
13
|
+
* - transform: translate(-50%, -50%); on .sliderMark (preserves MUI's own vertical mark
|
|
14
|
+
* centering while re-deriving the horizontal offset for our step-indicator-width token — see
|
|
15
|
+
* .sliderMark below)
|
|
16
|
+
* - transform: translateX(-50%); on .sliderMarkLabel (horizontal-only; matches Mantine's offset
|
|
17
|
+
* mechanism)
|
|
7
18
|
* - outline: none; border-style: solid; (Standard focus reset and border outlines)
|
|
8
19
|
* - Focus ring on thumb/input: the token schema no longer provides per-component focus
|
|
9
20
|
* colors for these (only `active` covers track/step-indicator-color). We apply the generic
|
|
@@ -164,7 +175,13 @@
|
|
|
164
175
|
background-color: var(
|
|
165
176
|
--recursica_ui-kit_components_slider_properties_colors_step-indicator-color
|
|
166
177
|
);
|
|
167
|
-
transform:
|
|
178
|
+
/* MUI's own mark is `top: 50%; transform: translate(-1px, -50%)` — the -1px assumes its
|
|
179
|
+
built-in 2px-wide dot and the -50% is real vertical centering, not decoration. Overriding
|
|
180
|
+
width/height above without preserving that vertical -50% would leave the dot vertically
|
|
181
|
+
un-centered (it would hang below the track's midpoint); translateX(-50%) alone stomped it.
|
|
182
|
+
translate(-50%, -50%) keeps the vertical centering and swaps the horizontal offset to match
|
|
183
|
+
our (non-2px) step-indicator-width. */
|
|
184
|
+
transform: translate(-50%, -50%);
|
|
168
185
|
border: none;
|
|
169
186
|
}
|
|
170
187
|
|
|
@@ -190,6 +190,71 @@ export const WithIconsAndLabels: Story = {
|
|
|
190
190
|
},
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
+
export const RangeMode: Story = {
|
|
194
|
+
args: {
|
|
195
|
+
label: "Price Range",
|
|
196
|
+
assistiveText: "Pass a [number, number] tuple to render two thumbs.",
|
|
197
|
+
defaultValue: [20, 80],
|
|
198
|
+
min: 0,
|
|
199
|
+
max: 100,
|
|
200
|
+
showMinMaxLabels: true,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const RangeModeWithInputs: Story = {
|
|
205
|
+
args: {
|
|
206
|
+
...RangeMode.args,
|
|
207
|
+
showInput: true,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const RangeModeWithIconsAndInputs: Story = {
|
|
212
|
+
args: {
|
|
213
|
+
label: "Price Range",
|
|
214
|
+
assistiveText:
|
|
215
|
+
"Full range usage: leading/trailing icons, min/max label overrides, and both bound inputs.",
|
|
216
|
+
defaultValue: [20, 80],
|
|
217
|
+
min: 0,
|
|
218
|
+
max: 100,
|
|
219
|
+
showInput: true,
|
|
220
|
+
minLabel: "$0",
|
|
221
|
+
maxLabel: "$100",
|
|
222
|
+
tooltipLabel: (value: number) => `$${value}`,
|
|
223
|
+
icon: (
|
|
224
|
+
<svg
|
|
225
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
226
|
+
width="16"
|
|
227
|
+
height="16"
|
|
228
|
+
viewBox="0 0 24 24"
|
|
229
|
+
fill="none"
|
|
230
|
+
stroke="currentColor"
|
|
231
|
+
strokeWidth="2"
|
|
232
|
+
strokeLinecap="round"
|
|
233
|
+
strokeLinejoin="round"
|
|
234
|
+
>
|
|
235
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
|
236
|
+
</svg>
|
|
237
|
+
),
|
|
238
|
+
trailingIcon: (
|
|
239
|
+
<svg
|
|
240
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
241
|
+
width="16"
|
|
242
|
+
height="16"
|
|
243
|
+
viewBox="0 0 24 24"
|
|
244
|
+
fill="none"
|
|
245
|
+
stroke="currentColor"
|
|
246
|
+
strokeWidth="2"
|
|
247
|
+
strokeLinecap="round"
|
|
248
|
+
strokeLinejoin="round"
|
|
249
|
+
>
|
|
250
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
|
251
|
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
|
252
|
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
|
|
253
|
+
</svg>
|
|
254
|
+
),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
193
258
|
export const FormLayouts: Story = {
|
|
194
259
|
render: () => (
|
|
195
260
|
<div
|
|
@@ -20,7 +20,13 @@ import { type RecursicaSliderProps as BaseRecursicaSliderProps } from "@recursic
|
|
|
20
20
|
export interface RecursicaSliderProps
|
|
21
21
|
extends Omit<
|
|
22
22
|
MuiSliderProps,
|
|
23
|
-
|
|
23
|
+
| "size"
|
|
24
|
+
| "color"
|
|
25
|
+
| "classes"
|
|
26
|
+
| "onChange"
|
|
27
|
+
| "onChangeCommitted"
|
|
28
|
+
| "value"
|
|
29
|
+
| "defaultValue"
|
|
24
30
|
>,
|
|
25
31
|
Omit<
|
|
26
32
|
RecursicaFormControlWrapperProps,
|
|
@@ -38,15 +44,23 @@ export type SliderProps = RecursicaOverStyled<RecursicaSliderProps>;
|
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
46
|
* Custom Read-Only visual representation of the Slider value.
|
|
41
|
-
* Utilizes component-specific read-only typography variables.
|
|
47
|
+
* Utilizes component-specific read-only typography variables. Renders a "lower – upper" pair
|
|
48
|
+
* when the value is a range tuple.
|
|
42
49
|
*/
|
|
43
|
-
const SliderReadOnlyValue: React.FC<{ value: number }> = ({
|
|
44
|
-
|
|
50
|
+
const SliderReadOnlyValue: React.FC<{ value: number | [number, number] }> = ({
|
|
51
|
+
value,
|
|
52
|
+
}) => {
|
|
53
|
+
const display = Array.isArray(value) ? `${value[0]} – ${value[1]}` : value;
|
|
54
|
+
return <div className={styles.readOnlyValue}>{display}</div>;
|
|
45
55
|
};
|
|
46
56
|
|
|
47
57
|
/**
|
|
48
58
|
* Recursica Slider component wrapping Mui's Slider.
|
|
49
59
|
*
|
|
60
|
+
* MUI's own `Slider` already renders two thumbs natively when given a tuple `value`, so range
|
|
61
|
+
* mode here is a typing/handler concern rather than a different underlying component (contrast
|
|
62
|
+
* with the mantine-adapter, which swaps in Mantine's separate `RangeSlider`).
|
|
63
|
+
*
|
|
50
64
|
* Implements a bidirectional text input field next to the slider track, responsive layouts,
|
|
51
65
|
* custom typography-bound min/max labels (optionally overridden via `minLabel`/`maxLabel`),
|
|
52
66
|
* optional leading/trailing icons, and an explicit read-only layout.
|
|
@@ -96,35 +110,45 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
96
110
|
...rest
|
|
97
111
|
} = props;
|
|
98
112
|
|
|
99
|
-
// Bidirectional state linking the slider track value to the input field string
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
// Bidirectional state linking the slider track value to the input field string
|
|
114
|
+
// representation. A `[number, number]` value/defaultValue switches the component into
|
|
115
|
+
// two-thumb range mode — MUI's own Slider already renders two thumbs for a tuple value.
|
|
116
|
+
type SliderValue = number | [number, number];
|
|
117
|
+
|
|
118
|
+
const [internalValue, setInternalValue] = useState<SliderValue>(() => {
|
|
119
|
+
if (value !== undefined) return value;
|
|
120
|
+
if (defaultValue !== undefined) return defaultValue;
|
|
104
121
|
return min;
|
|
105
122
|
});
|
|
106
123
|
|
|
107
|
-
const resolvedValue =
|
|
108
|
-
value !== undefined
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
const resolvedValue: SliderValue =
|
|
125
|
+
value !== undefined ? value : internalValue;
|
|
126
|
+
const isRange = Array.isArray(resolvedValue);
|
|
127
|
+
|
|
128
|
+
const [inputValue, setInputValue] = useState<string | [string, string]>(
|
|
129
|
+
() =>
|
|
130
|
+
Array.isArray(resolvedValue)
|
|
131
|
+
? [resolvedValue[0].toString(), resolvedValue[1].toString()]
|
|
132
|
+
: resolvedValue.toString(),
|
|
115
133
|
);
|
|
116
134
|
|
|
117
|
-
// Synchronize text input whenever the slider value changes
|
|
135
|
+
// Synchronize text input(s) whenever the slider value changes
|
|
118
136
|
useEffect(() => {
|
|
119
|
-
setInputValue(
|
|
137
|
+
setInputValue(
|
|
138
|
+
Array.isArray(resolvedValue)
|
|
139
|
+
? [resolvedValue[0].toString(), resolvedValue[1].toString()]
|
|
140
|
+
: resolvedValue.toString(),
|
|
141
|
+
);
|
|
120
142
|
}, [resolvedValue]);
|
|
121
143
|
|
|
122
144
|
const handleValueChange = (_e: Event, val: number | number[]) => {
|
|
123
|
-
const
|
|
145
|
+
const normalized: SliderValue = Array.isArray(val)
|
|
146
|
+
? [val[0], val[1]]
|
|
147
|
+
: val;
|
|
124
148
|
if (value === undefined) {
|
|
125
|
-
setInternalValue(
|
|
149
|
+
setInternalValue(normalized);
|
|
126
150
|
}
|
|
127
|
-
onChange?.(
|
|
151
|
+
onChange?.(normalized);
|
|
128
152
|
};
|
|
129
153
|
|
|
130
154
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -140,7 +164,38 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
140
164
|
};
|
|
141
165
|
|
|
142
166
|
const handleInputBlur = () => {
|
|
143
|
-
setInputValue(resolvedValue.toString());
|
|
167
|
+
setInputValue((resolvedValue as number).toString());
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Range-mode input handlers: each bound clamps against the other thumb rather than the
|
|
171
|
+
// shared min/max, so the lower thumb can never cross the upper one and vice versa.
|
|
172
|
+
const handleLowerInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
173
|
+
const current = resolvedValue as [number, number];
|
|
174
|
+
const valStr = e.target.value;
|
|
175
|
+
setInputValue([valStr, current[1].toString()]);
|
|
176
|
+
|
|
177
|
+
const parsed = parseFloat(valStr);
|
|
178
|
+
if (!isNaN(parsed)) {
|
|
179
|
+
const clamped = Math.max(min, Math.min(current[1], parsed));
|
|
180
|
+
handleValueChange(null as unknown as Event, [clamped, current[1]]);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleUpperInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
185
|
+
const current = resolvedValue as [number, number];
|
|
186
|
+
const valStr = e.target.value;
|
|
187
|
+
setInputValue([current[0].toString(), valStr]);
|
|
188
|
+
|
|
189
|
+
const parsed = parseFloat(valStr);
|
|
190
|
+
if (!isNaN(parsed)) {
|
|
191
|
+
const clamped = Math.max(current[0], Math.min(max, parsed));
|
|
192
|
+
handleValueChange(null as unknown as Event, [current[0], clamped]);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const handleRangeInputBlur = () => {
|
|
197
|
+
const current = resolvedValue as [number, number];
|
|
198
|
+
setInputValue([current[0].toString(), current[1].toString()]);
|
|
144
199
|
};
|
|
145
200
|
|
|
146
201
|
// Props this component intentionally doesn't support — deleted at runtime so they can't leak
|
|
@@ -232,8 +287,12 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
232
287
|
|
|
233
288
|
// Duplicates the raw numeric value next to the track by default; when `tooltipLabel` is a
|
|
234
289
|
// formatter, reuse it here too so both displays agree instead of one showing raw numbers.
|
|
235
|
-
|
|
236
|
-
|
|
290
|
+
// Range mode formats each thumb independently and joins them with an en dash.
|
|
291
|
+
const displayValue = Array.isArray(resolvedValue)
|
|
292
|
+
? typeof tooltipLabel === "function"
|
|
293
|
+
? `${tooltipLabel(resolvedValue[0])} – ${tooltipLabel(resolvedValue[1])}`
|
|
294
|
+
: `${resolvedValue[0]} – ${resolvedValue[1]}`
|
|
295
|
+
: typeof tooltipLabel === "function"
|
|
237
296
|
? tooltipLabel(resolvedValue)
|
|
238
297
|
: resolvedValue;
|
|
239
298
|
|
|
@@ -272,6 +331,22 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
272
331
|
data-error={error ? "true" : undefined}
|
|
273
332
|
data-suppress-focus-ring={suppressFocusRing ? "true" : undefined}
|
|
274
333
|
>
|
|
334
|
+
{isRange && showInput && (
|
|
335
|
+
<input
|
|
336
|
+
type="number"
|
|
337
|
+
className={styles.inputField}
|
|
338
|
+
value={(inputValue as [string, string])[0]}
|
|
339
|
+
onChange={handleLowerInputChange}
|
|
340
|
+
onBlur={handleRangeInputBlur}
|
|
341
|
+
min={min}
|
|
342
|
+
max={(resolvedValue as [number, number])[1]}
|
|
343
|
+
step={step ?? undefined}
|
|
344
|
+
disabled={disabled}
|
|
345
|
+
data-error={error ? "true" : undefined}
|
|
346
|
+
aria-label="Minimum value"
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
|
|
275
350
|
{leadingIcon}
|
|
276
351
|
|
|
277
352
|
{showMinMaxLabels && (
|
|
@@ -319,22 +394,37 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
319
394
|
)}
|
|
320
395
|
</div>
|
|
321
396
|
|
|
322
|
-
{showInput && (
|
|
323
|
-
<input
|
|
324
|
-
type="number"
|
|
325
|
-
className={styles.inputField}
|
|
326
|
-
value={inputValue}
|
|
327
|
-
onChange={handleInputChange}
|
|
328
|
-
onBlur={handleInputBlur}
|
|
329
|
-
min={min}
|
|
330
|
-
max={max}
|
|
331
|
-
step={step ?? undefined}
|
|
332
|
-
disabled={disabled}
|
|
333
|
-
data-error={error ? "true" : undefined}
|
|
334
|
-
/>
|
|
335
|
-
)}
|
|
336
|
-
|
|
337
397
|
{trailingIconEl}
|
|
398
|
+
|
|
399
|
+
{showInput &&
|
|
400
|
+
(isRange ? (
|
|
401
|
+
<input
|
|
402
|
+
type="number"
|
|
403
|
+
className={styles.inputField}
|
|
404
|
+
value={(inputValue as [string, string])[1]}
|
|
405
|
+
onChange={handleUpperInputChange}
|
|
406
|
+
onBlur={handleRangeInputBlur}
|
|
407
|
+
min={(resolvedValue as [number, number])[0]}
|
|
408
|
+
max={max}
|
|
409
|
+
step={step ?? undefined}
|
|
410
|
+
disabled={disabled}
|
|
411
|
+
data-error={error ? "true" : undefined}
|
|
412
|
+
aria-label="Maximum value"
|
|
413
|
+
/>
|
|
414
|
+
) : (
|
|
415
|
+
<input
|
|
416
|
+
type="number"
|
|
417
|
+
className={styles.inputField}
|
|
418
|
+
value={inputValue as string}
|
|
419
|
+
onChange={handleInputChange}
|
|
420
|
+
onBlur={handleInputBlur}
|
|
421
|
+
min={min}
|
|
422
|
+
max={max}
|
|
423
|
+
step={step ?? undefined}
|
|
424
|
+
disabled={disabled}
|
|
425
|
+
data-error={error ? "true" : undefined}
|
|
426
|
+
/>
|
|
427
|
+
))}
|
|
338
428
|
</div>
|
|
339
429
|
}
|
|
340
430
|
/>
|
|
@@ -39,4 +39,18 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
|
|
|
39
39
|
|
|
40
40
|
## 4. Key Integration Features & Constraints
|
|
41
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.
|
|
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, rendered right after the max label and before the numeric input.
|
|
43
|
+
|
|
44
|
+
### Range Mode
|
|
45
|
+
|
|
46
|
+
Pass a `[number, number]` tuple as `value`/`defaultValue` to render a two-thumb range slider — `onChange`/`onChangeEnd` are then called with a `[number, number]` tuple instead of a `number`. With `showInput` enabled, a second numeric input for the upper bound appears after the trailing icon, with the lower-bound input before the leading icon:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<Slider
|
|
50
|
+
label="Price Range"
|
|
51
|
+
defaultValue={[20, 80]}
|
|
52
|
+
min={0}
|
|
53
|
+
max={100}
|
|
54
|
+
onChange={(value) => console.log(value)} // [number, number]
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/* HARDCODED VALUES
|
|
2
10
|
*
|
|
3
11
|
* 1. track border: none; (we do not use border for the switch track)
|