@boxcustodia/library 2.0.0-alpha.12 → 2.0.0-alpha.14

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 +1087 -720
  3. package/dist/index.es.js +7011 -56097
  4. package/dist/theme.css +1 -1
  5. package/package.json +34 -26
  6. package/src/__doc__/Examples.tsx +1 -1
  7. package/src/__doc__/Intro.mdx +3 -3
  8. package/src/__doc__/Tabs.mdx +112 -0
  9. package/src/__doc__/V2.mdx +1246 -0
  10. package/src/components/accordion/accordion.stories.tsx +143 -0
  11. package/src/components/accordion/accordion.tsx +135 -0
  12. package/src/components/accordion/index.ts +1 -0
  13. package/src/components/alert/alert.stories.tsx +24 -4
  14. package/src/components/alert/alert.tsx +17 -9
  15. package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
  16. package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
  17. package/src/components/alert-dialog/alert-dialog.tsx +58 -10
  18. package/src/components/auto-complete/auto-complete.stories.tsx +616 -200
  19. package/src/components/auto-complete/auto-complete.tsx +420 -68
  20. package/src/components/auto-complete/index.ts +0 -1
  21. package/src/components/avatar/avatar.stories.tsx +162 -21
  22. package/src/components/avatar/avatar.tsx +79 -20
  23. package/src/components/button/button.stories.tsx +219 -294
  24. package/src/components/button/button.test.tsx +10 -17
  25. package/src/components/button/button.tsx +78 -19
  26. package/src/components/button/components/base-button.tsx +30 -53
  27. package/src/components/button/index.ts +0 -1
  28. package/src/components/calendar/calendar.stories.tsx +1 -1
  29. package/src/components/calendar/calendar.tsx +4 -4
  30. package/src/components/card/card.stories.tsx +141 -69
  31. package/src/components/card/card.tsx +155 -54
  32. package/src/components/center/center.stories.tsx +22 -39
  33. package/src/components/checkbox/checkbox.stories.tsx +25 -5
  34. package/src/components/checkbox/checkbox.tsx +76 -15
  35. package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
  36. package/src/components/checkbox-group/checkbox-group.tsx +84 -3
  37. package/src/components/combobox/combobox.stories.tsx +33 -23
  38. package/src/components/combobox/combobox.tsx +99 -77
  39. package/src/components/date-picker/date-input.stories.tsx +14 -6
  40. package/src/components/date-picker/date-input.tsx +2 -2
  41. package/src/components/date-picker/date-picker.model.ts +13 -4
  42. package/src/components/date-picker/date-picker.stories.tsx +38 -12
  43. package/src/components/date-picker/date-picker.tsx +28 -14
  44. package/src/components/dialog/dialog.stories.tsx +18 -0
  45. package/src/components/dialog/dialog.test.tsx +1 -1
  46. package/src/components/dialog/dialog.tsx +51 -20
  47. package/src/components/divider/divider.stories.tsx +126 -51
  48. package/src/components/divider/divider.tsx +16 -16
  49. package/src/components/dropzone/dropzone.stories.tsx +71 -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 +165 -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 +227 -4
  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 +4 -4
  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 +276 -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 +31 -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 +154 -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 +29 -9
  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 +767 -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 +373 -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 +155 -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 +323 -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 +365 -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 +67 -37
  130. package/src/utils/index.ts +1 -1
  131. package/src/__doc__/Migration.mdx +0 -475
  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
