@boxcustodia/library 2.0.0-alpha.13 → 2.0.0-alpha.15

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 (174) hide show
  1. package/dist/index.cjs.js +1 -138
  2. package/dist/index.d.ts +1083 -717
  3. package/dist/index.es.js +7059 -56179
  4. package/dist/theme.css +1 -1
  5. package/package.json +34 -26
  6. package/src/__doc__/Changelog.mdx +6 -6
  7. package/src/__doc__/Examples.tsx +1 -1
  8. package/src/__doc__/Intro.mdx +3 -3
  9. package/src/__doc__/Tabs.mdx +112 -0
  10. package/src/__doc__/V2.mdx +1245 -0
  11. package/src/components/accordion/accordion.stories.tsx +143 -0
  12. package/src/components/accordion/accordion.tsx +135 -0
  13. package/src/components/accordion/index.ts +1 -0
  14. package/src/components/alert/alert.stories.tsx +24 -4
  15. package/src/components/alert/alert.tsx +17 -9
  16. package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
  17. package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
  18. package/src/components/alert-dialog/alert-dialog.tsx +58 -10
  19. package/src/components/auto-complete/auto-complete.stories.tsx +615 -200
  20. package/src/components/auto-complete/auto-complete.tsx +420 -68
  21. package/src/components/auto-complete/index.ts +0 -1
  22. package/src/components/avatar/avatar.stories.tsx +162 -21
  23. package/src/components/avatar/avatar.tsx +79 -20
  24. package/src/components/button/button.stories.tsx +236 -294
  25. package/src/components/button/button.test.tsx +10 -17
  26. package/src/components/button/button.tsx +53 -18
  27. package/src/components/button/components/base-button.tsx +25 -53
  28. package/src/components/button/index.ts +0 -1
  29. package/src/components/calendar/calendar.stories.tsx +1 -1
  30. package/src/components/calendar/calendar.tsx +4 -4
  31. package/src/components/card/card.stories.tsx +140 -69
  32. package/src/components/card/card.tsx +155 -54
  33. package/src/components/center/center.stories.tsx +22 -39
  34. package/src/components/checkbox/checkbox.stories.tsx +25 -5
  35. package/src/components/checkbox/checkbox.tsx +76 -15
  36. package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
  37. package/src/components/checkbox-group/checkbox-group.tsx +84 -3
  38. package/src/components/combobox/combobox.stories.tsx +33 -23
  39. package/src/components/combobox/combobox.tsx +120 -104
  40. package/src/components/date-picker/date-input.stories.tsx +14 -6
  41. package/src/components/date-picker/date-input.tsx +3 -3
  42. package/src/components/date-picker/date-picker.model.ts +13 -4
  43. package/src/components/date-picker/date-picker.stories.tsx +38 -12
  44. package/src/components/date-picker/date-picker.tsx +29 -15
  45. package/src/components/dialog/dialog.stories.tsx +18 -0
  46. package/src/components/dialog/dialog.test.tsx +1 -1
  47. package/src/components/dialog/dialog.tsx +51 -20
  48. package/src/components/divider/divider.stories.tsx +6 -0
  49. package/src/components/dropzone/dropzone.stories.tsx +70 -90
  50. package/src/components/dropzone/dropzone.tsx +383 -105
  51. package/src/components/dropzone/index.ts +0 -1
  52. package/src/components/empty/empty.stories.tsx +164 -0
  53. package/src/components/empty/empty.tsx +156 -0
  54. package/src/components/empty/index.ts +1 -0
  55. package/src/components/field/field.stories.tsx +226 -3
  56. package/src/components/field/field.tsx +77 -42
  57. package/src/components/form/form.stories.tsx +320 -197
  58. package/src/components/form/form.tsx +3 -23
  59. package/src/components/index.ts +2 -6
  60. package/src/components/input/input.stories.tsx +5 -5
  61. package/src/components/input/input.tsx +5 -5
  62. package/src/components/kbd/kbd.stories.tsx +1 -0
  63. package/src/components/label/label.stories.tsx +16 -0
  64. package/src/components/label/label.tsx +13 -2
  65. package/src/components/loader/loader.stories.tsx +7 -5
  66. package/src/components/loader/loader.tsx +8 -3
  67. package/src/components/menu/menu-primitives.tsx +207 -196
  68. package/src/components/menu/menu.stories.tsx +275 -146
  69. package/src/components/menu/menu.tsx +146 -54
  70. package/src/components/number-input/number-input.stories.tsx +27 -4
  71. package/src/components/number-input/number-input.test.tsx +2 -2
  72. package/src/components/number-input/number-input.tsx +29 -33
  73. package/src/components/otp/index.ts +1 -0
  74. package/src/components/otp/otp.stories.tsx +209 -0
  75. package/src/components/otp/otp.tsx +100 -0
  76. package/src/components/pagination/index.ts +1 -0
  77. package/src/components/pagination/pagination.model.ts +2 -0
  78. package/src/components/pagination/pagination.stories.tsx +153 -59
  79. package/src/components/pagination/pagination.test.tsx +122 -57
  80. package/src/components/pagination/pagination.tsx +575 -77
  81. package/src/components/password/password.stories.tsx +18 -3
  82. package/src/components/password/password.tsx +26 -10
  83. package/src/components/popover/popover.stories.tsx +26 -5
  84. package/src/components/popover/popover.tsx +15 -23
  85. package/src/components/progress/progress.stories.tsx +1 -0
  86. package/src/components/radio-group/index.ts +1 -0
  87. package/src/components/radio-group/radio-group.stories.tsx +251 -0
  88. package/src/components/radio-group/radio-group.tsx +212 -0
  89. package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
  90. package/src/components/select/select.stories.tsx +118 -19
  91. package/src/components/select/select.tsx +67 -62
  92. package/src/components/skeleton/skeleton.stories.tsx +1 -0
  93. package/src/components/stack/stack.stories.tsx +179 -89
  94. package/src/components/stack/stack.tsx +2 -2
  95. package/src/components/stepper/index.ts +1 -1
  96. package/src/components/stepper/stepper.stories.tsx +766 -83
  97. package/src/components/stepper/stepper.test.tsx +18 -18
  98. package/src/components/stepper/stepper.tsx +554 -0
  99. package/src/components/switch/switch.stories.tsx +15 -1
  100. package/src/components/switch/switch.tsx +17 -4
  101. package/src/components/table/index.ts +0 -2
  102. package/src/components/table/table.stories.tsx +131 -18
  103. package/src/components/table/table.test.tsx +1 -1
  104. package/src/components/table/table.tsx +183 -77
  105. package/src/components/tabs/tabs.stories.tsx +372 -155
  106. package/src/components/tabs/tabs.test.tsx +12 -12
  107. package/src/components/tabs/tabs.tsx +72 -149
  108. package/src/components/tag/index.ts +0 -1
  109. package/src/components/tag/tag.stories.tsx +147 -120
  110. package/src/components/tag/tag.tsx +47 -95
  111. package/src/components/textarea/textarea.stories.tsx +8 -22
  112. package/src/components/textarea/textarea.tsx +17 -79
  113. package/src/components/timeline/timeline.stories.tsx +322 -42
  114. package/src/components/timeline/timeline.tsx +359 -132
  115. package/src/components/toast/toast.stories.tsx +1 -0
  116. package/src/components/tooltip/tooltip.tsx +11 -9
  117. package/src/components/tree/index.ts +0 -1
  118. package/src/components/tree/tree.stories.tsx +364 -408
  119. package/src/components/tree/tree.test.tsx +163 -0
  120. package/src/components/tree/tree.tsx +212 -36
  121. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
  122. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
  123. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
  124. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
  125. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
  126. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
  127. package/src/hooks/usePagination/usePagination.tsx +36 -24
  128. package/src/styles/theme.css +1 -1
  129. package/src/utils/form.tsx +69 -37
  130. package/src/utils/index.ts +1 -1
  131. package/src/__doc__/Migration.mdx +0 -451
  132. package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
  133. package/src/components/background-image/background-image.stories.tsx +0 -21
  134. package/src/components/background-image/background-image.test.tsx +0 -29
  135. package/src/components/background-image/background-image.tsx +0 -23
  136. package/src/components/background-image/index.ts +0 -1
  137. package/src/components/button/button.variants.ts +0 -44
  138. package/src/components/button/components/loader-overlay.tsx +0 -21
  139. package/src/components/button/components/loading-icon.tsx +0 -47
  140. package/src/components/dropzone/upload-primitives.tsx +0 -310
  141. package/src/components/dropzone/use-dropzone.ts +0 -122
  142. package/src/components/empty-state/empty-state.stories.tsx +0 -56
  143. package/src/components/empty-state/empty-state.tsx +0 -39
  144. package/src/components/empty-state/index.ts +0 -1
  145. package/src/components/heading/heading.stories.tsx +0 -74
  146. package/src/components/heading/heading.tsx +0 -28
  147. package/src/components/heading/heading.variants.ts +0 -27
  148. package/src/components/heading/index.ts +0 -1
  149. package/src/components/kbd/kbd.variants.ts +0 -26
  150. package/src/components/menu/util/render-menu-item.tsx +0 -54
  151. package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
  152. package/src/components/multi-select/index.ts +0 -1
  153. package/src/components/multi-select/multi-select.stories.tsx +0 -294
  154. package/src/components/multi-select/multi-select.tsx +0 -300
  155. package/src/components/multi-select/multi-select.variants.ts +0 -22
  156. package/src/components/pagination/components/pagination-option.tsx +0 -27
  157. package/src/components/show/index.ts +0 -1
  158. package/src/components/show/show.stories.tsx +0 -197
  159. package/src/components/show/show.test.tsx +0 -41
  160. package/src/components/show/show.tsx +0 -16
  161. package/src/components/stepper/Stepper.tsx +0 -190
  162. package/src/components/stepper/context/stepper-context.tsx +0 -11
  163. package/src/components/table/table-primitives.tsx +0 -122
  164. package/src/components/table/table.model.ts +0 -20
  165. package/src/components/table-pagination/index.ts +0 -2
  166. package/src/components/table-pagination/table-pagination.model.ts +0 -2
  167. package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
  168. package/src/components/table-pagination/table-pagination.test.tsx +0 -32
  169. package/src/components/table-pagination/table-pagination.tsx +0 -108
  170. package/src/components/tabs/context/tabs-context.tsx +0 -14
  171. package/src/components/tag/tag.variants.ts +0 -31
  172. package/src/components/timeline/timeline-status.ts +0 -5
  173. package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
  174. package/src/components/tree/tree-primitives.tsx +0 -126
