@rovula/ui 0.0.48 → 0.0.49

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 (33) hide show
  1. package/dist/cjs/bundle.css +9 -0
  2. package/dist/cjs/bundle.js +3 -3
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Tree/Tree.d.ts +4 -45
  5. package/dist/cjs/types/components/Tree/Tree.stories.d.ts +8 -1
  6. package/dist/cjs/types/components/Tree/TreeItem.d.ts +4 -0
  7. package/dist/cjs/types/components/Tree/index.d.ts +4 -0
  8. package/dist/cjs/types/components/Tree/type.d.ts +76 -0
  9. package/dist/cjs/types/index.d.ts +1 -0
  10. package/dist/components/Tree/Tree.js +15 -49
  11. package/dist/components/Tree/Tree.stories.js +117 -8
  12. package/dist/components/Tree/TreeItem.js +81 -0
  13. package/dist/components/Tree/index.js +4 -0
  14. package/dist/components/Tree/type.js +1 -0
  15. package/dist/esm/bundle.css +9 -0
  16. package/dist/esm/bundle.js +1 -1
  17. package/dist/esm/bundle.js.map +1 -1
  18. package/dist/esm/types/components/Tree/Tree.d.ts +4 -45
  19. package/dist/esm/types/components/Tree/Tree.stories.d.ts +8 -1
  20. package/dist/esm/types/components/Tree/TreeItem.d.ts +4 -0
  21. package/dist/esm/types/components/Tree/index.d.ts +4 -0
  22. package/dist/esm/types/components/Tree/type.d.ts +76 -0
  23. package/dist/esm/types/index.d.ts +1 -0
  24. package/dist/index.d.ts +82 -2
  25. package/dist/index.js +1 -0
  26. package/dist/src/theme/global.css +12 -0
  27. package/package.json +1 -1
  28. package/src/components/Tree/Tree.stories.tsx +230 -8
  29. package/src/components/Tree/Tree.tsx +44 -183
  30. package/src/components/Tree/TreeItem.tsx +231 -0
  31. package/src/components/Tree/index.ts +5 -0
  32. package/src/components/Tree/type.ts +90 -0
  33. package/src/index.ts +1 -0
@@ -1,170 +1,26 @@
1
- import { ActionButton, Checkbox, Icon, Loading, cn } from "@/index";
2
- import React, {
3
- FC,
4
- ReactNode,
5
- useCallback,
6
- useMemo,
7
- useState,
8
- useEffect,
9
- } from "react";
1
+ import React, { FC, useCallback, useEffect, useState } from "react";
2
+ import TreeItem from "./TreeItem";
3
+ import { TreeData, TreeProps } from "./type";
10
4
 
