tharaday 0.5.11 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/List/List.d.ts +5 -0
- package/dist/components/List/List.stories.d.ts +25 -0
- package/dist/components/List/List.types.d.ts +42 -0
- package/dist/components/List/ListItem.d.ts +2 -0
- package/dist/components/List/ListItem.types.d.ts +9 -0
- package/dist/components/Slider/Slider.d.ts +1 -1
- package/dist/components/Slider/Slider.stories.d.ts +2 -1
- package/dist/components/Slider/Slider.types.d.ts +1 -0
- package/dist/components/Tree/Tree.d.ts +5 -0
- package/dist/components/Tree/Tree.stories.d.ts +20 -0
- package/dist/components/Tree/Tree.types.d.ts +14 -0
- package/dist/components/Tree/TreeItem.d.ts +2 -0
- package/dist/components/Tree/TreeItem.types.d.ts +15 -0
- package/dist/ds.css +1 -1
- package/dist/ds.js +1557 -1254
- package/dist/ds.umd.cjs +1 -1
- package/dist/index.d.ts +8 -0
- package/package.json +3 -3
- package/src/components/List/List.module.css +85 -0
- package/src/components/List/List.stories.tsx +92 -0
- package/src/components/List/List.tsx +93 -0
- package/src/components/List/List.types.ts +45 -0
- package/src/components/List/ListItem.module.css +12 -0
- package/src/components/List/ListItem.tsx +12 -0
- package/src/components/List/ListItem.types.ts +10 -0
- package/src/components/Slider/Slider.module.css +19 -0
- package/src/components/Slider/Slider.stories.tsx +19 -0
- package/src/components/Slider/Slider.tsx +101 -2
- package/src/components/Slider/Slider.types.ts +1 -0
- package/src/components/Tree/Tree.module.css +5 -0
- package/src/components/Tree/Tree.stories.tsx +113 -0
- package/src/components/Tree/Tree.tsx +27 -0
- package/src/components/Tree/Tree.types.ts +16 -0
- package/src/components/Tree/TreeItem.module.css +63 -0
- package/src/components/Tree/TreeItem.tsx +146 -0
- package/src/components/Tree/TreeItem.types.ts +16 -0
- package/src/index.ts +8 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
import styles from './List.module.css';
|
|
5
|
+
import type { ListProps } from './List.types';
|
|
6
|
+
import { getSpacingStyles } from '../Box/helpers/getSpacingStyles';
|
|
7
|
+
import { ListItem } from './ListItem';
|
|
8
|
+
|
|
9
|
+
export const List = ({
|
|
10
|
+
children,
|
|
11
|
+
variant = 'unordered',
|
|
12
|
+
spacing = 0,
|
|
13
|
+
className,
|
|
14
|
+
margin,
|
|
15
|
+
marginX,
|
|
16
|
+
marginY,
|
|
17
|
+
marginTop,
|
|
18
|
+
marginBottom,
|
|
19
|
+
marginLeft,
|
|
20
|
+
marginRight,
|
|
21
|
+
padding,
|
|
22
|
+
paddingX,
|
|
23
|
+
paddingY,
|
|
24
|
+
paddingTop,
|
|
25
|
+
paddingBottom,
|
|
26
|
+
paddingLeft,
|
|
27
|
+
paddingRight,
|
|
28
|
+
style,
|
|
29
|
+
...props
|
|
30
|
+
}: ListProps) => {
|
|
31
|
+
const Component = variant === 'ordered' ? 'ol' : 'ul';
|
|
32
|
+
|
|
33
|
+
const listStyles: CSSProperties = {
|
|
34
|
+
...style,
|
|
35
|
+
...getSpacingStyles(
|
|
36
|
+
'padding',
|
|
37
|
+
padding,
|
|
38
|
+
paddingX,
|
|
39
|
+
paddingY,
|
|
40
|
+
paddingTop,
|
|
41
|
+
paddingBottom,
|
|
42
|
+
paddingLeft,
|
|
43
|
+
paddingRight
|
|
44
|
+
),
|
|
45
|
+
...getSpacingStyles(
|
|
46
|
+
'margin',
|
|
47
|
+
margin,
|
|
48
|
+
marginX,
|
|
49
|
+
marginY,
|
|
50
|
+
marginTop,
|
|
51
|
+
marginBottom,
|
|
52
|
+
marginLeft,
|
|
53
|
+
marginRight
|
|
54
|
+
),
|
|
55
|
+
'--list-spacing': typeof spacing === 'number' ? `${spacing * 0.25}rem` : spacing,
|
|
56
|
+
} as CSSProperties;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Component
|
|
60
|
+
className={clsx(
|
|
61
|
+
styles.root,
|
|
62
|
+
styles[variant],
|
|
63
|
+
typeof spacing === 'number' && styles[`gap-${spacing}`],
|
|
64
|
+
// If it's 'none' variant or has an icon, we might want to use flex to handle alignment
|
|
65
|
+
(variant === 'none' ||
|
|
66
|
+
(typeof spacing === 'number' && spacing > 0) ||
|
|
67
|
+
typeof spacing === 'string') &&
|
|
68
|
+
styles.flex,
|
|
69
|
+
typeof padding === 'number' && styles[`p-${padding}`],
|
|
70
|
+
typeof paddingX === 'number' && styles[`px-${paddingX}`],
|
|
71
|
+
typeof paddingY === 'number' && styles[`py-${paddingY}`],
|
|
72
|
+
typeof paddingTop === 'number' && styles[`pt-${paddingTop}`],
|
|
73
|
+
typeof paddingBottom === 'number' && styles[`pb-${paddingBottom}`],
|
|
74
|
+
typeof paddingLeft === 'number' && styles[`pl-${paddingLeft}`],
|
|
75
|
+
typeof paddingRight === 'number' && styles[`pr-${paddingRight}`],
|
|
76
|
+
typeof margin === 'number' && styles[`margin-${margin}`],
|
|
77
|
+
typeof marginX === 'number' && styles[`marginX-${marginX}`],
|
|
78
|
+
typeof marginY === 'number' && styles[`marginY-${marginY}`],
|
|
79
|
+
typeof marginTop === 'number' && styles[`marginTop-${marginTop}`],
|
|
80
|
+
typeof marginBottom === 'number' && styles[`marginBottom-${marginBottom}`],
|
|
81
|
+
typeof marginLeft === 'number' && styles[`marginLeft-${marginLeft}`],
|
|
82
|
+
typeof marginRight === 'number' && styles[`marginRight-${marginRight}`],
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
style={listStyles}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</Component>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
List.Item = ListItem;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReactNode, HTMLAttributes } from 'react';
|
|
2
|
+
import type { BoxPadding, BoxMargin } from '../Box/Box.types';
|
|
3
|
+
|
|
4
|
+
export type ListVariant = 'unordered' | 'ordered' | 'none';
|
|
5
|
+
|
|
6
|
+
export interface ListProps extends HTMLAttributes<HTMLUListElement | HTMLOListElement> {
|
|
7
|
+
/** The content of the list, usually ListItem components */
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** The visual variant of the list */
|
|
10
|
+
variant?: ListVariant;
|
|
11
|
+
/** Spacing between list items */
|
|
12
|
+
spacing?: BoxPadding;
|
|
13
|
+
/** Additional class name */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Margin for the entire list */
|
|
16
|
+
margin?: BoxMargin;
|
|
17
|
+
/** Horizontal margin */
|
|
18
|
+
marginX?: BoxMargin;
|
|
19
|
+
/** Vertical margin */
|
|
20
|
+
marginY?: BoxMargin;
|
|
21
|
+
/** Top margin */
|
|
22
|
+
marginTop?: BoxMargin;
|
|
23
|
+
/** Bottom margin */
|
|
24
|
+
marginBottom?: BoxMargin;
|
|
25
|
+
/** Left margin */
|
|
26
|
+
marginLeft?: BoxMargin;
|
|
27
|
+
/** Right margin */
|
|
28
|
+
marginRight?: BoxMargin;
|
|
29
|
+
/** Padding for the entire list */
|
|
30
|
+
padding?: BoxPadding;
|
|
31
|
+
/** Horizontal padding */
|
|
32
|
+
paddingX?: BoxPadding;
|
|
33
|
+
/** Vertical padding */
|
|
34
|
+
paddingY?: BoxPadding;
|
|
35
|
+
/** Top padding */
|
|
36
|
+
paddingTop?: BoxPadding;
|
|
37
|
+
/** Bottom padding */
|
|
38
|
+
paddingBottom?: BoxPadding;
|
|
39
|
+
/** Left padding */
|
|
40
|
+
paddingLeft?: BoxPadding;
|
|
41
|
+
/** Right padding */
|
|
42
|
+
paddingRight?: BoxPadding;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export * from './ListItem.types';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import type { ListItemProps } from './ListItem.types';
|
|
3
|
+
import styles from './ListItem.module.css';
|
|
4
|
+
|
|
5
|
+
export const ListItem = ({ children, icon, className, ...props }: ListItemProps) => {
|
|
6
|
+
return (
|
|
7
|
+
<li className={clsx(styles.item, className)} {...props}>
|
|
8
|
+
{icon && <span className={styles.iconWrapper}>{icon}</span>}
|
|
9
|
+
<div className={styles.content}>{children}</div>
|
|
10
|
+
</li>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode, HTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ListItemProps extends HTMLAttributes<HTMLLIElement> {
|
|
4
|
+
/** The content of the list item */
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Additional class name */
|
|
7
|
+
className?: string;
|
|
8
|
+
/** Optional icon or element to display before the content */
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
}
|
|
@@ -175,3 +175,22 @@
|
|
|
175
175
|
font-size: var(--ds-font-size-xs);
|
|
176
176
|
color: var(--ds-text-2);
|
|
177
177
|
}
|
|
178
|
+
|
|
179
|
+
.inputsRow {
|
|
180
|
+
display: grid;
|
|
181
|
+
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: var(--ds-space-3);
|
|
184
|
+
width: 100%;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.singleInputRow {
|
|
188
|
+
grid-template-columns: minmax(0, 1fr);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.separator {
|
|
192
|
+
font-family: var(--ds-font-family-base);
|
|
193
|
+
font-size: var(--ds-font-size-sm);
|
|
194
|
+
color: var(--ds-text-2);
|
|
195
|
+
line-height: 1;
|
|
196
|
+
}
|
|
@@ -79,3 +79,22 @@ export const DualValue: Story = {
|
|
|
79
79
|
showValue: true,
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
|
+
|
|
83
|
+
export const WithInputs: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
min: 0,
|
|
86
|
+
max: 500,
|
|
87
|
+
step: 10,
|
|
88
|
+
defaultValue: [100, 350],
|
|
89
|
+
label: 'Budget range',
|
|
90
|
+
helperText: 'Drag the handles or type exact values.',
|
|
91
|
+
showValue: true,
|
|
92
|
+
showInputs: true,
|
|
93
|
+
fullWidth: true,
|
|
94
|
+
},
|
|
95
|
+
render: (args) => (
|
|
96
|
+
<Box width="420px">
|
|
97
|
+
<Slider {...args} />
|
|
98
|
+
</Box>
|
|
99
|
+
),
|
|
100
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { useId, useMemo, useState } from 'react';
|
|
3
3
|
|
|
4
|
+
import { Input } from '../Input/Input.tsx';
|
|
4
5
|
import styles from './Slider.module.css';
|
|
5
6
|
import type { SliderProps, SliderValue } from './Slider.types.ts';
|
|
6
7
|
|
|
@@ -15,6 +16,20 @@ const toNumber = (value: number | string | undefined, fallback: number): number
|
|
|
15
16
|
const clamp = (value: number, min: number, max: number): number =>
|
|
16
17
|
Math.min(Math.max(value, min), max);
|
|
17
18
|
|
|
19
|
+
const alignToStep = (
|
|
20
|
+
value: number,
|
|
21
|
+
min: number,
|
|
22
|
+
max: number,
|
|
23
|
+
step: number | 'any' | undefined
|
|
24
|
+
): number => {
|
|
25
|
+
if (step == null || step === 'any' || step <= 0) {
|
|
26
|
+
return clamp(value, min, max);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const steps = Math.round((value - min) / step);
|
|
30
|
+
return clamp(min + steps * step, min, max);
|
|
31
|
+
};
|
|
32
|
+
|
|
18
33
|
const normalizeToPair = (
|
|
19
34
|
value: SliderValue | undefined,
|
|
20
35
|
min: number,
|
|
@@ -36,6 +51,7 @@ export const Slider = ({
|
|
|
36
51
|
helperText,
|
|
37
52
|
fullWidth = false,
|
|
38
53
|
showValue = false,
|
|
54
|
+
showInputs = false,
|
|
39
55
|
className,
|
|
40
56
|
id,
|
|
41
57
|
value,
|
|
@@ -48,6 +64,8 @@ export const Slider = ({
|
|
|
48
64
|
const helperId = helperText ? `${componentId}-help` : undefined;
|
|
49
65
|
const min = toNumber(props.min as number | string | undefined, 0);
|
|
50
66
|
const max = toNumber(props.max as number | string | undefined, 100);
|
|
67
|
+
const step =
|
|
68
|
+
props.step === 'any' ? 'any' : toNumber(props.step as number | string | undefined, 1);
|
|
51
69
|
const isDual = Array.isArray(value) || Array.isArray(defaultValue);
|
|
52
70
|
|
|
53
71
|
const initialPair = useMemo(
|
|
@@ -69,6 +87,36 @@ export const Slider = ({
|
|
|
69
87
|
onValueChange?.(isDual ? nextPair : nextPair[0]);
|
|
70
88
|
};
|
|
71
89
|
|
|
90
|
+
const commitStartInput = (rawValue: string) => {
|
|
91
|
+
if (!rawValue.trim()) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parsed = Number(rawValue);
|
|
96
|
+
|
|
97
|
+
if (Number.isNaN(parsed)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const next = alignToStep(parsed, min, isDual ? endValue : max, step);
|
|
102
|
+
emitChange([next, endValue]);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const commitEndInput = (rawValue: string) => {
|
|
106
|
+
if (!rawValue.trim()) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parsed = Number(rawValue);
|
|
111
|
+
|
|
112
|
+
if (Number.isNaN(parsed)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const next = alignToStep(parsed, startValue, max, step);
|
|
117
|
+
emitChange([startValue, next]);
|
|
118
|
+
};
|
|
119
|
+
|
|
72
120
|
return (
|
|
73
121
|
<div className={clsx(styles.wrapper, fullWidth && styles.fullWidth, className)}>
|
|
74
122
|
{(label || showValue) && (
|
|
@@ -100,9 +148,10 @@ export const Slider = ({
|
|
|
100
148
|
{...props}
|
|
101
149
|
min={min}
|
|
102
150
|
max={max}
|
|
151
|
+
step={step}
|
|
103
152
|
value={startValue}
|
|
104
153
|
onChange={(event) => {
|
|
105
|
-
const next =
|
|
154
|
+
const next = alignToStep(toNumber(event.target.value, min), min, endValue, step);
|
|
106
155
|
emitChange([next, endValue]);
|
|
107
156
|
}}
|
|
108
157
|
/>
|
|
@@ -116,14 +165,64 @@ export const Slider = ({
|
|
|
116
165
|
{...props}
|
|
117
166
|
min={min}
|
|
118
167
|
max={max}
|
|
168
|
+
step={step}
|
|
119
169
|
value={endValue}
|
|
120
170
|
onChange={(event) => {
|
|
121
|
-
const next =
|
|
171
|
+
const next = alignToStep(toNumber(event.target.value, max), startValue, max, step);
|
|
122
172
|
emitChange([startValue, next]);
|
|
123
173
|
}}
|
|
124
174
|
/>
|
|
125
175
|
)}
|
|
126
176
|
</div>
|
|
177
|
+
{showInputs && (
|
|
178
|
+
<div className={clsx(styles.inputsRow, !isDual && styles.singleInputRow)}>
|
|
179
|
+
<Input
|
|
180
|
+
key={`slider-start-${startValue}-${endValue}`}
|
|
181
|
+
type="number"
|
|
182
|
+
inputMode="decimal"
|
|
183
|
+
size={size}
|
|
184
|
+
defaultValue={startValue}
|
|
185
|
+
min={min}
|
|
186
|
+
max={isDual ? endValue : max}
|
|
187
|
+
step={step}
|
|
188
|
+
aria-label={
|
|
189
|
+
label ? `${label} minimum input` : isDual ? 'Slider minimum input' : 'Slider input'
|
|
190
|
+
}
|
|
191
|
+
fullWidth
|
|
192
|
+
onBlur={(event) => {
|
|
193
|
+
commitStartInput(event.target.value);
|
|
194
|
+
}}
|
|
195
|
+
onKeyDown={(event) => {
|
|
196
|
+
if (event.key === 'Enter') {
|
|
197
|
+
commitStartInput(event.currentTarget.value);
|
|
198
|
+
}
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
{isDual && <span className={styles.separator}>-</span>}
|
|
202
|
+
{isDual && (
|
|
203
|
+
<Input
|
|
204
|
+
key={`slider-end-${startValue}-${endValue}`}
|
|
205
|
+
type="number"
|
|
206
|
+
inputMode="decimal"
|
|
207
|
+
size={size}
|
|
208
|
+
defaultValue={endValue}
|
|
209
|
+
min={startValue}
|
|
210
|
+
max={max}
|
|
211
|
+
step={step}
|
|
212
|
+
aria-label={label ? `${label} maximum input` : 'Slider maximum input'}
|
|
213
|
+
fullWidth
|
|
214
|
+
onBlur={(event) => {
|
|
215
|
+
commitEndInput(event.target.value);
|
|
216
|
+
}}
|
|
217
|
+
onKeyDown={(event) => {
|
|
218
|
+
if (event.key === 'Enter') {
|
|
219
|
+
commitEndInput(event.currentTarget.value);
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
127
226
|
{helperText && (
|
|
128
227
|
<span id={helperId} className={styles.helperText}>
|
|
129
228
|
{helperText}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Tree } from './Tree';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/Tree',
|
|
6
|
+
component: Tree,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
} satisfies Meta<typeof Tree>;
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const Simple: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
data: {
|
|
16
|
+
name: 'John Doe',
|
|
17
|
+
age: 30,
|
|
18
|
+
city: 'New York',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Nested: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
data: {
|
|
26
|
+
user: {
|
|
27
|
+
id: 1,
|
|
28
|
+
profile: {
|
|
29
|
+
firstName: 'Jane',
|
|
30
|
+
lastName: 'Smith',
|
|
31
|
+
hobbies: ['reading', 'coding', 'hiking'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
status: 'active',
|
|
35
|
+
lastLogin: null,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const ArrayOnly: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
data: [
|
|
43
|
+
{ id: 1, text: 'Item 1' },
|
|
44
|
+
{ id: 2, text: 'Item 2', children: [1, 2, 3] },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Primitive: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
data: 'Hello World',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const NullValue: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
data: null,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Collapsed: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
data: {
|
|
64
|
+
nested: {
|
|
65
|
+
deeply: {
|
|
66
|
+
data: 'hidden',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
array: [1, 2, 3],
|
|
70
|
+
},
|
|
71
|
+
defaultExpanded: false,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const ExpandedByDefault: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
data: {
|
|
78
|
+
nested: {
|
|
79
|
+
deeply: {
|
|
80
|
+
data: 'visible',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
defaultExpanded: true,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const CustomIcons: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
data: {
|
|
91
|
+
folder1: {
|
|
92
|
+
file1: 'content',
|
|
93
|
+
},
|
|
94
|
+
folder2: {
|
|
95
|
+
file2: 'content',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
expandIcon: '+',
|
|
99
|
+
collapseIcon: '-',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const EmptyStructures: Story = {
|
|
104
|
+
args: {
|
|
105
|
+
data: {
|
|
106
|
+
emptyObj: {},
|
|
107
|
+
emptyArray: [],
|
|
108
|
+
nullValue: null,
|
|
109
|
+
undefinedValue: undefined,
|
|
110
|
+
notEmpty: [1],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import type { TreeProps } from './Tree.types';
|
|
3
|
+
import styles from './Tree.module.css';
|
|
4
|
+
import { TreeItem } from './TreeItem';
|
|
5
|
+
|
|
6
|
+
export const Tree = ({
|
|
7
|
+
data,
|
|
8
|
+
className,
|
|
9
|
+
defaultExpanded,
|
|
10
|
+
expandIcon,
|
|
11
|
+
collapseIcon,
|
|
12
|
+
...props
|
|
13
|
+
}: TreeProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<div className={clsx(styles.root, className)} {...props}>
|
|
16
|
+
<TreeItem
|
|
17
|
+
data={data}
|
|
18
|
+
defaultExpanded={defaultExpanded}
|
|
19
|
+
expandIcon={expandIcon}
|
|
20
|
+
collapseIcon={collapseIcon}
|
|
21
|
+
isRoot
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
Tree.Item = TreeItem;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface TreeProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** The data to be displayed in the tree */
|
|
5
|
+
data: unknown;
|
|
6
|
+
/** Additional class name */
|
|
7
|
+
className?: string;
|
|
8
|
+
/** Whether the tree items should be expanded by default */
|
|
9
|
+
defaultExpanded?: boolean;
|
|
10
|
+
/** Custom icon for expanded state */
|
|
11
|
+
expandIcon?: ReactNode;
|
|
12
|
+
/** Custom icon for collapsed state */
|
|
13
|
+
collapseIcon?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export * from './TreeItem.types';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
.list {
|
|
2
|
+
list-style: none;
|
|
3
|
+
padding-left: var(--ds-space-4);
|
|
4
|
+
margin: 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.item {
|
|
8
|
+
margin: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.itemHeader {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
min-height: var(--ds-space-6);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.toggleButton {
|
|
18
|
+
background: none;
|
|
19
|
+
border: none;
|
|
20
|
+
padding: 0;
|
|
21
|
+
margin: 0;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
color: var(--ds-text-2);
|
|
27
|
+
width: var(--ds-space-5);
|
|
28
|
+
height: var(--ds-space-5);
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
transition: transform var(--ds-transition-fast);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.toggleButton:hover {
|
|
34
|
+
color: var(--ds-text-1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.key {
|
|
38
|
+
font-weight: var(--ds-font-weight-medium);
|
|
39
|
+
color: var(--ds-text-2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.value {
|
|
43
|
+
color: var(--ds-text-1);
|
|
44
|
+
margin-left: var(--ds-space-1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.empty {
|
|
48
|
+
font-style: italic;
|
|
49
|
+
color: var(--ds-text-disabled);
|
|
50
|
+
margin-left: var(--ds-space-1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.collapsibleContent {
|
|
54
|
+
display: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.expanded {
|
|
58
|
+
display: block;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.rootList {
|
|
62
|
+
padding-left: 0;
|
|
63
|
+
}
|