@@ -1,468 +1,424 @@
1
- import { Meta, StoryObj } from "@storybook/react-vite";
2
- import { ComponentType } from "react";
3
- import { Collection } from "react-aria-components";
4
- import { createToastManager, ToastProvider } from "../toast";
1
+ import {
2
+ createOnDropHandler,
3
+ dragAndDropFeature,
4
+ hotkeysCoreFeature,
5
+ type ItemInstance,
6
+ keyboardDragAndDropFeature,
7
+ searchFeature,
8
+ selectionFeature,
9
+ syncDataLoaderFeature,
10
+ } from "@headless-tree/core";
11
+ import { useTree } from "@headless-tree/react";
12
+ import type { Meta, StoryObj } from "@storybook/react-vite";
13
+ import {
14
+ FileTextIcon,
15
+ FolderIcon,
16
+ FolderOpenIcon,
17
+ MoreHorizontalIcon,
18
+ } from "lucide-react";
19
+ import { type ComponentType, useState } from "react";
5
20
 
6
- const toastManager = createToastManager();
21
+ import { Input } from "../input/input";
22
+ import { Menu } from "../menu/menu";
23
+ import { TreeDragLine, TreeItem, TreeItemLabel, TreeRoot } from "./tree";
7
24
 
8
- import { Tree, TreeNode } from ".";
9
- import {
10
- TreeItem,
11
- TreeItemContent,
12
- TreeItemIndicator,
13
- TreeItemLabel,
14
- TreeRoot,
15
- } from "./tree-primitives";
25
+ interface Item {
26
+ name: string;
27
+ children?: string[];
28
+ }
16
29
 
