@recursica/mantine-adapter 0.36.1 → 0.38.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 +22 -0
- 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 +2352 -2099
- package/dist/mantine-adapter.js.map +1 -1
- package/dist/src/components/Dropdown/BareDropdown.d.ts +19 -0
- package/dist/src/components/TimePicker/TimePicker.d.ts +9 -3
- package/dist/src/components/Tree/Tree.d.ts +5 -0
- package/dist/src/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/USAGE.md +2 -41
- package/src/components/AutoComplete/USAGE.md +2 -18
- package/src/components/Avatar/USAGE.md +0 -27
- package/src/components/Badge/USAGE.md +3 -6
- package/src/components/Breadcrumb/USAGE.md +1 -5
- package/src/components/Button/Button.tsx +5 -0
- package/src/components/Button/IMPLEMENTATION_NOTES.md +8 -0
- package/src/components/Button/USAGE.md +5 -25
- package/src/components/Card/USAGE.md +4 -12
- package/src/components/Checkbox/USAGE.md +4 -20
- package/src/components/Chip/USAGE.md +3 -32
- package/src/components/DatePicker/USAGE.md +2 -12
- package/src/components/Dropdown/BareDropdown.tsx +85 -0
- package/src/components/Dropdown/Dropdown.tsx +12 -3
- package/src/components/Dropdown/USAGE.md +2 -6
- package/src/components/Flex/USAGE.md +1 -1
- package/src/components/FormControlWrapper/USAGE.md +3 -31
- package/src/components/Grid/USAGE.md +1 -1
- package/src/components/Group/USAGE.md +1 -1
- package/src/components/HoverCard/USAGE.md +3 -72
- package/src/components/Label/USAGE.md +8 -48
- package/src/components/Link/USAGE.md +4 -10
- package/src/components/Loader/USAGE.md +4 -23
- package/src/components/Menu/USAGE.md +3 -73
- package/src/components/Modal/USAGE.md +3 -3
- package/src/components/NumberInput/USAGE.md +5 -8
- package/src/components/Pagination/USAGE.md +0 -19
- package/src/components/Panel/USAGE.md +6 -95
- package/src/components/Popover/USAGE.md +6 -66
- package/src/components/ReadOnlyField/USAGE.md +2 -10
- package/src/components/SegmentedControl/USAGE.md +1 -15
- package/src/components/Slider/USAGE.md +1 -45
- package/src/components/Stack/USAGE.md +1 -1
- package/src/components/Switch/USAGE.md +1 -24
- package/src/components/TextArea/USAGE.md +2 -2
- package/src/components/TextField/USAGE.md +1 -17
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +72 -0
- package/src/components/TimePicker/TimePicker.module.css +225 -41
- package/src/components/TimePicker/TimePicker.stories.tsx +105 -4
- package/src/components/TimePicker/TimePicker.tsx +287 -7
- package/src/components/TimePicker/USAGE.md +23 -2
- package/src/components/Timeline/USAGE.md +2 -10
- package/src/components/Toast/USAGE.md +3 -33
- package/src/components/Tooltip/USAGE.md +7 -51
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +31 -3
- package/src/components/Tree/Tree.module.css +84 -40
- package/src/components/Tree/Tree.stories.tsx +13 -0
- package/src/components/Tree/Tree.tsx +116 -27
- package/src/components/Tree/USAGE.md +18 -1
- package/src/index.ts +1 -0
|
@@ -1,9 +1,289 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import {
|
|
1
|
+
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
TimePicker as MantineTimePicker,
|
|
4
|
+
type TimePickerProps as MantineTimePickerProps,
|
|
5
|
+
} from "@mantine/dates";
|
|
6
|
+
import { type InputWrapperProps } from "@mantine/core";
|
|
7
|
+
import { type ReadOnlyControlProps } from "@recursica/adapter-common";
|
|
8
|
+
import {
|
|
9
|
+
filterStylingProps,
|
|
10
|
+
type RecursicaOverStyled,
|
|
11
|
+
} from "../../utils/filterStylingProps";
|
|
12
|
+
import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
|
|
13
|
+
import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
|
|
14
|
+
import { BareDropdown } from "../Dropdown/BareDropdown";
|
|
15
|
+
import styles from "./TimePicker.module.css";
|
|
3
16
|
|
|
4
|
-
|
|
5
|
-
RecursicaTimePickerProps;
|
|
17
|
+
import { type RecursicaTimePickerProps as BaseRecursicaTimePickerProps } from "@recursica/adapter-common";
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
19
|
+
const AM_PM_DATA = [
|
|
20
|
+
{ value: "AM", label: "AM" },
|
|
21
|
+
{ value: "PM", label: "PM" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** Parses an "HH:mm"/"HH:mm:ss" string's hour, or undefined if not set/parseable. */
|
|
25
|
+
function getHour(value: string | undefined): number | undefined {
|
|
26
|
+
if (!value) return undefined;
|
|
27
|
+
const hour = parseInt(value.slice(0, 2), 10);
|
|
28
|
+
return Number.isNaN(hour) ? undefined : hour;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Replaces the hour segment of an "HH:mm"/"HH:mm:ss" string, preserving minutes/seconds. */
|
|
32
|
+
function withHour(value: string, hour: number): string {
|
|
33
|
+
return `${String(hour).padStart(2, "0")}${value.slice(2)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Formats an "HH:mm"/"HH:mm:ss" 24-hour value as a 12-hour + AM/PM string for read-only display
|
|
38
|
+
* (e.g. "14:30" -> "2:30 PM") — the raw 24-hour string was being shown as-is in read-only mode,
|
|
39
|
+
* with no AM/PM, unlike the interactive composite. Returns undefined if not parseable.
|
|
40
|
+
*/
|
|
41
|
+
function formatReadOnlyTime(value: string | undefined): string | undefined {
|
|
42
|
+
if (!value) return undefined;
|
|
43
|
+
const [hourStr, minute, second] = value.split(":");
|
|
44
|
+
const hour24 = parseInt(hourStr, 10);
|
|
45
|
+
if (Number.isNaN(hour24) || minute === undefined) return value;
|
|
46
|
+
const isPM = hour24 >= 12;
|
|
47
|
+
const hour12 = hour24 % 12 === 0 ? 12 : hour24 % 12;
|
|
48
|
+
const rest = second !== undefined ? `${minute}:${second}` : minute;
|
|
49
|
+
return `${hour12}:${rest} ${isPM ? "PM" : "AM"}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Simulates a real user interaction on Mantine's own (CSS-hidden) native AM/PM <select>, since it's
|
|
54
|
+
* a React-controlled element — setting `.value` directly and dispatching a plain DOM event doesn't
|
|
55
|
+
* trigger React's change handling; using the native property setter first does. See "Why AM/PM is
|
|
56
|
+
* seeded on mount" in TIMEPICKER_IMPLEMENTATION_NOTES.md.
|
|
57
|
+
*/
|
|
58
|
+
function setNativeSelectValue(el: HTMLSelectElement, value: string): void {
|
|
59
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
60
|
+
window.HTMLSelectElement.prototype,
|
|
61
|
+
"value",
|
|
62
|
+
)?.set;
|
|
63
|
+
nativeSetter?.call(el, value);
|
|
64
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RecursicaTimePickerProps
|
|
68
|
+
extends Omit<
|
|
69
|
+
MantineTimePickerProps,
|
|
70
|
+
| "size"
|
|
71
|
+
| "variant"
|
|
72
|
+
| "radius"
|
|
73
|
+
| "wrapperProps"
|
|
74
|
+
| "format"
|
|
75
|
+
| "min"
|
|
76
|
+
| "max"
|
|
77
|
+
// AM/PM is always shown via our own BareDropdown, driving a fixed 12h format — these all
|
|
78
|
+
// control Mantine's own native (now CSS-hidden) AM/PM select and would be misleading to
|
|
79
|
+
// expose, since they'd have no visible effect. See TIMEPICKER_IMPLEMENTATION_NOTES.md.
|
|
80
|
+
| "amPmInputLabel"
|
|
81
|
+
| "amPmLabels"
|
|
82
|
+
| "amPmSelectProps"
|
|
83
|
+
| "amPmRef"
|
|
84
|
+
// The optional time-presets dropdown isn't wired up; keep the public API to what's supported.
|
|
85
|
+
| "withDropdown"
|
|
86
|
+
| "presets"
|
|
87
|
+
| "maxDropdownContentHeight"
|
|
88
|
+
| "scrollAreaProps"
|
|
89
|
+
| "reverseTimeControlsList"
|
|
90
|
+
| "popoverProps"
|
|
91
|
+
>,
|
|
92
|
+
Pick<
|
|
93
|
+
InputWrapperProps,
|
|
94
|
+
"label" | "error" | "required" | "withAsterisk" | "id"
|
|
95
|
+
>,
|
|
96
|
+
Omit<
|
|
97
|
+
RecursicaFormControlWrapperProps,
|
|
98
|
+
"controlMaxWidth" | "controlMinWidth"
|
|
99
|
+
>,
|
|
100
|
+
ReadOnlyControlProps,
|
|
101
|
+
BaseRecursicaTimePickerProps {}
|
|
102
|
+
|
|
103
|
+
export type TimePickerProps = RecursicaOverStyled<RecursicaTimePickerProps>;
|
|
104
|
+
|
|
105
|
+
export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|
106
|
+
function TimePicker(props, ref) {
|
|
107
|
+
const {
|
|
108
|
+
overStyled = false,
|
|
109
|
+
formLayout = "stacked",
|
|
110
|
+
|
|
111
|
+
// Label & Wrapper Maps
|
|
112
|
+
labelSize,
|
|
113
|
+
labelAlignment,
|
|
114
|
+
labelOptionalText,
|
|
115
|
+
labelWithEditIcon,
|
|
116
|
+
onLabelEditClick,
|
|
117
|
+
|
|
118
|
+
label,
|
|
119
|
+
assistiveText,
|
|
120
|
+
assistiveWithIcon,
|
|
121
|
+
error,
|
|
122
|
+
required,
|
|
123
|
+
withAsterisk,
|
|
124
|
+
id,
|
|
125
|
+
className,
|
|
126
|
+
style,
|
|
127
|
+
disabled,
|
|
128
|
+
readOnly,
|
|
129
|
+
readOnlyComponent,
|
|
130
|
+
emptyValueComponent,
|
|
131
|
+
value,
|
|
132
|
+
defaultValue,
|
|
133
|
+
onChange,
|
|
134
|
+
withSeconds,
|
|
135
|
+
minTime,
|
|
136
|
+
maxTime,
|
|
137
|
+
...rest
|
|
138
|
+
} = props;
|
|
139
|
+
|
|
140
|
+
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
141
|
+
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
142
|
+
|
|
143
|
+
delete restRecord["size"];
|
|
144
|
+
delete restRecord["variant"];
|
|
145
|
+
delete restRecord["radius"];
|
|
146
|
+
|
|
147
|
+
// Internal full 24-hour value. Needed because Mantine's TimePicker (hour/minute/second entry)
|
|
148
|
+
// and our own BareDropdown (AM/PM) both mutate the same conceptual value — see
|
|
149
|
+
// TIMEPICKER_IMPLEMENTATION_NOTES.md.
|
|
150
|
+
const [internalValue, setInternalValue] = useState<string | undefined>(
|
|
151
|
+
() => value ?? defaultValue,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (value !== undefined) {
|
|
156
|
+
setInternalValue(value);
|
|
157
|
+
}
|
|
158
|
+
}, [value]);
|
|
159
|
+
|
|
160
|
+
// Mantine's own internal amPm state starts `null` whenever there's no initial hour to derive it
|
|
161
|
+
// from (see convertTimeTo12HourFormat in @mantine/dates), and it stays null — meaning Mantine
|
|
162
|
+
// never reports a valid onChange, no matter what's typed — until something interacts with the
|
|
163
|
+
// (CSS-hidden) native AM/PM <select>. Simulating that interaction once on mount, defaulting to
|
|
164
|
+
// AM, breaks the deadlock: a freshly-typed time now resolves and reports immediately, and our
|
|
165
|
+
// own BareDropdown (which drives the same native select the same way, see handleMeridiemChange)
|
|
166
|
+
// correctly displays and changes it from there. Skipped whenever a real initial value/defaultValue
|
|
167
|
+
// is already present — Mantine already derives the correct AM/PM from that on its own. See
|
|
168
|
+
// TIMEPICKER_IMPLEMENTATION_NOTES.md.
|
|
169
|
+
const amPmRef = useRef<HTMLSelectElement>(null);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (getHour(value ?? defaultValue) === undefined && amPmRef.current) {
|
|
172
|
+
setNativeSelectValue(amPmRef.current, "AM");
|
|
173
|
+
}
|
|
174
|
+
// Intentionally mount-only — this seeds Mantine's internal state once; after that it's driven
|
|
175
|
+
// by real interaction (typing, or our own BareDropdown).
|
|
176
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
const emitChange = (next: string) => {
|
|
180
|
+
setInternalValue(next);
|
|
181
|
+
onChange?.(next);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const hour = getHour(internalValue);
|
|
185
|
+
const isPM = hour !== undefined && hour >= 12;
|
|
186
|
+
|
|
187
|
+
const handleFieldChange = (next: string) => {
|
|
188
|
+
emitChange(next);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleMeridiemChange = (next: string | null) => {
|
|
192
|
+
if (hour === undefined || !internalValue || !next) return;
|
|
193
|
+
const wantsPM = next === "PM";
|
|
194
|
+
if (wantsPM === isPM) return;
|
|
195
|
+
const nextHour = wantsPM ? hour + 12 : hour - 12;
|
|
196
|
+
emitChange(withHour(internalValue, nextHour));
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const wrapperClass = className
|
|
200
|
+
? `${styles.layoutOverride} ${className}`
|
|
201
|
+
: styles.layoutOverride;
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<WithReadOnlyWrapper
|
|
205
|
+
className={wrapperClass}
|
|
206
|
+
style={style as React.CSSProperties}
|
|
207
|
+
controlMaxWidth={undefined}
|
|
208
|
+
controlMinWidth={undefined}
|
|
209
|
+
overStyled={overStyled as true}
|
|
210
|
+
formLayout={formLayout}
|
|
211
|
+
labelSize={labelSize}
|
|
212
|
+
labelAlignment={labelAlignment}
|
|
213
|
+
labelOptionalText={labelOptionalText}
|
|
214
|
+
labelWithEditIcon={labelWithEditIcon}
|
|
215
|
+
onLabelEditClick={onLabelEditClick}
|
|
216
|
+
label={label}
|
|
217
|
+
assistiveText={assistiveText}
|
|
218
|
+
assistiveWithIcon={assistiveWithIcon}
|
|
219
|
+
error={error}
|
|
220
|
+
required={required}
|
|
221
|
+
withAsterisk={withAsterisk}
|
|
222
|
+
id={id}
|
|
223
|
+
readOnly={readOnly}
|
|
224
|
+
readOnlyComponent={readOnlyComponent}
|
|
225
|
+
emptyValueComponent={emptyValueComponent}
|
|
226
|
+
readOnlyType="text"
|
|
227
|
+
readOnlyValue={formatReadOnlyTime(
|
|
228
|
+
value !== undefined ? value : defaultValue,
|
|
229
|
+
)}
|
|
230
|
+
readOnlyNativeProps={props}
|
|
231
|
+
activeComponent={
|
|
232
|
+
/* Naked field execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks.
|
|
233
|
+
format="12h" is always on — this is the only way this component operates, not a user
|
|
234
|
+
choice (see TIMEPICKER_IMPLEMENTATION_NOTES.md). Mantine's own native AM/PM <select>
|
|
235
|
+
(bundled unconditionally with format="12h") is CSS-hidden; our own BareDropdown next to
|
|
236
|
+
it is the only AM/PM control the user interacts with. */
|
|
237
|
+
<div
|
|
238
|
+
className={styles.root}
|
|
239
|
+
data-disabled={disabled ? "true" : undefined}
|
|
240
|
+
data-error={error ? "true" : undefined}
|
|
241
|
+
>
|
|
242
|
+
<MantineTimePicker
|
|
243
|
+
ref={ref}
|
|
244
|
+
classNames={{
|
|
245
|
+
wrapper: styles.timeWrapper,
|
|
246
|
+
input: styles.timeInput,
|
|
247
|
+
fieldsGroup: styles.fieldsGroup,
|
|
248
|
+
field: styles.timeField,
|
|
249
|
+
}}
|
|
250
|
+
disabled={disabled}
|
|
251
|
+
value={internalValue}
|
|
252
|
+
onChange={handleFieldChange}
|
|
253
|
+
format="12h"
|
|
254
|
+
withSeconds={withSeconds}
|
|
255
|
+
min={minTime}
|
|
256
|
+
max={maxTime}
|
|
257
|
+
withDropdown={false}
|
|
258
|
+
// Internal-only — not part of the public API (see the Omit list above) — used solely
|
|
259
|
+
// to seed the mount-time AM default onto Mantine's own hidden native select. See
|
|
260
|
+
// TIMEPICKER_IMPLEMENTATION_NOTES.md.
|
|
261
|
+
amPmRef={amPmRef}
|
|
262
|
+
{...(sanitizedProps as unknown as MantineTimePickerProps)}
|
|
263
|
+
/>
|
|
264
|
+
<BareDropdown
|
|
265
|
+
overStyled
|
|
266
|
+
className={styles.amPmSelect}
|
|
267
|
+
// Dropdown.module.css's own .root sets width: 100% (correct for a standalone
|
|
268
|
+
// Dropdown filling its form-control column) — overStyled lets us override just the
|
|
269
|
+
// width, keeping every other Recursica style (border, colors, padding) intact.
|
|
270
|
+
// A plain `style` prop won't do this: Mantine's Select/InputBase internals
|
|
271
|
+
// (useInputProps) route a top-level `style` prop to the *label* InputWrapper, not
|
|
272
|
+
// the bordered input box itself — `styles={{ wrapper: ... }}` is the styles-api hook
|
|
273
|
+
// that actually targets that box. See TIMEPICKER_IMPLEMENTATION_NOTES.md.
|
|
274
|
+
styles={{ wrapper: { width: "fit-content" } }}
|
|
275
|
+
data={AM_PM_DATA}
|
|
276
|
+
value={hour === undefined ? null : isPM ? "PM" : "AM"}
|
|
277
|
+
onChange={handleMeridiemChange}
|
|
278
|
+
disabled={disabled}
|
|
279
|
+
error={!!error}
|
|
280
|
+
aria-label="AM or PM"
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
}
|
|
284
|
+
/>
|
|
285
|
+
);
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
TimePicker.displayName = "TimePicker";
|
|
@@ -19,10 +19,25 @@ import React from "react";
|
|
|
19
19
|
import { TimePicker } from "@recursica/mantine-adapter";
|
|
20
20
|
|
|
21
21
|
export default function Demo() {
|
|
22
|
-
return <TimePicker label="Select Time"
|
|
22
|
+
return <TimePicker label="Select Time" />;
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
> [!IMPORTANT] > **Recursica-specific behavior:** `TimePicker` always renders in **12-hour format with a dedicated AM/PM `Dropdown`-style selector** next to the hour/minute input — this deviates from the underlying Mantine library's own default (24-hour, no AM/PM control) and is **not configurable**. There is no prop to switch to a plain 24-hour input; this is the only way the component operates.
|
|
27
|
+
|
|
28
|
+
Pass `withSeconds` to add a seconds segment, and `minTime`/`maxTime` (`"HH:mm"` or `"HH:mm:ss"` with `withSeconds`) to bound the allowed range — these always describe 24-hour boundaries.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<TimePicker
|
|
32
|
+
label="Precise Time"
|
|
33
|
+
withSeconds
|
|
34
|
+
minTime="09:00:00"
|
|
35
|
+
maxTime="17:00:00"
|
|
36
|
+
/>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The AM/PM control visually matches Recursica's `Dropdown` component exactly, rather than a native `<select>`.
|
|
40
|
+
|
|
26
41
|
---
|
|
27
42
|
|
|
28
43
|
## 3. Design System Integration
|
|
@@ -31,6 +46,12 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
31
46
|
|
|
32
47
|
> [!IMPORTANT]
|
|
33
48
|
>
|
|
34
|
-
> - **Anti-override protection**:
|
|
49
|
+
> - **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.
|
|
35
50
|
> - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
|
|
36
51
|
> - **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.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 4. Read-Only Mode
|
|
56
|
+
|
|
57
|
+
Pass `readOnly` to render the current value as static text, matching every other Recursica form control. The value is formatted as 12-hour + AM/PM (e.g. `"14:30"` displays as `"2:30 PM"`).
|
|
@@ -44,14 +44,6 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
44
44
|
|
|
45
45
|
## 4. Key Integration Features & Constraints
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
`Timeline.Item` accepts a `timestamp` prop that renders below the item's content, and a `bulletVariant` prop (`"default" | "avatar" | "icon" | "icon-alternative"`) to control the bullet's appearance. The `lineWidth` and `bulletSize` props are not configurable, since geometry is controlled by the design system tokens.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- `Timeline.tsx` intercepts overarching properties like `lineWidth` and `bulletSize` to strip them out via `overStyled`, strictly adhering to the CSS token mapping in `.item` rules instead.
|
|
52
|
-
- `TimelineItem.tsx` implements a custom `timestamp` React node rendering slot to match the design system, positioning the text directly below the item's `children`.
|
|
53
|
-
- `TimelineItem.tsx` supports a custom `bulletVariant` prop (`"default" | "avatar" | "icon" | "icon-alternative"`) mapped onto `data-variant` to handle CSS variations dynamically.
|
|
54
|
-
|
|
55
|
-
## Limitations & Missing Tokens
|
|
56
|
-
|
|
57
|
-
- **Avatar Bullet Size**: There is no specific pixel variable provided for the Avatar bullet size in the UI kit tokens (`avatar-size` evaluates to `"default"`). To maintain exact mathematical centering with Mantine's connector line `calc()` equations, the CSS falls back to inheriting the `default` bullet size (`20px`) for avatar nodes natively. If users supply a custom sized `img` tag, it must adhere to inline structural constraints or flex mappings.
|
|
49
|
+
A known limitation: when using `bulletVariant="avatar"`, the avatar bullet always renders at the default bullet size rather than a custom size.
|
|
@@ -23,7 +23,7 @@ export default function Demo() {
|
|
|
23
23
|
<Toast
|
|
24
24
|
title="Success"
|
|
25
25
|
message="Your action completed successfully"
|
|
26
|
-
|
|
26
|
+
variant="success"
|
|
27
27
|
/>
|
|
28
28
|
);
|
|
29
29
|
}
|
|
@@ -45,36 +45,6 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
45
45
|
|
|
46
46
|
## 4. Key Integration Features & Constraints
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
`Toast` can be used directly for a static or inline message, or wired up to `@mantine/notifications` for dynamic popups. The `variant` prop (`"default" | "error" | "success"`) controls the toast's color treatment.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
**Implementation:** The UI Kit provides variables for the `Toast` component itself (e.g., `--recursica_ui-kit_components_toast_*`). We use these variables to style the standard Mantine `Notification` element. This allows developers to use `<Toast>` manually if they want a static or inline message.
|
|
53
|
-
|
|
54
|
-
If dynamic popups are required, developers can configure `@mantine/notifications` to utilize this component or use its classes.
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## 2. Variant Mapping via `data-variant`
|
|
59
|
-
|
|
60
|
-
**Decision:** `variant` props (`"default" | "error" | "success"`) are mapped directly to `data-variant` on the Mantine root `Box`.
|
|
61
|
-
|
|
62
|
-
**Implementation:** Mantine's `Notification` doesn't inherently support our custom variants out of the box in the way we want them styled. By passing `data-variant` directly to the `Box`, we can explicitly target the root element in our `Toast.module.css` (e.g., `.root[data-variant="success"]`) and pipe in the corresponding UI Kit layer colors.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## 3. Minimal CSS Override Philosophy
|
|
67
|
-
|
|
68
|
-
**Decision:** The CSS module only overrides visual design tokens (colors, typography, padding, borders, shadows).
|
|
69
|
-
|
|
70
|
-
**Implementation:** We defer layout structure, icon rendering, loader transitions, and close button mechanics to Mantine. The `border-style: none;` is hardcoded to reset any underlying styles from Mantine's defaults, ensuring a clean mapping of elevation and shadows.
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## 4. Unsupported `loading` State
|
|
75
|
-
|
|
76
|
-
**Decision:** The native `loading` state is explicitly stripped and bypassed from the `<Toast />` component wrapper.
|
|
77
|
-
|
|
78
|
-
**Implementation:** Mantine's `Notification` inherently supports a `loading={true}` state that natively spins up a loader instead of an icon. However, Recursica's UI Kit strictly does not define structural tokens for loader states inside toasts.
|
|
79
|
-
Instead of attempting to tightly couple the internal `Loader` abstraction or mapping variables incorrectly, the `loading` property is explicitly omitted and `false`-enforced from the public API.
|
|
80
|
-
If consumers explicitly require a loading toast, they must manually inject a `<Loader />` component into the `icon` slot.
|
|
50
|
+
The `loading` state is not supported. If a loading toast is needed, pass a `<Loader />` component into the `icon` slot instead.
|
|
@@ -53,63 +53,21 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
-
## 2.
|
|
56
|
+
## 2. Behavior Notes
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
**Implementation:** The Recursica token system defines the `tooltip` namespace covering:
|
|
61
|
-
|
|
62
|
-
- Geometry: border-radius, border-size, min-width, min-height, max-width, padding
|
|
63
|
-
- Typography: text_font-\* (family, size, style, weight, letter-spacing, line-height, text-decoration, text-transform)
|
|
64
|
-
- Colors (layer-aware): background, border-color, text
|
|
65
|
-
- Elevation: box-shadow
|
|
66
|
-
- Beak: beak-size (16px), beak-inset (8px)
|
|
67
|
-
|
|
68
|
-
No tokens from other component namespaces are referenced.
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## 3. Hardcoded Values
|
|
73
|
-
|
|
74
|
-
### `border-style: solid` (CSS module)
|
|
75
|
-
|
|
76
|
-
Mantine renders the tooltip using its `Box` component, which does not set `border-style` natively. Without this hardcoded value, the border-width and border-color tokens would have no visible effect. Same pattern as Menu and HoverCard dropdowns.
|
|
77
|
-
|
|
78
|
-
### `arrowSize` defaulted to `16` (Tooltip.tsx)
|
|
79
|
-
|
|
80
|
-
Mantine's `arrowSize` prop is a JavaScript number used for inline style calculations: it sets `width`, `height`, and a positioning offset (`-arrowSize/2`) directly on the arrow `<div>` element. These inline styles cannot be overridden via CSS without `!important`, and the positioning offset has no CSS equivalent. The beak size cannot be fully CSS-driven.
|
|
81
|
-
|
|
82
|
-
The default value `16` matches the Recursica `beak-size` token (`--recursica_ui-kit_components_tooltip_properties_beak-size: 16px`). Developers can override `arrowSize` if needed. This is documented as an open issue in `docs/COMPONENT_ISSUES.md`.
|
|
83
|
-
|
|
84
|
-
**Note:** Mantine calls this the "arrow"; Recursica calls it the "beak". The Recursica prop `withBeak` (defaulting to `true`) maps to Mantine's `withArrow`. Both are accepted; `withBeak` takes precedence.
|
|
85
|
-
|
|
86
|
-
### `multiline={true}` (Tooltip.tsx)
|
|
87
|
-
|
|
88
|
-
Mantine's `multiline` prop controls whether tooltip text wraps (`white-space: nowrap` when false). Recursica always enables multiline because the design system defines a `max-width` token (300px) — text should wrap naturally within that constraint rather than overflowing. The `multiline` prop is not exposed to developers.
|
|
89
|
-
|
|
90
|
-
### Flexbox centering (CSS module)
|
|
91
|
-
|
|
92
|
-
`display: flex; align-items: center; justify-content: center;` is applied to the `.tooltip` class. This ensures text is vertically and horizontally centered within the `min-height: 48px` container defined by the design token. Without this, text sits at the top of the tooltip.
|
|
58
|
+
Tooltip text always wraps to fit within the token-defined max width, rather than staying on a single line or overflowing. The `multiline` prop is not exposed, since this behavior is always on.
|
|
93
59
|
|
|
94
60
|
---
|
|
95
61
|
|
|
96
|
-
##
|
|
62
|
+
## 3. Recursica `withBeak` Prop
|
|
97
63
|
|
|
98
64
|
**Decision:** `withBeak` is the official Recursica prop for controlling beak visibility, defaulting to `true`.
|
|
99
65
|
|
|
100
|
-
**Implementation:** Both `withBeak` and Mantine's `withArrow` are accepted.
|
|
66
|
+
**Implementation:** Both `withBeak` and Mantine's `withArrow` are accepted. When both are provided, `withBeak` takes precedence. The beak's size can be adjusted via the `arrowSize` prop (default `16`).
|
|
101
67
|
|
|
102
68
|
---
|
|
103
69
|
|
|
104
|
-
##
|
|
105
|
-
|
|
106
|
-
**Decision:** CSS module classes are bound via the `classNames` prop on Mantine's Tooltip root.
|
|
107
|
-
|
|
108
|
-
**Implementation:** The stylesNames for Tooltip are `tooltip` (the container) and `arrow` (the beak). Both are mapped to their respective CSS module classes: `{ tooltip: styles.tooltip, arrow: styles.arrow }`. Consumer-provided `classNames` are merged additively when `overStyled` is true.
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
## 6. Tooltip.Floating and Tooltip.Group
|
|
70
|
+
## 4. Tooltip.Floating and Tooltip.Group
|
|
113
71
|
|
|
114
72
|
**Decision:** These static sub-components are direct pass-throughs to Mantine with no Recursica styling.
|
|
115
73
|
|
|
@@ -117,8 +75,6 @@ Mantine's `multiline` prop controls whether tooltip text wraps (`white-space: no
|
|
|
117
75
|
|
|
118
76
|
---
|
|
119
77
|
|
|
120
|
-
##
|
|
121
|
-
|
|
122
|
-
**Decision:** Recursica defaults `position` to `"top"`. Mantine defaults to `"bottom"`.
|
|
78
|
+
## 5. Default Position
|
|
123
79
|
|
|
124
|
-
|
|
80
|
+
Recursica defaults `position` to `"top"` instead of Mantine's default of `"bottom"`. Pass your own `position` value to override it.
|
|
@@ -4,14 +4,42 @@
|
|
|
4
4
|
|
|
5
5
|
- **`elementProps` already carries the important behavioral wiring.** Mantine's `renderNode` payload includes an `elementProps` object with `onClick` (combined expand+select handler, respecting `expandOnClick`/`selectOnClick`), `data-selected`, `data-value`, and `data-hovered` (set on keyboard/mouse hover via Mantine's internal `hoveredNode` state). We spread these directly onto our own `.row` div rather than reimplementing click/keyboard behavior — only the `className` is overridden with our own token-driven class.
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **UPDATED (Matt Massey, 2026-08-08): the chevron is now a real `Button`, superseding the two notes below.** Forge's real design shows the chevron following Button's own styling rules (`variant="text"` `size="small"`) — see the new entry near the end of this file for the full swap and why it doesn't reopen the nested-focusable-element concern the original decision (kept below for history) was worried about.
|
|
8
|
+
- ~~The chevron is a decorative element inside the same clickable row, not a separate `<button>`.~~ The design tokens include a `button-node-gap` token (implying a "button" next to the node label in Figma), but structurally the whole row is already the click target via `elementProps.onClick` (which correctly bundles Mantine's `expandOnClick`/`selectOnClick` logic). Making the chevron its own nested interactive `<button>` inside an `<li>` that Mantine already treats as keyboard-focusable (`tabIndex`, arrow-key handling in `TreeNode.tsx`) would create nested-focusable-element accessibility problems for no real benefit. `button-node-gap` is used as the CSS `gap` between the (decorative) chevron and the label inside `.row` instead.
|
|
8
9
|
|
|
9
|
-
-
|
|
10
|
+
- ~~No dedicated icon-color/icon-size token exists for Tree.~~ The expand chevron is an inline SVG using `stroke="currentColor"`, sized at `1em` (relative to the row's own tokened `font-size`). This means it automatically tracks whichever text-color token (selected/unselected) is active on `.row` — no new token was invented, and no hardcoded color was used.
|
|
10
11
|
|
|
11
|
-
- **KNOWN GAP (2026-08-06): Forge's own preview shows a red chevron with more chevron-to-label spacing than what's currently implemented here.** Re-verified against both the compiled `recursica_variables_scoped.css` and the raw `recursica_ui-kit.json`
|
|
12
|
+
- **RESOLVED (2026-08-08) — KNOWN GAP (2026-08-06): Forge's own preview shows a red chevron with more chevron-to-label spacing than what's currently implemented here.** Explained by the Button swap below: Forge's chevron is Button's own "text" variant color (Recursica's red/alert-adjacent brand color), not a `tree`-namespace icon-color token — there never was a missing token to find. Re-verified against both the compiled `recursica_variables_scoped.css` and the raw `recursica_ui-kit.json` at the time: the `tree` schema genuinely has no `icon-color` property anywhere, and `button-node-gap` resolves to `brand.dimensions.general.sm` = 4px, exactly what `.row`'s `gap` uses — confirmed not a mapping bug on our side, just the wrong mental model (a hand-drawn glyph instead of an actual Button).
|
|
12
13
|
|
|
13
14
|
- **`--level-offset` override needs `!important`.** Mantine's `Tree` computes each node's indent via a `--level-offset` CSS variable, resolved internally through its `createVarsResolver` mechanism and applied as a scoped rule on the root `<ul>` (same category of override as `Timeline`'s `--mantine-spacing-xl` / `--tl-line-width`). We override it on `.root` with `!important` to reliably beat Mantine's own generated rule regardless of stylesheet insertion order, rather than passing a `levelOffset` prop from TSX (which would mean setting a design-token value from TSX, against `COMPONENT_DEV_GUIDE.md`'s "no custom properties set from TSX for styling" rule).
|
|
14
15
|
|
|
15
16
|
- **`onSelectedChange` is a Recursica addition, not a Mantine API.** Mantine's `useTree()` controller tracks `selectedState` but has no change callback of its own. We watch `tree.selectedState` with a `useEffect` and fire `onSelectedChange` when it changes. This is a behavioral callback, not a design-token reactivity concern, so it doesn't conflict with `COMPONENT_DEV_GUIDE.md §7`'s rule against using `useEffect` for token/styling reactivity.
|
|
16
17
|
|
|
17
18
|
- **Deliberately not implemented (no tokens back them):** checkbox/indeterminate node state (Mantine's `Tree` supports `checkNode`/`isNodeIndeterminate`, but the Figma UI Kit's `tree` tokens only define `selected`/`unselected`, not a checked state) and per-node `disabled` (no token, and no `disabled` field in Mantine's `TreeNodeData` either). If either is needed later, it needs a token/schema addition first, not a component-level workaround.
|
|
19
|
+
|
|
20
|
+
- **Keyboard nav (Matt Massey, 2026-08-08): arrow up/down already worked — only the focus ring was missing.** Mantine's own `TreeNode` already implements full roving-tabindex keyboard navigation (`ArrowUp`/`ArrowDown` move focus between rows, `ArrowLeft`/`ArrowRight` collapse/expand, `Space` toggles) — confirmed via reading `TreeNode.mjs` directly and verifying with Playwright that focus genuinely moves between rows on `ArrowDown`. The reason it looked broken: DOM focus lands on `.node` (the `<li role="treeitem">`), and neither `.node` nor `.row` had any focus style at all (`outline: none`, no box-shadow) — so keyboard navigation was invisible, and only `Space`/`ArrowLeft`/`ArrowRight` were noticeable at all, since those also cause a visible expand/collapse side effect. Fixed by adding a `:focus-visible` box-shadow ring (same tokens as every other focusable component), keyed off `.node:focus-visible` since that's where DOM focus actually lands. Originally drawn on `.row` (the whole button+label box); see the next entry for why it now targets `.label` only.
|
|
21
|
+
|
|
22
|
+
- **Chevron → `Button` (Matt Massey, 2026-08-08).** Forge's real design uses an actual `Button` (`variant="text"` `size="small"`, icon-only) for the expand/collapse chevron, not a hand-drawn glyph — see the resolved "KNOWN GAP" above. Swapped `ExpandGlyph`'s plain `<span>` wrapper for a real `<Button icon={<ExpandGlyph/>} tabIndex={-1} aria-hidden="true" .../>`:
|
|
23
|
+
|
|
24
|
+
- **Doesn't reopen the nested-focusable-element concern** the original (now-struck-through) decision above was worried about: the embedded Button is never independently focusable or tab-stoppable (`tabIndex={-1}`) and is hidden from assistive tech (`aria-hidden`) — the row (`.node`) stays the single focusable/interactive element. Confirmed `Button`/`UnstyledButton` don't call `stopPropagation()` on click, so clicking the chevron still bubbles up to the row's existing `elementProps.onClick` and triggers expand/collapse exactly as before — no `onClick` needed on the embedded Button itself.
|
|
25
|
+
- **Rendered for every row, including leaves**, so every row reserves identical layout space; `.row:not([data-has-children]) .expandButton { visibility: hidden; }` hides it on leaf rows without needing to duplicate Button's own size tokens for a placeholder.
|
|
26
|
+
- **Rotation targets the glyph directly, not Button's internals**: `ExpandGlyph`'s `<svg>` carries its own `styles.expandGlyph` class, targeted via `.row[data-expanded] .expandGlyph` — a plain descendant-combinator selector that reaches the glyph regardless of how deeply `Button` nests it internally (`Button`'s own `.iconWrapper` isn't reachable from `Tree.module.css` at all — CSS Modules don't expose a stable cross-file class name for it, unlike Mantine's own `mantine-*` global classes).
|
|
27
|
+
- **Focus ring narrowed to `.label` only, excluding the Button** (Matt: "the focus ring for an item should be around just the node, not including the chevron button" / "the chevron button should not have a focus state"). `.label` was given `flex: 1 1 auto; height: 100%` so the ring still reads as a clean box covering the row's remaining width, not a tight text-only outline.
|
|
28
|
+
- **Found and fixed a real, separate bug while embedding this**: `Button.tsx` (both adapters) explicitly sets `className={finalClass}` (merging `styles.root` with any caller className) but then spreads `{...sanitizedProps}` _after_ it — and `sanitizedProps` still contains the original, unmodified `className` key, since it was only _read_, never deleted. The later spread silently overwrote `finalClass` with just the caller's own class whenever one was passed (e.g. `styles.expandButton` here) — exact same bug class as `Dropdown.tsx`/`BareDropdown.tsx` had. In mui-adapter this was fully visible (the chevron rendered in MUI's own default blue, since `Button-module__root` — and therefore every `[data-variant]` color rule — was missing entirely). In mantine-adapter it happened to be masked: Mantine's `classNames={{root: ...}}` object prop is separate from the plain `className` string and unaffected by the bug, so `Button-module__root` still applied via that path — but the underlying bug was there too, and is now fixed in both.
|
|
29
|
+
|
|
30
|
+
- **Expand/collapse and select made fully independent (Matt Massey, 2026-08-10), superseding the click-bubbling behavior described above.** Previously the chevron was purely decorative and let clicks bubble up to the row's combined expand+select handler; Matt clarified the real requirement: the button must be the _only_ way to expand/collapse on click, a row click must _only_ select, `Enter`/`Space` must _only_ select (never expand, even on a node with children), and `ArrowLeft`/`ArrowRight` must _only_ expand/collapse (never select) — letting a user toggle a subtree open without ever changing selection, and vice versa. Removed the now ill-fitting `expandOnClick`/`selectOnClick` props entirely (per point 5 of that request) — the pattern is fixed, not configurable.
|
|
31
|
+
|
|
32
|
+
- **Click**: `<MantineTree>` is now called with fixed `expandOnClick={false}` `selectOnClick={true}` (so `elementProps.onClick`, spread onto `.row`, only ever selects), and the embedded chevron `Button` now has its own `onClick` that calls `event.stopPropagation()` (so the click never also reaches `.row`'s select handler) and `tree.toggleExpanded(node.value)` directly via the controller — using `tree`/`node`, both already present in `renderNode`'s payload.
|
|
33
|
+
- **Enter/Space**: Mantine's `TreeNode` has no `Enter` handling and only wires `Space` to `toggleExpanded` (`expandOnSpace`, now passed as `false`) — no select-on-key behavior exists in the library at all, and the key handler lives on the `<li role="treeitem">` itself, unreachable through `renderNode`'s `elementProps`. Added a native `keydown` listener on the tree's root `<ul>` (merged via `useMergedRef` with the forwarded `ref`) that calls `tree.select(value)` on `Enter`/`Space`, reading the focused node's value off `event.target`'s own `data-value` (already set there by Mantine). `ArrowLeft`/`ArrowRight` needed no changes — Mantine's own handling already only calls `controller.expand`/`collapse`, never `select`.
|
|
34
|
+
- **Background/focus scoped to the label, not the whole node** (Matt: "The focus/background color should be on the node's label, not the entire node"): moved every visual property (padding, border, font, unselected/selected colors, hover overlay) off `.row` and onto `.label` — `.row` is now a plain flex layout container (indentation + `button-node-gap` only). `data-selected` is read directly off `renderNode`'s own `selected` payload field and applied to `.label` itself (no need to also spread `elementProps`'s copy there); `data-hovered`/`:hover` are still only observable on `.row` (from `elementProps`), so those states reach `.label` via `.row:hover .label`/`.row[data-hovered] .label` descendant selectors instead. The focus ring (added in the entry above) already targeted `.label` only, so it needed no further change.
|
|
35
|
+
|
|
36
|
+
- **Three follow-on fixes to the label-scoping work above (Matt Massey, 2026-08-10):**
|
|
37
|
+
|
|
38
|
+
- **Selected/hover chip was full-row width, not label width.** `.label` had `flex: 1 1 auto` — the `1` flex-grow stretched it to fill `.row`'s entire remaining width (button aside), so the highlighted chip visually covered the whole row even though the _properties_ were correctly scoped to `.label`. Changed to `flex: 0 1 auto` (no grow, same shrink/basis) so the chip sizes to its own content, matching Forge.
|
|
39
|
+
- **Focusing an expanded parent drew the ring on every descendant label too.** `.node:focus-visible .label` is a plain descendant selector — every child node's `.label` (inside the nested `.subtree` `<ul>`) is _also_ a descendant of the focused parent's `.node` `<li>`, at any depth, so the ring matched all of them at once. Fixed by scoping to `.node:focus-visible > .row .label`: `.row` is always a direct child of its own `.node`, but never of an ancestor `.node` (those reach it through the intervening `.subtree` `<ul>` and nested `<li>` instead) — the `>` combinator excludes every one of them.
|
|
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
|
+
|
|
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
|
+
- **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
|
+
- **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
|
+
- **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.
|