@rovula/ui 0.0.47 → 0.0.48

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.
@@ -0,0 +1,331 @@
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";
10
+
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> = ({
160
+ data,
161
+ defaultExpandedId = [],
162
+ defaultCheckedId = [],
163
+ checkedId,
164
+ onCheckedChange,
165
+ defaultExpandAll = false,
166
+ defaultCheckAll = false,
167
+ hierarchicalCheck = false,
168
+ }) => {
169
+ const [checkedState, setCheckedState] = useState<Record<string, boolean>>({});
170
+ const [expandedState, setExpandedState] = useState<Record<string, boolean>>(
171
+ {}
172
+ );
173
+
174
+ const traverseTree = (
175
+ nodes: TreeData[],
176
+ callback: (node: TreeData) => void
177
+ ) => {
178
+ nodes.forEach((node) => {
179
+ callback(node);
180
+ if (node.children) {
181
+ traverseTree(node.children, callback);
182
+ }
183
+ });
184
+ };
185
+
186
+ useEffect(() => {
187
+ if (defaultExpandAll) {
188
+ const allExpanded: Record<string, boolean> = {};
189
+ traverseTree(data, (node) => {
190
+ allExpanded[node.id] = true;
191
+ });
192
+ setExpandedState(allExpanded);
193
+ } else {
194
+ const initialExpandedState = defaultExpandedId.reduce((acc, id) => {
195
+ acc[id] = true;
196
+ return acc;
197
+ }, {} as Record<string, boolean>);
198
+ setExpandedState(initialExpandedState);
199
+ }
200
+ }, [data, defaultExpandedId, defaultExpandAll]);
201
+
202
+ useEffect(() => {
203
+ if (defaultCheckAll) {
204
+ const allChecked: Record<string, boolean> = {};
205
+ traverseTree(data, (node) => {
206
+ allChecked[node.id] = true;
207
+ });
208
+ setCheckedState(allChecked);
209
+ } else if (!checkedId) {
210
+ const initialCheckedState = defaultCheckedId.reduce((acc, id) => {
211
+ acc[id] = true;
212
+ return acc;
213
+ }, {} as Record<string, boolean>);
214
+ setCheckedState(initialCheckedState);
215
+ }
216
+ }, [data, defaultCheckedId, checkedId, defaultCheckAll]);
217
+
218
+ const handleExpandChange = useCallback((id: string, expanded: boolean) => {
219
+ setExpandedState((prev) => ({ ...prev, [id]: expanded }));
220
+ }, []);
221
+
222
+ const handleCheckedChange = useCallback(
223
+ (id: string, checked: boolean) => {
224
+ let newState = { ...checkedState, [id]: checked };
225
+
226
+ if (hierarchicalCheck) {
227
+ const updateCheckedState = (
228
+ nodeId: string,
229
+ isChecked: boolean,
230
+ state: Record<string, boolean>
231
+ ) => {
232
+ state[nodeId] = isChecked;
233
+
234
+ // Update children recursively
235
+ const updateChildren = (parentId: string, isChecked: boolean) => {
236
+ traverseTree(data, (node) => {
237
+ if (node.id === parentId && node.children) {
238
+ node.children.forEach((child) => {
239
+ state[child.id] = isChecked;
240
+ updateChildren(child.id, isChecked);
241
+ });
242
+ }
243
+ });
244
+ };
245
+
246
+ // Update parents recursively
247
+ const updateParents = (
248
+ childId: string,
249
+ state: Record<string, boolean>
250
+ ) => {
251
+ traverseTree(data, (node) => {
252
+ if (node.children?.some((child) => child.id === childId)) {
253
+ const allChildrenChecked = node.children.every(
254
+ (child) => state[child.id]
255
+ );
256
+ state[node.id] = allChildrenChecked;
257
+ updateParents(node.id, state);
258
+ }
259
+ });
260
+ };
261
+
262
+ updateChildren(nodeId, isChecked);
263
+ updateParents(nodeId, state);
264
+
265
+ return state;
266
+ };
267
+
268
+ newState = updateCheckedState(id, checked, newState);
269
+ }
270
+
271
+ setCheckedState(newState);
272
+
273
+ if (onCheckedChange) {
274
+ const checkedIds = Object.keys(newState).filter((key) => newState[key]);
275
+ onCheckedChange(checkedIds);
276
+ }
277
+ },
278
+ [checkedState, data, onCheckedChange, hierarchicalCheck]
279
+ );
280
+
281
+ const checkIsExpanded = useCallback(
282
+ (id: string) => !!expandedState[id],
283
+ [expandedState]
284
+ );
285
+
286
+ const checkIsChecked = useCallback(
287
+ (id: string) => {
288
+ if (checkedId) {
289
+ return checkedId.includes(id);
290
+ }
291
+ return !!checkedState[id];
292
+ },
293
+ [checkedId, checkedState]
294
+ );
295
+
296
+ return (
297
+ <div className="w-full">
298
+ {data.map((item, idx) => (
299
+ <TreeItem
300
+ key={item.id}
301
+ {...item}
302
+ isFirstLevel
303
+ isLastItem={idx === data.length - 1}
304
+ checkIsExpanded={checkIsExpanded}
305
+ checkIsChecked={checkIsChecked}
306
+ onExpandChange={handleExpandChange}
307
+ onCheckedChange={handleCheckedChange}
308
+ />
309
+ ))}
310
+ </div>
311
+ );
312
+ };
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
+ */
@@ -12,16 +12,16 @@
12
12
  --input-color-error: #ed2f15;