17
- const files: TreeNode[] = [
18
- {
19
- id: "1",
20
- title: "Work",
21
- children: [
22
- {
23
- id: "1-1",
24
- title: "Reports",
25
- children: [
26
- {
27
- id: "1-1-1",
28
- title: "2023",
29
- children: [
30
- {
31
- id: "1-1-1-1",
32
- title: "Q1 Report",
33
- children: [
34
- { id: "1-1-1-1-1", title: "Summary", children: [] },
35
- { id: "1-1-1-1-2", title: "Details", children: [] },
36
- ],
37
- },
38
- ],
39
- },
40
- ],
41
- },
42
- {
43
- id: "1-2",
44
- title: "Presentations",
45
- children: [
46
- {
47
- id: "1-2-1",
48
- title: "2023 Projects",
49
- children: [
50
- {
51
- id: "1-2-1-1",
52
- title: "Project A",
53
- children: [
54
- { id: "1-2-1-1-1", title: "Draft", children: [] },
55
- { id: "1-2-1-1-2", title: "Final", children: [] },
56
- ],
57
- },
58
- {
59
- id: "1-2-1-2",
60
- title: "Project B",
61
- children: [
62
- { id: "1-2-1-2-1", title: "Research", children: [] },
63
- ],
64
- },
65
- ],
66
- },
67
- ],
68
- },
69
- ],
70
- },
71
- {
72
- id: "2",
73
- title: "Personal",
74
- children: [
75
- {
76
- id: "2-1",
77
- title: "Hobbies",
78
- children: [
79
- {
80
- id: "2-1-1",
81
- title: "Photography",
82
- children: [
83
- { id: "2-1-1-1", title: "Travel", children: [] },
84
- { id: "2-1-1-2", title: "Portraits", children: [] },
85
- ],
86
- },
87
- {
88
- id: "2-1-2",
89
- title: "Cooking",
90
- children: [{ id: "2-1-2-1", title: "Recipes", children: [] }],
91
- },
92
- ],
93
- },
94
- ],
95
- },
96
- {
97
- id: "3",
98
- title: "Projects",
99
- children: [
100
- {
101
- id: "3-1",
102
- title: "Web Development",
103
- children: [
104
- {
105
- id: "3-1-1",
106
- title: "Portfolio",
107
- children: [
108
- { id: "3-1-1-1", title: "Images", children: [] },
109
- { id: "3-1-1-2", title: "CSS", children: [] },
110
- ],
111
- },
112
- { id: "3-1-2", title: "Landing Page", children: [] },
113
- ],
114
- },
115
- {
116
- id: "3-2",
117
- title: "Mobile Apps",
118
- children: [{ id: "3-2-1", title: "Weather App", children: [] }],
119
- },
120
- ],
121
- },
122
- {
123
- id: "4",
124
- title: "Finance",
125
- children: [
126
- {
127
- id: "4-1",
128
- title: "Budget",
129
- children: [
130
- {
131
- id: "4-1-1",
132
- title: "2023",
133
- children: [
134
- { id: "4-1-1-1", title: "January", children: [] },
135
- { id: "4-1-1-2", title: "February", children: [] },
136
- ],
137
- },
138
- ],
139
- },
140
- ],
141
- },
142
- {
143
- id: "5",
144
- title: "Health",
145
- children: [
146
- {
147
- id: "5-1",
148
- title: "Fitness",
149
- children: [
150
- { id: "5-1-1", title: "Workouts", children: [] },
151
- { id: "5-1-2", title: "Nutrition", children: [] },
152
- ],
153
- },
154
- { id: "5-2", title: "Wellness", children: [] },
155
- ],
30
+ const items: Record<string, Item> = {
31
+ root: { name: "root", children: ["docs", "src", "tests"] },
32
+ docs: { name: "docs", children: ["readme", "api"] },
33
+ readme: { name: "README.md" },
34
+ api: { name: "API.md" },
35
+ src: { name: "src", children: ["components", "lib"] },
36
+ components: { name: "components", children: ["button", "tree"] },
37
+ button: { name: "button.tsx" },
38
+ tree: { name: "tree.tsx" },
39
+ lib: { name: "lib", children: ["utils"] },
40
+ utils: { name: "utils.ts" },
41
+ tests: { name: "tests", children: ["spec"] },
42
+ spec: { name: "tree.test.tsx" },
43
+ };
44
+
45
+ const baseConfig = {
46
+ rootItemId: "root",
47
+ getItemName: (item: ItemInstance<Item>) => item.getItemData().name,
48
+ isItemFolder: (item: ItemInstance<Item>) =>
49
+ (item.getItemData().children?.length ?? 0) > 0,
50
+ dataLoader: {
51
+ getItem: (itemId: string) => items[itemId],
52
+ getChildren: (itemId: string) => items[itemId]?.children ?? [],
156
53
  },
157
- ];
54
+ initialState: { expandedItems: ["src", "components", "docs"] },
55
+ };
158
56
 
159
57
  /**
160
- * El componente Tree proporciona una vista jerárquica de datos en forma de árbol.
161
- * Está construido sobre [@react-aria](https://react-spectrum.adobe.com/react-aria/Tree.html#tree)
162
- * y soporta selección, expansión, navegación por teclado y más.
58
+ * Accessible hierarchical tree built on top of
59
+ * [@headless-tree](https://headless-tree.lukasbach.com/). The consumer creates
60
+ * a tree instance with `useTree(config)`, registers the features they need,
61
+ * and iterates `tree.getItems()` to render rows.
163
62
  *
164
- * ## Nodo
165
- * Los nodos del árbol deben tener las siguientes propiedades:
63
+ * Setup:
166
64
  *
167
65
  * ```tsx
168
- * interface TreeNode {
169
- * id: string | number;
170
- * title: string;
171
- * children: TreeNode[];
172
- * }
66
+ * const tree = useTree<Item>({
67
+ * rootItemId: "root",
68
+ * getItemName: (item) => item.getItemData().name,
69
+ * isItemFolder: (item) => (item.getItemData().children?.length ?? 0) > 0,
70
+ * dataLoader: {
71
+ * getItem: (itemId) => items[itemId],
72
+ * getChildren: (itemId) => items[itemId]?.children ?? [],
73
+ * },
74
+ * features: [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature],
75
+ * });
76
+ *
77
+ * <TreeRoot tree={tree}>
78
+ * {tree.getItems().map((item) => (
79
+ * <TreeItem
80
+ * key={item.getId()}
81
+ * item={item}
82
+ * leafIcon={<FileIcon />}
83
+ * parentIcon={(isExpanded) =>
84
+ * isExpanded ? <FolderOpenIcon /> : <FolderIcon />
85
+ * }
86
+ * />
87
+ * ))}
88
+ * </TreeRoot>
173
89
  * ```
90
+ *
91
+ * Features (from `@headless-tree/core` — register only what you use):
92
+ * - `syncDataLoaderFeature` — sync data via `dataLoader.getItem` /
93
+ * `dataLoader.getChildren`.
94
+ * - `asyncDataLoaderFeature` — returns Promises; children load on expand.
95
+ * - `selectionFeature` — single/multi selection, exposes `item.isSelected()`.
96
+ * - `hotkeysCoreFeature` — Arrow keys to navigate, Enter to activate, Space to
97
+ * select, Right/Left to expand/collapse.
98
+ * - `dragAndDropFeature` — drag/drop with `onDrop`. Pair with `TreeDragLine`.
99
+ * - `keyboardDragAndDropFeature` — keyboard-driven reorder.
100
+ * - `searchFeature` — drive with `tree.setSearch(value)`; matches expose
101
+ * `item.isMatchingSearch()`.
102
+ * - `expandAllFeature` — `tree.expandAll()` / `tree.collapseAll()`.
103
+ * - `propMemoizationFeature` — memoizes `getProps()` (large trees).
104
+ *
105
+ * Key behaviors:
106
+ * - The consumer owns the tree instance, the data shape (`T`), and the
107
+ * iteration. There is no `items` composite prop — that pattern doesn't fit
108
+ * the breadth of real tree use cases.
109
+ * - `TreeItem` accepts `leafIcon` / `parentIcon` to render an icon before the
110
+ * item's name without writing a custom label per row. `parentIcon` defaults
111
+ * to a rotating chevron — pass your own `ReactNode` (use CSS via
112
+ * `in-data-[expanded=true]:…` for state-driven styles) or a function
113
+ * `(isExpanded) => ReactNode` to swap elements based on state. Pass
114
+ * `parentIcon={null}` to opt out of the default. `leafIcon` defaults to no
115
+ * icon. When `children` are provided, the icon props are ignored — children
116
+ * take over the entire row.
117
+ * - `TreeItem` is the `<button>` row. It handles focus, selection, indent
118
+ * (via the `--tree-indent` CSS variable on `TreeRoot`, default `20px`) and
119
+ * exposes state through data attributes: `data-folder`, `data-expanded`,
120
+ * `data-selected`, `data-drag-target`, `data-search-match`, `data-focused`.
121
+ * - `TreeItemLabel` is the visual zone (background, hover, selected,
122
+ * search-match). It defaults to `item.getItemName()` when no children are
123
+ * provided. It renders no toggle icon on its own — the icon comes from
124
+ * `TreeItem`'s `leafIcon` / `parentIcon`, or from custom children.
125
+ * - `TreeDragLine` is optional; only render it when `dragAndDropFeature` is
126
+ * registered. It positions itself absolutely inside `TreeRoot`.
127
+ * - State binding (`expandedItems`, `selectedItems`, etc.) is controlled via
128
+ * `state` + `setState` on the `useTree` config, or left to internal state
129
+ * with `initialState`.
130
+ *
131
+ * Reference: [headless-tree docs](https://headless-tree.lukasbach.com/docs/)
174
132
  */
175
- const meta: Meta<typeof Tree> = {
176
- title: "Data display/Tree",
177
- component: Tree,
133
+ const meta: Meta<typeof TreeRoot> = {
134
+ title: "Components/Tree",
135
+ component: TreeRoot,
136
+ parameters: { layout: "centered" },
178
137
  subcomponents: {
179
138
  TreeRoot: TreeRoot as ComponentType<unknown>,
180
139
  TreeItem: TreeItem as ComponentType<unknown>,
181
- TreeItemContent: TreeItemContent as ComponentType<unknown>,
182
- TreeItemIndicator: TreeItemIndicator as ComponentType<unknown>,
183
140
  TreeItemLabel: TreeItemLabel as ComponentType<unknown>,
184
- Collection: Collection as ComponentType<unknown>,
185
- },
186
- args: {
187
- items: files,
188
- selectionMode: "single",
141
+ TreeDragLine: TreeDragLine as ComponentType<unknown>,
189
142
  },
190
143
  argTypes: {
191
- selectedKeys: {
192
- description: "Las claves actualmente seleccionadas en la colección",
193
- control: "object",
194
- table: {
195
- type: { summary: "'all' | Iterable<Key>" },
196
- defaultValue: { summary: "—" },
197
- },
198
- },
199
- defaultSelectedKeys: {
200
- description: "Las claves inicialmente seleccionadas en la colección",
201
- control: "object",
202
- table: {
203
- type: { summary: "'all' | Iterable<Key>" },
204
- defaultValue: { summary: "—" },
205
- },
206
- },
207
- expandedKeys: {
208
- description: "Las claves actualmente expandidas en la colección",
209
- control: "object",
210
- table: {
211
- type: { summary: "Iterable<Key>" },
212
- defaultValue: { summary: "—" },
213
- },
214
- },
215
- defaultExpandedKeys: {
216
- description: "Las claves inicialmente expandidas en la colección",
217
- control: "object",
218
- table: {
219
- type: { summary: "Iterable<Key>" },
220
- defaultValue: { summary: "—" },
221
- },
222
- },
223
- selectionMode: {
224
- description: "El tipo de selección que está permitida en el árbol",
225
- control: "select",
226
- options: ["none", "single", "multiple"],
227
- table: {
228
- type: { summary: "'none' | 'single' | 'multiple'" },
229
- defaultValue: { summary: "'none'" },
230
- },
231
- },
232
- disabledKeys: {
233
- description: "Las claves de los elementos que están deshabilitados",
234
- control: "object",
235
- table: {
236
- type: { summary: "Iterable<Key>" },
237
- defaultValue: { summary: "—" },
238
- },
239
- },
240
- disallowEmptySelection: {
241
- description:
242
- "Si se debe permitir que no haya ningún elemento seleccionado",
243
- control: "boolean",
244
- table: {
245
- type: { summary: "boolean" },
246
- },
247
- },
248
- onSelectionChange: {
249
- description: "Manejador llamado cuando cambia la selección",
250
- table: {
251
- type: { summary: "(keys: 'all' | Iterable<Key>) => void" },
252
- defaultValue: { summary: "—" },
253
- },
254
- },
255
- onExpandedChange: {
256
- description: "Manejador llamado cuando cambian los elementos expandidos",
257
- table: {
258
- type: { summary: "(keys: Iterable<Key>) => void" },
259
- defaultValue: { summary: "—" },
260
- },
261
- },
262
- onAction: {
263
- description:
264
- "Manejador llamado cuando un usuario realiza una acción en un elemento. El evento exacto depende del prop selectionBehavior y la modalidad de interacción",
265
- table: {
266
- type: { summary: "(key: Key) => void" },
267
- defaultValue: { summary: "—" },
268
- },
269
- },
270
- onScroll: {
271
- description: "Manejador llamado cuando un usuario hace scroll",
272
- table: {
273
- type: { summary: "(e: UIEvent<Element>) => void" },
274
- defaultValue: { summary: "—" },
275
- },
276
- },
144
+ children: { control: false },
145
+ tree: { control: false },
146
+ render: { control: false },
277
147
  },
278
148
  };
279
149
 
280
150
  export default meta;
281
- type Story = StoryObj<typeof Tree>;
151
+ type Story = StoryObj<typeof TreeRoot>;
282
152
 
283
- /**
284
- * Ejemplo básico del árbol con selección simple habilitada.
285
- * Los usuarios pueden expandir/colapsar nodos y seleccionar un elemento a la vez.
286
- */
287
- export const Default: Story = {};
153
+ export const Default: Story = {
154
+ render: () => {
155
+ const tree = useTree<Item>({
156
+ ...baseConfig,
157
+ features: [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature],
158
+ });
288
159
 
289
- /**
290
- * Configuración que permite seleccionar un solo elemento a la vez.
291
- * Útil cuando necesitas que el usuario elija una única opción del árbol.
292
- */
293
- export const SingleSelection: Story = {
294
- args: {
295
- selectionMode: "single",
160
+ return (
161
+ <TreeRoot tree={tree} className="w-72">
162
+ {tree.getItems().map((item) => (
163
+ <TreeItem key={item.getId()} item={item} />
164
+ ))}
165
+ </TreeRoot>
166
+ );
296
167
  },
297
168
  };
298
169
 
299
170
  /**
300
- * Permite seleccionar múltiples elementos simultáneamente.
301
- * Los usuarios pueden usar `click` para selección individual
302
- * o `shift + click` para selección en rango.
171
+ * Most real trees show distinct icons for folders and files. The function form
172
+ * of `parentIcon` swaps between open and closed folder icons based on the
173
+ * current expanded state.
303
174
  */
304
- export const MultipleSelection: Story = {
305
- args: {
306
- selectionMode: "multiple",
307
- },
308
- };
175
+ export const WithIcons: Story = {
176
+ render: () => {
177
+ const tree = useTree<Item>({
178
+ ...baseConfig,
179
+ features: [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature],
180
+ });
309
181
 
310
- /**
311
- * Ejemplo de árbol con nodos deshabilitados completamente.
312
- * Los elementos deshabilitados no pueden ser seleccionados ni expandidos.
313
- *
314
- * `disabledBehavior="all"` - Deshabilita todas las interacciones
315
- *
316
- * `disabledKeys` - Array de IDs de elementos a deshabilitar
317
- */
318
- export const Disabled: Story = {
319
- args: {
320
- disabledBehavior: "all",
321
- disabledKeys: ["1", "2", "3"],
182
+ return (
183
+ <TreeRoot tree={tree} className="w-72">
184
+ {tree.getItems().map((item) => (
185
+ <TreeItem
186
+ key={item.getId()}
187
+ item={item}
188
+ leafIcon={<FileTextIcon className="size-4 text-muted-foreground" />}
189
+ parentIcon={(isExpanded) =>
190
+ isExpanded ? (
191
+ <FolderOpenIcon className="size-4 text-muted-foreground" />
192
+ ) : (
193
+ <FolderIcon className="size-4 text-muted-foreground" />
194
+ )
195
+ }
196
+ />
197
+ ))}
198
+ </TreeRoot>
199
+ );
322
200
  },
323
201
  };
324
202
 
325
203
  /**
326
- * Ejemplo donde solo la selección está deshabilitada.
327
- * Los elementos pueden expandirse pero no pueden ser seleccionados.
328
- *
329
- * `disabledBehavior="selection"` - Solo deshabilita la selección
330
- *
331
- * `disabledKeys` - Array de IDs de elementos a deshabilitar
204
+ * `selectionFeature` is multi-select by default. Click to select a single
205
+ * item, `Shift`-click for a range, `Ctrl`/`Cmd`-click to toggle. Read the
206
+ * current selection via `tree.getSelectedItems()`.
332
207
  */
333
- export const DisabledSelection: Story = {
334
- args: {
335
- disabledBehavior: "selection",
336
- disabledKeys: ["1", "2", "3"],
337
- },
338
- };
208
+ export const MultipleSelection: Story = {
209
+ render: () => {
210
+ const tree = useTree<Item>({
211
+ ...baseConfig,
212
+ features: [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature],
213
+ });
339
214
 
340
- /**
341
- * Árbol con nodos inicialmente expandidos.
342
- * Útil para mostrar cierta estructura desde el inicio.
343
- *
344
- * `defaultExpandedKeys` - Array de IDs de elementos expandidos por defecto
345
- */
346
- export const DefaultExpanded: Story = {
347
- args: {
348
- defaultExpandedKeys: ["1", "1-1", "1-1-1", "1-1-1-1"],
215
+ const selected = tree.getSelectedItems();
216
+
217
+ return (
218
+ <div className="flex w-72 flex-col gap-3">
219
+ <div className="flex items-center justify-between text-sm">
220
+ <span className="text-muted-foreground">
221
+ {selected.length} selected
222
+ </span>
223
+ {selected.length > 0 && (
224
+ <button
225
+ type="button"
226
+ onClick={() => tree.setSelectedItems([])}
227
+ className="text-muted-foreground underline-offset-2 hover:underline"
228
+ >
229
+ Clear
230
+ </button>
231
+ )}
232
+ </div>
233
+ <TreeRoot tree={tree}>
234
+ {tree.getItems().map((item) => (
235
+ <TreeItem
236
+ key={item.getId()}
237
+ item={item}
238
+ leafIcon={
239
+ <FileTextIcon className="size-4 text-muted-foreground" />
240
+ }
241
+ parentIcon={
242
+ <FolderIcon className="size-4 text-muted-foreground" />
243
+ }
244
+ />
245
+ ))}
246
+ </TreeRoot>
247
+ </div>
248
+ );
349
249
  },
350
250
  };
351
251
 
352
252
  /**
353
- * Ejemplo de árbol con expansión controlada.
354
- * El estado de expansión es manejado externamente.
253
+ * Drop the `leafIcon` / `parentIcon` shorthand and pass `children` directly to
254
+ * place action triggers **outside** `TreeItemLabel`. Wrap the trigger in a
255
+ * `stopPropagation` container so clicks on the menu don't activate the row.
355
256
  *
356
- * `expandedKeys` - Control explícito de los elementos expandidos
257
+ * Note: `TreeItem` renders as `<button>` (headless-tree requires it for
258
+ * focus/keyboard), so the nested `Menu` trigger must be a non-button element
259
+ * (e.g. a `<span role="button">`) to keep the HTML valid.
357
260
  */
358
- export const ControlledExpanded: Story = {
359
- args: {
360
- expandedKeys: ["1", "7"],
261
+ export const WithActions: Story = {
262
+ render: () => {
263
+ const tree = useTree<Item>({
264
+ ...baseConfig,
265
+ features: [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature],
266
+ });
267
+
268
+ return (
269
+ <TreeRoot tree={tree} className="w-80">
270
+ {tree.getItems().map((item) => (
271
+ <TreeItem
272
+ key={item.getId()}
273
+ item={item}
274
+ className="group/row flex items-center"
275
+ >
276
+ <TreeItemLabel className="flex-1">
277
+ {item.isFolder() ? (
278
+ <FolderIcon className="size-4 text-muted-foreground" />
279
+ ) : (
280
+ <FileTextIcon className="size-4 text-muted-foreground" />
281
+ )}
282
+ {item.getItemName()}
283
+ </TreeItemLabel>
284
+ <span
285
+ onClick={(event) => event.stopPropagation()}
286
+ onKeyDown={(event) => event.stopPropagation()}
287
+ className="opacity-0 transition-opacity group-hover/row:opacity-100"
288
+ >
289
+ <Menu
290
+ trigger={
291
+ <span
292
+ role="button"
293
+ tabIndex={-1}
294
+ className="flex rounded-sm p-1 hover:bg-accent"
295
+ >
296
+ <MoreHorizontalIcon className="size-4 text-muted-foreground" />
297
+ </span>
298
+ }
299
+ items={[
300
+ {
301
+ type: "item",
302
+ label: "Rename",
303
+ onSelect: () => console.log("Rename", item.getItemName()),
304
+ },
305
+ {
306
+ type: "item",
307
+ label: "Delete",
308
+ onSelect: () => console.log("Delete", item.getItemName()),
309
+ },
310
+ ]}
311
+ />
312
+ </span>
313
+ </TreeItem>
314
+ ))}
315
+ </TreeRoot>
316
+ );
361
317
  },
362
318
  };
363
319
 
364
320
  /**
365
- * Árbol con selección inicial predeterminada.
321
+ * Drag-and-drop with reordering. `createOnDropHandler` (from
322
+ * `@headless-tree/core`) writes the reordered child ids back to your data
323
+ * source — set `children` on the parent item and the tree rebuilds itself.
324
+ * `TreeDragLine` renders the visual indicator and must live inside `TreeRoot`
325
+ * (it positions absolutely against the root).
326
+ *
327
+ * Features needed: `dragAndDropFeature` (mouse) and
328
+ * `keyboardDragAndDropFeature` (keyboard accessibility).
366
329
  *
367
- * `defaultSelectedKeys` - Array de IDs de elementos seleccionados por defecto
330
+ * Reference: [headless-tree drag-and-drop docs](https://headless-tree.lukasbach.com/dnd/overview)
368
331
  */
369
- export const DefaultSelected: Story = {
370
- args: {
371
- selectionMode: "single",
372
- defaultSelectedKeys: ["4"],
373
- },
374
- };
332
+ export const DragAndDrop: Story = {
333
+ render: () => {
334
+ const [data] = useState(() => structuredClone(items));
375
335
 
376
- /**
377
- * Demostración de los eventos disponibles en el árbol.
378
- * Muestra notificaciones toast para:
379
- * - Cambios en expansión
380
- * - Cambios en selección
381
- * - Eventos de scroll
382
- */
383
- export const Events: Story = {
384
- args: {
385
- className: "h-96 overflow-y-auto border",
386
- onExpandedChange: (keys: any) =>
387
- toastManager.add({
388
- variant: "success",
389
- description: `Expanded: ${Array.from(keys)}`,
390
- }),
391
- onSelectionChange: (keys: any) =>
392
- toastManager.add({
393
- variant: "success",
394
- description: `Selected: ${Array.from(keys)}`,
336
+ const tree = useTree<Item>({
337
+ ...baseConfig,
338
+ dataLoader: {
339
+ getItem: (itemId: string) => data[itemId],
340
+ getChildren: (itemId: string) => data[itemId]?.children ?? [],
341
+ },
342
+ canReorder: true,
343
+ onDrop: createOnDropHandler<Item>((parent, newChildrenIds) => {
344
+ const node = data[parent.getId()];
345
+ if (node) node.children = newChildrenIds;
395
346
  }),
396
- onScroll: () =>
397
- toastManager.add({ variant: "success", description: "Scroll" }),
347
+ features: [
348
+ syncDataLoaderFeature,
349
+ selectionFeature,
350
+ hotkeysCoreFeature,
351
+ dragAndDropFeature,
352
+ keyboardDragAndDropFeature,
353
+ ],
354
+ });
355
+
356
+ return (
357
+ <TreeRoot tree={tree} className="relative w-72">
358
+ {tree.getItems().map((item) => (
359
+ <TreeItem
360
+ key={item.getId()}
361
+ item={item}
362
+ leafIcon={<FileTextIcon className="size-4 text-muted-foreground" />}
363
+ parentIcon={(isExpanded) =>
364
+ isExpanded ? (
365
+ <FolderOpenIcon className="size-4 text-muted-foreground" />
366
+ ) : (
367
+ <FolderIcon className="size-4 text-muted-foreground" />
368
+ )
369
+ }
370
+ />
371
+ ))}
372
+ <TreeDragLine />
373
+ </TreeRoot>
374
+ );
398
375
  },
399
- decorators: [
400
- (Story: any) => (
401
- <ToastProvider toastManager={toastManager}>
402
- <Story />
403
- </ToastProvider>
404
- ),
405
- ],
406
376
  };
407
377
 
408
378
  /**
409
- * ## Estructura con componentes primitivos
410
- * ```tsx
411
- * import { TreeRoot, TreeItem, Collection } from '../../components/tree'
412
- *
413
- * const Estructura = () => {
414
- * return (
415
- * <TreeRoot>
416
- * <TreeItem>
417
- * <TreeItemContent>
418
- * <TreeItemIndicator />
419
- * <TreeItemLabel>Item Label</TreeItemLabel>
420
- * </TreeItemContent>
421
- * <Collection>
422
- * Child items
423
- * </Collection>
424
- * </TreeItem>
425
- * </TreeRoot>
426
- * )
427
- * }
428
- * ```
429
- *
430
- * Los componentes primitivos del Tree son:
431
- *
432
- * - `TreeRoot`: Contenedor principal que maneja la selección y expansión
433
- * - `TreeItem`: Representa cada elemento del árbol
434
- * - `TreeItemContent`: Contenedor del contenido del item
435
- * - `TreeItemIndicator`: Indicador de expansión (chevron)
436
- * - `TreeItemLabel`: Etiqueta del item
437
- * - `Collection`: Contenedor de items hijos que permite manejo con teclado
379
+ * Drive the search through `tree.setSearch(value)` and read the current
380
+ * query via `tree.getSearchValue()`. Matching rows expose
381
+ * `item.isMatchingSearch()` and receive `data-search-match="true"` on
382
+ * `TreeItem`, which `TreeItemLabel` styles automatically.
438
383
  */
439
-
440
- export const Primitive: Story = {
384
+ export const Search: Story = {
441
385
  render: () => {
442
- const renderItem = (item: TreeNode) => {
443
- return (
444
- <TreeItem key={item.id} id={item.id} textValue={item.title}>
445
- <TreeItemContent>
446
- {item.children.length > 0 && <TreeItemIndicator />}
447
- <TreeItemLabel>{item.title}</TreeItemLabel>
448
- </TreeItemContent>
449
- {item.children.length > 0 && (
450
- <Collection>
451
- {item.children.map((child: any) => renderItem(child))}
452
- </Collection>
453
- )}
454
- </TreeItem>
455
- );
456
- };
386
+ const tree = useTree<Item>({
387
+ ...baseConfig,
388
+ initialState: {
389
+ expandedItems: ["docs", "src", "components", "lib", "tests"],
390
+ search: "tree",
391
+ },
392
+ features: [
393
+ syncDataLoaderFeature,
394
+ selectionFeature,
395
+ hotkeysCoreFeature,
396
+ searchFeature,
397
+ ],
398
+ });
457
399
 
458
400
  return (
459
- <TreeRoot
460
- aria-label="Files"
461
- selectionMode="multiple"
462
- selectionBehavior="replace"
463
- >
464
- {files.map((item) => renderItem(item))}
465
- </TreeRoot>
401
+ <div className="flex w-72 flex-col gap-3">
402
+ <Input
403
+ placeholder="Search…"
404
+ value={tree.getSearchValue()}
405
+ onChange={(value) => tree.setSearch(value || null)}
406
+ />
407
+ <TreeRoot tree={tree}>
408
+ {tree.getItems().map((item) => (
409
+ <TreeItem
410
+ key={item.getId()}
411
+ item={item}
412
+ leafIcon={
413
+ <FileTextIcon className="size-4 text-muted-foreground" />
414
+ }
415
+ parentIcon={
416
+ <FolderIcon className="size-4 text-muted-foreground" />
417
+ }
418
+ />
419
+ ))}
420
+ </TreeRoot>
421
+ </div>
466
422
  );
467
423
  },
468
424
  };