@recursica/mui-adapter 0.18.1 → 0.19.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 +17 -0
- package/dist/mui-adapter.cjs +172 -23
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +12963 -4695
- package/dist/mui-adapter.js.map +1 -1
- package/dist/src/components/Tree/Tree.d.ts +9 -5
- package/package.json +6 -1
- package/src/components/Accordion/Accordion.module.css +98 -62
- package/src/components/Autocomplete/Autocomplete.module.css +50 -47
- package/src/components/Autocomplete/Autocomplete.tsx +2 -2
- package/src/components/Avatar/Avatar.module.css +7 -7
- package/src/components/Badge/Badge.module.css +8 -8
- package/src/components/Button/Button.module.css +25 -43
- package/src/components/Card/Card.module.css +3 -3
- package/src/components/Checkbox/Checkbox.module.css +57 -16
- package/src/components/Chip/Chip.module.css +32 -20
- package/src/components/Dropdown/Dropdown.module.css +47 -44
- package/src/components/Dropdown/Dropdown.tsx +2 -2
- package/src/components/FormControlWrapper/FormControlWrapper.module.css +12 -12
- package/src/components/HoverCard/HoverCard.module.css +1 -1
- package/src/components/Label/Label.module.css +20 -3
- package/src/components/Link/Link.module.css +18 -71
- package/src/components/Loader/Loader.module.css +3 -3
- package/src/components/Menu/Menu.module.css +33 -41
- package/src/components/Modal/Modal.module.css +27 -11
- package/src/components/NumberInput/NumberInput.module.css +39 -37
- package/src/components/NumberInput/NumberInput.tsx +2 -2
- package/src/components/Pagination/Pagination.module.css +10 -43
- package/src/components/Panel/Panel.module.css +3 -3
- package/src/components/Radio/Radio.module.css +32 -15
- package/src/components/ReadOnlyField/ReadOnlyField.module.css +1 -1
- package/src/components/SegmentedControl/SegmentedControl.module.css +37 -29
- package/src/components/Slider/Slider.module.css +51 -35
- package/src/components/Stepper/Stepper.module.css +3 -3
- package/src/components/Switch/Switch.module.css +27 -11
- package/src/components/Tabs/Tabs.module.css +309 -166
- package/src/components/TextArea/TextArea.module.css +36 -21
- package/src/components/TextArea/TextArea.tsx +2 -2
- package/src/components/TextField/TextField.module.css +38 -33
- package/src/components/TextField/TextField.tsx +2 -2
- package/src/components/Timeline/IMPLEMENTATION_NOTES.md +5 -0
- package/src/components/Timeline/Timeline.module.css +35 -34
- package/src/components/Toast/Toast.module.css +24 -11
- package/src/components/Tooltip/Tooltip.module.css +4 -2
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +21 -0
- package/src/components/Tree/Tree.module.css +204 -39
- package/src/components/Tree/Tree.stories.tsx +81 -4
- package/src/components/Tree/Tree.tsx +214 -13
- package/src/components/Tree/USAGE.md +40 -3
|
@@ -1,43 +1,208 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_selected-text_letter-spacing */
|
|
13
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_selected-text_line-height */
|
|
14
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_selected-text_text-decoration */
|
|
15
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_selected-text_text-transform */
|
|
16
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_unselected-text_font-family */
|
|
17
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_unselected-text_font-size */
|
|
18
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_unselected-text_font-style */
|
|
19
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_unselected-text_font-weight */
|
|
20
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_unselected-text_letter-spacing */
|
|
21
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_unselected-text_line-height */
|
|
22
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_unselected-text_text-decoration */
|
|
23
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_unselected-text_text-transform */
|
|
24
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_vertical-padding */
|
|
25
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_colors_hover-background */
|
|
26
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_colors_hover-border-color */
|
|
27
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_colors_hover-text */
|
|
28
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_colors_selected-background */
|
|
29
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_colors_selected-border-color */
|
|
30
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_colors_selected-text */
|
|
31
|
-
/* recursica-ignore: --recursica_ui-kit_components_tree_properties_colors_unselected-text */
|
|
1
|
+
/* HARDCODED VALUES:
|
|
2
|
+
* - list-style/margin/padding resets on .root/.subtree/.node: layout resets, no corresponding
|
|
3
|
+
* design tokens (structural, not visual design values).
|
|
4
|
+
* - .chevron size (1em) and rotation: no dedicated icon-size/icon-color token exists for Tree;
|
|
5
|
+
* the glyph is drawn with `currentColor` so it always tracks the row's already-tokened text
|
|
6
|
+
* color, and sized relative to the row's own font-size.
|
|
7
|
+
* - Hover overlay uses the generic --recursica_brand_states_hover_* tokens (same technique as
|
|
8
|
+
* Menu/Accordion/Button): Tree has no component-specific hover tokens of its own.
|
|
9
|
+
* - .row's padding-left keeps MUI's own depth-based indentation formula (base + indent * depth)
|
|
10
|
+
* but substitutes our own horizontal-padding token as the base instead of MUI's theme spacing.
|
|
11
|
+
*/
|
|
32
12
|
|
|
33
13
|
.root {
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
list-style: none;
|
|
17
|
+
outline: 0;
|
|
18
|
+
max-width: var(--recursica_ui-kit_components_tree_properties_max-width);
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: var(--recursica_ui-kit_components_tree_properties_item-gap);
|
|
22
|
+
|
|
23
|
+
/* MUI sets this as an inline style on the root <ul> (see useTreeViewRootProps in
|
|
24
|
+
@mui/x-tree-view). Override with !important to reliably beat it regardless of stylesheet
|
|
25
|
+
injection order (same category of override as mantine-adapter's --level-offset, itself
|
|
26
|
+
modeled on Timeline's --mantine-spacing-xl / --tl-line-width). */
|
|
27
|
+
--TreeView-itemChildrenIndentation: var(
|
|
28
|
+
--recursica_ui-kit_components_tree_properties_indent
|
|
29
|
+
) !important;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.subtree {
|
|
33
|
+
margin: 0;
|
|
34
|
+
padding: 0;
|
|
35
|
+
list-style: none;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: var(--recursica_ui-kit_components_tree_properties_item-gap);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.node {
|
|
42
|
+
list-style: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.row {
|
|
46
|
+
position: relative;
|
|
34
47
|
box-sizing: border-box;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
width: 100%;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
gap: var(--recursica_ui-kit_components_tree_properties_button-node-gap);
|
|
53
|
+
|
|
54
|
+
padding-top: var(
|
|
55
|
+
--recursica_ui-kit_components_tree_properties_vertical-padding
|
|
56
|
+
);
|
|
57
|
+
padding-bottom: var(
|
|
58
|
+
--recursica_ui-kit_components_tree_properties_vertical-padding
|
|
59
|
+
);
|
|
60
|
+
padding-right: var(
|
|
61
|
+
--recursica_ui-kit_components_tree_properties_horizontal-padding
|
|
62
|
+
);
|
|
63
|
+
padding-left: calc(
|
|
64
|
+
var(--recursica_ui-kit_components_tree_properties_horizontal-padding) +
|
|
65
|
+
var(--TreeView-itemChildrenIndentation, 0px) *
|
|
66
|
+
var(--TreeView-itemDepth, 0)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
border-style: solid; /* HARDCODE: structural border rule; thickness/color are tokened */
|
|
70
|
+
border-width: var(--recursica_ui-kit_components_tree_properties_border-size);
|
|
71
|
+
border-radius: var(
|
|
72
|
+
--recursica_ui-kit_components_tree_properties_border-radius
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
font-family: var(
|
|
76
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_font-family
|
|
77
|
+
);
|
|
78
|
+
font-size: var(
|
|
79
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_font-size
|
|
80
|
+
);
|
|
81
|
+
font-style: var(
|
|
82
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_font-style
|
|
83
|
+
);
|
|
84
|
+
font-weight: var(
|
|
85
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_font-weight
|
|
86
|
+
);
|
|
87
|
+
letter-spacing: var(
|
|
88
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_letter-spacing
|
|
89
|
+
);
|
|
90
|
+
line-height: var(
|
|
91
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_line-height
|
|
92
|
+
);
|
|
93
|
+
text-decoration: var(
|
|
94
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_text-decoration
|
|
95
|
+
);
|
|
96
|
+
text-transform: var(
|
|
97
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_text-transform
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
color: var(
|
|
101
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_colors_text-color
|
|
102
|
+
);
|
|
103
|
+
background-color: var(
|
|
104
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_colors_background-color
|
|
105
|
+
);
|
|
106
|
+
border-color: var(
|
|
107
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_colors_border-color
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Neutralize MUI's own hover/focused background-color changes (see TreeItemContent's defaults
|
|
112
|
+
in @mui/x-tree-view) so only the Recursica hover overlay below controls the hover look. */
|
|
113
|
+
.row:hover,
|
|
114
|
+
.row[data-focused] {
|
|
115
|
+
background-color: var(
|
|
116
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_colors_background-color
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Hover overlay via ::after pseudo-element (same technique as Menu/Accordion) */
|
|
121
|
+
.row::after {
|
|
122
|
+
content: "";
|
|
123
|
+
position: absolute;
|
|
124
|
+
inset: 0;
|
|
125
|
+
border-radius: inherit;
|
|
126
|
+
z-index: 0;
|
|
127
|
+
pointer-events: none;
|
|
128
|
+
background-color: var(--recursica_brand_states_hover_color);
|
|
129
|
+
opacity: 0;
|
|
130
|
+
transition: opacity 150ms ease;
|
|
131
|
+
}
|
|
132
|
+
.row:hover::after {
|
|
133
|
+
opacity: var(--recursica_brand_states_hover_opacity);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Selected row state — written after the hover/focus resets above so it wins the equal-
|
|
137
|
+
specificity tie via source order, including when hovered/focused while selected. */
|
|
138
|
+
.row[data-selected],
|
|
139
|
+
.row[data-selected]:hover,
|
|
140
|
+
.row[data-selected][data-focused] {
|
|
141
|
+
font-family: var(
|
|
142
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-family
|
|
143
|
+
);
|
|
144
|
+
font-size: var(
|
|
145
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-size
|
|
146
|
+
);
|
|
147
|
+
font-style: var(
|
|
148
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-style
|
|
149
|
+
);
|
|
150
|
+
font-weight: var(
|
|
151
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-weight
|
|
152
|
+
);
|
|
153
|
+
letter-spacing: var(
|
|
154
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_letter-spacing
|
|
155
|
+
);
|
|
156
|
+
line-height: var(
|
|
157
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_line-height
|
|
158
|
+
);
|
|
159
|
+
text-decoration: var(
|
|
160
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_text-decoration
|
|
161
|
+
);
|
|
162
|
+
text-transform: var(
|
|
163
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_text-transform
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
color: var(
|
|
167
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_colors_text-color
|
|
168
|
+
);
|
|
169
|
+
background-color: var(
|
|
170
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_colors_background-color
|
|
171
|
+
);
|
|
172
|
+
border-color: var(
|
|
173
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_colors_border-color
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.row[data-disabled] {
|
|
178
|
+
opacity: var(--recursica_brand_states_disabled);
|
|
179
|
+
cursor: auto;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.iconContainer {
|
|
183
|
+
position: relative;
|
|
184
|
+
z-index: 1;
|
|
185
|
+
width: 1em;
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
flex-shrink: 0;
|
|
190
|
+
color: inherit;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.chevron {
|
|
194
|
+
transition: transform 150ms ease;
|
|
195
|
+
}
|
|
196
|
+
.chevronExpanded {
|
|
197
|
+
transform: rotate(90deg);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.label {
|
|
201
|
+
position: relative;
|
|
202
|
+
z-index: 1;
|
|
203
|
+
color: inherit;
|
|
204
|
+
font: inherit;
|
|
205
|
+
letter-spacing: inherit;
|
|
206
|
+
text-decoration: inherit;
|
|
207
|
+
text-transform: inherit;
|
|
43
208
|
}
|
|
@@ -1,14 +1,91 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { Tree } from "./Tree";
|
|
3
|
+
import { Layer, type RecursicaTreeNode } from "@recursica/adapter-common";
|
|
4
|
+
|
|
5
|
+
const sampleData: RecursicaTreeNode[] = [
|
|
6
|
+
{
|
|
7
|
+
value: "documents",
|
|
8
|
+
label: "Documents",
|
|
9
|
+
children: [
|
|
10
|
+
{ value: "documents/resume.pdf", label: "resume.pdf" },
|
|
11
|
+
{ value: "documents/cover-letter.docx", label: "cover-letter.docx" },
|
|
12
|
+
{
|
|
13
|
+
value: "documents/taxes",
|
|
14
|
+
label: "Taxes",
|
|
15
|
+
children: [
|
|
16
|
+
{ value: "documents/taxes/2023.pdf", label: "2023.pdf" },
|
|
17
|
+
{ value: "documents/taxes/2024.pdf", label: "2024.pdf" },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
value: "photos",
|
|
24
|
+
label: "Photos",
|
|
25
|
+
children: [
|
|
26
|
+
{ value: "photos/vacation.jpg", label: "vacation.jpg" },
|
|
27
|
+
{ value: "photos/family.jpg", label: "family.jpg" },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{ value: "readme.md", label: "readme.md" },
|
|
31
|
+
];
|
|
3
32
|
|
|
4
33
|
const meta: Meta<typeof Tree> = {
|
|
5
|
-
title: "
|
|
34
|
+
title: "UI-Kit/Tree",
|
|
6
35
|
component: Tree,
|
|
36
|
+
tags: ["autodocs"],
|
|
37
|
+
parameters: {
|
|
38
|
+
docs: {
|
|
39
|
+
description: {
|
|
40
|
+
component: `
|
|
41
|
+
The \`Tree\` component renders hierarchical \`data\` as an expandable/selectable tree, wrapping \`@mui/x-tree-view\`'s \`RichTreeView\` with a fully custom item renderer so every visual property (row box model, selected/unselected colors and typography, indent, item spacing) comes from Recursica's \`tree\` design tokens.
|
|
42
|
+
|
|
43
|
+
Each node needs a unique \`value\` and a \`label\`; a \`children\` array (even empty) makes a node expandable.
|
|
44
|
+
`,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
7
48
|
};
|
|
8
49
|
|
|
9
50
|
export default meta;
|
|
10
|
-
type Story = StoryObj<typeof Tree>;
|
|
11
51
|
|
|
12
|
-
export const
|
|
13
|
-
|
|
52
|
+
export const Default: StoryObj<typeof Tree> = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<Tree data={sampleData} initialExpandedValues={["documents"]} />
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const AllExpanded: StoryObj<typeof Tree> = {
|
|
59
|
+
render: () => <Tree data={sampleData} initialExpandedValues="*" />,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const PreSelected: StoryObj<typeof Tree> = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<Tree
|
|
65
|
+
data={sampleData}
|
|
66
|
+
initialExpandedValues={["documents"]}
|
|
67
|
+
initialSelectedValues={["documents/resume.pdf"]}
|
|
68
|
+
/>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const MultipleSelection: StoryObj<typeof Tree> = {
|
|
73
|
+
render: () => (
|
|
74
|
+
<Tree
|
|
75
|
+
data={sampleData}
|
|
76
|
+
initialExpandedValues="*"
|
|
77
|
+
initialSelectedValues={["documents/resume.pdf", "photos/vacation.jpg"]}
|
|
78
|
+
multiple
|
|
79
|
+
/>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Demonstrates the component nested inside a non-default layer — the one case where an
|
|
84
|
+
* explicit `<Layer>` wrap belongs in a story (see COMPONENT_STORYBOOK_GUIDE.md §9). */
|
|
85
|
+
export const LayerOne: StoryObj<typeof Tree> = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<Layer layer={1} style={{ padding: "24px" }}>
|
|
88
|
+
<Tree data={sampleData} initialExpandedValues={["documents"]} />
|
|
89
|
+
</Layer>
|
|
90
|
+
),
|
|
14
91
|
};
|
|
@@ -1,24 +1,225 @@
|
|
|
1
1
|
import React, { forwardRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { RichTreeView } from "@mui/x-tree-view/RichTreeView";
|
|
3
|
+
import {
|
|
4
|
+
useTreeItem,
|
|
5
|
+
type UseTreeItemParameters,
|
|
6
|
+
} from "@mui/x-tree-view/useTreeItem";
|
|
7
|
+
import { useTreeItemModel } from "@mui/x-tree-view/hooks";
|
|
8
|
+
import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
|
|
9
|
+
import {
|
|
10
|
+
TreeItemRoot,
|
|
11
|
+
TreeItemContent,
|
|
12
|
+
TreeItemIconContainer,
|
|
13
|
+
TreeItemGroupTransition,
|
|
14
|
+
TreeItemLabel,
|
|
15
|
+
} from "@mui/x-tree-view/TreeItem";
|
|
16
|
+
import { TreeItemIcon } from "@mui/x-tree-view/TreeItemIcon";
|
|
17
|
+
import {
|
|
18
|
+
filterStylingProps,
|
|
19
|
+
type RecursicaOverStyled,
|
|
20
|
+
} from "../../utils/filterStylingProps";
|
|
21
|
+
import {
|
|
22
|
+
type RecursicaTreeProps,
|
|
23
|
+
type RecursicaTreeNode,
|
|
24
|
+
} from "@recursica/adapter-common";
|
|
3
25
|
import styles from "./Tree.module.css";
|
|
4
26
|
|
|
5
|
-
export
|
|
6
|
-
|
|
27
|
+
export type TreeProps = RecursicaOverStyled<
|
|
28
|
+
RecursicaTreeProps & Omit<React.ComponentPropsWithoutRef<"ul">, "children">
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
function collectAllValues(nodes: RecursicaTreeNode[]): string[] {
|
|
32
|
+
const values: string[] = [];
|
|
33
|
+
const walk = (list: RecursicaTreeNode[]) => {
|
|
34
|
+
list.forEach((node) => {
|
|
35
|
+
values.push(node.value);
|
|
36
|
+
if (node.children) walk(node.children);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
walk(nodes);
|
|
40
|
+
return values;
|
|
7
41
|
}
|
|
8
42
|
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
43
|
+
/** Simple chevron glyph; MUI renders a different component for expanded vs collapsed rather
|
|
44
|
+
* than one rotating icon (unlike Mantine's `[data-expanded]` approach), so collapseIcon carries
|
|
45
|
+
* its own rotated variant. Both use `currentColor` — no dedicated icon-color token exists. */
|
|
46
|
+
function ExpandGlyph() {
|
|
47
|
+
return (
|
|
48
|
+
<svg
|
|
49
|
+
className={styles.chevron}
|
|
50
|
+
viewBox="0 0 16 16"
|
|
51
|
+
width="1em"
|
|
52
|
+
height="1em"
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
strokeWidth="2"
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
strokeLinejoin="round"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
>
|
|
60
|
+
<path d="M5 3l5 5-5 5" />
|
|
61
|
+
</svg>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function CollapseGlyph() {
|
|
65
|
+
return (
|
|
66
|
+
<svg
|
|
67
|
+
className={`${styles.chevron} ${styles.chevronExpanded}`}
|
|
68
|
+
viewBox="0 0 16 16"
|
|
69
|
+
width="1em"
|
|
70
|
+
height="1em"
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
strokeWidth="2"
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
strokeLinejoin="round"
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
>
|
|
78
|
+
<path d="M5 3l5 5-5 5" />
|
|
79
|
+
</svg>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
/** Rendered for leaf items so the icon-container box (and therefore row alignment) stays
|
|
83
|
+
* consistent whether or not a row has an expand chevron, matching the mantine-adapter
|
|
84
|
+
* implementation's always-present `.expandIcon` span. */
|
|
85
|
+
function EndGlyph() {
|
|
86
|
+
return <span aria-hidden="true" />;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface CustomTreeItemProps extends UseTreeItemParameters {
|
|
90
|
+
className?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Custom `slots.item` renderer. Mirrors the mantine-adapter's `renderNode`: every visual
|
|
94
|
+
* property comes from Tree.module.css rather than @mui/x-tree-view's own default TreeItem
|
|
95
|
+
* styles. `useTreeItemModel` retrieves the *original* `RecursicaTreeNode` (with its real
|
|
96
|
+
* `React.ReactNode` label) — `getItemLabel` on `RichTreeView` only ever sees a coerced string
|
|
97
|
+
* (required for its internal a11y/typeahead search), so this is the only way to render rich
|
|
98
|
+
* (non-string) labels. See IMPLEMENTATION_NOTES.md. */
|
|
99
|
+
const CustomTreeItem = forwardRef<HTMLLIElement, CustomTreeItemProps>(
|
|
100
|
+
function CustomTreeItem(
|
|
101
|
+
{ id, itemId, label, disabled, children, className, ...rest },
|
|
102
|
+
ref,
|
|
103
|
+
) {
|
|
104
|
+
const {
|
|
105
|
+
getContextProviderProps,
|
|
106
|
+
getRootProps,
|
|
107
|
+
getContentProps,
|
|
108
|
+
getIconContainerProps,
|
|
109
|
+
getLabelProps,
|
|
110
|
+
getGroupTransitionProps,
|
|
111
|
+
status,
|
|
112
|
+
} = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
|
|
113
|
+
|
|
114
|
+
const item = useTreeItemModel<RecursicaTreeNode>(itemId);
|
|
115
|
+
|
|
16
116
|
return (
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
117
|
+
<TreeItemProvider {...getContextProviderProps()}>
|
|
118
|
+
<TreeItemRoot
|
|
119
|
+
{...getRootProps({
|
|
120
|
+
...rest,
|
|
121
|
+
className: `${styles.node} ${className ?? ""}`,
|
|
122
|
+
})}
|
|
123
|
+
>
|
|
124
|
+
<TreeItemContent
|
|
125
|
+
{...getContentProps({ className: styles.row })}
|
|
126
|
+
status={status}
|
|
127
|
+
>
|
|
128
|
+
<TreeItemIconContainer
|
|
129
|
+
{...getIconContainerProps({ className: styles.iconContainer })}
|
|
130
|
+
>
|
|
131
|
+
<TreeItemIcon status={status} />
|
|
132
|
+
</TreeItemIconContainer>
|
|
133
|
+
<TreeItemLabel {...getLabelProps({ className: styles.label })}>
|
|
134
|
+
{item?.label ?? label}
|
|
135
|
+
</TreeItemLabel>
|
|
136
|
+
</TreeItemContent>
|
|
137
|
+
{children && (
|
|
138
|
+
<TreeItemGroupTransition
|
|
139
|
+
{...getGroupTransitionProps({ className: styles.subtree })}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
</TreeItemRoot>
|
|
143
|
+
</TreeItemProvider>
|
|
20
144
|
);
|
|
21
145
|
},
|
|
22
146
|
);
|
|
23
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Recursively renders hierarchical `data` as an expandable/selectable tree.
|
|
150
|
+
*
|
|
151
|
+
* **Recursica Abstract:**
|
|
152
|
+
* Wraps `@mui/x-tree-view`'s `RichTreeView` with a fully custom item renderer so every visual
|
|
153
|
+
* aspect (row box model, selected/unselected colors and typography, indent, item spacing)
|
|
154
|
+
* comes from Recursica's `tree` design tokens rather than MUI's defaults.
|
|
155
|
+
*/
|
|
156
|
+
export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
|
|
157
|
+
{
|
|
158
|
+
overStyled = false,
|
|
159
|
+
data,
|
|
160
|
+
initialExpandedValues,
|
|
161
|
+
initialSelectedValues,
|
|
162
|
+
multiple = false,
|
|
163
|
+
expandOnClick = true,
|
|
164
|
+
selectOnClick = true,
|
|
165
|
+
onNodeExpand,
|
|
166
|
+
onNodeCollapse,
|
|
167
|
+
onSelectedChange,
|
|
168
|
+
...rest
|
|
169
|
+
},
|
|
170
|
+
ref,
|
|
171
|
+
) {
|
|
172
|
+
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
173
|
+
|
|
174
|
+
const defaultExpandedItems =
|
|
175
|
+
initialExpandedValues === "*"
|
|
176
|
+
? collectAllValues(data)
|
|
177
|
+
: initialExpandedValues;
|
|
178
|
+
|
|
179
|
+
const classNameProp = (sanitizedProps as Record<string, unknown>)
|
|
180
|
+
.className as string | undefined;
|
|
181
|
+
const rootClass = classNameProp
|
|
182
|
+
? `${styles.root} ${classNameProp}`
|
|
183
|
+
: styles.root;
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<RichTreeView
|
|
187
|
+
ref={ref}
|
|
188
|
+
items={data}
|
|
189
|
+
getItemId={(item: RecursicaTreeNode) => item.value}
|
|
190
|
+
getItemLabel={(item: RecursicaTreeNode) =>
|
|
191
|
+
typeof item.label === "string" ? item.label : item.value
|
|
192
|
+
}
|
|
193
|
+
defaultExpandedItems={defaultExpandedItems}
|
|
194
|
+
defaultSelectedItems={
|
|
195
|
+
multiple ? (initialSelectedValues ?? []) : initialSelectedValues?.[0]
|
|
196
|
+
}
|
|
197
|
+
multiSelect={multiple}
|
|
198
|
+
// MUI has no separate expandOnClick/selectOnClick toggles; `expansionTrigger` controls
|
|
199
|
+
// whether clicking the row content (vs only the icon container) also expands/collapses.
|
|
200
|
+
// `selectOnClick={false}` has no exact MUI equivalent (selection always follows content
|
|
201
|
+
// clicks unless disabled entirely) — see IMPLEMENTATION_NOTES.md.
|
|
202
|
+
expansionTrigger={expandOnClick ? "content" : "iconContainer"}
|
|
203
|
+
disableSelection={!selectOnClick}
|
|
204
|
+
onItemExpansionToggle={(_event, itemId, isExpanded) => {
|
|
205
|
+
if (isExpanded) onNodeExpand?.(itemId);
|
|
206
|
+
else onNodeCollapse?.(itemId);
|
|
207
|
+
}}
|
|
208
|
+
onSelectedItemsChange={(_event, itemIds) => {
|
|
209
|
+
onSelectedChange?.(
|
|
210
|
+
Array.isArray(itemIds) ? itemIds : itemIds ? [itemIds] : [],
|
|
211
|
+
);
|
|
212
|
+
}}
|
|
213
|
+
slots={{
|
|
214
|
+
expandIcon: ExpandGlyph,
|
|
215
|
+
collapseIcon: CollapseGlyph,
|
|
216
|
+
endIcon: EndGlyph,
|
|
217
|
+
item: CustomTreeItem,
|
|
218
|
+
}}
|
|
219
|
+
classes={{ root: rootClass }}
|
|
220
|
+
{...(sanitizedProps as unknown as Record<string, unknown>)}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
24
225
|
Tree.displayName = "Tree";
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
This document describes how to integrate and use the `Tree` component in your projects using `@recursica/mui-adapter`.
|
|
4
4
|
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> This component requires `@mui/x-tree-view` (MIT-licensed, free community edition) as a peer dependency, in addition to `@mui/material`. Install it alongside your other MUI packages: `npm install @mui/x-tree-view`.
|
|
7
|
+
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
## 1. Import Reference
|
|
@@ -25,22 +28,56 @@ export default function Demo() {
|
|
|
25
28
|
{
|
|
26
29
|
value: "1",
|
|
27
30
|
label: "Root",
|
|
28
|
-
children: [
|
|
31
|
+
children: [
|
|
32
|
+
{ value: "1.1", label: "Child A" },
|
|
33
|
+
{ value: "1.2", label: "Child B" },
|
|
34
|
+
],
|
|
29
35
|
},
|
|
36
|
+
{ value: "2", label: "Sibling" },
|
|
30
37
|
]}
|
|
31
38
|
/>
|
|
32
39
|
);
|
|
33
40
|
}
|
|
34
41
|
```
|
|
35
42
|
|
|
43
|
+
Each node needs a unique `value` and a `label`. Any node with a `children` array (even an empty one) renders an expand/collapse chevron.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 3. Controlled Selection / Expansion
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<Tree
|
|
51
|
+
data={data}
|
|
52
|
+
initialExpandedValues={["1"]}
|
|
53
|
+
initialSelectedValues={["1.1"]}
|
|
54
|
+
multiple
|
|
55
|
+
onSelectedChange={(values) => console.log("selected:", values)}
|
|
56
|
+
onNodeExpand={(value) => console.log("expanded:", value)}
|
|
57
|
+
onNodeCollapse={(value) => console.log("collapsed:", value)}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `initialExpandedValues` accepts an array of node values, or `"*"` to start with every node expanded.
|
|
62
|
+
- `multiple` allows more than one node to be selected at once (default: single-select).
|
|
63
|
+
- `expandOnClick` (default `true`) controls whether clicking a row's content also expands/collapses it (vs. only clicking its chevron); `selectOnClick` (default `true`) controls whether selection is enabled at all. See **Known Constraints** below for how these map onto the underlying library.
|
|
64
|
+
|
|
36
65
|
---
|
|
37
66
|
|
|
38
|
-
##
|
|
67
|
+
## 4. Design System Integration
|
|
39
68
|
|
|
40
69
|
All Recursica components in the `@recursica/mui-adapter` package adhere strictly to design system spacing, scaling, and behavior patterns.
|
|
41
70
|
|
|
42
71
|
> [!IMPORTANT]
|
|
43
72
|
>
|
|
44
|
-
> - **Anti-override protection**:
|
|
73
|
+
> - **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.
|
|
45
74
|
> - **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.
|
|
46
75
|
> - **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.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 5. Known Constraints
|
|
80
|
+
|
|
81
|
+
- **`expandOnClick`/`selectOnClick` are approximated, not exact.** `@mui/x-tree-view` doesn't expose the same two independent toggles Mantine's `Tree` does. `expandOnClick` maps to MUI's `expansionTrigger` (`"content"` vs `"iconContainer"`); `selectOnClick={false}` maps to `disableSelection` (which disables selection entirely, rather than only suppressing it on a content click while leaving some other path to select). See `IMPLEMENTATION_NOTES.md`.
|
|
82
|
+
- No checkbox/multi-check styling is exposed — `@mui/x-tree-view` supports checkbox selection, but Recursica's `tree` design tokens don't yet define a checked visual state, so it isn't part of the Recursica API.
|
|
83
|
+
- No per-node `disabled` state — the Figma UI Kit tokens don't define one, and `RecursicaTreeNode` has no `disabled` field either.
|