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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pimelon-ui",
3
- "version": "0.1.67",
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.5",
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
- <textarea
3
- :placeholder="placeholder"
4
- :class="inputClasses"
5
- :disabled="disabled"
6
- :id="id"
7
- :value="modelValue"
8
- :rows="rows"
9
- @input="handleChange"
10
- @change="handleChange"
11
- v-bind="attrs"
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
  }
@@ -47,7 +47,7 @@ const delayDuration = computed(() => props.hoverDelay * 1000)
47
47
  v-if="props.text || $slots.body"
48
48
  :side="props.placement"
49
49
  :side-offset="4"
50
- class="z-10"
50
+ class="z-[100]"
51
51
  >
52
52
  <slot name="body">
53
53
  <div
@@ -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'