@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/mui-adapter.cjs +172 -23
  3. package/dist/mui-adapter.cjs.map +1 -1
  4. package/dist/mui-adapter.css +1 -1
  5. package/dist/mui-adapter.js +12963 -4695
  6. package/dist/mui-adapter.js.map +1 -1
  7. package/dist/src/components/Tree/Tree.d.ts +9 -5
  8. package/package.json +6 -1
  9. package/src/components/Accordion/Accordion.module.css +98 -62
  10. package/src/components/Autocomplete/Autocomplete.module.css +50 -47
  11. package/src/components/Autocomplete/Autocomplete.tsx +2 -2
  12. package/src/components/Avatar/Avatar.module.css +7 -7
  13. package/src/components/Badge/Badge.module.css +8 -8
  14. package/src/components/Button/Button.module.css +25 -43
  15. package/src/components/Card/Card.module.css +3 -3
  16. package/src/components/Checkbox/Checkbox.module.css +57 -16
  17. package/src/components/Chip/Chip.module.css +32 -20
  18. package/src/components/Dropdown/Dropdown.module.css +47 -44
  19. package/src/components/Dropdown/Dropdown.tsx +2 -2
  20. package/src/components/FormControlWrapper/FormControlWrapper.module.css +12 -12
  21. package/src/components/HoverCard/HoverCard.module.css +1 -1
  22. package/src/components/Label/Label.module.css +20 -3
  23. package/src/components/Link/Link.module.css +18 -71
  24. package/src/components/Loader/Loader.module.css +3 -3
  25. package/src/components/Menu/Menu.module.css +33 -41
  26. package/src/components/Modal/Modal.module.css +27 -11
  27. package/src/components/NumberInput/NumberInput.module.css +39 -37
  28. package/src/components/NumberInput/NumberInput.tsx +2 -2
  29. package/src/components/Pagination/Pagination.module.css +10 -43
  30. package/src/components/Panel/Panel.module.css +3 -3
  31. package/src/components/Radio/Radio.module.css +32 -15
  32. package/src/components/ReadOnlyField/ReadOnlyField.module.css +1 -1
  33. package/src/components/SegmentedControl/SegmentedControl.module.css +37 -29
  34. package/src/components/Slider/Slider.module.css +51 -35
  35. package/src/components/Stepper/Stepper.module.css +3 -3
  36. package/src/components/Switch/Switch.module.css +27 -11
  37. package/src/components/Tabs/Tabs.module.css +309 -166
  38. package/src/components/TextArea/TextArea.module.css +36 -21
  39. package/src/components/TextArea/TextArea.tsx +2 -2
  40. package/src/components/TextField/TextField.module.css +38 -33
  41. package/src/components/TextField/TextField.tsx +2 -2
  42. package/src/components/Timeline/IMPLEMENTATION_NOTES.md +5 -0
  43. package/src/components/Timeline/Timeline.module.css +35 -34
  44. package/src/components/Toast/Toast.module.css +24 -11
  45. package/src/components/Tooltip/Tooltip.module.css +4 -2
  46. package/src/components/Tree/IMPLEMENTATION_NOTES.md +21 -0
  47. package/src/components/Tree/Tree.module.css +204 -39
  48. package/src/components/Tree/Tree.stories.tsx +81 -4
  49. package/src/components/Tree/Tree.tsx +214 -13
  50. package/src/components/Tree/USAGE.md +40 -3
@@ -1,43 +1,208 @@
1
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_border-radius */
2
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_border-size */
3
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_button-node-gap */
4
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_horizontal-padding */
5
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_indent */
6
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_item-gap */
7
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_max-width */
8
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_selected-text_font-family */
9
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_selected-text_font-size */
10
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_selected-text_font-style */
11
- /* recursica-ignore: --recursica_ui-kit_components_tree_properties_selected-text_font-weight */
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
- font-family: sans-serif;
36
- padding: 24px;
37
- border: 1px dashed #ccc;
38
- border-radius: 8px;
39
- color: #666;
40
- text-align: center;
41
- font-weight: 500;
42
- background-color: #fafafa;
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: "Components/Tree",
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 ComingSoon: Story = {
13
- args: {},
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 { type RecursicaOverStyled } from "../../utils/filterStylingProps";
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 interface TreeProps extends React.ComponentPropsWithoutRef<"div"> {
6
- overStyled?: boolean;
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
- * Placeholder component for Tree (Coming Soon).
11
- */
12
- export const Tree = forwardRef<HTMLDivElement, RecursicaOverStyled<TreeProps>>(
13
- function Tree({ overStyled: _overStyled = false, className, ...rest }, ref) {
14
- void _overStyled;
15
- const finalClass = className ? `${styles.root} ${className}` : styles.root;
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
- <div ref={ref} className={finalClass} {...rest}>
18
- Tree (Coming Soon)
19
- </div>
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: [{ value: "2", label: "Child" }],
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
- ## 3. Design System Integration
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**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
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.