@dolanske/vui 0.1.0 → 0.1.2
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/README.md +7 -0
- package/dist/components/Accordion/Accordion.vue.d.ts +45 -0
- package/dist/components/Accordion/AccordionGroup.vue.d.ts +32 -0
- package/dist/components/Alert/Alert.vue.d.ts +29 -0
- package/dist/components/Avatar/Avatar.vue.d.ts +9 -0
- package/dist/components/Badge/Badge.vue.d.ts +21 -0
- package/dist/components/Breadcrumbs/BreadcrumbItem.vue.d.ts +21 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.vue.d.ts +27 -0
- package/dist/components/Button/Button.vue.d.ts +41 -0
- package/dist/components/ButtonGroup/ButtonGroup.vue.d.ts +19 -0
- package/dist/components/Calendar/Calendar.vue.d.ts +27 -0
- package/dist/components/Card/Card.vue.d.ts +25 -0
- package/dist/components/Checkbox/Checkbox.vue.d.ts +31 -0
- package/dist/components/CopyClipboard/CopyClipboard.vue.d.ts +40 -0
- package/dist/components/Divider/Divider.vue.d.ts +24 -0
- package/dist/components/Drawer/Drawer.vue.d.ts +52 -0
- package/dist/components/Dropdown/DropdownItem.vue.d.ts +21 -0
- package/dist/components/Dropdown/DropdownTitle.vue.d.ts +17 -0
- package/dist/components/Flex/Flex.vue.d.ts +38 -0
- package/dist/components/Grid/Grid.vue.d.ts +27 -0
- package/dist/components/Input/Counter.vue.d.ts +19 -0
- package/dist/components/Input/Dropzone.vue.d.ts +107 -0
- package/dist/components/Input/File.vue.d.ts +7 -0
- package/dist/components/Input/Input.vue.d.ts +54 -0
- package/dist/components/Input/Password.vue.d.ts +6 -0
- package/dist/components/Input/Textarea.vue.d.ts +30 -0
- package/dist/components/Kbd/Kbd.vue.d.ts +23 -0
- package/dist/components/Kbd/KbdGroup.vue.d.ts +31 -0
- package/dist/components/Modal/Confirm.vue.d.ts +45 -0
- package/dist/components/Modal/Modal.vue.d.ts +55 -0
- package/dist/components/Pagination/Pagination.vue.d.ts +42 -0
- package/dist/components/Pagination/pagination.d.ts +12 -0
- package/dist/components/Popout/Popout.vue.d.ts +34 -0
- package/dist/components/Progress/Progress.vue.d.ts +31 -0
- package/dist/components/Radio/Radio.vue.d.ts +27 -0
- package/dist/components/Radio/RadioGroup.vue.d.ts +40 -0
- package/dist/components/Select/Select.vue.d.ts +37 -0
- package/dist/components/Sheet/Sheet.vue.d.ts +35 -0
- package/dist/components/Skeleton/Skeleton.vue.d.ts +8 -0
- package/dist/components/Spinner/Spinner.vue.d.ts +6 -0
- package/dist/components/Switch/Switch.vue.d.ts +26 -0
- package/dist/components/Table/Cell.vue.d.ts +19 -0
- package/dist/components/Table/Header.vue.d.ts +29 -0
- package/dist/components/Table/Row.vue.d.ts +16 -0
- package/dist/components/Table/SelectAll.vue.d.ts +2 -0
- package/dist/components/Table/SelectRow.vue.d.ts +6 -0
- package/dist/components/Table/Table.vue.d.ts +40 -0
- package/dist/components/Table/table.d.ts +68 -0
- package/dist/components/Tabs/Tab.vue.d.ts +8 -0
- package/dist/components/Tabs/Tabs.vue.d.ts +43 -0
- package/dist/components/Toast/Toasts.vue.d.ts +2 -0
- package/dist/components/Toast/toast.d.ts +42 -0
- package/dist/components/Tooltip/Tooltip.vue.d.ts +32 -0
- package/dist/index.d.ts +54 -1
- package/dist/internal/Backdrop/Backdrop.vue.d.ts +20 -0
- package/dist/shared/composables.d.ts +3 -0
- package/dist/shared/helpers.d.ts +16 -0
- package/dist/shared/types.d.ts +10 -0
- package/dist/style.css +1 -1
- package/dist/vui.js +199 -214
- package/package.json +11 -9
- package/src/App.vue +162 -0
- package/src/components/Accordion/Accordion.vue +75 -0
- package/src/components/Accordion/AccordionGroup.vue +43 -0
- package/src/components/Accordion/accordion.scss +44 -0
- package/src/components/Alert/Alert.vue +53 -0
- package/src/components/Alert/alert.scss +80 -0
- package/src/components/Avatar/Avatar.vue +36 -0
- package/src/components/Avatar/avatar.scss +46 -0
- package/src/components/Badge/Badge.vue +21 -0
- package/src/components/Badge/badge.scss +89 -0
- package/src/components/Breadcrumbs/BreadcrumbItem.vue +26 -0
- package/src/components/Breadcrumbs/Breadcrumbs.vue +33 -0
- package/src/components/Breadcrumbs/breadcrumbs.scss +30 -0
- package/src/components/Button/Button.vue +90 -0
- package/src/components/Button/button.scss +176 -0
- package/src/components/ButtonGroup/ButtonGroup.vue +25 -0
- package/src/components/ButtonGroup/button-group.scss +51 -0
- package/src/components/Calendar/Calendar.vue +58 -0
- package/src/components/Calendar/calendar.scss +56 -0
- package/src/components/Card/Card.vue +48 -0
- package/src/components/Card/card.scss +53 -0
- package/src/components/Checkbox/Checkbox.vue +49 -0
- package/src/components/Checkbox/checkbox.scss +60 -0
- package/src/components/CopyClipboard/CopyClipboard.vue +82 -0
- package/src/components/CopyClipboard/copy-clipboard.scss +17 -0
- package/src/components/Divider/Divider.vue +34 -0
- package/src/components/Divider/divider.scss +35 -0
- package/src/components/Drawer/Drawer.vue +93 -0
- package/src/components/Drawer/drawer.scss +49 -0
- package/src/components/Dropdown/Dropdown.vue +100 -0
- package/src/components/Dropdown/DropdownItem.vue +29 -0
- package/src/components/Dropdown/DropdownTitle.vue +8 -0
- package/src/components/Dropdown/dropdown.scss +112 -0
- package/src/components/Flex/Flex.vue +109 -0
- package/src/components/Grid/Grid.vue +59 -0
- package/src/components/Input/Counter.vue +70 -0
- package/src/components/Input/Dropzone.vue +63 -0
- package/src/components/Input/File.vue +15 -0
- package/src/components/Input/Input.vue +118 -0
- package/src/components/Input/Password.vue +47 -0
- package/src/components/Input/Textarea.vue +73 -0
- package/src/components/Input/input.scss +199 -0
- package/src/components/Kbd/Kbd.vue +48 -0
- package/src/components/Kbd/KbdGroup.vue +31 -0
- package/src/components/Kbd/kbd.scss +18 -0
- package/src/components/Modal/Confirm.vue +56 -0
- package/src/components/Modal/Modal.vue +91 -0
- package/src/components/Modal/modal.scss +49 -0
- package/src/components/Pagination/Pagination.vue +74 -0
- package/src/components/Pagination/pagination.ts +78 -0
- package/src/components/Popout/Popout.vue +39 -0
- package/src/components/Popout/popout.scss +7 -0
- package/src/components/Progress/Progress.vue +84 -0
- package/src/components/Progress/progress.scss +41 -0
- package/src/components/Radio/Radio.vue +36 -0
- package/src/components/Radio/RadioGroup.vue +35 -0
- package/src/components/Radio/radio.scss +59 -0
- package/src/components/Select/Select.vue +180 -0
- package/src/components/Select/select.scss +43 -0
- package/src/components/Sheet/Sheet.vue +91 -0
- package/src/components/Sheet/sheet.scss +56 -0
- package/src/components/Skeleton/Skeleton.vue +46 -0
- package/src/components/Skeleton/skeleton.scss +14 -0
- package/src/components/Spinner/Spinner.vue +44 -0
- package/src/components/Spinner/spinner.scss +46 -0
- package/src/components/Switch/Switch.vue +30 -0
- package/src/components/Switch/switch.scss +52 -0
- package/src/components/Table/Cell.vue +23 -0
- package/src/components/Table/Header.vue +59 -0
- package/src/components/Table/Row.vue +9 -0
- package/src/components/Table/SelectAll.vue +23 -0
- package/src/components/Table/SelectRow.vue +29 -0
- package/src/components/Table/Table.vue +66 -0
- package/src/components/Table/table.scss +134 -0
- package/src/components/Table/table.ts +243 -0
- package/src/components/Tabs/Tab.vue +21 -0
- package/src/components/Tabs/Tabs.vue +76 -0
- package/src/components/Tabs/tabs.scss +78 -0
- package/src/components/Toast/Toasts.vue +47 -0
- package/src/components/Toast/toast.scss +41 -0
- package/src/components/Toast/toast.ts +92 -0
- package/src/components/Tooltip/Tooltip.vue +80 -0
- package/src/components/Tooltip/tooltip.scss +4 -0
- package/src/index.scss +1 -0
- package/src/index.ts +111 -0
- package/src/internal/Backdrop/Backdrop.vue +22 -0
- package/src/internal/Backdrop/backdrop.scss +28 -0
- package/src/main.ts +5 -0
- package/src/shared/composables.ts +18 -0
- package/src/shared/helpers.ts +53 -0
- package/src/shared/types.ts +11 -0
- package/src/style/animation.scss +21 -0
- package/src/style/core.scss +128 -0
- package/src/style/fonts.scss +0 -0
- package/src/style/layout.scss +9 -0
- package/src/style/media-query.scss +29 -0
- package/src/style/reset.scss +135 -0
- package/src/style/tooltip.scss +128 -0
- package/src/style/typography.scss +339 -0
- package/src/style/utils.scss +22 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey, MaybeRefOrGetter, Ref } from 'vue'
|
|
2
|
+
import type { DeepRequired } from '../../shared/types'
|
|
3
|
+
import { computed, provide, readonly, ref, toValue } from 'vue'
|
|
4
|
+
import { searchString } from '../../shared/helpers'
|
|
5
|
+
import { paginate } from '../Pagination/pagination'
|
|
6
|
+
|
|
7
|
+
export type BaseRow = Record<string, string | number>
|
|
8
|
+
|
|
9
|
+
export interface TableSelectionProvide {
|
|
10
|
+
selectedRows: Ref<Set<BaseRow>>
|
|
11
|
+
selectRow: (row: BaseRow) => void
|
|
12
|
+
selectAllRows: () => void
|
|
13
|
+
enabled: ComputedRef<boolean>
|
|
14
|
+
isSelectedAll: ComputedRef<boolean>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const TableSelectionProvideSymbol = Symbol('select-row-provide') as InjectionKey<TableSelectionProvide>
|
|
18
|
+
|
|
19
|
+
interface Sorting<K> {
|
|
20
|
+
key?: K
|
|
21
|
+
type: 'asc' | 'desc'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Header {
|
|
25
|
+
label: string
|
|
26
|
+
sortToggle: () => void
|
|
27
|
+
sortKey?: 'asc' | 'desc'
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TableOptionsInput {
|
|
32
|
+
pagination?: {
|
|
33
|
+
enabled?: boolean
|
|
34
|
+
perPage?: number
|
|
35
|
+
maxPages?: number
|
|
36
|
+
}
|
|
37
|
+
select?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// eslint-disable-next-line ts/explicit-function-return-type
|
|
41
|
+
export function defineTable<const Dataset extends Array<BaseRow>>(
|
|
42
|
+
computedDataset: MaybeRefOrGetter<Dataset>,
|
|
43
|
+
tableOptions?: TableOptionsInput,
|
|
44
|
+
) {
|
|
45
|
+
const $data = computed(() => toValue(computedDataset))
|
|
46
|
+
|
|
47
|
+
//
|
|
48
|
+
// Reactive options + defaults
|
|
49
|
+
const options = ref(Object.assign({
|
|
50
|
+
pagination: {
|
|
51
|
+
enabled: false,
|
|
52
|
+
perPage: 10,
|
|
53
|
+
maxPages: 3,
|
|
54
|
+
},
|
|
55
|
+
select: false,
|
|
56
|
+
}, tableOptions) as DeepRequired<TableOptionsInput>)
|
|
57
|
+
|
|
58
|
+
//
|
|
59
|
+
// Pagination
|
|
60
|
+
const currentPage = ref(1)
|
|
61
|
+
|
|
62
|
+
const pagination = computed(() => paginate(
|
|
63
|
+
$data.value.length,
|
|
64
|
+
currentPage.value,
|
|
65
|
+
options.value.pagination?.perPage,
|
|
66
|
+
options.value.pagination?.maxPages,
|
|
67
|
+
))
|
|
68
|
+
|
|
69
|
+
const canNextPage = computed(() => pagination.value.currentPage < pagination.value.endPage)
|
|
70
|
+
const canPrevPage = computed(() => pagination.value.currentPage > pagination.value.startPage)
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sets the currently active data page. Please note that you provide the page
|
|
74
|
+
* number, no its index. So this is 1-indexed input
|
|
75
|
+
*
|
|
76
|
+
* @param page Page number
|
|
77
|
+
*/
|
|
78
|
+
const setPage = (page: number): void => {
|
|
79
|
+
if ((page > currentPage.value && canNextPage.value)
|
|
80
|
+
|| (page < currentPage.value && canPrevPage.value)) {
|
|
81
|
+
currentPage.value = page
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//
|
|
86
|
+
// Sorting
|
|
87
|
+
|
|
88
|
+
const sorting = ref<Sorting<Ref<keyof Dataset[number]>>>({
|
|
89
|
+
key: undefined,
|
|
90
|
+
type: 'asc',
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const setSort = (key: keyof Dataset[number], type: 'asc' | 'desc' | 'toggle' = 'asc'): void => {
|
|
94
|
+
sorting.value.key = key
|
|
95
|
+
sorting.value.type = type === 'toggle'
|
|
96
|
+
// Toggle between descending & ascending whenever the set sort fn is called
|
|
97
|
+
? sorting.value.type === 'asc' ? 'desc' : 'asc'
|
|
98
|
+
: type
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const clearSort = (): void => {
|
|
102
|
+
sorting.value.key = undefined
|
|
103
|
+
sorting.value.type = 'asc'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//
|
|
107
|
+
// Searching
|
|
108
|
+
const search = ref<string>()
|
|
109
|
+
|
|
110
|
+
//
|
|
111
|
+
// Dataset formatting
|
|
112
|
+
const filteredRows = computed(() => {
|
|
113
|
+
const searchValue = search.value
|
|
114
|
+
let final = $data.value
|
|
115
|
+
|
|
116
|
+
if (searchValue) {
|
|
117
|
+
final = final.filter((row: Dataset[number]) => {
|
|
118
|
+
const matches = Object
|
|
119
|
+
.values(row)
|
|
120
|
+
.map(row => `${row}`)
|
|
121
|
+
return searchString(matches, searchValue)
|
|
122
|
+
}) as Dataset
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const key = sorting.value.key
|
|
126
|
+
|
|
127
|
+
if (key) {
|
|
128
|
+
final = final.toSorted((a: Dataset[number], b: Dataset[number]) => {
|
|
129
|
+
const aValue = a[key]
|
|
130
|
+
const bValue = b[key]
|
|
131
|
+
return sorting.value.type === 'asc'
|
|
132
|
+
? aValue > bValue ? 1 : -1
|
|
133
|
+
: aValue > bValue ? -1 : 1
|
|
134
|
+
}) as Dataset
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return final
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const headers = computed(() => Object
|
|
141
|
+
.keys($data.value[0])
|
|
142
|
+
.map((key) => {
|
|
143
|
+
return {
|
|
144
|
+
label: key,
|
|
145
|
+
sortKey: sorting.value.key === key && sorting.value.type,
|
|
146
|
+
sortToggle: () => {
|
|
147
|
+
// 3-way toggle asc -> desc -> turn off (reset to undefined)
|
|
148
|
+
if (sorting.value.key === key) {
|
|
149
|
+
switch (sorting.value.type) {
|
|
150
|
+
case 'asc': {
|
|
151
|
+
sorting.value.type = 'desc'
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
case 'desc': {
|
|
155
|
+
sorting.value.key = undefined
|
|
156
|
+
sorting.value.key = 'asc'
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
default: {
|
|
160
|
+
sorting.value.key = key
|
|
161
|
+
sorting.value.type = 'asc'
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
setSort(key)
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
} as Header
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const rows = computed(() => {
|
|
175
|
+
if (options.value.pagination?.enabled === true) {
|
|
176
|
+
return filteredRows.value.slice(
|
|
177
|
+
pagination.value.startIndex,
|
|
178
|
+
pagination.value.endIndex + 1,
|
|
179
|
+
) as Dataset
|
|
180
|
+
}
|
|
181
|
+
return filteredRows.value as Dataset
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
//
|
|
185
|
+
// Row selecting
|
|
186
|
+
const selectedRows = ref<Set<BaseRow>>(new Set() as Set<BaseRow>)
|
|
187
|
+
const selectingEnabled = computed(() => options.value.select)
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Accepts either an existing index of a row within the dataset or the dataset
|
|
191
|
+
* row itself. If the item is already selected, it will be deselected.
|
|
192
|
+
*
|
|
193
|
+
* @param row {Number | RowObject}
|
|
194
|
+
*/
|
|
195
|
+
function selectRow(row: Dataset[number]): void {
|
|
196
|
+
if (selectedRows.value.has(row)) {
|
|
197
|
+
selectedRows.value.delete(row)
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
selectedRows.value.add(row)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const isSelectedAll = computed(() => $data.value.length === selectedRows.value.size)
|
|
205
|
+
|
|
206
|
+
function selectAllRows(): void {
|
|
207
|
+
if (isSelectedAll.value) {
|
|
208
|
+
// If the selected indexes have the same length as the data array, we can
|
|
209
|
+
// assume all of them are selected. Therefore we toggle it by deselecting
|
|
210
|
+
// all of them
|
|
211
|
+
selectedRows.value = new Set()
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
selectedRows.value = new Set($data.value.map(row => row))
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
provide(TableSelectionProvideSymbol, {
|
|
219
|
+
selectedRows,
|
|
220
|
+
selectRow,
|
|
221
|
+
selectAllRows,
|
|
222
|
+
enabled: selectingEnabled,
|
|
223
|
+
isSelectedAll,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
setSort,
|
|
228
|
+
clearSort,
|
|
229
|
+
search,
|
|
230
|
+
rows: readonly(rows),
|
|
231
|
+
allRows: readonly(filteredRows),
|
|
232
|
+
selectedRows: readonly(selectedRows),
|
|
233
|
+
headers: readonly(headers),
|
|
234
|
+
pagination,
|
|
235
|
+
canPrevPage,
|
|
236
|
+
canNextPage,
|
|
237
|
+
setPage,
|
|
238
|
+
options,
|
|
239
|
+
selectRow,
|
|
240
|
+
selectAllRows,
|
|
241
|
+
isSelectedAll,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Icon } from '@iconify/vue'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
export interface TabProps {
|
|
6
|
+
disabled?: boolean
|
|
7
|
+
id?: string
|
|
8
|
+
label: string
|
|
9
|
+
icon?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = defineProps<TabProps>()
|
|
13
|
+
const id = computed(() => props.id ?? props.label)
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<button class="vui-tab" :data-tab-id="id" :class="{ disabled: props.disabled }">
|
|
18
|
+
<Icon v-if="props.icon" :icon="props.icon" />
|
|
19
|
+
{{ props.label }}
|
|
20
|
+
</button>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TabProps } from './Tab.vue'
|
|
3
|
+
import { useEventListener } from '@vueuse/core'
|
|
4
|
+
import { onMounted, useTemplateRef, type VNode, watch } from 'vue'
|
|
5
|
+
import './tabs.scss'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
variant?: 'default' | 'filled'
|
|
9
|
+
expand?: boolean
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
expand,
|
|
15
|
+
disabled,
|
|
16
|
+
variant = 'default',
|
|
17
|
+
} = defineProps<Props>()
|
|
18
|
+
|
|
19
|
+
const slots = defineSlots<{
|
|
20
|
+
default: () => Array<VNode & { props: TabProps }>
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const active = defineModel()
|
|
24
|
+
|
|
25
|
+
// Underline calculation
|
|
26
|
+
const underlineRef = useTemplateRef('underline')
|
|
27
|
+
const tabsRef = useTemplateRef('tabs')
|
|
28
|
+
|
|
29
|
+
function computeUnderlinePosition() {
|
|
30
|
+
if (tabsRef.value && underlineRef.value) {
|
|
31
|
+
const activeBounds = tabsRef.value.querySelector('.vui-tab.active')?.getBoundingClientRect()
|
|
32
|
+
const parentBounds = tabsRef.value.getBoundingClientRect()
|
|
33
|
+
if (!activeBounds || !parentBounds)
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
underlineRef.value.style.width = `${activeBounds.width}px`
|
|
37
|
+
underlineRef.value.style.left = `${activeBounds.left - parentBounds.left}px`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onMounted(() => {
|
|
42
|
+
watch(
|
|
43
|
+
[active, () => expand],
|
|
44
|
+
computeUnderlinePosition,
|
|
45
|
+
{
|
|
46
|
+
immediate: true,
|
|
47
|
+
flush: 'post',
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
useEventListener(window, 'resize', computeUnderlinePosition)
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<div
|
|
57
|
+
ref="tabs"
|
|
58
|
+
class="vui-tabs"
|
|
59
|
+
:class="[
|
|
60
|
+
{ expand, disabled },
|
|
61
|
+
variant === 'default'
|
|
62
|
+
? ''
|
|
63
|
+
: `vui-tabs-variant-${variant}`,
|
|
64
|
+
]"
|
|
65
|
+
>
|
|
66
|
+
<Component
|
|
67
|
+
:is="vnode"
|
|
68
|
+
v-for="vnode of slots.default()"
|
|
69
|
+
:key="vnode.props.id"
|
|
70
|
+
:class="{ active: vnode.props.id === active }"
|
|
71
|
+
@click="active = vnode.props.id"
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<div ref="underline" class="vui-tab-underline" />
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
.vui-tabs {
|
|
2
|
+
display: flex;
|
|
3
|
+
width: 100%;
|
|
4
|
+
gap: 4px;
|
|
5
|
+
border-bottom: 1px solid var(--color-border);
|
|
6
|
+
position: relative;
|
|
7
|
+
|
|
8
|
+
&.vui-tabs-variant-filled {
|
|
9
|
+
background-color: var(--color-bg-raised);
|
|
10
|
+
border-bottom: none;
|
|
11
|
+
z-index: 1;
|
|
12
|
+
border-radius: var(--border-radius-m);
|
|
13
|
+
padding-inline: 4px;
|
|
14
|
+
|
|
15
|
+
.vui-tab-underline {
|
|
16
|
+
border-bottom: none;
|
|
17
|
+
background-color: var(--color-bg-lowered);
|
|
18
|
+
top: 4px;
|
|
19
|
+
bottom: 4px;
|
|
20
|
+
z-index: -1;
|
|
21
|
+
border-radius: var(--border-radius-m);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&.disabled .vui-tab,
|
|
26
|
+
.vui-tab.disabled {
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
color: var(--color-text-lighter);
|
|
29
|
+
|
|
30
|
+
&.active {
|
|
31
|
+
color: var(--color-text-lighter) !important;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&.disabled {
|
|
36
|
+
.vui-tab-underline {
|
|
37
|
+
border-color: var(--color-text-lighter);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&.expand .vui-tab {
|
|
42
|
+
flex: 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.vui-tab {
|
|
46
|
+
display: flex;
|
|
47
|
+
height: 40px;
|
|
48
|
+
// place-content: center;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
padding: 0 10px;
|
|
52
|
+
font-size: var(--font-size-ms);
|
|
53
|
+
position: relative;
|
|
54
|
+
color: var(--color-text-light);
|
|
55
|
+
transition: var(--transition);
|
|
56
|
+
cursor: default;
|
|
57
|
+
gap: var(--space-xs);
|
|
58
|
+
border-radius: var(--border-radius-m);
|
|
59
|
+
|
|
60
|
+
svg {
|
|
61
|
+
width: 20px;
|
|
62
|
+
height: 20px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&:hover,
|
|
66
|
+
&.active {
|
|
67
|
+
color: var(--color-text);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.vui-tab-underline {
|
|
72
|
+
transition: var(--transition);
|
|
73
|
+
display: block;
|
|
74
|
+
border-bottom: 1px solid var(--color-text);
|
|
75
|
+
position: absolute;
|
|
76
|
+
bottom: 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Button from '../Button/Button.vue'
|
|
3
|
+
import { toasts } from './toast'
|
|
4
|
+
import './toast.scss'
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<Teleport to="body">
|
|
9
|
+
<div class="vui-toast-wrapper">
|
|
10
|
+
<TransitionGroup name="toast" tag="ul" class="vui-toast-list">
|
|
11
|
+
<li v-for="[toastId, toast] in toasts" :key="toastId" class="vui-toast-item">
|
|
12
|
+
<div class="vui-toast-item-content">
|
|
13
|
+
<strong>{{ toast.title }}</strong>
|
|
14
|
+
<p v-if="toast.description">
|
|
15
|
+
{{ toast.description }}
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
<Button v-if="toast.action" @click="toast.action.handler(toast.id)">
|
|
19
|
+
{{ toast.action.label }}
|
|
20
|
+
</Button>
|
|
21
|
+
</li>
|
|
22
|
+
</TransitionGroup>
|
|
23
|
+
</div>
|
|
24
|
+
</Teleport>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
.toast-move,
|
|
29
|
+
.toast-enter-active,
|
|
30
|
+
.toast-leave-active {
|
|
31
|
+
transition: var(--transition);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.toast-enter-from {
|
|
35
|
+
opacity: 0;
|
|
36
|
+
transform: translateY(24px) scale(0.95);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.toast-leave-to {
|
|
40
|
+
opacity: 0;
|
|
41
|
+
transform: translateY(-24px) scale(0.95);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.toast-leave-active {
|
|
45
|
+
position: absolute;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.vui-toast-wrapper {
|
|
2
|
+
position: fixed;
|
|
3
|
+
bottom: 32px;
|
|
4
|
+
right: 32px;
|
|
5
|
+
width: 100%;
|
|
6
|
+
max-width: 392px;
|
|
7
|
+
|
|
8
|
+
.vui-toast-list {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--space-xs);
|
|
12
|
+
|
|
13
|
+
.vui-toast-item {
|
|
14
|
+
display: flex;
|
|
15
|
+
border: 1px solid var(--color-border);
|
|
16
|
+
border-radius: var(--border-radius-m);
|
|
17
|
+
padding: var(--space-m) var(--space-s);
|
|
18
|
+
background-color: var(--color-bg);
|
|
19
|
+
width: 100%;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: var(--space-m);
|
|
22
|
+
box-shadow: var(--box-shadow);
|
|
23
|
+
|
|
24
|
+
.vui-toast-item-content {
|
|
25
|
+
flex: 1;
|
|
26
|
+
|
|
27
|
+
strong {
|
|
28
|
+
color: var(--color-text);
|
|
29
|
+
margin-bottom: var(--space-xs);
|
|
30
|
+
display: block;
|
|
31
|
+
font-size: var(--font-size-m);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
p {
|
|
35
|
+
color: var(--color-text-lighter);
|
|
36
|
+
font-size: var(--font-size-s);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// share some tiny global state
|
|
2
|
+
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
interface ToastAction {
|
|
6
|
+
label: string
|
|
7
|
+
handler: (toastId: number) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ToastOptions {
|
|
11
|
+
persist?: boolean
|
|
12
|
+
timeout?: number
|
|
13
|
+
action?: ToastAction
|
|
14
|
+
description?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Toast {
|
|
18
|
+
id: number
|
|
19
|
+
// type: ToastType
|
|
20
|
+
title: string
|
|
21
|
+
action?: ToastAction
|
|
22
|
+
createdAt: number
|
|
23
|
+
expiresAt: number
|
|
24
|
+
description?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Store in a ref so the toast component can import it
|
|
28
|
+
export const toasts = ref(new Map<number, Toast>())
|
|
29
|
+
|
|
30
|
+
// Simple incremental id system
|
|
31
|
+
let id = 0
|
|
32
|
+
|
|
33
|
+
// function toast(type: ToastType, title: string, options?: ToastOptions): Toast {
|
|
34
|
+
export function pushToast(title: string, options?: ToastOptions): Toast {
|
|
35
|
+
const parsedOptions = Object.assign({
|
|
36
|
+
persist: false,
|
|
37
|
+
timeout: 7000,
|
|
38
|
+
}, options)
|
|
39
|
+
|
|
40
|
+
const newToast = {
|
|
41
|
+
id,
|
|
42
|
+
// type,
|
|
43
|
+
title,
|
|
44
|
+
persist: parsedOptions.persist,
|
|
45
|
+
description: parsedOptions.description,
|
|
46
|
+
action: parsedOptions.action,
|
|
47
|
+
createdAt: Date.now(),
|
|
48
|
+
expiresAt: Date.now() + parsedOptions.timeout,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
toasts.value.set(id, newToast)
|
|
52
|
+
|
|
53
|
+
// If options include timeout (by default) remove the toast after timeout
|
|
54
|
+
// passes
|
|
55
|
+
if (!parsedOptions.persist) {
|
|
56
|
+
setTimeout((_id: number) => {
|
|
57
|
+
toasts.value.delete(_id)
|
|
58
|
+
// Pass Id as an optional argument, becasue by the time it is executed the
|
|
59
|
+
// Id will have been increased
|
|
60
|
+
}, parsedOptions.timeout, id)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
id++
|
|
64
|
+
|
|
65
|
+
return newToast
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function removeToast(id: number): void {
|
|
69
|
+
toasts.value.delete(id)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//////
|
|
73
|
+
|
|
74
|
+
// export const toastError: NewToastFn = (title, options) => {
|
|
75
|
+
// return pushToast('error', title, options)
|
|
76
|
+
// }
|
|
77
|
+
|
|
78
|
+
// export const toastSuccess: NewToastFn = (title, options) => {
|
|
79
|
+
// return pushToast('success', title, options)
|
|
80
|
+
// }
|
|
81
|
+
|
|
82
|
+
// export const toastInfo: NewToastFn = (title, options) => {
|
|
83
|
+
// return pushToast('info', title, options)
|
|
84
|
+
// }
|
|
85
|
+
|
|
86
|
+
// export const toastNeutral: NewToastFn = (title, options) => {
|
|
87
|
+
// return pushToast('neutral', title, options)
|
|
88
|
+
// }
|
|
89
|
+
|
|
90
|
+
// export const toastWarning: NewToastFn = (title, options) => {
|
|
91
|
+
// return pushToast('warning', title, options)
|
|
92
|
+
// }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import type { Placement } from '@floating-ui/vue'
|
|
3
|
+
import { ref, useTemplateRef, watch } from 'vue'
|
|
4
|
+
import Popout from '../Popout/Popout.vue'
|
|
5
|
+
import './tooltip.scss'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/**
|
|
9
|
+
* Tooltip placement related to the anchor
|
|
10
|
+
*/
|
|
11
|
+
placement?: Placement
|
|
12
|
+
/**
|
|
13
|
+
* Amount of time user should hover the anchor until the tooltip shows up
|
|
14
|
+
*/
|
|
15
|
+
delay?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
placement = 'bottom',
|
|
20
|
+
delay = 0,
|
|
21
|
+
} = defineProps<Props>()
|
|
22
|
+
|
|
23
|
+
const popoutAnchorRef = useTemplateRef('popoutAnchor')
|
|
24
|
+
// Track if user is hovering the anchor
|
|
25
|
+
const hoverAnchor = ref(false)
|
|
26
|
+
|
|
27
|
+
// Display tooltip
|
|
28
|
+
const showTooltip = ref(false)
|
|
29
|
+
|
|
30
|
+
let timeoutId: NodeJS.Timeout
|
|
31
|
+
watch(hoverAnchor, (isHovering) => {
|
|
32
|
+
if (isHovering) {
|
|
33
|
+
if (!delay || delay <= 0) {
|
|
34
|
+
showTooltip.value = true
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
clearTimeout(timeoutId)
|
|
39
|
+
|
|
40
|
+
timeoutId = setTimeout(() => {
|
|
41
|
+
// Need to reference the ref itself as this will execute without the
|
|
42
|
+
// outside scope (as far as I know tbh)
|
|
43
|
+
if (hoverAnchor.value) {
|
|
44
|
+
showTooltip.value = true
|
|
45
|
+
}
|
|
46
|
+
}, delay)
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
clearTimeout(timeoutId)
|
|
50
|
+
showTooltip.value = false
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<div
|
|
57
|
+
ref="popoutAnchor"
|
|
58
|
+
@mouseenter="hoverAnchor = true"
|
|
59
|
+
@mouseleave="hoverAnchor = false"
|
|
60
|
+
>
|
|
61
|
+
<slot />
|
|
62
|
+
</div>
|
|
63
|
+
<Transition appear name="tooltip">
|
|
64
|
+
<Popout v-if="showTooltip" :anchor="popoutAnchorRef" class="vui-tooltip" :placement>
|
|
65
|
+
<slot name="tooltip" />
|
|
66
|
+
</Popout>
|
|
67
|
+
</Transition>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<style scoped>
|
|
71
|
+
.tooltip-enter-active,
|
|
72
|
+
.tooltip-leave-active {
|
|
73
|
+
transition: 0.1s opacity ease-in-out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.tooltip-enter-from,
|
|
77
|
+
.tooltip-leave-to {
|
|
78
|
+
opacity: 0;
|
|
79
|
+
}
|
|
80
|
+
</style>
|
package/src/index.scss
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url(./style/core.scss);
|