pimelon-ui 0.1.67 → 0.1.69
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/package.json +2 -2
- package/src/components/CircularProgressBar.story.md +43 -0
- package/src/components/CircularProgressBar.story.vue +62 -0
- package/src/components/CircularProgressBar.vue +190 -0
- package/src/components/Textarea.story.vue +25 -0
- package/src/components/Textarea.vue +29 -11
- package/src/components/Tooltip/Tooltip.vue +1 -1
- package/src/components/Tree/Tree.story.md +56 -0
- package/src/components/Tree/Tree.story.vue +73 -0
- package/src/components/Tree/Tree.vue +124 -0
- package/src/components/types/Tree.ts +12 -0
- package/src/index.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pimelon-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"autoprefixer": "^10.4.13",
|
|
67
67
|
"cross-fetch": "^3.1.5",
|
|
68
68
|
"histoire": "^0.17.14",
|
|
69
|
-
"husky": "^9.1.
|
|
69
|
+
"husky": "^9.1.6",
|
|
70
70
|
"lint-staged": ">=10",
|
|
71
71
|
"postcss": "^8.4.21",
|
|
72
72
|
"prettier": "3.3.2",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
## Props
|
|
2
|
+
|
|
3
|
+
### step
|
|
4
|
+
|
|
5
|
+
The current step of the progress bar. It is required.
|
|
6
|
+
|
|
7
|
+
### totalSteps
|
|
8
|
+
|
|
9
|
+
The total number of steps in the progress bar. Default value is 100. It is
|
|
10
|
+
required.
|
|
11
|
+
|
|
12
|
+
### showPercentage
|
|
13
|
+
|
|
14
|
+
If true, the percentage of the progress will be shown in the center of the
|
|
15
|
+
circle. Else, the absolute value of the current step will be shown. Default
|
|
16
|
+
value is false.
|
|
17
|
+
|
|
18
|
+
### size
|
|
19
|
+
|
|
20
|
+
The size of the progress bar. Default value is 'md'. Available options are 'xs',
|
|
21
|
+
'sm', 'md', 'lg', 'xl'.
|
|
22
|
+
|
|
23
|
+
### theme
|
|
24
|
+
|
|
25
|
+
The theme of the progress bar. Default value is 'black'. Available options are
|
|
26
|
+
'black', 'red', 'green', 'blue', 'orange'.
|
|
27
|
+
|
|
28
|
+
If a string is passed, the predefined theme will be used, and if the color does
|
|
29
|
+
not match any predefined theme, the default theme will be used. If a custom
|
|
30
|
+
theme is needed, an object with primary and secondary colors can be passed.
|
|
31
|
+
|
|
32
|
+
### themeComplete
|
|
33
|
+
|
|
34
|
+
The color of the completed progress. Default value is #76f7be (light green).
|
|
35
|
+
|
|
36
|
+
### variant
|
|
37
|
+
|
|
38
|
+
The variant of the progress bar. Default value is 'solid'. Available options are
|
|
39
|
+
'solid', 'outline'.
|
|
40
|
+
|
|
41
|
+
When the variant is 'solid', the progress bar on complete will be filled with
|
|
42
|
+
the progress color. When the variant is 'outline', the progress bar on complete
|
|
43
|
+
will be an outline with the progress color.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Story :layout="{ type: 'grid', width: 500, heigt: 500 }">
|
|
3
|
+
<Variant title="Default">
|
|
4
|
+
<div class="p-2 w-full h-full">
|
|
5
|
+
<CircularProgressBar :step="1" :totalSteps="4" />
|
|
6
|
+
</div>
|
|
7
|
+
</Variant>
|
|
8
|
+
<Variant title="Size">
|
|
9
|
+
<div class="p-2 w-full h-full">
|
|
10
|
+
<CircularProgressBar
|
|
11
|
+
:step="1"
|
|
12
|
+
:totalSteps="4"
|
|
13
|
+
size="lg"
|
|
14
|
+
:showPercentage="true"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
</Variant>
|
|
18
|
+
<Variant title="Theme">
|
|
19
|
+
<div class="p-2 w-full h-full">
|
|
20
|
+
<CircularProgressBar :step="3" :totalSteps="4" theme="orange" />
|
|
21
|
+
</div>
|
|
22
|
+
</Variant>
|
|
23
|
+
<Variant title="Custom Theme">
|
|
24
|
+
<div class="p-2 w-full h-full">
|
|
25
|
+
<CircularProgressBar
|
|
26
|
+
:step="2"
|
|
27
|
+
:totalSteps="6"
|
|
28
|
+
:theme="{
|
|
29
|
+
primary: '#2376f5',
|
|
30
|
+
secondary: '#ddd5d5',
|
|
31
|
+
}"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
</Variant>
|
|
35
|
+
<Variant title="Solid Variant">
|
|
36
|
+
<div class="p-2 w-full h-full">
|
|
37
|
+
<CircularProgressBar
|
|
38
|
+
:step="9"
|
|
39
|
+
:totalSteps="9"
|
|
40
|
+
variant="solid"
|
|
41
|
+
themeComplete="lightgreen"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
</Variant>
|
|
45
|
+
<Variant title="Outline Variant">
|
|
46
|
+
<div class="p-2 w-full h-full">
|
|
47
|
+
<CircularProgressBar
|
|
48
|
+
:step="9"
|
|
49
|
+
:totalSteps="9"
|
|
50
|
+
variant="outline"
|
|
51
|
+
themeComplete="lightgreen"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</Variant>
|
|
55
|
+
</Story>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup>
|
|
59
|
+
import CircularProgressBar from './CircularProgressBar.vue'
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<style></style>
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="progressbar"
|
|
4
|
+
role="progressbar"
|
|
5
|
+
:class="{
|
|
6
|
+
completed: isCompleted,
|
|
7
|
+
fillOuter: variant === 'outline',
|
|
8
|
+
}"
|
|
9
|
+
>
|
|
10
|
+
<div v-if="!isCompleted">
|
|
11
|
+
<p v-if="!showPercentage">{{ step }}</p>
|
|
12
|
+
<p v-else>{{ progress.toFixed(0) }}%</p>
|
|
13
|
+
</div>
|
|
14
|
+
<div v-else class="check-icon" />
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { computed } from 'vue'
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
step: number
|
|
23
|
+
totalSteps: number
|
|
24
|
+
showPercentage?: boolean
|
|
25
|
+
variant?: Variant
|
|
26
|
+
theme?: string | ThemeProps
|
|
27
|
+
size?: Size
|
|
28
|
+
themeComplete?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
32
|
+
step: 1,
|
|
33
|
+
totalSteps: 4,
|
|
34
|
+
showPercentage: false,
|
|
35
|
+
theme: 'black',
|
|
36
|
+
size: 'md',
|
|
37
|
+
themeComplete: 'lightgreen',
|
|
38
|
+
variant: 'solid',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
type Variant = 'solid' | 'outline'
|
|
42
|
+
|
|
43
|
+
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
44
|
+
interface SizeProps {
|
|
45
|
+
ringSize: string
|
|
46
|
+
ringBarWidth: string
|
|
47
|
+
innerTextFontSize: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// predefined sizes for the circular progress bar
|
|
51
|
+
const sizeMap: Record<Size, SizeProps> = {
|
|
52
|
+
xs: {
|
|
53
|
+
ringSize: '30px',
|
|
54
|
+
ringBarWidth: '6px',
|
|
55
|
+
innerTextFontSize: props.showPercentage ? '8px' : '12px',
|
|
56
|
+
},
|
|
57
|
+
sm: {
|
|
58
|
+
ringSize: '42px',
|
|
59
|
+
ringBarWidth: '10px',
|
|
60
|
+
innerTextFontSize: props.showPercentage ? '12px' : '16px',
|
|
61
|
+
},
|
|
62
|
+
md: {
|
|
63
|
+
ringSize: '60px',
|
|
64
|
+
ringBarWidth: '14px',
|
|
65
|
+
innerTextFontSize: props.showPercentage ? '16px' : '20px',
|
|
66
|
+
},
|
|
67
|
+
lg: {
|
|
68
|
+
ringSize: '84px',
|
|
69
|
+
ringBarWidth: '18px',
|
|
70
|
+
innerTextFontSize: props.showPercentage ? '20px' : '24px',
|
|
71
|
+
},
|
|
72
|
+
xl: {
|
|
73
|
+
ringSize: '108px',
|
|
74
|
+
ringBarWidth: '22px',
|
|
75
|
+
innerTextFontSize: props.showPercentage ? '24px' : '28px',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const size = computed(() => sizeMap[props.size] || sizeMap['md'])
|
|
80
|
+
|
|
81
|
+
type Theme = 'black' | 'red' | 'green' | 'blue' | 'orange'
|
|
82
|
+
interface ThemeProps {
|
|
83
|
+
primary: string
|
|
84
|
+
secondary: string
|
|
85
|
+
}
|
|
86
|
+
// predefined themes for the circular progress bar
|
|
87
|
+
const themeMap: Record<Theme, ThemeProps> = {
|
|
88
|
+
black: {
|
|
89
|
+
primary: '#333',
|
|
90
|
+
secondary: '#888',
|
|
91
|
+
},
|
|
92
|
+
red: {
|
|
93
|
+
primary: '#FF0000',
|
|
94
|
+
secondary: '#FFD7D7',
|
|
95
|
+
},
|
|
96
|
+
green: {
|
|
97
|
+
primary: '#22C55E',
|
|
98
|
+
secondary: '#b1ffda',
|
|
99
|
+
},
|
|
100
|
+
blue: {
|
|
101
|
+
primary: '#2376f5',
|
|
102
|
+
secondary: '#D7D7FF',
|
|
103
|
+
},
|
|
104
|
+
orange: {
|
|
105
|
+
primary: '#FFA500',
|
|
106
|
+
secondary: '#FFE5CC',
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const theme = computed(() => {
|
|
111
|
+
if (typeof props.theme === 'string') {
|
|
112
|
+
return themeMap[props.theme as Theme] || themeMap['black']
|
|
113
|
+
}
|
|
114
|
+
return props.theme
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const progress = computed(() => (props.step / props.totalSteps) * 100)
|
|
118
|
+
const isCompleted = computed(() => props.step === props.totalSteps)
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<style scoped>
|
|
122
|
+
.progressbar {
|
|
123
|
+
--size: v-bind(size.ringSize);
|
|
124
|
+
--bar-width: v-bind(size.ringBarWidth);
|
|
125
|
+
--font-size: v-bind(size.innerTextFontSize);
|
|
126
|
+
--color-progress: v-bind(theme.primary);
|
|
127
|
+
--color-remaining-circle: v-bind(theme.secondary);
|
|
128
|
+
--color-complete: v-bind($props.themeComplete);
|
|
129
|
+
--progress: v-bind(progress + '%');
|
|
130
|
+
|
|
131
|
+
width: var(--size);
|
|
132
|
+
height: var(--size);
|
|
133
|
+
border-radius: 50%;
|
|
134
|
+
display: grid;
|
|
135
|
+
place-items: center;
|
|
136
|
+
|
|
137
|
+
position: relative;
|
|
138
|
+
font-size: var(--font-size);
|
|
139
|
+
}
|
|
140
|
+
@property --progress {
|
|
141
|
+
syntax: '<length-percentage>';
|
|
142
|
+
inherits: true;
|
|
143
|
+
initial-value: 0%;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.progressbar::before {
|
|
147
|
+
content: '';
|
|
148
|
+
position: absolute;
|
|
149
|
+
inset: 0;
|
|
150
|
+
border-radius: inherit;
|
|
151
|
+
background: conic-gradient(
|
|
152
|
+
var(--color-progress) var(--progress),
|
|
153
|
+
var(--color-remaining-circle) 0%
|
|
154
|
+
);
|
|
155
|
+
transition: --progress 500ms linear;
|
|
156
|
+
aspect-ratio: 1 / 1;
|
|
157
|
+
align-self: center;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.progressbar::after {
|
|
161
|
+
content: '';
|
|
162
|
+
position: absolute;
|
|
163
|
+
background: white;
|
|
164
|
+
border-radius: inherit;
|
|
165
|
+
z-index: 1;
|
|
166
|
+
width: calc(100% - var(--bar-width));
|
|
167
|
+
aspect-ratio: 1 / 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.progressbar > div {
|
|
171
|
+
z-index: 2;
|
|
172
|
+
position: relative;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.progressbar.completed:not(.fillOuter)::after {
|
|
176
|
+
background: var(--color-complete);
|
|
177
|
+
}
|
|
178
|
+
.progressbar.completed.fillOuter::before {
|
|
179
|
+
background: var(--color-complete);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.check-icon {
|
|
183
|
+
width: 15px;
|
|
184
|
+
height: 15px;
|
|
185
|
+
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODUiIGhlaWdodD0iODUiIHZpZXdCb3g9IjUgMzAgNzUgMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0zNS40MjM3IDUzLjczMjdMNjcuOTc4NyAyMS4xNzc3TDcyLjk4OTUgMjYuMTg0MkwzNS40MTk1IDYzLjc1TDEyLjg4NiA0MS4yMTIyTDE3Ljg5MjUgMzYuMjAxNUwzNS40MjM3IDUzLjczMjdaIiBmaWxsPSIjMWYxYTM4Ii8+Cjwvc3ZnPgo=');
|
|
186
|
+
background-size: contain;
|
|
187
|
+
background-repeat: no-repeat;
|
|
188
|
+
background-position: center;
|
|
189
|
+
}
|
|
190
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { reactive } from 'vue'
|
|
3
|
+
import Textarea from './Textarea.vue'
|
|
4
|
+
const state = reactive({
|
|
5
|
+
size: 'sm',
|
|
6
|
+
placeholder: 'Placeholder',
|
|
7
|
+
disabled: false,
|
|
8
|
+
modelValue: '',
|
|
9
|
+
label: 'Label',
|
|
10
|
+
})
|
|
11
|
+
const sizes = ['sm', 'md', 'lg', 'xl']
|
|
12
|
+
const variants = ['subtle', 'outline']
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<Story :layout="{ type: 'grid', width: 500 }">
|
|
17
|
+
<Variant v-for="type in variants" :key="type" :title="`${type} variant`">
|
|
18
|
+
<Textarea :variant="type" v-bind="state" />
|
|
19
|
+
</Variant>
|
|
20
|
+
|
|
21
|
+
<template #controls>
|
|
22
|
+
<HstSelect v-model="state.size" :options="sizes" title="Size" />
|
|
23
|
+
</template>
|
|
24
|
+
</Story>
|
|
25
|
+
</template>
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
<div class="space-y-1.5">
|
|
3
|
+
<label class="block" :class="labelClasses" v-if="label" :for="id">
|
|
4
|
+
{{ label }}
|
|
5
|
+
</label>
|
|
6
|
+
<textarea
|
|
7
|
+
:placeholder="placeholder"
|
|
8
|
+
:class="inputClasses"
|
|
9
|
+
:disabled="disabled"
|
|
10
|
+
:id="id"
|
|
11
|
+
:value="modelValue"
|
|
12
|
+
:rows="rows"
|
|
13
|
+
@input="handleChange"
|
|
14
|
+
@change="handleChange"
|
|
15
|
+
v-bind="attrs"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
13
18
|
</template>
|
|
14
19
|
|
|
15
20
|
<script setup lang="ts">
|
|
@@ -26,6 +31,7 @@ interface TextareaProps {
|
|
|
26
31
|
modelValue?: string
|
|
27
32
|
debounce?: number
|
|
28
33
|
rows?: number
|
|
34
|
+
label?: string
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const props = withDefaults(defineProps<TextareaProps>(), {
|
|
@@ -74,6 +80,18 @@ const inputClasses = computed(() => {
|
|
|
74
80
|
]
|
|
75
81
|
})
|
|
76
82
|
|
|
83
|
+
const labelClasses = computed(() => {
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
sm: 'text-xs',
|
|
87
|
+
md: 'text-base',
|
|
88
|
+
lg: 'text-lg',
|
|
89
|
+
xl: 'text-xl',
|
|
90
|
+
}[props.size],
|
|
91
|
+
'text-gray-600',
|
|
92
|
+
]
|
|
93
|
+
})
|
|
94
|
+
|
|
77
95
|
let emitChange = (value: string) => {
|
|
78
96
|
emit('update:modelValue', value)
|
|
79
97
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
## Props
|
|
2
|
+
|
|
3
|
+
#### Node
|
|
4
|
+
|
|
5
|
+
An object representing the Root node of the Tree. Each node must contain the
|
|
6
|
+
following properties:
|
|
7
|
+
|
|
8
|
+
- **label** (string) - Name of the node.
|
|
9
|
+
|
|
10
|
+
- **children** (Node[]) - An array of nodes representing the children of the
|
|
11
|
+
current node.
|
|
12
|
+
|
|
13
|
+
<br>
|
|
14
|
+
|
|
15
|
+
#### Options
|
|
16
|
+
|
|
17
|
+
- **rowHeight** (string) - Line height for the nodes passed. Defaults to `25px`.
|
|
18
|
+
|
|
19
|
+
- **indentWidth** (string) - Width for the indentation at each depth of the
|
|
20
|
+
tree. Gets incremented with every nested sub-tree. Defaults to `15px`.
|
|
21
|
+
|
|
22
|
+
- **showIndentationGuides** (boolean) - Flag for displaying LHS lines. Defaults
|
|
23
|
+
to `true`.
|
|
24
|
+
|
|
25
|
+
<br>
|
|
26
|
+
|
|
27
|
+
## Slots
|
|
28
|
+
|
|
29
|
+
#### 1. Custom Node
|
|
30
|
+
|
|
31
|
+
Slot to optionally override the template for the entire node. It exposes the
|
|
32
|
+
following slot props:
|
|
33
|
+
|
|
34
|
+
- **node** (object) - The current node containing label and children attributes.
|
|
35
|
+
|
|
36
|
+
- **hasChildren** (boolean) - Whether current node is a leaf node.
|
|
37
|
+
|
|
38
|
+
- **isCollapsed** (boolean) - Whether current node is collapsed.
|
|
39
|
+
|
|
40
|
+
- **toggleCollapsed** (function) - Function to expand or collapse the node.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
|
|
44
|
+
// Customising node to show needed Context Menu
|
|
45
|
+
<template #node="{ node, hasChildren, isCollapsed, toggleCollapsed }">
|
|
46
|
+
<div class="flex items-center" @click.right="showContextMenu(node)">
|
|
47
|
+
...
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<br>
|
|
52
|
+
|
|
53
|
+
#### 2. Custom Label / Icon
|
|
54
|
+
|
|
55
|
+
`label` and `icon` slots to quickly add custom styles without overriding the
|
|
56
|
+
entire node.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Story :layout="{ type: 'grid', width: 500 }">
|
|
3
|
+
<Variant title="default">
|
|
4
|
+
<Tree
|
|
5
|
+
:options="{
|
|
6
|
+
showIndentationGuides: state.showIndentationGuides,
|
|
7
|
+
rowHeight: state.rowHeight,
|
|
8
|
+
indentWidth: state.indentWidth,
|
|
9
|
+
}"
|
|
10
|
+
nodeKey="name"
|
|
11
|
+
:node="state.node"
|
|
12
|
+
/>
|
|
13
|
+
</Variant>
|
|
14
|
+
<template #controls>
|
|
15
|
+
<HstCheckbox
|
|
16
|
+
v-model="state.showIndentationGuides"
|
|
17
|
+
title="Show Indentation Guides"
|
|
18
|
+
/>
|
|
19
|
+
<HstText v-model="state.rowHeight" title="Row Height" />
|
|
20
|
+
<HstText v-model="state.indentWidth" title="Indent Width" />
|
|
21
|
+
</template>
|
|
22
|
+
</Story>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { reactive } from 'vue'
|
|
27
|
+
import Tree from './Tree.vue'
|
|
28
|
+
|
|
29
|
+
const state = reactive({
|
|
30
|
+
showIndentationGuides: true,
|
|
31
|
+
rowHeight: '25px',
|
|
32
|
+
indentWidth: '15px',
|
|
33
|
+
node: {
|
|
34
|
+
name: 'guest',
|
|
35
|
+
label: 'Guest',
|
|
36
|
+
children: [
|
|
37
|
+
{
|
|
38
|
+
name: 'downloads',
|
|
39
|
+
label: 'Downloads',
|
|
40
|
+
children: [
|
|
41
|
+
{
|
|
42
|
+
name: 'download.zip',
|
|
43
|
+
label: 'download.zip',
|
|
44
|
+
children: [
|
|
45
|
+
{
|
|
46
|
+
name: 'image.png',
|
|
47
|
+
label: 'image.png',
|
|
48
|
+
children: [],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'documents',
|
|
56
|
+
label: 'Documents',
|
|
57
|
+
children: [
|
|
58
|
+
{
|
|
59
|
+
name: 'somefile.txt',
|
|
60
|
+
label: 'somefile.txt',
|
|
61
|
+
children: [],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'somefile.pdf',
|
|
65
|
+
label: 'somefile.pdf',
|
|
66
|
+
children: [],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Current Tree Node -->
|
|
3
|
+
<slot
|
|
4
|
+
name="node"
|
|
5
|
+
v-bind="{ node, hasChildren, isCollapsed, toggleCollapsed }"
|
|
6
|
+
>
|
|
7
|
+
<div
|
|
8
|
+
class="flex items-center cursor-pointer gap-1"
|
|
9
|
+
:style="{ height: options.rowHeight }"
|
|
10
|
+
@click="toggleCollapsed"
|
|
11
|
+
>
|
|
12
|
+
<div ref="iconRef">
|
|
13
|
+
<!-- slot to only override the Icon -->
|
|
14
|
+
<slot name="icon" v-bind="{ hasChildren, isCollapsed }">
|
|
15
|
+
<FeatherIcon
|
|
16
|
+
v-if="hasChildren && !isCollapsed"
|
|
17
|
+
name="chevron-down"
|
|
18
|
+
class="h-3.5"
|
|
19
|
+
/>
|
|
20
|
+
<FeatherIcon
|
|
21
|
+
v-else-if="hasChildren"
|
|
22
|
+
name="chevron-right"
|
|
23
|
+
class="h-3.5"
|
|
24
|
+
/>
|
|
25
|
+
</slot>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- slot to only override the label -->
|
|
29
|
+
<slot name="label" v-bind="{ node, hasChildren, isCollapsed }">
|
|
30
|
+
<div class="text-base truncate" :class="hasChildren ? '' : 'pl-3.5'">
|
|
31
|
+
{{ node.label }}
|
|
32
|
+
</div>
|
|
33
|
+
</slot>
|
|
34
|
+
</div>
|
|
35
|
+
</slot>
|
|
36
|
+
|
|
37
|
+
<!-- Recursively render the children -->
|
|
38
|
+
<div v-if="hasChildren && !isCollapsed" class="flex">
|
|
39
|
+
<div
|
|
40
|
+
:style="{ paddingLeft: linePadding }"
|
|
41
|
+
class="border-r"
|
|
42
|
+
v-if="options.showIndentationGuides"
|
|
43
|
+
></div>
|
|
44
|
+
<ul class="w-full" :style="{ paddingLeft: options.indentWidth }">
|
|
45
|
+
<li v-for="child in node.children" :key="child[nodeKey] as string">
|
|
46
|
+
<Tree :node="child" :nodeKey="nodeKey" :options="options">
|
|
47
|
+
<!-- Pass the parent slots to the children of current node -->
|
|
48
|
+
<template #node="{ node, hasChildren, isCollapsed, toggleCollapsed }">
|
|
49
|
+
<slot
|
|
50
|
+
name="node"
|
|
51
|
+
v-bind="{ node, hasChildren, isCollapsed, toggleCollapsed }"
|
|
52
|
+
/>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<template #icon="{ hasChildren, isCollapsed }">
|
|
56
|
+
<slot name="icon" v-bind="{ hasChildren, isCollapsed }" />
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<template #label="{ node, hasChildren, isCollapsed }">
|
|
60
|
+
<slot name="label" v-bind="{ node, hasChildren, isCollapsed }" />
|
|
61
|
+
</template>
|
|
62
|
+
</Tree>
|
|
63
|
+
</li>
|
|
64
|
+
</ul>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref, computed, onMounted } from 'vue'
|
|
70
|
+
import FeatherIcon from '../FeatherIcon.vue'
|
|
71
|
+
import type { TreeNode, TreeOptions } from '../types/Tree'
|
|
72
|
+
|
|
73
|
+
const props = withDefaults(
|
|
74
|
+
defineProps<{
|
|
75
|
+
node: TreeNode
|
|
76
|
+
nodeKey: string
|
|
77
|
+
options?: TreeOptions
|
|
78
|
+
}>(),
|
|
79
|
+
{
|
|
80
|
+
options: () => ({
|
|
81
|
+
rowHeight: '25px',
|
|
82
|
+
indentWidth: '20px',
|
|
83
|
+
showIndentationGuides: true,
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const slots = defineSlots<{
|
|
89
|
+
node: {
|
|
90
|
+
node: TreeNode
|
|
91
|
+
hasChildren: boolean
|
|
92
|
+
isCollapsed: boolean
|
|
93
|
+
toggleCollapsed: (event: MouseEvent) => void
|
|
94
|
+
}
|
|
95
|
+
icon: {
|
|
96
|
+
hasChildren: boolean
|
|
97
|
+
isCollapsed: boolean
|
|
98
|
+
}
|
|
99
|
+
label: {
|
|
100
|
+
node: TreeNode
|
|
101
|
+
hasChildren: boolean
|
|
102
|
+
isCollapsed: boolean
|
|
103
|
+
}
|
|
104
|
+
}>()
|
|
105
|
+
|
|
106
|
+
const isCollapsed = ref(true)
|
|
107
|
+
|
|
108
|
+
const linePadding = ref('')
|
|
109
|
+
|
|
110
|
+
const hasChildren = computed(() => props.node.children?.length > 0)
|
|
111
|
+
|
|
112
|
+
const iconRef = ref<HTMLElement | null>(null)
|
|
113
|
+
|
|
114
|
+
const toggleCollapsed = (event: MouseEvent) => {
|
|
115
|
+
event.stopPropagation()
|
|
116
|
+
if (hasChildren.value) isCollapsed.value = !isCollapsed.value
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onMounted(() => {
|
|
120
|
+
if (iconRef.value?.clientWidth)
|
|
121
|
+
// Set the padding for the LHS line to align with the center of icon
|
|
122
|
+
linePadding.value = iconRef.value.clientWidth / 2 + 'px'
|
|
123
|
+
})
|
|
124
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type TreeNode = {
|
|
2
|
+
label: string
|
|
3
|
+
children: TreeNode[]
|
|
4
|
+
// added TreeNode[] due to enforcement that dynamic key types should accommodate all static key types
|
|
5
|
+
[nodeKey: string]: string | number | TreeNode[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type TreeOptions = {
|
|
9
|
+
rowHeight?: string
|
|
10
|
+
indentWidth?: string
|
|
11
|
+
showIndentationGuides?: boolean
|
|
12
|
+
}
|
package/src/index.js
CHANGED
|
@@ -42,6 +42,7 @@ export {
|
|
|
42
42
|
TextEditorContent,
|
|
43
43
|
} from './components/TextEditor'
|
|
44
44
|
export { default as ListView } from './components/ListView/ListView.vue'
|
|
45
|
+
export { default as List } from './components/ListView/ListView.vue'
|
|
45
46
|
export { default as ListHeader } from './components/ListView/ListHeader.vue'
|
|
46
47
|
export { default as ListHeaderItem } from './components/ListView/ListHeaderItem.vue'
|
|
47
48
|
export { default as ListEmptyState } from './components/ListView/ListEmptyState.vue'
|
|
@@ -61,6 +62,8 @@ export { default as CommandPaletteItem } from './components/CommandPalette/Comma
|
|
|
61
62
|
export { default as ListFilter } from './components/ListFilter/ListFilter.vue'
|
|
62
63
|
export { default as Calendar } from './components/Calendar/Calendar.vue'
|
|
63
64
|
export { default as NestedPopover } from './components/ListFilter/NestedPopover.vue'
|
|
65
|
+
export { default as CircularProgressBar } from './components/CircularProgressBar.vue'
|
|
66
|
+
export { default as Tree } from './components/Tree/Tree.vue'
|
|
64
67
|
|
|
65
68
|
// directives
|
|
66
69
|
export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
|