@@ -0,0 +1,163 @@
1
+ import {
2
+ hotkeysCoreFeature,
3
+ selectionFeature,
4
+ syncDataLoaderFeature,
5
+ } from "@headless-tree/core";
6
+ import { useTree } from "@headless-tree/react";
7
+ import { render, screen } from "@testing-library/react";
8
+ import type { ReactNode } from "react";
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import { TreeItem, TreeItemLabel, TreeRoot } from "./tree";
12
+
13
+ interface Item {
14
+ name: string;
15
+ children?: string[];
16
+ }
17
+
18
+ const items: Record<string, Item> = {
19
+ root: { name: "Root", children: ["folder", "leaf"] },
20
+ folder: { name: "Folder", children: ["nested"] },
21
+ nested: { name: "Nested" },
22
+ leaf: { name: "Leaf" },
23
+ };
24
+
25
+ interface HarnessProps {
26
+ initialExpanded?: string[];
27
+ leafIcon?: ReactNode;
28
+ parentIcon?: ReactNode | ((isExpanded: boolean) => ReactNode);
29
+ withChildren?: boolean;
30
+ }
31
+
32
+ function Harness({
33
+ initialExpanded = [],
34
+ leafIcon,
35
+ parentIcon,
36
+ withChildren = false,
37
+ }: HarnessProps) {
38
+ const tree = useTree<Item>({
39
+ rootItemId: "root",
40
+ getItemName: (item) => item.getItemData().name,
41
+ isItemFolder: (item) => (item.getItemData().children?.length ?? 0) > 0,
42
+ dataLoader: {
43
+ getItem: (itemId) => items[itemId],
44
+ getChildren: (itemId) => items[itemId]?.children ?? [],
45
+ },
46
+ initialState: { expandedItems: initialExpanded },
47
+ features: [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature],
48
+ });
49
+
50
+ return (
51
+ <TreeRoot tree={tree}>
52
+ {tree.getItems().map((item) =>
53
+ withChildren ? (
54
+ <TreeItem
55
+ key={item.getId()}
56
+ item={item}
57
+ data-testid={`item-${item.getId()}`}
58
+ leafIcon={leafIcon}
59
+ parentIcon={parentIcon}
60
+ >
61
+ <TreeItemLabel>Custom: {item.getItemName()}</TreeItemLabel>
62
+ </TreeItem>
63
+ ) : (
64
+ <TreeItem
65
+ key={item.getId()}
66
+ item={item}
67
+ data-testid={`item-${item.getId()}`}
68
+ leafIcon={leafIcon}
69
+ parentIcon={parentIcon}
70
+ />
71
+ ),
72
+ )}
73
+ </TreeRoot>
74
+ );
75
+ }
76
+
77
+ describe("Tree component", () => {
78
+ it("renders the item name by default", () => {
79
+ render(<Harness />);
80
+ expect(screen.getByText("Folder")).toBeInTheDocument();
81
+ expect(screen.getByText("Leaf")).toBeInTheDocument();
82
+ });
83
+
84
+ it("renders a default chevron parentIcon on folders only", () => {
85
+ render(<Harness />);
86
+ expect(
87
+ screen.getByTestId("item-folder").querySelector("svg"),
88
+ ).toBeInTheDocument();
89
+ expect(
90
+ screen.getByTestId("item-leaf").querySelector("svg"),
91
+ ).not.toBeInTheDocument();
92
+ });
93
+
94
+ it("opts out of the default chevron when parentIcon is null", () => {
95
+ render(<Harness parentIcon={null} />);
96
+ expect(
97
+ screen.getByTestId("item-folder").querySelector("svg"),
98
+ ).not.toBeInTheDocument();
99
+ });
100
+
101
+ it("renders leafIcon only on non-folder items", () => {
102
+ render(
103
+ <Harness
104
+ leafIcon={<span data-testid="leaf-icon">L</span>}
105
+ parentIcon={<span data-testid="parent-icon">P</span>}
106
+ />,
107
+ );
108
+
109
+ const leafIcons = screen.getAllByTestId("leaf-icon");
110
+ const parentIcons = screen.getAllByTestId("parent-icon");
111
+
112
+ expect(leafIcons).toHaveLength(1);
113
+ expect(parentIcons).toHaveLength(1);
114
+ expect(screen.getByTestId("item-leaf")).toContainElement(leafIcons[0]!);
115
+ expect(screen.getByTestId("item-folder")).toContainElement(parentIcons[0]!);
116
+ });
117
+
118
+ it("calls the parentIcon function with the expanded state", () => {
119
+ render(
120
+ <Harness
121
+ initialExpanded={["folder"]}
122
+ parentIcon={(isExpanded) => (
123
+ <span data-testid="parent-icon">
124
+ {isExpanded ? "open" : "closed"}
125
+ </span>
126
+ )}
127
+ />,
128
+ );
129
+
130
+ expect(screen.getByTestId("item-folder")).toHaveTextContent("open");
131
+ });
132
+
133
+ it("ignores leafIcon / parentIcon when children are provided", () => {
134
+ render(
135
+ <Harness
136
+ withChildren
137
+ leafIcon={<span data-testid="leaf-icon">L</span>}
138
+ parentIcon={<span data-testid="parent-icon">P</span>}
139
+ />,
140
+ );
141
+
142
+ expect(screen.queryByTestId("leaf-icon")).not.toBeInTheDocument();
143
+ expect(screen.queryByTestId("parent-icon")).not.toBeInTheDocument();
144
+ expect(screen.getByText("Custom: Folder")).toBeInTheDocument();
145
+ });
146
+
147
+ it("sets data-folder on folder items and aria-expanded based on state", () => {
148
+ render(<Harness initialExpanded={["folder"]} />);
149
+
150
+ const folder = screen.getByTestId("item-folder");
151
+ expect(folder).toHaveAttribute("data-folder", "true");
152
+ expect(folder).toHaveAttribute("aria-expanded", "true");
153
+
154
+ const leaf = screen.getByTestId("item-leaf");
155
+ expect(leaf).not.toHaveAttribute("data-folder");
156
+ });
157
+
158
+ it("exposes the --tree-indent CSS variable on the root", () => {
159
+ render(<Harness />);
160
+ const root = document.querySelector('[data-slot="tree"]');
161
+ expect(root).toHaveStyle({ "--tree-indent": "20px" });
162
+ });
163
+ });
@@ -1,42 +1,218 @@
1
- import * as React from "react";
1
+ "use client";
2
+
3
+ import { mergeProps } from "@base-ui/react/merge-props";
4
+ import { useRender } from "@base-ui/react/use-render";
5
+ import type { ItemInstance, TreeInstance } from "@headless-tree/core";
6
+ import { ChevronRightIcon } from "lucide-react";
2
7
  import {
3
- Collection,
4
- TreeItem,
5
- TreeItemContent,
6
- TreeItemIndicator,
7
- TreeItemLabel,
8
- TreeRoot,
9
- } from "./tree-primitives";
10
-
11
- export interface TreeNode {
12
- id: string | number;
13
- title: string;
14
- children: TreeNode[];
15
- }
16
-
17
- type TreeRootProps = React.ComponentProps<typeof TreeRoot>;
18
- type Props<T extends TreeNode> = TreeRootProps & { items: T[] };
19
-
20
- export function Tree<T extends TreeNode>({ items, ...props }: Props<T>) {
21
- const renderItem = (item: T): React.ReactNode => {
22
- return (
23
- <TreeItem key={item.id} id={item.id} textValue={item.title}>
24
- <TreeItemContent>
25
- {item.children?.length ? <TreeItemIndicator /> : null}
26
- <TreeItemLabel>{item.title}</TreeItemLabel>
27
- </TreeItemContent>
28
- {item.children?.length ? (
29
- <Collection items={item.children as Iterable<T>}>
30
- {renderItem}
31
- </Collection>
32
- ) : null}
33
- </TreeItem>
34
- );
8
+ type CSSProperties,
9
+ createContext,
10
+ type ReactNode,
11
+ useContext,
12
+ } from "react";
13
+
14
+ import { cn } from "../../lib";
15
+
16
+ const DEFAULT_PARENT_ICON = (
17
+ <ChevronRightIcon
18
+ aria-hidden
19
+ className="size-4 shrink-0 text-muted-foreground transition-transform duration-200 in-data-[expanded=true]:rotate-90"
20
+ />
21
+ );
22
+
23
+ interface TreeContextValue {
24
+ tree: TreeInstance<unknown>;
25
+ }
26
+
27
+ const TreeContext = createContext<TreeContextValue | undefined>(undefined);
28
+
29
+ function useTreeContext() {
30
+ const context = useContext(TreeContext);
31
+ if (!context) {
32
+ throw new Error("Tree components must be used within a TreeRoot");
33
+ }
34
+ return context;
35
+ }
36
+
37
+ interface TreeItemContextValue {
38
+ item: ItemInstance<unknown>;
39
+ }
40
+
41
+ const TreeItemContext = createContext<TreeItemContextValue | undefined>(
42
+ undefined,
43
+ );
44
+
45
+ function useTreeItemContext() {
46
+ const context = useContext(TreeItemContext);
47
+ if (!context) {
48
+ throw new Error("TreeItemLabel must be used within a TreeItem");
49
+ }
50
+ return context;
51
+ }
52
+
53
+ interface TreeRootProps<T> extends useRender.ComponentProps<"div"> {
54
+ tree: TreeInstance<T>;
55
+ indent?: number;
56
+ }
57
+
58
+ export function TreeRoot<T>({
59
+ tree,
60
+ indent = 20,
61
+ className,
62
+ render,
63
+ style,
64
+ ...props
65
+ }: TreeRootProps<T>) {
66
+ const { style: containerStyle, ...containerProps } = tree.getContainerProps();
67
+
68
+ const defaultProps = {
69
+ "data-slot": "tree",
70
+ ...containerProps,
71
+ className: cn("flex flex-col outline-none", className),
72
+ style: {
73
+ ...containerStyle,
74
+ ...style,
75
+ "--tree-indent": `${indent}px`,
76
+ } as CSSProperties,
35
77
  };
36
78
 
37
79
  return (
38
- <TreeRoot items={items} {...props}>
39
- {items.map((item) => renderItem(item))}
40
- </TreeRoot>
80
+ <TreeContext.Provider value={{ tree: tree as TreeInstance<unknown> }}>
81
+ {useRender({
82
+ defaultTagName: "div",
83
+ render,
84
+ props: mergeProps<"div">(defaultProps, props),
85
+ })}
86
+ </TreeContext.Provider>
41
87
  );
42
88
  }
89
+
90
+ type ParentIcon = ReactNode | ((isExpanded: boolean) => ReactNode);
91
+
92
+ interface TreeItemProps<T> extends useRender.ComponentProps<"button"> {
93
+ item: ItemInstance<T>;
94
+ leafIcon?: ReactNode;
95
+ parentIcon?: ParentIcon;
96
+ }
97
+
98
+ export function TreeItem<T>({
99
+ item,
100
+ leafIcon,
101
+ parentIcon = DEFAULT_PARENT_ICON,
102
+ className,
103
+ render,
104
+ children,
105
+ style,
106
+ ...props
107
+ }: TreeItemProps<T>) {
108
+ const { style: itemStyle, ...itemProps } = item.getProps();
109
+ const level = item.getItemMeta().level;
110
+ const isFolder = item.isFolder();
111
+ const isExpanded = item.isExpanded();
112
+
113
+ const icon = isFolder
114
+ ? typeof parentIcon === "function"
115
+ ? parentIcon(isExpanded)
116
+ : parentIcon
117
+ : leafIcon;
118
+
119
+ const autoLabel = (
120
+ <TreeItemLabel>
121
+ {icon}
122
+ {item.getItemName()}
123
+ </TreeItemLabel>
124
+ );
125
+
126
+ const defaultProps = {
127
+ "data-slot": "tree-item",
128
+ "data-focused": item.isFocused() || undefined,
129
+ "data-folder": isFolder || undefined,
130
+ "data-selected": item.isSelected?.() || undefined,
131
+ "data-drag-target": item.isDragTarget?.() || undefined,
132
+ "data-search-match": item.isMatchingSearch?.() || undefined,
133
+ "data-expanded": isExpanded || undefined,
134
+ "aria-expanded": isFolder ? isExpanded : undefined,
135
+ ...itemProps,
136
+ style: {
137
+ ...itemStyle,
138
+ ...style,
139
+ paddingInlineStart: `calc(var(--tree-indent) * ${level})`,
140
+ } as CSSProperties,
141
+ className: cn(
142
+ "z-10 not-last:pb-0.5 outline-hidden select-none focus:z-20",
143
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
144
+ className,
145
+ ),
146
+ children: children ?? autoLabel,
147
+ };
148
+
149
+ return (
150
+ <TreeItemContext.Provider value={{ item: item as ItemInstance<unknown> }}>
151
+ {useRender({
152
+ defaultTagName: "button",
153
+ render,
154
+ props: mergeProps<"button">(defaultProps, props),
155
+ })}
156
+ </TreeItemContext.Provider>
157
+ );
158
+ }
159
+
160
+ export function TreeItemLabel({
161
+ className,
162
+ render,
163
+ children,
164
+ ...props
165
+ }: useRender.ComponentProps<"span">) {
166
+ const { item } = useTreeItemContext();
167
+
168
+ const defaultProps = {
169
+ "data-slot": "tree-item-label",
170
+ className: cn(
171
+ "flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors",
172
+ "bg-background hover:bg-accent",
173
+ "in-data-[selected=true]:bg-accent in-data-[selected=true]:text-accent-foreground",
174
+ "in-data-[drag-target=true]:bg-accent",
175
+ "in-data-[search-match=true]:bg-blue-50",
176
+ "in-focus-visible:ring-3 in-focus-visible:ring-ring/50",
177
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0",
178
+ className,
179
+ ),
180
+ children: children ?? item.getItemName(),
181
+ };
182
+
183
+ return useRender({
184
+ defaultTagName: "span",
185
+ render,
186
+ props: mergeProps<"span">(defaultProps, props),
187
+ });
188
+ }
189
+
190
+ export function TreeDragLine({
191
+ className,
192
+ render,
193
+ style,
194
+ ...props
195
+ }: useRender.ComponentProps<"div">) {
196
+ const { tree } = useTreeContext();
197
+ const dragLineStyle = tree.getDragLineStyle?.();
198
+ if (!dragLineStyle) return null;
199
+
200
+ const defaultProps = {
201
+ "data-slot": "tree-drag-line",
202
+ style: { ...dragLineStyle, ...style },
203
+ className: cn(
204
+ "absolute z-30 -mt-px h-0.5 w-[unset] bg-primary",
205
+ "before:absolute before:-top-[3px] before:left-0 before:size-2 before:rounded-full",
206
+ "before:border-2 before:border-primary before:bg-background",
207
+ className,
208
+ ),
209
+ };
210
+
211
+ return useRender({
212
+ defaultTagName: "div",
213
+ render,
214
+ props: mergeProps<"div">(defaultProps, props),
215
+ });
216
+ }
217
+
218
+ export type { TreeRootProps, TreeItemProps };
@@ -2,8 +2,8 @@ import { Meta, StoryObj } from "@storybook/react-vite";
2
2
  import { isUndefined } from "lodash";
3
3
  import { ChevronLeft, ChevronRight } from "lucide-react";
4
4
  import { useState } from "react";
5
- import { createToastManager, ToastProvider } from "@/components/toast";
6
- import { Button, Input } from "../../../components";
5
+ import { Button, createToastManager, Input } from "../../../components";
6
+ import { LibraryProvider } from "../../../providers/library-provider";
7
7
  import useDebouncedValue from "../../useDebounceValue/useDebouncedValue";
8
8
  import useAsync from "../useAsync";
9
9
 
@@ -37,7 +37,7 @@ export const Basic = {
37
37
  <Input
38
38
  type="text"
39
39
  value={pokemonName}
40
- onChange={setPokemonName}
40
+ onValueChange={setPokemonName}
41
41
  placeholder="Enter Pokémon name"
42
42
  />
43
43
  {loading && <p>Loading...</p>}
@@ -75,9 +75,9 @@ const fetchPokemonById = async (id: number) => {
75
75
  export const Advanced: Story = {
76
76
  decorators: [
77
77
  (Story) => (
78
- <ToastProvider toastManager={toastManager}>
78
+ <LibraryProvider>
79
79
  <Story />
80
- </ToastProvider>
80
+ </LibraryProvider>
81
81
  ),
82
82
  ],
83
83
  render: () => {
@@ -36,9 +36,7 @@ export const Default: Story = {
36
36
  </code>
37
37
  </pre>
38
38
 
39
- <Button showLoader={false} onClick={() => copy(args.textoACopiar)}>
40
- Copy
41
- </Button>
39
+ <Button onClick={() => copy(args.textoACopiar)}>Copy</Button>
42
40
  </div>
43
41
  );
44
42
  },
@@ -1,5 +1,5 @@
1
1
  import { Meta, StoryObj } from "@storybook/react-vite";
2
- import { Heading, Input } from "../../../components";
2
+ import { Input } from "../../../components";
3
3
  import { createToastManager, ToastProvider } from "../../../components/toast";
4
4
  import { useDebouncedCallback } from "../../useDebounceCallback";
5
5
 
@@ -37,7 +37,7 @@ export const Basic = {
37
37
  <code className="block">Revisa la consola para ver los logs.</code>
38
38
  </pre>
39
39
  <Input
40
- onChange={(value) => handleSearch(value)}
40
+ onValueChange={(value) => handleSearch(value)}
41
41
  placeholder="Escribe para buscar..."
42
42
  />
43
43
  </div>
@@ -64,17 +64,17 @@ export const DifferentDelays: StoryObj = {
64
64
  return (
65
65
  <div className="space-y-4">
66
66
  <div>
67
- <Heading>Debounce Rápido (200ms)</Heading>
67
+ <h3 className="text-sm font-semibold">Debounce Rápido (200ms)</h3>
68
68
  <Input
69
- onChange={(value) => handleFastDebounce(value)}
69
+ onValueChange={(value) => handleFastDebounce(value)}
70
70
  placeholder="Debounce rápido..."
71
71
  />
72
72
  </div>
73
73
 
74
74
  <div>
75
- <Heading>Debounce Lento (1000ms)</Heading>
75
+ <h3 className="text-sm font-semibold">Debounce Lento (1000ms)</h3>
76
76
  <Input
77
- onChange={(value) => handleSlowDebounce(value)}
77
+ onValueChange={(value) => handleSlowDebounce(value)}
78
78
  placeholder="Debounce lento..."
79
79
  />
80
80
  </div>
@@ -18,7 +18,7 @@ export const Default: Story = {
18
18
 
19
19
  return (
20
20
  <div className="space-y-2">
21
- <Input onChange={setTitle} value={title} />
21
+ <Input onValueChange={setTitle} value={title} />
22
22
  <Button onClick={() => setTitle(title)}>Set document title</Button>
23
23
  </div>
24
24
  );
@@ -1,7 +1,7 @@
1
1
  import { screen } from "@testing-library/dom";
2
2
  import { render, waitFor } from "@testing-library/react";
3
3
  import { describe, expect, it, vi } from "vitest";
4
- import { click } from "../../../utils";
4
+ import { click } from "../../../utils/tests";
5
5
  import useEventListener from "../useEventListener";
6
6
 
7
7
  describe("useEventListener hook", () => {
@@ -35,7 +35,7 @@ export const Default = {
35
35
 
36
36
  return (
37
37
  <div className="space-y-2">
38
- <Input value={value} onChange={setValue} />
38
+ <Input value={value} onValueChange={setValue} />
39
39
  <Button onClick={removeValue}>Clear</Button>
40
40
  </div>
41
41
  );
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
2
2
 
3
3
  export type usePaginationProps = {
4
4
  /**
@@ -10,53 +10,65 @@ export type usePaginationProps = {
10
10
  */
11
11
  pageSize: number;
12
12
  /**
13
- * Pagina inicial
13
+ * Página controlada
14
+ */
15
+ currentPage?: number;
16
+ /**
17
+ * Página inicial (uncontrolled)
18
+ */
19
+ defaultCurrentPage?: number;
20
+ /**
21
+ * Callback cuando cambia la página
22
+ */
23
+ onCurrentPageChange?: (page: number) => void;
24
+ /**
25
+ * @deprecated Usá `defaultCurrentPage`
14
26
  */
15
27
  initialCurrentPage?: number;
16
28
  /**
17
- * Callback que se ejecuta cuando cambia la pagina
29
+ * @deprecated Usá `onCurrentPageChange`
18
30
  */
19
31
  onChange?: (value: { currentPage: number; pageSize: number }) => void;
20
- siblingCount?: number;
21
32
  };
22
33
 
23
34
  function usePagination({
24
35
  totalItems,
25
36
  pageSize,
26
- initialCurrentPage = 1,
37
+ currentPage: currentPageProp,
38
+ defaultCurrentPage,
39
+ onCurrentPageChange,
40
+ initialCurrentPage,
27
41
  onChange,
28
42
  }: usePaginationProps) {
29
- const [currentPage, setCurrentPage] = useState<number>(initialCurrentPage);
30
- const maxPage = Math.ceil(totalItems / pageSize);
31
- const isLastPage = currentPage === maxPage;
32
- const isFirstPage = currentPage === 1;
43
+ const [currentPage = 1, setCurrentPage] = useControllableState<number>({
44
+ prop: currentPageProp,
45
+ defaultProp: defaultCurrentPage ?? initialCurrentPage ?? 1,
46
+ onChange: (page) => {
47
+ onCurrentPageChange?.(page);
48
+ onChange?.({ currentPage: page, pageSize });
49
+ },
50
+ });
51
+
52
+ const maxPage = Math.max(1, Math.ceil(totalItems / pageSize));
53
+ const isLastPage = currentPage >= maxPage;
54
+ const isFirstPage = currentPage <= 1;
33
55
  const start = (currentPage - 1) * pageSize + 1;
34
56
  const end = Math.min(currentPage * pageSize, totalItems);
35
57
 
36
58
  const next = () => {
37
59
  if (isLastPage) return;
38
- const newPage = Math.min(currentPage + 1, maxPage);
39
- handleNewPage(newPage);
60
+ setCurrentPage(Math.min(currentPage + 1, maxPage));
40
61
  };
41
62
 
42
63
  const prev = () => {
43
64
  if (isFirstPage) return;
44
- const newPage = Math.max(currentPage - 1, 1);
45
- handleNewPage(newPage);
65
+ setCurrentPage(Math.max(currentPage - 1, 1));
46
66
  };
47
67
 
48
68
  const goTo = (page: number) => {
49
- if (page === 1 && isFirstPage) return;
50
- if (page === maxPage && isLastPage) return;
51
-
52
- const pageNumber = Math.max(1, page);
53
- const newPage = Math.min(pageNumber, maxPage);
54
- handleNewPage(newPage);
55
- };
56
-
57
- const handleNewPage = (page: number) => {
58
- setCurrentPage(page);
59
- onChange?.({ currentPage: page, pageSize });
69
+ const clamped = Math.min(Math.max(1, page), maxPage);
70
+ if (clamped === currentPage) return;
71
+ setCurrentPage(clamped);
60
72
  };
61
73
 
62
74
  return {
@@ -29,7 +29,7 @@
29
29
  --color-success-foreground: oklch(98.42% 0.003 247.86);
30
30
 
31
31
  --color-warning: oklch(79.65% 0.16 70.08);
32
- --color-warning-foreground: oklch(17.97% 0.034 59.63);
32
+ --color-warning-foreground: oklch(98.42% 0.003 247.86);
33
33
 
34
34
  --color-info: oklch(62.8% 0.152 237.57);
35
35
  --color-info-foreground: oklch(98.42% 0.003 247.86);