13
13
 
14
14
  /* Function button */
15
- --function-default-solid: #1e3249;
16
- --function-default-hover: #35475b;
17
- --function-default-hover-bg: rgba(30 50 73 / 0.08);
18
- --function-default-stroke: rgba(30 50 73 / 0.48);
15
+ --function-default-solid: var(--state-color-primary-default);
16
+ --function-default-hover: var(--state-color-primary-hover);
17
+ --function-default-hover-bg: var(--state-color-primary-hover-bg);
18
+ --function-default-stroke: var(--state-color-primary-stroke);
19
19
  --function-default-icon: #ffffff;
20
- --function-default-outline-icon: #1e3249;
21
- --function-active-solid: #9b8f00;
22
- --function-active-hover: #b1a400;
23
- --function-active-hover-bg: rgba(221 205 0 / 0.08);
24
- --function-active-stroke: rgba(177 164 0 / 0.48);
20
+ --function-default-outline-icon: var(--state-color-primary-default);
21
+ --function-active-solid: var(--state-color-secondary-default);
22
+ --function-active-hover: var(--state-color-secondary-hover);
23
+ --function-active-hover-bg: var(--state-color-secondary-hover-bg);
24
+ --function-active-stroke: var(--state-color-secondary-stroke);
25
25
  --function-active-icon: #ffffff;
26
26
 
27
27
  --text-black: #000000;
@@ -49,7 +49,7 @@
49
49
  --foreground: var(--common-black);
50
50
 
51
51
  --primary: var(--primary-ramps-primary-100);
52
- --secondary: var(--secondary-ramps-secondary-100);
52
+ --primary: var(--secondary-ramps-secondary-100);
53
53
  --tertiary: var(--tertiary-ramps-tertiary-100);
54
54
  --info: var(--info-info-100);
55
55
  --success: var(--success-success-100);
@@ -6,3 +6,4 @@
6
6
  @import url(components/action-button.css);
7
7
  @import url(components/loading.css);
8
8
  @import url(components/dropdown-menu.css);
9
+ @import url(components/switch.css);
@@ -0,0 +1,30 @@
1
+ :root[data-theme="xspector"] {
2
+ /* ------------------------------------------------------------------ */
3
+ /* Switch Component Tokens */
4
+ /* ------------------------------------------------------------------ */
5
+ /* Naming Convention: --[component]-[element]-[state]-[property] */
6
+ /* Element: [progress, track] */
7
+ /* ------------------------------------------------------------------ */
8
+
9
+ /* Default State */
10
+ --switch-default-color: rgb(from var(--state-color-secondary-active) r g b / 0.32);
11
+ --switch-thumb-default-color: var(--state-color-secondary-active);
12
+
13
+ /* Hover State */
14
+ --switch-hover-color: rgb(from var(--state-color-secondary-active) r g b / 0.48);
15
+ --switch-thumb-hover-color: var(--switch-thumb-default-color);
16
+ --switch-thumb-hover-ring: var(--state-color-secondary-hover-bg);
17
+
18
+ /* Active State */
19
+ --switch-active-color: rgb(from var(--state-color-primary-active) r g b / 0.32);
20
+ --switch-thumb-active-color: var(--state-color-primary-active);
21
+
22
+ /* Active Hover State */
23
+ --switch-active-hover-color: rgb(from var(--state-color-primary-active) r g b / 0.48);
24
+ --switch-thumb-active-hover-color: var(--switch-thumb-active-color);
25
+ --switch-thumb-active-hover-ring: var(--state-color-primary-hover-bg);
26
+
27
+ /* Disabled State */
28
+ --switch-disabled-color: rgb(from var(--state-color-disable-solid) r g b / 0.32);
29
+ --switch-thumb-disabled-color: var(--state-color-disable-solid)
30
+ }