@recursica/mantine-adapter 0.47.0 → 0.48.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 +9 -4
- package/dist/mantine-adapter.cjs +2 -2
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +1633 -1559
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.module.css +11 -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 +16 -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 +22 -2
- package/src/components/Slider/Slider.module.css +36 -2
- package/src/components/Slider/Slider.stories.tsx +65 -0
- package/src/components/Slider/Slider.tsx +149 -42
- package/src/components/Slider/USAGE.md +15 -1
- package/src/components/Switch/Switch.module.css +8 -0
- package/src/components/Table/Table.module.css +6 -0
- package/src/components/Tabs/Tabs.module.css +7 -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 +11 -6
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React, { forwardRef, useState, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Slider as MantineSlider,
|
|
4
|
+
RangeSlider as MantineRangeSlider,
|
|
4
5
|
type SliderProps as MantineSliderProps,
|
|
6
|
+
type RangeSliderProps as MantineRangeSliderProps,
|
|
5
7
|
type InputWrapperProps,
|
|
6
8
|
} from "@mantine/core";
|
|
7
9
|
import { type ReadOnlyControlProps } from "@recursica/adapter-common";
|
|
@@ -28,6 +30,10 @@ export interface RecursicaSliderProps
|
|
|
28
30
|
| "classNames"
|
|
29
31
|
| "styles"
|
|
30
32
|
| "label"
|
|
33
|
+
| "value"
|
|
34
|
+
| "defaultValue"
|
|
35
|
+
| "onChange"
|
|
36
|
+
| "onChangeEnd"
|
|
31
37
|
>,
|
|
32
38
|
Omit<
|
|
33
39
|
RecursicaFormControlWrapperProps,
|
|
@@ -41,14 +47,19 @@ export type SliderProps = RecursicaOverStyled<RecursicaSliderProps>;
|
|
|
41
47
|
|
|
42
48
|
/**
|
|
43
49
|
* Custom Read-Only visual representation of the Slider value.
|
|
44
|
-
* Utilizes component-specific read-only typography variables.
|
|
50
|
+
* Utilizes component-specific read-only typography variables. Renders a "lower – upper" pair
|
|
51
|
+
* when the value is a range tuple.
|
|
45
52
|
*/
|
|
46
|
-
const SliderReadOnlyValue: React.FC<{ value: number }> = ({
|
|
47
|
-
|
|
53
|
+
const SliderReadOnlyValue: React.FC<{ value: number | [number, number] }> = ({
|
|
54
|
+
value,
|
|
55
|
+
}) => {
|
|
56
|
+
const display = Array.isArray(value) ? `${value[0]} – ${value[1]}` : value;
|
|
57
|
+
return <div className={styles.readOnlyValue}>{display}</div>;
|
|
48
58
|
};
|
|
49
59
|
|
|
50
60
|
/**
|
|
51
|
-
* Recursica Slider component wrapping Mantine's Slider
|
|
61
|
+
* Recursica Slider component wrapping Mantine's Slider (or, when `value`/`defaultValue` is a
|
|
62
|
+
* `[number, number]` tuple, Mantine's two-thumb RangeSlider).
|
|
52
63
|
*
|
|
53
64
|
* Implements a bidirectional text input field next to the slider track, responsive layouts,
|
|
54
65
|
* custom typography-bound min/max labels (optionally overridden via `minLabel`/`maxLabel`),
|
|
@@ -98,24 +109,38 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
98
109
|
...rest
|
|
99
110
|
} = props;
|
|
100
111
|
|
|
101
|
-
// Bidirectional state linking the slider track value to the input field string
|
|
102
|
-
|
|
112
|
+
// Bidirectional state linking the slider track value to the input field string
|
|
113
|
+
// representation. A `[number, number]` value/defaultValue switches the component into
|
|
114
|
+
// two-thumb range mode, backed by Mantine's RangeSlider instead of Slider.
|
|
115
|
+
type SliderValue = number | [number, number];
|
|
116
|
+
|
|
117
|
+
const [internalValue, setInternalValue] = useState<SliderValue>(() => {
|
|
103
118
|
if (value !== undefined) return value;
|
|
104
119
|
if (defaultValue !== undefined) return defaultValue;
|
|
105
120
|
return min;
|
|
106
121
|
});
|
|
107
122
|
|
|
108
|
-
const resolvedValue =
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
const resolvedValue: SliderValue =
|
|
124
|
+
value !== undefined ? value : internalValue;
|
|
125
|
+
const isRange = Array.isArray(resolvedValue);
|
|
126
|
+
|
|
127
|
+
const [inputValue, setInputValue] = useState<string | [string, string]>(
|
|
128
|
+
() =>
|
|
129
|
+
Array.isArray(resolvedValue)
|
|
130
|
+
? [resolvedValue[0].toString(), resolvedValue[1].toString()]
|
|
131
|
+
: resolvedValue.toString(),
|
|
111
132
|
);
|
|
112
133
|
|
|
113
|
-
// Synchronize text input whenever the slider value changes
|
|
134
|
+
// Synchronize text input(s) whenever the slider value changes
|
|
114
135
|
useEffect(() => {
|
|
115
|
-
setInputValue(
|
|
136
|
+
setInputValue(
|
|
137
|
+
Array.isArray(resolvedValue)
|
|
138
|
+
? [resolvedValue[0].toString(), resolvedValue[1].toString()]
|
|
139
|
+
: resolvedValue.toString(),
|
|
140
|
+
);
|
|
116
141
|
}, [resolvedValue]);
|
|
117
142
|
|
|
118
|
-
const handleValueChange = (val:
|
|
143
|
+
const handleValueChange = (val: SliderValue) => {
|
|
119
144
|
if (value === undefined) {
|
|
120
145
|
setInternalValue(val);
|
|
121
146
|
}
|
|
@@ -136,7 +161,38 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
136
161
|
|
|
137
162
|
const handleInputBlur = () => {
|
|
138
163
|
// Clean up text field on blur to reflect the final clamped resolved value
|
|
139
|
-
setInputValue(resolvedValue.toString());
|
|
164
|
+
setInputValue((resolvedValue as number).toString());
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Range-mode input handlers: each bound clamps against the other thumb rather than the
|
|
168
|
+
// shared min/max, so the lower thumb can never cross the upper one and vice versa.
|
|
169
|
+
const handleLowerInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
170
|
+
const current = resolvedValue as [number, number];
|
|
171
|
+
const valStr = e.target.value;
|
|
172
|
+
setInputValue([valStr, current[1].toString()]);
|
|
173
|
+
|
|
174
|
+
const parsed = parseFloat(valStr);
|
|
175
|
+
if (!isNaN(parsed)) {
|
|
176
|
+
const clamped = Math.max(min, Math.min(current[1], parsed));
|
|
177
|
+
handleValueChange([clamped, current[1]]);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleUpperInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
182
|
+
const current = resolvedValue as [number, number];
|
|
183
|
+
const valStr = e.target.value;
|
|
184
|
+
setInputValue([current[0].toString(), valStr]);
|
|
185
|
+
|
|
186
|
+
const parsed = parseFloat(valStr);
|
|
187
|
+
if (!isNaN(parsed)) {
|
|
188
|
+
const clamped = Math.max(current[0], Math.min(max, parsed));
|
|
189
|
+
handleValueChange([current[0], clamped]);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleRangeInputBlur = () => {
|
|
194
|
+
const current = resolvedValue as [number, number];
|
|
195
|
+
setInputValue([current[0].toString(), current[1].toString()]);
|
|
140
196
|
};
|
|
141
197
|
|
|
142
198
|
// Props this component intentionally doesn't support — deleted at runtime so they can't leak
|
|
@@ -164,6 +220,7 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
164
220
|
track: styles.sliderTrack,
|
|
165
221
|
bar: styles.sliderBar,
|
|
166
222
|
thumb: styles.sliderThumb,
|
|
223
|
+
markWrapper: styles.sliderMarkWrapper,
|
|
167
224
|
mark: styles.sliderMark,
|
|
168
225
|
markLabel: styles.sliderMarkLabel,
|
|
169
226
|
},
|
|
@@ -188,8 +245,12 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
188
245
|
|
|
189
246
|
// Duplicates the raw numeric value next to the track by default; when `tooltipLabel` is a
|
|
190
247
|
// formatter, reuse it here too so both displays agree instead of one showing raw numbers.
|
|
191
|
-
|
|
192
|
-
|
|
248
|
+
// Range mode formats each thumb independently and joins them with an en dash.
|
|
249
|
+
const displayValue = Array.isArray(resolvedValue)
|
|
250
|
+
? typeof tooltipLabel === "function"
|
|
251
|
+
? `${tooltipLabel(resolvedValue[0])} – ${tooltipLabel(resolvedValue[1])}`
|
|
252
|
+
: `${resolvedValue[0]} – ${resolvedValue[1]}`
|
|
253
|
+
: typeof tooltipLabel === "function"
|
|
193
254
|
? tooltipLabel(resolvedValue)
|
|
194
255
|
: resolvedValue;
|
|
195
256
|
|
|
@@ -228,6 +289,22 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
228
289
|
data-disabled={disabled ? "true" : undefined}
|
|
229
290
|
data-error={error ? "true" : undefined}
|
|
230
291
|
>
|
|
292
|
+
{isRange && showInput && (
|
|
293
|
+
<input
|
|
294
|
+
type="number"
|
|
295
|
+
className={styles.inputField}
|
|
296
|
+
value={(inputValue as [string, string])[0]}
|
|
297
|
+
onChange={handleLowerInputChange}
|
|
298
|
+
onBlur={handleRangeInputBlur}
|
|
299
|
+
min={min}
|
|
300
|
+
max={(resolvedValue as [number, number])[1]}
|
|
301
|
+
step={step}
|
|
302
|
+
disabled={disabled}
|
|
303
|
+
data-error={error ? "true" : undefined}
|
|
304
|
+
aria-label="Minimum value"
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
307
|
+
|
|
231
308
|
{leadingIcon}
|
|
232
309
|
|
|
233
310
|
{showMinMaxLabels && (
|
|
@@ -235,18 +312,33 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
235
312
|
)}
|
|
236
313
|
|
|
237
314
|
<div className={styles.sliderTrackWrapper}>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
315
|
+
{isRange ? (
|
|
316
|
+
<MantineRangeSlider
|
|
317
|
+
{...(sanitizedProps as unknown as MantineRangeSliderProps)}
|
|
318
|
+
classNames={mergedClassNames}
|
|
319
|
+
disabled={disabled}
|
|
320
|
+
value={resolvedValue as [number, number]}
|
|
321
|
+
onChange={handleValueChange}
|
|
322
|
+
onChangeEnd={onChangeEnd}
|
|
323
|
+
min={min}
|
|
324
|
+
max={max}
|
|
325
|
+
step={step}
|
|
326
|
+
label={tooltipLabel}
|
|
327
|
+
/>
|
|
328
|
+
) : (
|
|
329
|
+
<MantineSlider
|
|
330
|
+
{...(sanitizedProps as unknown as MantineSliderProps)}
|
|
331
|
+
classNames={mergedClassNames}
|
|
332
|
+
disabled={disabled}
|
|
333
|
+
value={resolvedValue as number}
|
|
334
|
+
onChange={handleValueChange}
|
|
335
|
+
onChangeEnd={onChangeEnd}
|
|
336
|
+
min={min}
|
|
337
|
+
max={max}
|
|
338
|
+
step={step}
|
|
339
|
+
label={tooltipLabel}
|
|
340
|
+
/>
|
|
341
|
+
)}
|
|
250
342
|
</div>
|
|
251
343
|
|
|
252
344
|
<div className={styles.rightGuideContainer}>
|
|
@@ -258,22 +350,37 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
258
350
|
)}
|
|
259
351
|
</div>
|
|
260
352
|
|
|
261
|
-
{showInput && (
|
|
262
|
-
<input
|
|
263
|
-
type="number"
|
|
264
|
-
className={styles.inputField}
|
|
265
|
-
value={inputValue}
|
|
266
|
-
onChange={handleInputChange}
|
|
267
|
-
onBlur={handleInputBlur}
|
|
268
|
-
min={min}
|
|
269
|
-
max={max}
|
|
270
|
-
step={step}
|
|
271
|
-
disabled={disabled}
|
|
272
|
-
data-error={error ? "true" : undefined}
|
|
273
|
-
/>
|
|
274
|
-
)}
|
|
275
|
-
|
|
276
353
|
{trailingIconEl}
|
|
354
|
+
|
|
355
|
+
{showInput &&
|
|
356
|
+
(isRange ? (
|
|
357
|
+
<input
|
|
358
|
+
type="number"
|
|
359
|
+
className={styles.inputField}
|
|
360
|
+
value={(inputValue as [string, string])[1]}
|
|
361
|
+
onChange={handleUpperInputChange}
|
|
362
|
+
onBlur={handleRangeInputBlur}
|
|
363
|
+
min={(resolvedValue as [number, number])[0]}
|
|
364
|
+
max={max}
|
|
365
|
+
step={step}
|
|
366
|
+
disabled={disabled}
|
|
367
|
+
data-error={error ? "true" : undefined}
|
|
368
|
+
aria-label="Maximum value"
|
|
369
|
+
/>
|
|
370
|
+
) : (
|
|
371
|
+
<input
|
|
372
|
+
type="number"
|
|
373
|
+
className={styles.inputField}
|
|
374
|
+
value={inputValue as string}
|
|
375
|
+
onChange={handleInputChange}
|
|
376
|
+
onBlur={handleInputBlur}
|
|
377
|
+
min={min}
|
|
378
|
+
max={max}
|
|
379
|
+
step={step}
|
|
380
|
+
disabled={disabled}
|
|
381
|
+
data-error={error ? "true" : undefined}
|
|
382
|
+
/>
|
|
383
|
+
))}
|
|
277
384
|
</div>
|
|
278
385
|
}
|
|
279
386
|
/>
|
|
@@ -39,4 +39,18 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
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 Mantine'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 Mantine'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`. This is a real component swap under the hood (Mantine's `RangeSlider`, not `Slider`), so `value` and `defaultValue` must agree on shape for a given instance. 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)
|
|
@@ -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-collapse: separate; border-spacing: 0; (To support custom border-radius on table elements)
|
|
3
9
|
* - overflow: hidden; (To prevent content from bleeding outside the rounded table border)
|
|
@@ -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_hover_color
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_disabled
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
/*
|
|
2
9
|
Recursica Tabs CSS module natively binding to Figma variables.
|
|
3
10
|
*/
|
|
@@ -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 --textarea-control-{max,min}-width hooks consumed inline in TextArea.tsx, since
|
|
@@ -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 --text-field-control-{max,min}-width hooks consumed inline in TextField.tsx,
|
|
@@ -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
|
.layoutOverride {
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
- No MUI-style leftover "default selected background" issue existed here — Mantine's own `Tree` has no default row styling of its own once a custom `renderNode` is supplied (see the very first entry in this file), so there was nothing to neutralize on this side.
|
|
41
41
|
|
|
42
42
|
- **Whole-tree `disabled` (Matt Massey, 2026-08-10), added to support a `Disabled` story.** Mantine's `Tree`/`useTree`/`TreeNode` have no `disabled` concept anywhere in their API, unlike `@mui/x-tree-view` (which already had per-item `disabled` plumbing sitting mostly unused — see mui-adapter's own note on this). Per-node disabling still isn't exposed (no token, no `disabled` field on `RecursicaTreeNode` — same reasoning as the existing "Deliberately not implemented" entry above), only a single whole-tree toggle.
|
|
43
|
+
|
|
43
44
|
- **Mouse**: `selectOnClick={!disabled}` reuses Mantine's own flag for row clicks. The chevron `Button`'s `onClick` bypasses that flag entirely (calls `tree.toggleExpanded` directly), so it needs its own explicit `if (disabled) return;` guard. `.root[data-disabled] { pointer-events: none; }` is a second, CSS-only backstop covering both at once — belt-and-suspenders, not strictly required given the two guards above, but consistent with how little the library gives us to rely on here.
|
|
44
45
|
- **Keyboard**: our own `Enter`/`Space` → `select` listener (added in the entry above) just checks `disabled` at the top now. `ArrowLeft`/`ArrowRight` expand/collapse is baked into Mantine's own `TreeNode.handleKeyDown`, on the `<li>` itself, with no prop to disable it — the only reachable way to block it is a _second_, capture-phase `keydown` listener on the tree root that unconditionally calls `stopPropagation()` when disabled. Since capture fires before the event ever reaches its target (the focused `<li>`), this keeps Mantine's internal handler — and our own bubble-phase listener on the same root — from ever running, without needing to fork `TreeNode`.
|
|
45
46
|
- **Visual**: `opacity: var(--recursica_brand_states_disabled)` on `.root` — the generic disabled token, same convention used everywhere else in the design system for components without a dedicated disabled token (no `tree`-specific one exists). A selected node's chip stays visible underneath, just dimmed along with everything else, per Matt's ask to verify that combination looks right.
|
|
47
|
+
|
|
48
|
+
- **Chevron `Button` could still steal DOM focus despite `tabIndex={-1}` (Matt Massey, 2026-08-24), caught via a live browser console warning.** The embedded chevron `Button`'s `tabIndex={-1}`/`aria-hidden` combination (added when the chevron became a real `Button` — see above) only removes it from the sequential tab order; it doesn't stop a native `<button>` from receiving DOM focus on a direct mouse click, which browsers still do regardless of `tabIndex`. Since the button also has `aria-hidden="true"`, a click on it left an aria-hidden element holding focus — flagged by Chrome as "Blocked aria-hidden on an element because its descendant retained focus." Fixed with `onMouseDown={(event) => event.preventDefault()}` on the same `Button`: this suppresses the browser's default click-focuses-the-target behavior without affecting the `onClick` handler (`click` still fires normally on mouseup). Same fix applied to mui-adapter's `ExpandToggleButton`.
|
|
@@ -1,3 +1,14 @@
|
|
|
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
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
9
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
/* HARDCODED VALUES:
|
|
2
13
|
* - list-style/margin/padding resets on .root/.subtree: layout resets, no corresponding
|
|
3
14
|
* design tokens (structural, not visual design values).
|
|
@@ -65,12 +65,16 @@ function createRenderTreeNode(disabled: boolean) {
|
|
|
65
65
|
row. Stops propagation so the click never also reaches `.row`'s own handler and
|
|
66
66
|
selects the node. Never independently focusable/tab-stoppable (tabIndex={-1},
|
|
67
67
|
aria-hidden), so the row stays the only focusable element and this button never
|
|
68
|
-
shows its own focus state.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
shows its own focus state. `onMouseDown` prevents the browser's default
|
|
69
|
+
click-focuses-the-button behavior (tabIndex={-1} only removes it from the tab
|
|
70
|
+
order, it doesn't stop a direct mouse click from focusing it) — without this, a
|
|
71
|
+
click here leaves the (aria-hidden) button focused, which browsers now flag as an
|
|
72
|
+
accessibility violation ("focus must not be hidden from assistive technology").
|
|
73
|
+
Always rendered, even for leaf nodes, so every row reserves the same layout space;
|
|
74
|
+
CSS hides it (visibility, not display) when there are no children to toggle. Guards
|
|
75
|
+
`disabled` itself (not just via the CSS `pointer-events: none` on `.root`) since this
|
|
76
|
+
handler bypasses Mantine's own `expandOnClick`/`selectOnClick` flags entirely by
|
|
77
|
+
calling `tree.toggleExpanded` directly. */}
|
|
74
78
|
<Button
|
|
75
79
|
overStyled
|
|
76
80
|
variant="text"
|
|
@@ -80,6 +84,7 @@ function createRenderTreeNode(disabled: boolean) {
|
|
|
80
84
|
aria-hidden="true"
|
|
81
85
|
tabIndex={-1}
|
|
82
86
|
className={styles.expandButton}
|
|
87
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
83
88
|
onClick={(event) => {
|
|
84
89
|
event.stopPropagation();
|
|
85
90
|
if (disabled) return;
|