11
- export type TreeData = {
12
- id: string;
13
- title: string;
14
- icon?: ReactNode;
15
- children?: TreeData[];
16
- };
17
-
18
- export interface TreeItemProps extends TreeData {
19
- isFirstLevel?: boolean;
20
- isLastItem: boolean;
21
- checkIsExpanded: (id: string) => boolean;
22
- checkIsChecked: (id: string) => boolean;
23
- onExpandChange?: (id: string, expanded: boolean) => void;
24
- onCheckedChange?: (id: string, checked: boolean) => void;
25
- }
26
-
27
- const TreeItem: FC<TreeItemProps> = ({
28
- id,
29
- title,
30
- children,
31
- isFirstLevel = false,
32
- isLastItem,
33
- checkIsExpanded,
34
- checkIsChecked,
35
- onExpandChange,
36
- onCheckedChange,
37
- }) => {
38
- const isChecked = useMemo(() => checkIsChecked(id), [checkIsChecked, id]);
39
- const isExpanded = useMemo(() => checkIsExpanded(id), [checkIsExpanded, id]);
40
- const hasChildren = useMemo(() => !!children?.length, [children]);
41
-
42
- const handleExpandToggle = useCallback(() => {
43
- onExpandChange?.(id, !isExpanded);
44
- }, [id, isExpanded, onExpandChange]);
45
-
46
- // TODO move to props
47
- const lineSize = 2;
48
- const horizontalLineWidth = 4;
49
- const expandButtonSize = 30;
50
- const spacing = 2;
51
-
52
- const styles = {
53
- branch: {
54
- height: isLastItem
55
- ? `calc(50% + ${lineSize}px)`
56
- : `calc(100% + ${lineSize}px)`,
57
- width: lineSize,
58
- marginTop: -lineSize,
59
- borderBottomLeftRadius: lineSize / 2,
60
- },
61
- horizontalLine: {
62
- height: lineSize,
63
- width:
64
- lineSize +
65
- horizontalLineWidth +
66
- (hasChildren ? 0 : expandButtonSize + spacing),
67
- marginLeft: -lineSize + 0.1,
68
- borderBottomLeftRadius: lineSize / 2,
69
- },
70
- expandButton: {
71
- width: expandButtonSize,
72
- height: expandButtonSize,
73
- },
74
- childPadding: {
75
- paddingLeft: isFirstLevel
76
- ? expandButtonSize / 2 - lineSize / 2
77
- : expandButtonSize / 2 + horizontalLineWidth - lineSize / 2,
78
- },
79
- };
80
-
81
- return (
82
- <div className="flex flex-row w-full">
83
- <div
84
- className={cn("bg-grey-150", { "h-1/2": isLastItem })}
85
- style={styles.branch}
86
- />
87
- <div className="flex flex-col w-full">
88
- <div className="flex items-center py-2 min-h-10">
89
- {!isFirstLevel && (
90
- <div className="bg-grey-150" style={styles.horizontalLine} />
91
- )}
92
-
93
- {isFirstLevel && !hasChildren && (
94
- <div className="flex mr-[2px]" style={styles.expandButton} />
95
- )}
96
-
97
- {hasChildren && (
98
- <div className="flex mr-[2px]" style={styles.expandButton}>
99
- <ActionButton
100
- variant="icon"
101
- size="sm"
102
- onClick={handleExpandToggle}
103
- >
104
- <Icon name={isExpanded ? "chevron-down" : "chevron-right"} />
105
- </ActionButton>
106
- </div>
107
- )}
108
- <Checkbox
109
- id={id}
110
- className="size-[16pt]"
111
- checked={isChecked}
112
- onCheckedChange={(newChecked) =>
113
- onCheckedChange?.(id, newChecked as boolean)
114
- }
115
- />
116
- <div className="ml-2 gap-1 flex items-center text-foreground">
117
- <Icon
118
- name={isExpanded ? "folder-open" : "folder"}
119
- className="fill-warning"
120
- />
121
- <label
122
- htmlFor={id}
123
- className="flex-1 cursor-pointer text-subtitle5 text-ellipsis"
124
- >
125
- {title}
126
- </label>
127
- </div>
128
- </div>
129
- {isExpanded && hasChildren && (
130
- <div className="flex flex-col" style={styles.childPadding}>
131
- {children?.map((child, idx) => (
132
- <TreeItem
133
- key={child.id}
134
- {...child}
135
- isLastItem={idx === children.length - 1}
136
- checkIsExpanded={checkIsExpanded}
137
- checkIsChecked={checkIsChecked}
138
- onExpandChange={onExpandChange}
139
- onCheckedChange={onCheckedChange}
140
- />
141
- ))}
142
- </div>
143
- )}
144
- </div>
145
- </div>
146
- );
147
- };
148
- export type TreeProps = {
149
- data: TreeData[];
150
- defaultExpandedId?: string[];
151
- defaultCheckedId?: string[];
152
- checkedId?: string[];
153
- onCheckedChange?: (checkedId: string[]) => void;
154
- defaultExpandAll?: boolean;
155
- defaultCheckAll?: boolean;
156
- hierarchicalCheck?: boolean;
157
- };
158
-
159
- export const Tree: FC<TreeProps> = ({
5
+ const Tree: FC<TreeProps> = ({
6
+ classes,
160
7
  data,
161
8
  defaultExpandedId = [],
162
9
  defaultCheckedId = [],
163
10
  checkedId,
11
+ loadingId,
12
+ renderIcon,
13
+ renderRightSection,
14
+ renderElement,
15
+ renderTitle,
16
+ onExpandChange,
164
17
  onCheckedChange,
165
18
  defaultExpandAll = false,
166
19
  defaultCheckAll = false,
167
20
  hierarchicalCheck = false,
21
+ showIcon = true,
22
+ disabled,
23
+ enableSeparatorLine = true,
168
24
  }) => {
169
25
  const [checkedState, setCheckedState] = useState<Record<string, boolean>>({});
170
26
  const [expandedState, setExpandedState] = useState<Record<string, boolean>>(
@@ -190,7 +46,7 @@ export const Tree: FC<TreeProps> = ({
190
46
  allExpanded[node.id] = true;
191
47
  });
192
48
  setExpandedState(allExpanded);
193
- } else {
49
+ } else if (defaultExpandedId?.length) {
194
50
  const initialExpandedState = defaultExpandedId.reduce((acc, id) => {
195
51
  acc[id] = true;
196
52
  return acc;
@@ -206,7 +62,7 @@ export const Tree: FC<TreeProps> = ({
206
62
  allChecked[node.id] = true;
207
63
  });
208
64
  setCheckedState(allChecked);
209
- } else if (!checkedId) {
65
+ } else if (!checkedId && defaultCheckedId?.length) {
210
66
  const initialCheckedState = defaultCheckedId.reduce((acc, id) => {
211
67
  acc[id] = true;
212
68
  return acc;
@@ -215,9 +71,13 @@ export const Tree: FC<TreeProps> = ({
215
71
  }
216
72
  }, [data, defaultCheckedId, checkedId, defaultCheckAll]);
217
73
 
218
- const handleExpandChange = useCallback((id: string, expanded: boolean) => {
219
- setExpandedState((prev) => ({ ...prev, [id]: expanded }));
220
- }, []);
74
+ const handleExpandChange = useCallback(
75
+ (id: string, expanded: boolean) => {
76
+ onExpandChange?.(id, expanded);
77
+ setExpandedState((prev) => ({ ...prev, [id]: expanded }));
78
+ },
79
+ [onExpandChange]
80
+ );
221
81
 
222
82
  const handleCheckedChange = useCallback(
223
83
  (id: string, checked: boolean) => {
@@ -293,39 +153,40 @@ export const Tree: FC<TreeProps> = ({
293
153
  [checkedId, checkedState]
294
154
  );
295
155
 
156
+ const checkIsLoading = useCallback(
157
+ (id: string) => {
158
+ if (loadingId) {
159
+ return loadingId.includes(id);
160
+ }
161
+ },
162
+ [loadingId]
163
+ );
164
+
296
165
  return (
297
166
  <div className="w-full">
298
167
  {data.map((item, idx) => (
299
168
  <TreeItem
300
169
  key={item.id}
301
- {...item}
170
+ classes={classes}
302
171
  isFirstLevel
303
172
  isLastItem={idx === data.length - 1}
304
173
  checkIsExpanded={checkIsExpanded}
305
174
  checkIsChecked={checkIsChecked}
306
175
  onExpandChange={handleExpandChange}
307
176
  onCheckedChange={handleCheckedChange}
177
+ checkIsLoading={checkIsLoading}
178
+ renderIcon={renderIcon}
179
+ renderElement={renderElement}
180
+ renderTitle={renderTitle}
181
+ renderRightSection={renderRightSection}
182
+ enableSeparatorLine={enableSeparatorLine}
183
+ disabled={disabled}
184
+ showIcon={showIcon}
185
+ {...item}
308
186
  />
309
187
  ))}
310
188
  </div>
311
189
  );
312
190
  };
313
- /**
314
- * TODO
315
- * -----
316
- * - Custom style
317
- * - Custom icon, elm and render props -> callback with selected*expanded
318
- * - OnClick item
319
- * - OnClick expandButton
320
- * - disabled props
321
- * - right section icon
322
- * - props for show icon, line
323
- * - props for render item
324
- * - OnLoad mode
325
- * -----
326
- * - props onLoad item
327
- * - props for hasChildren * for check to trigger on load
328
- * - animate expand
329
- * - check duplicate reversive on updateChildren
330
- * - write storybook
331
- */
191
+
192
+ export default Tree;
@@ -0,0 +1,231 @@
1
+ import { ActionButton, Checkbox, Loading } from "@/index";
2
+ import { cn } from "@/utils/cn";
3
+ import React, { FC, ReactNode, useCallback, useEffect, useMemo } from "react";
4
+ import Icon from "../Icon/Icon";
5
+ import { TreeItemProps } from "./type";
6
+
7
+ const TreeItem: FC<TreeItemProps> = ({
8
+ id,
9
+ title,
10
+ classes,
11
+ children,
12
+ isFirstLevel = false,
13
+ disabled,
14
+ icon,
15
+ showIcon,
16
+ showExpandButton,
17
+ enableSeparatorLine = true,
18
+ isLastItem,
19
+ checkIsExpanded,
20
+ checkIsChecked,
21
+ checkIsLoading,
22
+ onExpandChange,
23
+ onCheckedChange,
24
+ onClickItem,
25
+ renderIcon,
26
+ renderElement,
27
+ renderTitle,
28
+ renderRightSection,
29
+ }) => {
30
+ const isLoading = useMemo(() => checkIsLoading?.(id), [checkIsLoading, id]);
31
+ const isChecked = useMemo(() => checkIsChecked(id), [checkIsChecked, id]);
32
+ const isExpanded = useMemo(() => checkIsExpanded(id), [checkIsExpanded, id]);
33
+ const hasChildren = useMemo(() => !!children?.length, [children]);
34
+ const shouldExpandButton = useMemo(
35
+ () => (showExpandButton !== undefined ? showExpandButton : hasChildren),
36
+ [hasChildren, showExpandButton]
37
+ );
38
+
39
+ const handleExpandToggle = useCallback(() => {
40
+ onExpandChange?.(id, !isExpanded);
41
+ }, [id, isExpanded, onExpandChange]);
42
+
43
+ // TODO move to props
44
+ const lineSize = 2;
45
+ const horizontalLineWidth = 4;
46
+ const expandButtonSize = 30;
47
+ const spacing = 2;
48
+
49
+ const styles = {
50
+ branch: {
51
+ height: isLastItem
52
+ ? `calc(50% + ${lineSize}px)`
53
+ : `calc(100% + ${lineSize}px)`,
54
+ width: lineSize,
55
+ marginTop: -lineSize,
56
+ borderBottomLeftRadius: lineSize / 2,
57
+ },
58
+ horizontalLine: {
59
+ height: lineSize,
60
+ width:
61
+ lineSize +
62
+ horizontalLineWidth +
63
+ (shouldExpandButton ? 0 : expandButtonSize + spacing),
64
+ marginLeft: -lineSize + 0.1,
65
+ borderBottomLeftRadius: lineSize / 2,
66
+ },
67
+ expandButton: {
68
+ width: expandButtonSize,
69
+ height: expandButtonSize,
70
+ },
71
+ childPadding: {
72
+ paddingLeft: isFirstLevel
73
+ ? expandButtonSize / 2 - lineSize / 2
74
+ : expandButtonSize / 2 + horizontalLineWidth - lineSize / 2,
75
+ },
76
+ };
77
+
78
+ useEffect(() => {
79
+ if (isExpanded && !isLoading && !hasChildren) {
80
+ handleExpandToggle();
81
+ }
82
+ }, [isLoading, handleExpandToggle]);
83
+
84
+ const handleOnClickItem = useCallback(() => {
85
+ onClickItem?.(id);
86
+ }, [onClickItem, id]);
87
+
88
+ const defaultIcon = (
89
+ <Icon
90
+ name={isExpanded ? "folder-open" : "folder"}
91
+ className="fill-warning"
92
+ />
93
+ );
94
+
95
+ const customIcon =
96
+ icon ??
97
+ renderIcon?.({
98
+ id,
99
+ expanded: isExpanded,
100
+ selected: isChecked,
101
+ });
102
+
103
+ const rightIcon = renderRightSection?.({
104
+ id,
105
+ expanded: isExpanded,
106
+ selected: isChecked,
107
+ });
108
+
109
+ const titleContent = renderTitle
110
+ ? renderTitle({ id, title, expanded: isExpanded, selected: isChecked })
111
+ : title;
112
+
113
+ const elementWrapper = (content: ReactNode) =>
114
+ renderElement
115
+ ? renderElement({
116
+ id,
117
+ expanded: isExpanded,
118
+ selected: isChecked,
119
+ children: content,
120
+ styles,
121
+ onClick: handleOnClickItem,
122
+ })
123
+ : content;
124
+
125
+ return elementWrapper(
126
+ <div className={cn("flex flex-row w-full", classes?.elementWrapper)}>
127
+ <div
128
+ className={cn("bg-grey-150", { "h-1/2": isLastItem }, classes?.branch)}
129
+ style={styles.branch}
130
+ />
131
+ <div className={cn("flex flex-col w-full", classes?.itemWrapper)}>
132
+ <div
133
+ className={cn(
134
+ "flex items-center py-2 min-h-10",
135
+ classes?.itemContainer
136
+ )}
137
+ >
138
+ {!isFirstLevel && (
139
+ <div
140
+ className={cn("bg-grey-150", classes?.horizontalLine)}
141
+ style={styles.horizontalLine}
142
+ />
143
+ )}
144
+ {isFirstLevel && !shouldExpandButton && (
145
+ <div
146
+ className={cn("flex mr-[2px]", classes?.expandButton)}
147
+ style={styles.expandButton}
148
+ />
149
+ )}
150
+ {shouldExpandButton && (
151
+ <div
152
+ className={cn("flex mr-[2px]", classes?.expandButton)}
153
+ style={styles.expandButton}
154
+ onClick={!isLoading && handleExpandToggle}
155
+ >
156
+ <ActionButton variant="icon" size="sm">
157
+ {isLoading ? (
158
+ <Loading />
159
+ ) : (
160
+ <Icon name={isExpanded ? "chevron-down" : "chevron-right"} />
161
+ )}
162
+ </ActionButton>
163
+ </div>
164
+ )}
165
+ <Checkbox
166
+ id={id}
167
+ className={cn("size-[16pt]", classes?.checkbox)}
168
+ checked={isChecked}
169
+ disabled={disabled}
170
+ onCheckedChange={(newChecked) =>
171
+ onCheckedChange?.(id, newChecked as boolean)
172
+ }
173
+ />
174
+ <div
175
+ className={cn(
176
+ "ml-2 gap-1 flex flex-1 items-center text-foreground",
177
+ classes?.item
178
+ )}
179
+ onClick={handleOnClickItem}
180
+ >
181
+ {showIcon ? customIcon || defaultIcon : null}
182
+ <div
183
+ className={cn(
184
+ "flex flex-1 cursor-pointer text-subtitle5 text-ellipsis",
185
+ classes?.title
186
+ )}
187
+ >
188
+ {titleContent}
189
+ </div>
190
+ </div>
191
+ {rightIcon}
192
+ </div>
193
+ {isExpanded && hasChildren && (
194
+ <div
195
+ className={cn("flex flex-col", classes?.childrenWrapper)}
196
+ style={styles.childPadding}
197
+ >
198
+ {children?.map((child, idx) => (
199
+ <TreeItem
200
+ key={child.id}
201
+ classes={classes}
202
+ isLastItem={idx === children.length - 1}
203
+ checkIsExpanded={checkIsExpanded}
204
+ checkIsChecked={checkIsChecked}
205
+ checkIsLoading={checkIsLoading}
206
+ onExpandChange={onExpandChange}
207
+ onCheckedChange={onCheckedChange}
208
+ renderIcon={renderIcon}
209
+ renderElement={renderElement}
210
+ renderTitle={renderTitle}
211
+ disabled={disabled}
212
+ showIcon={showIcon}
213
+ {...child}
214
+ />
215
+ ))}
216
+ </div>
217
+ )}
218
+ {enableSeparatorLine && isFirstLevel && !isLastItem && (
219
+ <div
220
+ className={cn(
221
+ "bg-grey-150 w-full h-[2px] rounded",
222
+ classes?.separatorLine
223
+ )}
224
+ />
225
+ )}
226
+ </div>
227
+ </div>
228
+ );
229
+ };
230
+
231
+ export default TreeItem;
@@ -0,0 +1,5 @@
1
+ import Tree from "./Tree";
2
+ import TreeItem from "./TreeItem";
3
+
4
+ export * from "./type";
5
+ export { Tree, TreeItem };
@@ -0,0 +1,90 @@
1
+ import { CSSProperties, ReactNode } from "react";
2
+
3
+ export type TreeData = {
4
+ id: string;
5
+ title: string;
6
+ icon?: ReactNode;
7
+ disabled?: boolean;
8
+ onClickItem?: (id: string) => void;
9
+ children?: TreeData[];
10
+ renderIcon?: (params: {
11
+ id: string;
12
+ expanded: boolean;
13
+ selected: boolean;
14
+ }) => ReactNode;
15
+ };
16
+
17
+ export interface TreeItemProps extends TreeData {
18
+ isFirstLevel?: boolean;
19
+ isLastItem: boolean;
20
+ disabled?: boolean;
21
+ showIcon?: boolean;
22
+ showExpandButton?: boolean;
23
+ enableSeparatorLine?: boolean;
24
+ checkIsExpanded: (id: string) => boolean;
25
+ checkIsChecked: (id: string) => boolean;
26
+ checkIsLoading?: (id: string) => void;
27
+ onExpandChange?: (id: string, expanded: boolean) => void;
28
+ onCheckedChange?: (id: string, checked: boolean) => void;
29
+ renderRightSection?: (params: {
30
+ id: string;
31
+ expanded: boolean;
32
+ selected: boolean;
33
+ }) => ReactNode;
34
+ renderElement?: (params: {
35
+ id: string;
36
+ expanded: boolean;
37
+ selected: boolean;
38
+ children: ReactNode;
39
+ styles: {
40
+ branch: CSSProperties;
41
+ horizontalLine: CSSProperties;
42
+ expandButton: CSSProperties;
43
+ childPadding: CSSProperties;
44
+ };
45
+ onClick?: TreeItemProps["onClickItem"];
46
+ }) => ReactNode;
47
+ renderTitle?: (params: {
48
+ id: string;
49
+ title: string;
50
+ expanded: boolean;
51
+ selected: boolean;
52
+ }) => ReactNode;
53
+ classes?: Partial<{
54
+ elementWrapper: string;
55
+ branch: string;
56
+ itemWrapper: string;
57
+ itemContainer: string;
58
+ horizontalLine: string;
59
+ expandButton: string;
60
+ separatorLine: string;
61
+ checkbox: string;
62
+ item: string;
63
+ title: string;
64
+ childrenWrapper: string;
65
+ }>;
66
+ }
67
+
68
+ export interface TreeProps
69
+ extends Pick<
70
+ TreeItemProps,
71
+ | "renderIcon"
72
+ | "renderRightSection"
73
+ | "renderElement"
74
+ | "renderTitle"
75
+ | "showIcon"
76
+ | "disabled"
77
+ | "enableSeparatorLine"
78
+ | "classes"
79
+ > {
80
+ data: TreeData[];
81
+ defaultExpandedId?: string[];
82
+ defaultCheckedId?: string[];
83
+ checkedId?: string[];
84
+ loadingId?: string[];
85
+ onExpandChange?: (id: string, expanded: boolean) => void;
86
+ onCheckedChange?: (checkedId: string[]) => void;
87
+ defaultExpandAll?: boolean;
88
+ defaultCheckAll?: boolean;
89
+ hierarchicalCheck?: boolean;
90
+ }
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ export * from "./components/Tooltip/TooltipSimple";
39
39
  export * from "./components/Toast/Toast";
40
40
  export * from "./components/Toast/Toaster";
41
41
  export * from "./components/Toast/useToast";
42
+ export * from "./components/Tree";
42
43
 
43
44
  // Export component types
44
45
  export type { ButtonProps } from "./components/Button/Button";