@recursica/adapter-mantine-v8 1.0.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/.storybook/commonArgTypes.ts +148 -0
- package/ARCHITECTURE.md +18 -0
- package/CHANGELOG.md +688 -0
- package/LICENSE +21 -0
- package/OVERSTYLING.md +56 -0
- package/README.md +70 -0
- package/SETUP.md +93 -0
- package/USAGE.md +57 -0
- package/dist/adapter-mantine-v8.cjs +12 -0
- package/dist/adapter-mantine-v8.cjs.map +1 -0
- package/dist/adapter-mantine-v8.css +1 -0
- package/dist/adapter-mantine-v8.js +5759 -0
- package/dist/adapter-mantine-v8.js.map +1 -0
- package/dist/index.d.ts +2095 -0
- package/dist/test.html +1 -0
- package/dist/vite.svg +1 -0
- package/docs/PHILOSOPHY.md +39 -0
- package/llms.txt +74 -0
- package/package.json +131 -0
- package/recursica_brand.json +2677 -0
- package/recursica_tokens.json +799 -0
- package/recursica_ui-kit.json +14981 -0
- package/recursica_variables_scoped.css +30489 -0
- package/src/GlobalExemptions.modules.css +7 -0
- package/src/Introduction.stories.tsx +154 -0
- package/src/OverStyling.tsx +79 -0
- package/src/Version.tsx +44 -0
- package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +72 -0
- package/src/components/Accordion/Accordion.module.css +367 -0
- package/src/components/Accordion/Accordion.stories.tsx +189 -0
- package/src/components/Accordion/Accordion.test.tsx +1 -0
- package/src/components/Accordion/Accordion.tsx +220 -0
- package/src/components/Accordion/USAGE.md +51 -0
- package/src/components/Accordion/index.ts +1 -0
- package/src/components/AssistiveElement/ASSISTIVEELEMENT_IMPLEMENTATION_NOTES.md +45 -0
- package/src/components/AssistiveElement/AssistiveElement.module.css +111 -0
- package/src/components/AssistiveElement/AssistiveElement.stories.tsx +68 -0
- package/src/components/AssistiveElement/AssistiveElement.test.tsx +1 -0
- package/src/components/AssistiveElement/AssistiveElement.tsx +93 -0
- package/src/components/AssistiveElement/USAGE.md +36 -0
- package/src/components/AutoComplete/AUTOCOMPLETE_IMPLEMENTATION_NOTES.md +42 -0
- package/src/components/AutoComplete/AutoComplete.module.css +430 -0
- package/src/components/AutoComplete/AutoComplete.stories.tsx +336 -0
- package/src/components/AutoComplete/AutoComplete.test.tsx +1 -0
- package/src/components/AutoComplete/AutoComplete.tsx +179 -0
- package/src/components/AutoComplete/USAGE.md +80 -0
- package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +34 -0
- package/src/components/Avatar/Avatar.module.css +336 -0
- package/src/components/Avatar/Avatar.stories.tsx +92 -0
- package/src/components/Avatar/Avatar.test.tsx +1 -0
- package/src/components/Avatar/Avatar.tsx +108 -0
- package/src/components/Avatar/USAGE.md +38 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/BADGE_IMPLEMENTATION_NOTES.md +18 -0
- package/src/components/Badge/Badge.module.css +127 -0
- package/src/components/Badge/Badge.stories.tsx +78 -0
- package/src/components/Badge/Badge.test.tsx +1 -0
- package/src/components/Badge/Badge.tsx +64 -0
- package/src/components/Badge/USAGE.md +56 -0
- package/src/components/Breadcrumb/BREADCRUMB_IMPLEMENTATION_NOTES.md +47 -0
- package/src/components/Breadcrumb/Breadcrumb.module.css +48 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
- package/src/components/Breadcrumb/Breadcrumb.test.tsx +1 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +85 -0
- package/src/components/Breadcrumb/USAGE.md +55 -0
- package/src/components/Breadcrumb/markCurrentPageItem.test.ts +49 -0
- package/src/components/Button/Button.module.css +446 -0
- package/src/components/Button/Button.stories.tsx +196 -0
- package/src/components/Button/Button.styleIsolation.dom.test.tsx +98 -0
- package/src/components/Button/Button.tsx +184 -0
- package/src/components/Button/IMPLEMENTATION_NOTES.md +109 -0
- package/src/components/Button/USAGE.md +80 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Card/CARD_IMPLEMENTATION_NOTES.md +17 -0
- package/src/components/Card/Card.module.css +119 -0
- package/src/components/Card/Card.stories.tsx +131 -0
- package/src/components/Card/Card.test.tsx +1 -0
- package/src/components/Card/Card.tsx +248 -0
- package/src/components/Card/USAGE.md +63 -0
- package/src/components/Checkbox/CHECKBOX_IMPLEMENTATION_NOTES.md +25 -0
- package/src/components/Checkbox/Checkbox.module.css +267 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +97 -0
- package/src/components/Checkbox/Checkbox.test.tsx +1 -0
- package/src/components/Checkbox/Checkbox.tsx +151 -0
- package/src/components/Checkbox/CheckboxGroup.stories.tsx +139 -0
- package/src/components/Checkbox/CheckboxGroup.tsx +147 -0
- package/src/components/Checkbox/USAGE.md +48 -0
- package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +110 -0
- package/src/components/Chip/Chip.module.css +288 -0
- package/src/components/Chip/Chip.stories.tsx +128 -0
- package/src/components/Chip/Chip.test.tsx +1 -0
- package/src/components/Chip/Chip.tsx +157 -0
- package/src/components/Chip/USAGE.md +55 -0
- package/src/components/Container/CONTAINER_IMPLEMENTATION_NOTES.md +4 -0
- package/src/components/Container/Container.module.css +7 -0
- package/src/components/Container/Container.stories.tsx +118 -0
- package/src/components/Container/Container.test.tsx +1 -0
- package/src/components/Container/Container.tsx +69 -0
- package/src/components/Container/USAGE.md +47 -0
- package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +31 -0
- package/src/components/DatePicker/DatePicker.icons.tsx +27 -0
- package/src/components/DatePicker/DatePicker.module.css +566 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +158 -0
- package/src/components/DatePicker/DatePicker.test.tsx +1 -0
- package/src/components/DatePicker/DatePicker.tsx +218 -0
- package/src/components/DatePicker/USAGE.md +49 -0
- package/src/components/Dropdown/BareDropdown.tsx +97 -0
- package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Dropdown/Dropdown.module.css +477 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +252 -0
- package/src/components/Dropdown/Dropdown.test.tsx +1 -0
- package/src/components/Dropdown/Dropdown.tsx +213 -0
- package/src/components/Dropdown/USAGE.md +69 -0
- package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +153 -0
- package/src/components/FileInput/FileInput.module.css +280 -0
- package/src/components/FileInput/FileInput.stories.tsx +313 -0
- package/src/components/FileInput/FileInput.test.tsx +1 -0
- package/src/components/FileInput/FileInput.tsx +417 -0
- package/src/components/FileInput/USAGE.md +145 -0
- package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +244 -0
- package/src/components/FileUpload/FileUpload.module.css +205 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +362 -0
- package/src/components/FileUpload/FileUpload.test.tsx +1 -0
- package/src/components/FileUpload/FileUpload.tsx +354 -0
- package/src/components/FileUpload/USAGE.md +199 -0
- package/src/components/Flex/FLEX_IMPLEMENTATION_NOTES.md +4 -0
- package/src/components/Flex/Flex.module.css +7 -0
- package/src/components/Flex/Flex.stories.tsx +128 -0
- package/src/components/Flex/Flex.test.tsx +1 -0
- package/src/components/Flex/Flex.tsx +75 -0
- package/src/components/Flex/USAGE.md +48 -0
- package/src/components/FormControlLayout/FormControlLayout.module.css +90 -0
- package/src/components/FormControlLayout/FormControlLayout.stories.tsx +118 -0
- package/src/components/FormControlLayout/FormControlLayout.test.tsx +1 -0
- package/src/components/FormControlLayout/FormControlLayout.tsx +74 -0
- package/src/components/FormControlLayout/USAGE.md +40 -0
- package/src/components/FormControlWrapper/FORMCONTROLWRAPPER_IMPLEMENTATION_NOTES.md +32 -0
- package/src/components/FormControlWrapper/FormControlWrapper.module.css +75 -0
- package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +129 -0
- package/src/components/FormControlWrapper/FormControlWrapper.test.tsx +1 -0
- package/src/components/FormControlWrapper/FormControlWrapper.tsx +151 -0
- package/src/components/FormControlWrapper/USAGE.md +47 -0
- package/src/components/Grid/GRID_IMPLEMENTATION_NOTES.md +18 -0
- package/src/components/Grid/Grid.module.css +11 -0
- package/src/components/Grid/Grid.stories.tsx +206 -0
- package/src/components/Grid/Grid.test.tsx +1 -0
- package/src/components/Grid/Grid.tsx +140 -0
- package/src/components/Grid/USAGE.md +47 -0
- package/src/components/Group/GROUP_IMPLEMENTATION_NOTES.md +4 -0
- package/src/components/Group/Group.module.css +7 -0
- package/src/components/Group/Group.stories.tsx +120 -0
- package/src/components/Group/Group.test.tsx +1 -0
- package/src/components/Group/Group.tsx +60 -0
- package/src/components/Group/USAGE.md +48 -0
- package/src/components/Heading/HEADING_IMPLEMENTATION_NOTES.md +18 -0
- package/src/components/Heading/Heading.module.css +13 -0
- package/src/components/Heading/Heading.stories.tsx +50 -0
- package/src/components/Heading/Heading.test.tsx +1 -0
- package/src/components/Heading/Heading.tsx +52 -0
- package/src/components/Heading/USAGE.md +36 -0
- package/src/components/HoverCard/HOVERCARD_IMPLEMENTATION_NOTES.md +77 -0
- package/src/components/HoverCard/HoverCard.module.css +90 -0
- package/src/components/HoverCard/HoverCard.stories.tsx +172 -0
- package/src/components/HoverCard/HoverCard.test.tsx +1 -0
- package/src/components/HoverCard/HoverCard.tsx +133 -0
- package/src/components/HoverCard/USAGE.md +52 -0
- package/src/components/Label/LABEL_IMPLEMENTATION_NOTES.md +48 -0
- package/src/components/Label/Label.module.css +141 -0
- package/src/components/Label/Label.stories.tsx +131 -0
- package/src/components/Label/Label.test.tsx +1 -0
- package/src/components/Label/Label.tsx +113 -0
- package/src/components/Label/USAGE.md +47 -0
- package/src/components/Label/index.ts +1 -0
- package/src/components/Layer/USAGE.md +54 -0
- package/src/components/Link/IMPLEMENTATION_NOTES.md +19 -0
- package/src/components/Link/Link.module.css +110 -0
- package/src/components/Link/Link.stories.tsx +58 -0
- package/src/components/Link/Link.test.tsx +1 -0
- package/src/components/Link/Link.tsx +67 -0
- package/src/components/Link/USAGE.md +56 -0
- package/src/components/Link/index.ts +1 -0
- package/src/components/Loader/LOADER_IMPLEMENTATION_NOTES.md +24 -0
- package/src/components/Loader/Loader.animate.dom.test.tsx +82 -0
- package/src/components/Loader/Loader.module.css +63 -0
- package/src/components/Loader/Loader.stories.tsx +123 -0
- package/src/components/Loader/Loader.test.tsx +1 -0
- package/src/components/Loader/Loader.tsx +69 -0
- package/src/components/Loader/USAGE.md +44 -0
- package/src/components/Menu/MENU_IMPLEMENTATION_NOTES.md +88 -0
- package/src/components/Menu/Menu.module.css +280 -0
- package/src/components/Menu/Menu.stories.tsx +371 -0
- package/src/components/Menu/Menu.test.tsx +1 -0
- package/src/components/Menu/Menu.tsx +373 -0
- package/src/components/Menu/USAGE.md +55 -0
- package/src/components/Modal/MODAL_IMPLEMENTATION_NOTES.md +32 -0
- package/src/components/Modal/Modal.module.css +302 -0
- package/src/components/Modal/Modal.stories.tsx +105 -0
- package/src/components/Modal/Modal.test.tsx +1 -0
- package/src/components/Modal/Modal.tsx +368 -0
- package/src/components/Modal/USAGE.md +58 -0
- package/src/components/Modal/index.ts +1 -0
- package/src/components/NumberInput/NUMBER_INPUT_IMPLEMENTATION_NOTES.md +24 -0
- package/src/components/NumberInput/NumberInput.module.css +302 -0
- package/src/components/NumberInput/NumberInput.stories.tsx +96 -0
- package/src/components/NumberInput/NumberInput.test.tsx +1 -0
- package/src/components/NumberInput/NumberInput.tsx +157 -0
- package/src/components/NumberInput/USAGE.md +52 -0
- package/src/components/Pagination/IMPLEMENTATION_NOTES.md +16 -0
- package/src/components/Pagination/Pagination.icons.tsx +66 -0
- package/src/components/Pagination/Pagination.module.css +188 -0
- package/src/components/Pagination/Pagination.stories.tsx +48 -0
- package/src/components/Pagination/Pagination.test.tsx +1 -0
- package/src/components/Pagination/Pagination.tsx +265 -0
- package/src/components/Pagination/USAGE.md +36 -0
- package/src/components/Pagination/index.ts +5 -0
- package/src/components/Panel/PANEL_IMPLEMENTATION_NOTES.md +116 -0
- package/src/components/Panel/Panel.module.css +209 -0
- package/src/components/Panel/Panel.stories.tsx +227 -0
- package/src/components/Panel/Panel.test.tsx +1 -0
- package/src/components/Panel/Panel.tsx +269 -0
- package/src/components/Panel/USAGE.md +56 -0
- package/src/components/Popover/IMPLEMENTATION_NOTES.md +77 -0
- package/src/components/Popover/Popover.module.css +86 -0
- package/src/components/Popover/Popover.stories.tsx +133 -0
- package/src/components/Popover/Popover.test.tsx +1 -0
- package/src/components/Popover/Popover.tsx +142 -0
- package/src/components/Popover/USAGE.md +61 -0
- package/src/components/Popover/index.ts +1 -0
- package/src/components/Radio/Radio.module.css +231 -0
- package/src/components/Radio/Radio.stories.tsx +93 -0
- package/src/components/Radio/Radio.test.tsx +1 -0
- package/src/components/Radio/Radio.tsx +168 -0
- package/src/components/Radio/RadioGroup.stories.tsx +117 -0
- package/src/components/Radio/RadioGroup.tsx +124 -0
- package/src/components/Radio/USAGE.md +36 -0
- package/src/components/ReadOnlyField/READ_ONLY_FIELD_IMPLEMENTATION_NOTES.md +15 -0
- package/src/components/ReadOnlyField/ReadOnlyBooleanField.tsx +21 -0
- package/src/components/ReadOnlyField/ReadOnlyField.module.css +54 -0
- package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +163 -0
- package/src/components/ReadOnlyField/ReadOnlyField.test.tsx +1 -0
- package/src/components/ReadOnlyField/ReadOnlyField.tsx +114 -0
- package/src/components/ReadOnlyField/ReadOnlySwitchField.tsx +21 -0
- package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +48 -0
- package/src/components/ReadOnlyField/USAGE.md +44 -0
- package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +93 -0
- package/src/components/ReadOnlyField/index.ts +5 -0
- package/src/components/RecursicaThemeProvider/USAGE.md +60 -0
- package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +25 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +185 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +106 -0
- package/src/components/SegmentedControl/SegmentedControl.test.tsx +1 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +110 -0
- package/src/components/SegmentedControl/USAGE.md +55 -0
- package/src/components/Slider/IMPLEMENTATION_NOTES.md +92 -0
- package/src/components/Slider/Slider.module.css +588 -0
- package/src/components/Slider/Slider.stories.tsx +282 -0
- package/src/components/Slider/Slider.test.tsx +1 -0
- package/src/components/Slider/Slider.tsx +391 -0
- package/src/components/Slider/USAGE.md +56 -0
- package/src/components/Stack/STACK_IMPLEMENTATION_NOTES.md +4 -0
- package/src/components/Stack/Stack.module.css +7 -0
- package/src/components/Stack/Stack.stories.tsx +108 -0
- package/src/components/Stack/Stack.test.tsx +1 -0
- package/src/components/Stack/Stack.tsx +62 -0
- package/src/components/Stack/USAGE.md +48 -0
- package/src/components/Stepper/IMPLEMENTATION_NOTES.md +28 -0
- package/src/components/Stepper/Stepper.module.css +404 -0
- package/src/components/Stepper/Stepper.stories.tsx +148 -0
- package/src/components/Stepper/Stepper.test.tsx +1 -0
- package/src/components/Stepper/Stepper.tsx +93 -0
- package/src/components/Stepper/USAGE.md +41 -0
- package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +64 -0
- package/src/components/Switch/Switch.module.css +279 -0
- package/src/components/Switch/Switch.stories.tsx +114 -0
- package/src/components/Switch/Switch.test.tsx +1 -0
- package/src/components/Switch/Switch.tsx +177 -0
- package/src/components/Switch/SwitchGroup.stories.tsx +132 -0
- package/src/components/Switch/SwitchGroup.tsx +127 -0
- package/src/components/Switch/USAGE.md +42 -0
- package/src/components/Table/TABLE_IMPLEMENTATION_NOTES.md +78 -0
- package/src/components/Table/Table.icons.tsx +40 -0
- package/src/components/Table/Table.module.css +444 -0
- package/src/components/Table/Table.stories.tsx +140 -0
- package/src/components/Table/Table.test.tsx +1 -0
- package/src/components/Table/Table.tsx +243 -0
- package/src/components/Table/USAGE.md +78 -0
- package/src/components/Tabs/IMPLEMENTATION_NOTES.md +27 -0
- package/src/components/Tabs/Tabs.module.css +591 -0
- package/src/components/Tabs/Tabs.stories.tsx +154 -0
- package/src/components/Tabs/Tabs.test.tsx +1 -0
- package/src/components/Tabs/Tabs.tsx +114 -0
- package/src/components/Tabs/USAGE.md +45 -0
- package/src/components/Text/TEXT_IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Text/Text.module.css +13 -0
- package/src/components/Text/Text.stories.tsx +75 -0
- package/src/components/Text/Text.test.tsx +1 -0
- package/src/components/Text/Text.tsx +53 -0
- package/src/components/Text/USAGE.md +40 -0
- package/src/components/TextArea/TEXTAREA_IMPLEMENTATION_NOTES.md +7 -0
- package/src/components/TextArea/TextArea.module.css +183 -0
- package/src/components/TextArea/TextArea.stories.tsx +94 -0
- package/src/components/TextArea/TextArea.test.tsx +1 -0
- package/src/components/TextArea/TextArea.tsx +137 -0
- package/src/components/TextArea/USAGE.md +48 -0
- package/src/components/TextField/TEXTFIELD_IMPLEMENTATION_NOTES.md +19 -0
- package/src/components/TextField/TextField.module.css +264 -0
- package/src/components/TextField/TextField.stories.tsx +162 -0
- package/src/components/TextField/TextField.test.tsx +1 -0
- package/src/components/TextField/TextField.tsx +139 -0
- package/src/components/TextField/USAGE.md +44 -0
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +119 -0
- package/src/components/TimePicker/TimePicker.module.css +291 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +141 -0
- package/src/components/TimePicker/TimePicker.test.tsx +1 -0
- package/src/components/TimePicker/TimePicker.tsx +304 -0
- package/src/components/TimePicker/USAGE.md +57 -0
- package/src/components/Timeline/TIMELINE_IMPLEMENTATION_NOTES.md +13 -0
- package/src/components/Timeline/Timeline.module.css +373 -0
- package/src/components/Timeline/Timeline.stories.tsx +128 -0
- package/src/components/Timeline/Timeline.test.tsx +1 -0
- package/src/components/Timeline/Timeline.tsx +73 -0
- package/src/components/Timeline/TimelineItem.tsx +79 -0
- package/src/components/Timeline/USAGE.md +49 -0
- package/src/components/Toast/TOAST_IMPLEMENTATION_NOTES.md +39 -0
- package/src/components/Toast/Toast.module.css +184 -0
- package/src/components/Toast/Toast.stories.tsx +67 -0
- package/src/components/Toast/Toast.test.tsx +1 -0
- package/src/components/Toast/Toast.tsx +82 -0
- package/src/components/Toast/USAGE.md +50 -0
- package/src/components/Toast/index.ts +1 -0
- package/src/components/Tooltip/TOOLTIP_IMPLEMENTATION_NOTES.md +85 -0
- package/src/components/Tooltip/Tooltip.module.css +91 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +165 -0
- package/src/components/Tooltip/Tooltip.test.tsx +1 -0
- package/src/components/Tooltip/Tooltip.tsx +117 -0
- package/src/components/Tooltip/USAGE.md +80 -0
- package/src/components/TransferList/TRANSFERLIST_IMPLEMENTATION_NOTES.md +111 -0
- package/src/components/TransferList/TransferList.module.css +182 -0
- package/src/components/TransferList/TransferList.stories.tsx +121 -0
- package/src/components/TransferList/TransferList.test.tsx +1 -0
- package/src/components/TransferList/TransferList.tsx +419 -0
- package/src/components/TransferList/USAGE.md +77 -0
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +48 -0
- package/src/components/Tree/Tree.module.css +232 -0
- package/src/components/Tree/Tree.stories.tsx +104 -0
- package/src/components/Tree/Tree.test.tsx +1 -0
- package/src/components/Tree/Tree.tsx +220 -0
- package/src/components/Tree/USAGE.md +96 -0
- package/src/components/Tree/index.ts +1 -0
- package/src/components/index.ts +52 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +179 -0
- package/src/types/index.ts +9 -0
- package/src/types/mantine.d.ts +7 -0
- package/src/types/scss-modules.d.ts +9 -0
- package/src/utils/ColorSchemeWrapper.tsx +27 -0
- package/src/utils/RequireAccessibleLabel.ts +1 -0
- package/src/utils/copyToClipboard.ts +36 -0
- package/src/utils/filterStylingProps.test.ts +85 -0
- package/src/utils/filterStylingProps.ts +150 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/renderRichOption.tsx +65 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
|
|
3
|
+
* recursica-allow-brand: --recursica_brand_states_disabled
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
8
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
9
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* HARDCODED VALUES:
|
|
13
|
+
* - list-style/margin/padding resets on .root/.subtree: layout resets, no corresponding
|
|
14
|
+
* design tokens (structural, not visual design values).
|
|
15
|
+
* - .expandGlyph's rotation transition (150ms ease): no dedicated timing token exists for Tree;
|
|
16
|
+
* the glyph itself is sized/colored entirely by Button's own "text"/"small" tokens now, not a
|
|
17
|
+
* hand-drawn size/color like the previous plain-SVG implementation.
|
|
18
|
+
* - Hover overlay uses the generic --recursica_brand_states_hover_* tokens (same technique as
|
|
19
|
+
* Menu/Accordion/Button): Tree has no component-specific hover tokens of its own.
|
|
20
|
+
*
|
|
21
|
+
* NOTE: the row's box model (padding/border/font/color/background) lives entirely on `.label`,
|
|
22
|
+
* not `.row` — the highlighted "chip" (hover/selected/focus) covers only the label, never the
|
|
23
|
+
* expand button. `.row` itself is a plain flex layout container (indentation + button-node-gap).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
.root {
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: 0;
|
|
29
|
+
list-style: none;
|
|
30
|
+
max-width: var(--recursica_ui-kit_components_tree_properties_max-width);
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: var(--recursica_ui-kit_components_tree_properties_item-gap);
|
|
34
|
+
|
|
35
|
+
/* Mantine computes each node's --label-offset (per-level indent) from --level-offset via its
|
|
36
|
+
own internal vars resolver (same category of override as Timeline's --mantine-spacing-xl /
|
|
37
|
+
--tl-line-width). Use !important to reliably beat Mantine's own generated rule. */
|
|
38
|
+
--level-offset: var(
|
|
39
|
+
--recursica_ui-kit_components_tree_properties_indent
|
|
40
|
+
) !important;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Whole-tree `disabled` (no per-node concept — see RecursicaTreeProps.ts). `pointer-events: none`
|
|
44
|
+
blocks every mouse interaction tree-wide in one rule (row clicks, chevron clicks); keyboard is
|
|
45
|
+
separately blocked in Tree.tsx, since pointer-events has no effect on Tab/Enter/Space/arrows.
|
|
46
|
+
Uses the generic disabled opacity token, same convention as every other disabled component in
|
|
47
|
+
the design system (no tree-specific disabled token exists). */
|
|
48
|
+
.root[data-disabled] {
|
|
49
|
+
opacity: var(--recursica_brand_states_disabled);
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.subtree {
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
list-style: none;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: var(--recursica_ui-kit_components_tree_properties_item-gap);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.node {
|
|
63
|
+
/* Mantine's own base class already resets margin/padding/list-style and sets cursor:pointer
|
|
64
|
+
on this <li>; it also holds the subtree as a second child, so all node-level visual styling
|
|
65
|
+
(box model, colors, typography) lives on .label instead, not here. */
|
|
66
|
+
position: relative;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Mantine's roving-tabindex keyboard navigation (arrow up/down moves focus between rows, left/
|
|
70
|
+
right expand/collapse, matching the WAI-ARIA treeitem pattern) already works out of the box —
|
|
71
|
+
but with no visible indicator, it looked broken. DOM focus lands on this <li> (.node), not the
|
|
72
|
+
visible row box, so the ring is drawn on .label instead — the "node" content area, deliberately
|
|
73
|
+
excluding the expand Button, which never shows a focus state of its own (it's not independently
|
|
74
|
+
focusable at all: tabIndex={-1}). */
|
|
75
|
+
.node:focus-visible {
|
|
76
|
+
outline: none;
|
|
77
|
+
}
|
|
78
|
+
/* `> .row` (child combinator), not a plain descendant selector: a node's own `.row` is always a
|
|
79
|
+
direct child of its `.node` <li>, while every descendant node's `.row` (inside the nested
|
|
80
|
+
`.subtree` <ul>) is not — without the `>`, focusing a parent node drew the ring on every
|
|
81
|
+
expanded descendant's label too, since they're all still descendants of the focused `.node`. */
|
|
82
|
+
.node:focus-visible > .row .label {
|
|
83
|
+
box-shadow:
|
|
84
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
85
|
+
var(--recursica_brand_states_focus_color),
|
|
86
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
87
|
+
var(--recursica_brand_states_focus_margin)
|
|
88
|
+
var(--recursica_brand_states_focus_color);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.row {
|
|
92
|
+
position: relative;
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
margin-left: var(--label-offset, 0px);
|
|
98
|
+
gap: var(--recursica_ui-kit_components_tree_properties_button-node-gap);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* The expand Button is always rendered (even for leaf nodes, which have nothing to toggle) so
|
|
102
|
+
every row reserves identical layout space — hidden via visibility (not display) on leaf rows so
|
|
103
|
+
the label stays aligned across the whole tree without duplicating Button's own size tokens. */
|
|
104
|
+
.expandButton {
|
|
105
|
+
position: relative;
|
|
106
|
+
z-index: 1;
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
}
|
|
109
|
+
.row:not([data-has-children]) .expandButton {
|
|
110
|
+
visibility: hidden;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.expandGlyph {
|
|
114
|
+
color: inherit;
|
|
115
|
+
}
|
|
116
|
+
.row[data-has-children] .expandGlyph {
|
|
117
|
+
transition: transform 150ms ease;
|
|
118
|
+
}
|
|
119
|
+
.row[data-expanded] .expandGlyph {
|
|
120
|
+
transform: rotate(90deg);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.label {
|
|
124
|
+
position: relative;
|
|
125
|
+
z-index: 1;
|
|
126
|
+
/* Sized to its own content, not stretched to fill the row's remaining width — the
|
|
127
|
+
hover/selected/focus "chip" should cover just the label, not the full row width. */
|
|
128
|
+
flex: 0 1 auto;
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
padding: var(--recursica_ui-kit_components_tree_properties_vertical-padding)
|
|
133
|
+
var(--recursica_ui-kit_components_tree_properties_horizontal-padding);
|
|
134
|
+
border-style: solid; /* HARDCODE: structural border rule; thickness/color are tokened */
|
|
135
|
+
border-width: var(--recursica_ui-kit_components_tree_properties_border-size);
|
|
136
|
+
border-radius: var(
|
|
137
|
+
--recursica_ui-kit_components_tree_properties_border-radius
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
font-family: var(
|
|
141
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_font-family
|
|
142
|
+
);
|
|
143
|
+
font-size: var(
|
|
144
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_font-size
|
|
145
|
+
);
|
|
146
|
+
font-style: var(
|
|
147
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_font-style
|
|
148
|
+
);
|
|
149
|
+
font-weight: var(
|
|
150
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_font-weight
|
|
151
|
+
);
|
|
152
|
+
letter-spacing: var(
|
|
153
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_letter-spacing
|
|
154
|
+
);
|
|
155
|
+
line-height: var(
|
|
156
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_line-height
|
|
157
|
+
);
|
|
158
|
+
text-decoration: var(
|
|
159
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_text-decoration
|
|
160
|
+
);
|
|
161
|
+
text-transform: var(
|
|
162
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_text_text-transform
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
color: var(
|
|
166
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_colors_text-color
|
|
167
|
+
);
|
|
168
|
+
background-color: var(
|
|
169
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_colors_background-color
|
|
170
|
+
);
|
|
171
|
+
border-color: var(
|
|
172
|
+
--recursica_ui-kit_components_tree_variants_selection-states_unselected_properties_colors_border-color
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Hover overlay via ::after pseudo-element (same technique as Menu/Accordion), scoped to the
|
|
177
|
+
label's own box so hover never visually covers the expand button. `.label`'s `position:
|
|
178
|
+
relative` + `z-index: 1` above already forms a stacking context, so `z-index: -1` here paints
|
|
179
|
+
the overlay behind the label's real text content within that context, not behind `.label`
|
|
180
|
+
itself. */
|
|
181
|
+
.label::after {
|
|
182
|
+
content: "";
|
|
183
|
+
position: absolute;
|
|
184
|
+
inset: 0;
|
|
185
|
+
border-radius: inherit;
|
|
186
|
+
z-index: -1;
|
|
187
|
+
pointer-events: none;
|
|
188
|
+
background-color: var(--recursica_brand_states_hover_color);
|
|
189
|
+
opacity: 0;
|
|
190
|
+
transition: opacity 150ms ease;
|
|
191
|
+
}
|
|
192
|
+
.row:hover .label::after,
|
|
193
|
+
.row[data-hovered] .label::after {
|
|
194
|
+
opacity: var(--recursica_brand_states_hover_opacity);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.label[data-selected] {
|
|
198
|
+
font-family: var(
|
|
199
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-family
|
|
200
|
+
);
|
|
201
|
+
font-size: var(
|
|
202
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-size
|
|
203
|
+
);
|
|
204
|
+
font-style: var(
|
|
205
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-style
|
|
206
|
+
);
|
|
207
|
+
font-weight: var(
|
|
208
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-weight
|
|
209
|
+
);
|
|
210
|
+
letter-spacing: var(
|
|
211
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_letter-spacing
|
|
212
|
+
);
|
|
213
|
+
line-height: var(
|
|
214
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_line-height
|
|
215
|
+
);
|
|
216
|
+
text-decoration: var(
|
|
217
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_text-decoration
|
|
218
|
+
);
|
|
219
|
+
text-transform: var(
|
|
220
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_text-transform
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
color: var(
|
|
224
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_colors_text-color
|
|
225
|
+
);
|
|
226
|
+
background-color: var(
|
|
227
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_colors_background-color
|
|
228
|
+
);
|
|
229
|
+
border-color: var(
|
|
230
|
+
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_colors_border-color
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
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
|
+
];
|
|
32
|
+
|
|
33
|
+
const meta: Meta<typeof Tree> = {
|
|
34
|
+
title: "UI-Kit/Tree",
|
|
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 \`@mantine/core\`'s \`Tree\` with a fully custom node 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
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default meta;
|
|
51
|
+
|
|
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
|
+
/** Whole tree disabled, with a node pre-selected so the selected chip's styling under the
|
|
84
|
+
* disabled dimming can be checked alongside unselected rows. */
|
|
85
|
+
export const Disabled: StoryObj<typeof Tree> = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<Tree
|
|
88
|
+
data={sampleData}
|
|
89
|
+
initialExpandedValues={["documents"]}
|
|
90
|
+
initialSelectedValues={["documents/resume.pdf"]}
|
|
91
|
+
disabled
|
|
92
|
+
/>
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Demonstrates the component nested inside a non-default layer — the one case where an
|
|
97
|
+
* explicit `<Layer>` wrap belongs in a story (see COMPONENT_STORYBOOK_GUIDE.md §9). */
|
|
98
|
+
export const LayerOne: StoryObj<typeof Tree> = {
|
|
99
|
+
render: () => (
|
|
100
|
+
<Layer layer={1} style={{ padding: "24px" }}>
|
|
101
|
+
<Tree data={sampleData} initialExpandedValues={["documents"]} />
|
|
102
|
+
</Layer>
|
|
103
|
+
),
|
|
104
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Placeholder for future Tree component tests. Add tests here if/when needed.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Tree as MantineTree,
|
|
4
|
+
useTree,
|
|
5
|
+
getTreeExpandedState,
|
|
6
|
+
type RenderTreeNodePayload,
|
|
7
|
+
} from "@mantine/core";
|
|
8
|
+
import { useMergedRef } from "@mantine/hooks";
|
|
9
|
+
import {
|
|
10
|
+
filterStylingProps,
|
|
11
|
+
mergeClassNames,
|
|
12
|
+
type RecursicaOverStyled,
|
|
13
|
+
} from "../../utils/filterStylingProps";
|
|
14
|
+
import { type RecursicaTreeProps } from "@recursica/adapter-common";
|
|
15
|
+
import { Button } from "../Button/Button";
|
|
16
|
+
import styles from "./Tree.module.css";
|
|
17
|
+
|
|
18
|
+
export type TreeProps = RecursicaOverStyled<
|
|
19
|
+
RecursicaTreeProps & Omit<React.ComponentPropsWithoutRef<"ul">, "children">
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
/** Simple chevron glyph; rotates via the row's `[data-expanded]` in CSS (`.row[data-expanded]
|
|
23
|
+
* .expandGlyph`) — this reaches the glyph regardless of how deeply `Button` nests it internally,
|
|
24
|
+
* since it's a plain descendant-combinator selector, not dependent on Button's own DOM structure.
|
|
25
|
+
* Uses `currentColor` so it always matches Button's own tokened icon color for the "text" variant. */
|
|
26
|
+
function ExpandGlyph() {
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
className={styles.expandGlyph}
|
|
30
|
+
viewBox="0 0 16 16"
|
|
31
|
+
width="1em"
|
|
32
|
+
height="1em"
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
strokeWidth="2"
|
|
36
|
+
strokeLinecap="round"
|
|
37
|
+
strokeLinejoin="round"
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
>
|
|
40
|
+
<path d="M5 3l5 5-5 5" />
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Curried so the fixed-shape `renderNode` callback (Mantine controls its signature) can still
|
|
46
|
+
* close over `disabled`, which isn't part of `RenderTreeNodePayload`. */
|
|
47
|
+
function createRenderTreeNode(disabled: boolean) {
|
|
48
|
+
return function renderTreeNode({
|
|
49
|
+
node,
|
|
50
|
+
hasChildren,
|
|
51
|
+
expanded,
|
|
52
|
+
selected,
|
|
53
|
+
tree,
|
|
54
|
+
elementProps,
|
|
55
|
+
}: RenderTreeNodePayload) {
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
{...elementProps}
|
|
59
|
+
className={styles.row}
|
|
60
|
+
data-has-children={hasChildren || undefined}
|
|
61
|
+
data-expanded={hasChildren && expanded ? true : undefined}
|
|
62
|
+
>
|
|
63
|
+
{/* The only element that toggles expand/collapse on click — independent of selection,
|
|
64
|
+
which `elementProps.onClick` (spread onto `.row` above) handles for the rest of the
|
|
65
|
+
row. Stops propagation so the click never also reaches `.row`'s own handler and
|
|
66
|
+
selects the node. Never independently focusable/tab-stoppable (tabIndex={-1},
|
|
67
|
+
aria-hidden), so the row stays the only focusable element and this button never
|
|
68
|
+
shows its own focus state. `onMouseDown` prevents the browser's default
|
|
69
|
+
click-focuses-the-button behavior (tabIndex={-1} only removes it from the tab
|
|
70
|
+
order, it doesn't stop a direct mouse click from focusing it) — without this, a
|
|
71
|
+
click here leaves the (aria-hidden) button focused, which browsers now flag as an
|
|
72
|
+
accessibility violation ("focus must not be hidden from assistive technology").
|
|
73
|
+
Always rendered, even for leaf nodes, so every row reserves the same layout space;
|
|
74
|
+
CSS hides it (visibility, not display) when there are no children to toggle. Guards
|
|
75
|
+
`disabled` itself (not just via the CSS `pointer-events: none` on `.root`) since this
|
|
76
|
+
handler bypasses Mantine's own `expandOnClick`/`selectOnClick` flags entirely by
|
|
77
|
+
calling `tree.toggleExpanded` directly. */}
|
|
78
|
+
<Button
|
|
79
|
+
overStyled
|
|
80
|
+
variant="text"
|
|
81
|
+
size="small"
|
|
82
|
+
icon={<ExpandGlyph />}
|
|
83
|
+
aria-label="Toggle subtree"
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
tabIndex={-1}
|
|
86
|
+
className={styles.expandButton}
|
|
87
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
88
|
+
onClick={(event) => {
|
|
89
|
+
event.stopPropagation();
|
|
90
|
+
if (disabled) return;
|
|
91
|
+
tree.toggleExpanded(node.value);
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
<span className={styles.label} data-selected={selected || undefined}>
|
|
95
|
+
{node.label}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Recursively renders hierarchical `data` as an expandable/selectable tree.
|
|
104
|
+
*
|
|
105
|
+
* **Recursica Abstract:**
|
|
106
|
+
* Wraps Mantine's `Tree` with a fully custom node renderer so every visual aspect (row box
|
|
107
|
+
* model, selected/unselected colors and typography, indent, item spacing) comes from
|
|
108
|
+
* Recursica's `tree` design tokens rather than Mantine's defaults.
|
|
109
|
+
*
|
|
110
|
+
* **Interaction pattern (fixed, not prop-configurable):** expand/collapse and select are
|
|
111
|
+
* independent — the chevron button toggles a node's subtree only, clicking the rest of a row
|
|
112
|
+
* (or pressing `Enter`/`Space`) selects it only, and `ArrowLeft`/`ArrowRight` toggle expansion
|
|
113
|
+
* only. See `RecursicaTreeProps` for the full breakdown.
|
|
114
|
+
*/
|
|
115
|
+
export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
|
|
116
|
+
{
|
|
117
|
+
overStyled = false,
|
|
118
|
+
data,
|
|
119
|
+
initialExpandedValues,
|
|
120
|
+
initialSelectedValues,
|
|
121
|
+
multiple = false,
|
|
122
|
+
disabled = false,
|
|
123
|
+
onNodeExpand,
|
|
124
|
+
onNodeCollapse,
|
|
125
|
+
onSelectedChange,
|
|
126
|
+
...rest
|
|
127
|
+
},
|
|
128
|
+
ref,
|
|
129
|
+
) {
|
|
130
|
+
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
131
|
+
|
|
132
|
+
const tree = useTree({
|
|
133
|
+
initialExpandedState: initialExpandedValues
|
|
134
|
+
? getTreeExpandedState(data, initialExpandedValues)
|
|
135
|
+
: undefined,
|
|
136
|
+
initialSelectedState: initialSelectedValues,
|
|
137
|
+
multiple,
|
|
138
|
+
onNodeExpand,
|
|
139
|
+
onNodeCollapse,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Mantine's `useTree` tracks selection internally but has no `onSelectedChange` callback of
|
|
143
|
+
// its own; surface one at the Recursica layer by watching the controller's selectedState.
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
onSelectedChange?.(tree.selectedState);
|
|
146
|
+
}, [tree.selectedState, onSelectedChange]);
|
|
147
|
+
|
|
148
|
+
// `Enter`/`Space` select the focused node. Mantine's own `TreeNode` only wires this for
|
|
149
|
+
// `Space`, and only as an expand toggle (`expandOnSpace`, disabled below) — it has no
|
|
150
|
+
// select-on-key behavior and no `Enter` handling at all, and its key handler lives on the
|
|
151
|
+
// `<li role="treeitem">` itself (not reachable through `renderNode`'s `elementProps`), so this
|
|
152
|
+
// is added as a native listener on the tree root instead. `tree.select` is referentially
|
|
153
|
+
// stable (Mantine wraps it in `useCallback` with no deps), so this effect only re-runs when
|
|
154
|
+
// `disabled` actually changes.
|
|
155
|
+
const rootRef = React.useRef<HTMLUListElement>(null);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const root = rootRef.current;
|
|
158
|
+
if (!root) return;
|
|
159
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
160
|
+
if (disabled) return;
|
|
161
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
162
|
+
const treeItem = (event.target as HTMLElement).closest<HTMLElement>(
|
|
163
|
+
'[role="treeitem"]',
|
|
164
|
+
);
|
|
165
|
+
const value = treeItem?.dataset.value;
|
|
166
|
+
if (!value) return;
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
tree.select(value);
|
|
169
|
+
};
|
|
170
|
+
root.addEventListener("keydown", handleKeyDown);
|
|
171
|
+
return () => root.removeEventListener("keydown", handleKeyDown);
|
|
172
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
173
|
+
}, [tree.select, disabled]);
|
|
174
|
+
|
|
175
|
+
// When disabled, block every keydown at the capture phase — before it can reach either the
|
|
176
|
+
// listener above or Mantine's own internal per-node keydown handling (`ArrowLeft`/`ArrowRight`
|
|
177
|
+
// expand/collapse, baked into `TreeNode` itself with no prop to disable it). A capture-phase
|
|
178
|
+
// `stopPropagation` here keeps the event from ever reaching its target, so neither listener
|
|
179
|
+
// fires — this is the only reachable way to block Mantine's internal handling for a component
|
|
180
|
+
// with no `disabled` concept of its own. Mouse clicks are separately blocked via `pointer-
|
|
181
|
+
// events: none` in CSS (`.root[data-disabled]`); this effect only needs to cover keyboard.
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const root = rootRef.current;
|
|
184
|
+
if (!root || !disabled) return;
|
|
185
|
+
const blockAllKeydown = (event: KeyboardEvent) => {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
event.stopPropagation();
|
|
188
|
+
};
|
|
189
|
+
root.addEventListener("keydown", blockAllKeydown, { capture: true });
|
|
190
|
+
return () =>
|
|
191
|
+
root.removeEventListener("keydown", blockAllKeydown, { capture: true });
|
|
192
|
+
}, [disabled]);
|
|
193
|
+
const mergedRef = useMergedRef(ref, rootRef);
|
|
194
|
+
|
|
195
|
+
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
196
|
+
const classNameProp = restRecord.className as string | undefined;
|
|
197
|
+
const rootClass = classNameProp
|
|
198
|
+
? `${styles.root} ${classNameProp}`
|
|
199
|
+
: styles.root;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<MantineTree
|
|
203
|
+
ref={mergedRef}
|
|
204
|
+
{...(sanitizedProps as unknown as Record<string, unknown>)}
|
|
205
|
+
data={data}
|
|
206
|
+
tree={tree}
|
|
207
|
+
expandOnClick={false}
|
|
208
|
+
selectOnClick={!disabled}
|
|
209
|
+
expandOnSpace={false}
|
|
210
|
+
renderNode={createRenderTreeNode(disabled)}
|
|
211
|
+
data-disabled={disabled || undefined}
|
|
212
|
+
classNames={mergeClassNames(
|
|
213
|
+
{ root: rootClass, node: styles.node, subtree: styles.subtree },
|
|
214
|
+
restRecord.classNames as Partial<Record<string, string>> | undefined,
|
|
215
|
+
)}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
Tree.displayName = "Tree";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Tree - Usage Guide
|
|
2
|
+
|
|
3
|
+
This document describes how to integrate and use the `Tree` component in your projects using `@recursica/adapter-mantine-v8`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Import Reference
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Tree } from "@recursica/adapter-mantine-v8";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. Basic Example
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import React from "react";
|
|
19
|
+
import { Tree } from "@recursica/adapter-mantine-v8";
|
|
20
|
+
|
|
21
|
+
export default function Demo() {
|
|
22
|
+
return (
|
|
23
|
+
<Tree
|
|
24
|
+
data={[
|
|
25
|
+
{
|
|
26
|
+
value: "1",
|
|
27
|
+
label: "Root",
|
|
28
|
+
children: [
|
|
29
|
+
{ value: "1.1", label: "Child A" },
|
|
30
|
+
{ value: "1.2", label: "Child B" },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{ value: "2", label: "Sibling" },
|
|
34
|
+
]}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Each node needs a unique `value` and a `label`. Any node with a `children` array (even an empty one) renders an expand/collapse chevron.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 3. Controlled Selection / Expansion
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<Tree
|
|
48
|
+
data={data}
|
|
49
|
+
initialExpandedValues={["1"]}
|
|
50
|
+
initialSelectedValues={["1.1"]}
|
|
51
|
+
multiple
|
|
52
|
+
onSelectedChange={(values) => console.log("selected:", values)}
|
|
53
|
+
onNodeExpand={(value) => console.log("expanded:", value)}
|
|
54
|
+
onNodeCollapse={(value) => console.log("collapsed:", value)}
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `initialExpandedValues` accepts an array of node values, or `"*"` to start with every node expanded.
|
|
59
|
+
- `multiple` allows more than one node to be selected at once (default: single-select).
|
|
60
|
+
|
|
61
|
+
### Interaction pattern
|
|
62
|
+
|
|
63
|
+
Expanding/collapsing and selecting are independent, fixed interactions (not prop-configurable):
|
|
64
|
+
|
|
65
|
+
- Clicking the expand/collapse chevron toggles that node's subtree only — it never selects.
|
|
66
|
+
- Clicking anywhere else on a row selects it — it never toggles expansion.
|
|
67
|
+
- `Enter`/`Space` on a focused node selects it, even if the node has children.
|
|
68
|
+
- `ArrowLeft`/`ArrowRight` expand/collapse the focused node only, without changing selection.
|
|
69
|
+
- `ArrowUp`/`ArrowDown` move focus between rows.
|
|
70
|
+
|
|
71
|
+
### Disabled
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Tree data={data} initialSelectedValues={["1.1"]} disabled />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`disabled` disables the whole tree — no expand/collapse or select via click or keyboard, dimmed to the standard disabled opacity. A pre-selected node stays visibly selected, just dimmed along with the rest. There's no per-node disabled state (see **Known Constraints** below).
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 4. Design System Integration
|
|
82
|
+
|
|
83
|
+
All Recursica components in the `@recursica/adapter-mantine-v8` package adhere strictly to design system spacing, scaling, and behavior patterns.
|
|
84
|
+
|
|
85
|
+
> [!IMPORTANT]
|
|
86
|
+
>
|
|
87
|
+
> - **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.
|
|
88
|
+
> - **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.
|
|
89
|
+
> - **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.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 5. Known Constraints
|
|
94
|
+
|
|
95
|
+
- No checkbox/multi-check styling is exposed — the underlying Mantine `Tree` supports a `checked` node state, but Recursica's `tree` design tokens don't yet define a checked visual state, so it isn't part of the Recursica API.
|
|
96
|
+
- No per-node `disabled` state — the Figma UI Kit tokens don't define one, and Mantine's `Tree` data shape has no built-in `disabled` field either.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Tree";
|