@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.
Files changed (162) hide show
  1. package/README.md +7 -0
  2. package/dist/components/Accordion/Accordion.vue.d.ts +45 -0
  3. package/dist/components/Accordion/AccordionGroup.vue.d.ts +32 -0
  4. package/dist/components/Alert/Alert.vue.d.ts +29 -0
  5. package/dist/components/Avatar/Avatar.vue.d.ts +9 -0
  6. package/dist/components/Badge/Badge.vue.d.ts +21 -0
  7. package/dist/components/Breadcrumbs/BreadcrumbItem.vue.d.ts +21 -0
  8. package/dist/components/Breadcrumbs/Breadcrumbs.vue.d.ts +27 -0
  9. package/dist/components/Button/Button.vue.d.ts +41 -0
  10. package/dist/components/ButtonGroup/ButtonGroup.vue.d.ts +19 -0
  11. package/dist/components/Calendar/Calendar.vue.d.ts +27 -0
  12. package/dist/components/Card/Card.vue.d.ts +25 -0
  13. package/dist/components/Checkbox/Checkbox.vue.d.ts +31 -0
  14. package/dist/components/CopyClipboard/CopyClipboard.vue.d.ts +40 -0
  15. package/dist/components/Divider/Divider.vue.d.ts +24 -0
  16. package/dist/components/Drawer/Drawer.vue.d.ts +52 -0
  17. package/dist/components/Dropdown/DropdownItem.vue.d.ts +21 -0
  18. package/dist/components/Dropdown/DropdownTitle.vue.d.ts +17 -0
  19. package/dist/components/Flex/Flex.vue.d.ts +38 -0
  20. package/dist/components/Grid/Grid.vue.d.ts +27 -0
  21. package/dist/components/Input/Counter.vue.d.ts +19 -0
  22. package/dist/components/Input/Dropzone.vue.d.ts +107 -0
  23. package/dist/components/Input/File.vue.d.ts +7 -0
  24. package/dist/components/Input/Input.vue.d.ts +54 -0
  25. package/dist/components/Input/Password.vue.d.ts +6 -0
  26. package/dist/components/Input/Textarea.vue.d.ts +30 -0
  27. package/dist/components/Kbd/Kbd.vue.d.ts +23 -0
  28. package/dist/components/Kbd/KbdGroup.vue.d.ts +31 -0
  29. package/dist/components/Modal/Confirm.vue.d.ts +45 -0
  30. package/dist/components/Modal/Modal.vue.d.ts +55 -0
  31. package/dist/components/Pagination/Pagination.vue.d.ts +42 -0
  32. package/dist/components/Pagination/pagination.d.ts +12 -0
  33. package/dist/components/Popout/Popout.vue.d.ts +34 -0
  34. package/dist/components/Progress/Progress.vue.d.ts +31 -0
  35. package/dist/components/Radio/Radio.vue.d.ts +27 -0
  36. package/dist/components/Radio/RadioGroup.vue.d.ts +40 -0
  37. package/dist/components/Select/Select.vue.d.ts +37 -0
  38. package/dist/components/Sheet/Sheet.vue.d.ts +35 -0
  39. package/dist/components/Skeleton/Skeleton.vue.d.ts +8 -0
  40. package/dist/components/Spinner/Spinner.vue.d.ts +6 -0
  41. package/dist/components/Switch/Switch.vue.d.ts +26 -0
  42. package/dist/components/Table/Cell.vue.d.ts +19 -0
  43. package/dist/components/Table/Header.vue.d.ts +29 -0
  44. package/dist/components/Table/Row.vue.d.ts +16 -0
  45. package/dist/components/Table/SelectAll.vue.d.ts +2 -0
  46. package/dist/components/Table/SelectRow.vue.d.ts +6 -0
  47. package/dist/components/Table/Table.vue.d.ts +40 -0
  48. package/dist/components/Table/table.d.ts +68 -0
  49. package/dist/components/Tabs/Tab.vue.d.ts +8 -0
  50. package/dist/components/Tabs/Tabs.vue.d.ts +43 -0
  51. package/dist/components/Toast/Toasts.vue.d.ts +2 -0
  52. package/dist/components/Toast/toast.d.ts +42 -0
  53. package/dist/components/Tooltip/Tooltip.vue.d.ts +32 -0
  54. package/dist/index.d.ts +54 -1
  55. package/dist/internal/Backdrop/Backdrop.vue.d.ts +20 -0
  56. package/dist/shared/composables.d.ts +3 -0
  57. package/dist/shared/helpers.d.ts +16 -0
  58. package/dist/shared/types.d.ts +10 -0
  59. package/dist/style.css +1 -1
  60. package/dist/vui.js +199 -214
  61. package/package.json +11 -9
  62. package/src/App.vue +162 -0
  63. package/src/components/Accordion/Accordion.vue +75 -0
  64. package/src/components/Accordion/AccordionGroup.vue +43 -0
  65. package/src/components/Accordion/accordion.scss +44 -0
  66. package/src/components/Alert/Alert.vue +53 -0
  67. package/src/components/Alert/alert.scss +80 -0
  68. package/src/components/Avatar/Avatar.vue +36 -0
  69. package/src/components/Avatar/avatar.scss +46 -0
  70. package/src/components/Badge/Badge.vue +21 -0
  71. package/src/components/Badge/badge.scss +89 -0
  72. package/src/components/Breadcrumbs/BreadcrumbItem.vue +26 -0
  73. package/src/components/Breadcrumbs/Breadcrumbs.vue +33 -0
  74. package/src/components/Breadcrumbs/breadcrumbs.scss +30 -0
  75. package/src/components/Button/Button.vue +90 -0
  76. package/src/components/Button/button.scss +176 -0
  77. package/src/components/ButtonGroup/ButtonGroup.vue +25 -0
  78. package/src/components/ButtonGroup/button-group.scss +51 -0
  79. package/src/components/Calendar/Calendar.vue +58 -0
  80. package/src/components/Calendar/calendar.scss +56 -0
  81. package/src/components/Card/Card.vue +48 -0
  82. package/src/components/Card/card.scss +53 -0
  83. package/src/components/Checkbox/Checkbox.vue +49 -0
  84. package/src/components/Checkbox/checkbox.scss +60 -0
  85. package/src/components/CopyClipboard/CopyClipboard.vue +82 -0
  86. package/src/components/CopyClipboard/copy-clipboard.scss +17 -0
  87. package/src/components/Divider/Divider.vue +34 -0
  88. package/src/components/Divider/divider.scss +35 -0
  89. package/src/components/Drawer/Drawer.vue +93 -0
  90. package/src/components/Drawer/drawer.scss +49 -0
  91. package/src/components/Dropdown/Dropdown.vue +100 -0
  92. package/src/components/Dropdown/DropdownItem.vue +29 -0
  93. package/src/components/Dropdown/DropdownTitle.vue +8 -0
  94. package/src/components/Dropdown/dropdown.scss +112 -0
  95. package/src/components/Flex/Flex.vue +109 -0
  96. package/src/components/Grid/Grid.vue +59 -0
  97. package/src/components/Input/Counter.vue +70 -0
  98. package/src/components/Input/Dropzone.vue +63 -0
  99. package/src/components/Input/File.vue +15 -0
  100. package/src/components/Input/Input.vue +118 -0
  101. package/src/components/Input/Password.vue +47 -0
  102. package/src/components/Input/Textarea.vue +73 -0
  103. package/src/components/Input/input.scss +199 -0
  104. package/src/components/Kbd/Kbd.vue +48 -0
  105. package/src/components/Kbd/KbdGroup.vue +31 -0
  106. package/src/components/Kbd/kbd.scss +18 -0
  107. package/src/components/Modal/Confirm.vue +56 -0
  108. package/src/components/Modal/Modal.vue +91 -0
  109. package/src/components/Modal/modal.scss +49 -0
  110. package/src/components/Pagination/Pagination.vue +74 -0
  111. package/src/components/Pagination/pagination.ts +78 -0
  112. package/src/components/Popout/Popout.vue +39 -0
  113. package/src/components/Popout/popout.scss +7 -0
  114. package/src/components/Progress/Progress.vue +84 -0
  115. package/src/components/Progress/progress.scss +41 -0
  116. package/src/components/Radio/Radio.vue +36 -0
  117. package/src/components/Radio/RadioGroup.vue +35 -0
  118. package/src/components/Radio/radio.scss +59 -0
  119. package/src/components/Select/Select.vue +180 -0
  120. package/src/components/Select/select.scss +43 -0
  121. package/src/components/Sheet/Sheet.vue +91 -0
  122. package/src/components/Sheet/sheet.scss +56 -0
  123. package/src/components/Skeleton/Skeleton.vue +46 -0
  124. package/src/components/Skeleton/skeleton.scss +14 -0
  125. package/src/components/Spinner/Spinner.vue +44 -0
  126. package/src/components/Spinner/spinner.scss +46 -0
  127. package/src/components/Switch/Switch.vue +30 -0
  128. package/src/components/Switch/switch.scss +52 -0
  129. package/src/components/Table/Cell.vue +23 -0
  130. package/src/components/Table/Header.vue +59 -0
  131. package/src/components/Table/Row.vue +9 -0
  132. package/src/components/Table/SelectAll.vue +23 -0
  133. package/src/components/Table/SelectRow.vue +29 -0
  134. package/src/components/Table/Table.vue +66 -0
  135. package/src/components/Table/table.scss +134 -0
  136. package/src/components/Table/table.ts +243 -0
  137. package/src/components/Tabs/Tab.vue +21 -0
  138. package/src/components/Tabs/Tabs.vue +76 -0
  139. package/src/components/Tabs/tabs.scss +78 -0
  140. package/src/components/Toast/Toasts.vue +47 -0
  141. package/src/components/Toast/toast.scss +41 -0
  142. package/src/components/Toast/toast.ts +92 -0
  143. package/src/components/Tooltip/Tooltip.vue +80 -0
  144. package/src/components/Tooltip/tooltip.scss +4 -0
  145. package/src/index.scss +1 -0
  146. package/src/index.ts +111 -0
  147. package/src/internal/Backdrop/Backdrop.vue +22 -0
  148. package/src/internal/Backdrop/backdrop.scss +28 -0
  149. package/src/main.ts +5 -0
  150. package/src/shared/composables.ts +18 -0
  151. package/src/shared/helpers.ts +53 -0
  152. package/src/shared/types.ts +11 -0
  153. package/src/style/animation.scss +21 -0
  154. package/src/style/core.scss +128 -0
  155. package/src/style/fonts.scss +0 -0
  156. package/src/style/layout.scss +9 -0
  157. package/src/style/media-query.scss +29 -0
  158. package/src/style/reset.scss +135 -0
  159. package/src/style/tooltip.scss +128 -0
  160. package/src/style/typography.scss +339 -0
  161. package/src/style/utils.scss +22 -0
  162. 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>
@@ -0,0 +1,4 @@
1
+ .vui-tooltip {
2
+ padding: var(--space-s);
3
+ max-width: 512px;
4
+ }
package/src/index.scss ADDED
@@ -0,0 +1 @@
1
+ @import url(./style/core.scss);