@dolanske/vui 1.0.4 → 1.1.1

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 (195) hide show
  1. package/LICENSE +673 -673
  2. package/README.md +42 -42
  3. package/dist/components/Accordion/Accordion.vue.d.ts +3 -2
  4. package/dist/components/Accordion/AccordionGroup.vue.d.ts +5 -2
  5. package/dist/components/Alert/Alert.vue.d.ts +3 -2
  6. package/dist/components/Avatar/Avatar.vue.d.ts +3 -2
  7. package/dist/components/Badge/Badge.vue.d.ts +3 -2
  8. package/dist/components/Breadcrumbs/BreadcrumbItem.vue.d.ts +3 -2
  9. package/dist/components/Breadcrumbs/Breadcrumbs.vue.d.ts +3 -2
  10. package/dist/components/Button/Button.vue.d.ts +3 -2
  11. package/dist/components/ButtonGroup/ButtonGroup.vue.d.ts +3 -2
  12. package/dist/components/Calendar/Calendar.vue.d.ts +6 -6
  13. package/dist/components/Card/Card.vue.d.ts +4 -3
  14. package/dist/components/Checkbox/Checkbox.vue.d.ts +7 -6
  15. package/dist/components/CopyClipboard/CopyClipboard.vue.d.ts +2 -1
  16. package/dist/components/Divider/Divider.vue.d.ts +3 -2
  17. package/dist/components/Drawer/Drawer.vue.d.ts +10 -9
  18. package/dist/components/Dropdown/Dropdown.vue.d.ts +66 -3
  19. package/dist/components/Dropdown/DropdownItem.vue.d.ts +3 -2
  20. package/dist/components/Dropdown/DropdownTitle.vue.d.ts +6 -6
  21. package/dist/components/Flex/Flex.vue.d.ts +13 -12
  22. package/dist/components/Grid/Grid.vue.d.ts +7 -6
  23. package/dist/components/Input/Color.vue.d.ts +5 -5
  24. package/dist/components/Input/Counter.vue.d.ts +5 -5
  25. package/dist/components/Input/Dropzone.vue.d.ts +95 -10
  26. package/dist/components/Input/File.vue.d.ts +4 -3
  27. package/dist/components/Input/Input.vue.d.ts +7 -6
  28. package/dist/components/Input/Password.vue.d.ts +1 -1
  29. package/dist/components/Input/Textarea.vue.d.ts +7 -6
  30. package/dist/components/Kbd/Kbd.vue.d.ts +1 -1
  31. package/dist/components/Kbd/KbdGroup.vue.d.ts +2 -1
  32. package/dist/components/Modal/Confirm.vue.d.ts +7 -9
  33. package/dist/components/Modal/Modal.vue.d.ts +16 -13
  34. package/dist/components/OTP/OTP.vue.d.ts +7 -6
  35. package/dist/components/OTP/OTPItem.vue.d.ts +1 -1
  36. package/dist/components/Pagination/Pagination.vue.d.ts +3 -2
  37. package/dist/components/Popout/Popout.vue.d.ts +3 -2
  38. package/dist/components/Progress/Progress.vue.d.ts +5 -5
  39. package/dist/components/Radio/Radio.vue.d.ts +7 -6
  40. package/dist/components/Radio/RadioGroup.vue.d.ts +7 -6
  41. package/dist/components/Select/Select.vue.d.ts +4 -8
  42. package/dist/components/Sheet/Sheet.vue.d.ts +22 -13
  43. package/dist/components/Sidebar/Sidebar.vue.d.ts +7 -6
  44. package/dist/components/Skeleton/Skeleton.vue.d.ts +1 -1
  45. package/dist/components/Spinner/Spinner.vue.d.ts +1 -1
  46. package/dist/components/Switch/Switch.vue.d.ts +7 -6
  47. package/dist/components/Table/Cell.vue.d.ts +5 -2
  48. package/dist/components/Table/Head.vue.d.ts +3 -2
  49. package/dist/components/Table/Root.vue.d.ts +3 -2
  50. package/dist/components/Table/table.d.ts +2 -2
  51. package/dist/components/Tabs/Tab.vue.d.ts +3 -2
  52. package/dist/components/Tabs/Tabs.vue.d.ts +7 -6
  53. package/dist/components/Toast/toast.d.ts +6 -6
  54. package/dist/components/Tooltip/Tooltip.vue.d.ts +2 -1
  55. package/dist/internal/Backdrop/Backdrop.vue.d.ts +3 -2
  56. package/dist/vui.css +1 -0
  57. package/dist/vui.js +8090 -7884
  58. package/package.json +73 -72
  59. package/src/App.vue +95 -95
  60. package/src/components/Accordion/Accordion.vue +91 -91
  61. package/src/components/Accordion/AccordionGroup.vue +43 -43
  62. package/src/components/Accordion/accordion.scss +82 -82
  63. package/src/components/Alert/Alert.vue +59 -59
  64. package/src/components/Alert/alert.scss +161 -161
  65. package/src/components/Avatar/Avatar.vue +53 -53
  66. package/src/components/Avatar/avatar.scss +52 -52
  67. package/src/components/Badge/Badge.vue +21 -21
  68. package/src/components/Badge/badge.scss +210 -206
  69. package/src/components/Breadcrumbs/BreadcrumbItem.vue +26 -26
  70. package/src/components/Breadcrumbs/Breadcrumbs.vue +30 -30
  71. package/src/components/Breadcrumbs/breadcrumbs.scss +31 -31
  72. package/src/components/Button/Button.vue +85 -85
  73. package/src/components/Button/button.scss +279 -279
  74. package/src/components/ButtonGroup/ButtonGroup.vue +28 -28
  75. package/src/components/ButtonGroup/button-group.scss +51 -51
  76. package/src/components/Calendar/Calendar.vue +66 -66
  77. package/src/components/Calendar/calendar.scss +83 -83
  78. package/src/components/Card/Card.vue +48 -48
  79. package/src/components/Card/card.scss +53 -53
  80. package/src/components/Checkbox/Checkbox.vue +54 -54
  81. package/src/components/Checkbox/checkbox.scss +80 -80
  82. package/src/components/CopyClipboard/CopyClipboard.vue +91 -91
  83. package/src/components/CopyClipboard/copy-clipboard.scss +25 -25
  84. package/src/components/Divider/Divider.vue +44 -44
  85. package/src/components/Divider/divider.scss +35 -35
  86. package/src/components/Drawer/Drawer.vue +102 -97
  87. package/src/components/Drawer/drawer.scss +37 -37
  88. package/src/components/Dropdown/Dropdown.vue +135 -135
  89. package/src/components/Dropdown/DropdownItem.vue +33 -33
  90. package/src/components/Dropdown/DropdownTitle.vue +14 -14
  91. package/src/components/Dropdown/dropdown-item.scss +84 -84
  92. package/src/components/Dropdown/dropdown.scss +53 -53
  93. package/src/components/Flex/Flex.vue +113 -113
  94. package/src/components/Grid/Grid.vue +79 -80
  95. package/src/components/Input/Color.vue +26 -26
  96. package/src/components/Input/Counter.vue +66 -66
  97. package/src/components/Input/Dropzone.vue +65 -65
  98. package/src/components/Input/File.vue +15 -15
  99. package/src/components/Input/Input.vue +123 -123
  100. package/src/components/Input/Password.vue +35 -35
  101. package/src/components/Input/Textarea.vue +78 -78
  102. package/src/components/Input/input.scss +302 -302
  103. package/src/components/Kbd/Kbd.vue +48 -48
  104. package/src/components/Kbd/KbdGroup.vue +27 -27
  105. package/src/components/Kbd/kbd.scss +19 -19
  106. package/src/components/Modal/Confirm.vue +56 -56
  107. package/src/components/Modal/Modal.vue +103 -99
  108. package/src/components/Modal/modal.scss +54 -54
  109. package/src/components/OTP/OTP.vue +133 -133
  110. package/src/components/OTP/OTPItem.vue +37 -37
  111. package/src/components/OTP/otp.scss +84 -84
  112. package/src/components/Pagination/Pagination.vue +77 -77
  113. package/src/components/Pagination/pagination.ts +78 -78
  114. package/src/components/Popout/Popout.vue +52 -52
  115. package/src/components/Popout/popout.scss +15 -15
  116. package/src/components/Progress/Progress.vue +103 -103
  117. package/src/components/Progress/progress.scss +47 -47
  118. package/src/components/Radio/Radio.vue +38 -38
  119. package/src/components/Radio/RadioGroup.vue +40 -40
  120. package/src/components/Radio/radio.scss +78 -78
  121. package/src/components/Select/Select.vue +211 -211
  122. package/src/components/Select/select.scss +77 -77
  123. package/src/components/Sheet/Sheet.vue +108 -98
  124. package/src/components/Sheet/sheet.scss +69 -69
  125. package/src/components/Sidebar/Sidebar.vue +115 -115
  126. package/src/components/Sidebar/sidebar.scss +124 -124
  127. package/src/components/Skeleton/Skeleton.vue +43 -43
  128. package/src/components/Skeleton/skeleton.scss +14 -14
  129. package/src/components/Spinner/Spinner.vue +42 -42
  130. package/src/components/Spinner/spinner.scss +47 -47
  131. package/src/components/Switch/Switch.vue +31 -31
  132. package/src/components/Switch/switch.scss +93 -93
  133. package/src/components/Table/Cell.vue +23 -23
  134. package/src/components/Table/Head.vue +59 -59
  135. package/src/components/Table/Root.vue +66 -66
  136. package/src/components/Table/SelectAll.vue +23 -23
  137. package/src/components/Table/SelectRow.vue +30 -30
  138. package/src/components/Table/index.ts +7 -7
  139. package/src/components/Table/table.scss +154 -154
  140. package/src/components/Table/table.ts +248 -248
  141. package/src/components/Tabs/Tab.vue +25 -25
  142. package/src/components/Tabs/Tabs.vue +90 -90
  143. package/src/components/Tabs/tabs.scss +87 -87
  144. package/src/components/Toast/Toasts.vue +52 -52
  145. package/src/components/Toast/toast.scss +45 -45
  146. package/src/components/Toast/toast.ts +75 -75
  147. package/src/components/Tooltip/Tooltip.vue +86 -86
  148. package/src/components/Tooltip/tooltip.scss +8 -8
  149. package/src/examples/ExampleAccordions.vue +58 -58
  150. package/src/examples/ExampleAlerts.vue +78 -78
  151. package/src/examples/ExampleAvatars.vue +44 -44
  152. package/src/examples/ExampleBadges.vue +48 -48
  153. package/src/examples/ExampleBreadcrumbs.vue +46 -46
  154. package/src/examples/ExampleButtons.vue +140 -140
  155. package/src/examples/ExampleCalendars.vue +40 -40
  156. package/src/examples/ExampleCards.vue +94 -94
  157. package/src/examples/ExampleCheckboxes.vue +123 -123
  158. package/src/examples/ExampleCopyClipboard.vue +47 -47
  159. package/src/examples/ExampleDividers.vue +39 -39
  160. package/src/examples/ExampleDrawers.vue +67 -67
  161. package/src/examples/ExampleDropdowns.vue +114 -114
  162. package/src/examples/ExampleFlexGrid.vue +124 -122
  163. package/src/examples/ExampleInputs.vue +234 -234
  164. package/src/examples/ExampleKBD.vue +65 -65
  165. package/src/examples/ExampleModals.vue +143 -143
  166. package/src/examples/ExamplePalette.vue +159 -159
  167. package/src/examples/ExamplePopouts.vue +41 -41
  168. package/src/examples/ExampleSheets.vue +77 -77
  169. package/src/examples/ExampleSidebars.vue +270 -270
  170. package/src/examples/ExampleSkeletons.vue +26 -26
  171. package/src/examples/ExampleSpinners.vue +80 -78
  172. package/src/examples/ExampleTables.vue +195 -195
  173. package/src/examples/ExampleTabs.vue +119 -119
  174. package/src/examples/ExampleToasts.vue +96 -96
  175. package/src/examples/ExampleTooltips.vue +70 -70
  176. package/src/examples/shared/ExampleColor.vue +28 -28
  177. package/src/index.ts +116 -116
  178. package/src/internal/Backdrop/Backdrop.vue +22 -22
  179. package/src/internal/Backdrop/backdrop.scss +34 -34
  180. package/src/main.ts +5 -5
  181. package/src/shared/helpers.ts +117 -117
  182. package/src/shared/theme.ts +22 -22
  183. package/src/shared/types.ts +29 -29
  184. package/src/style/animation.scss +22 -22
  185. package/src/style/core.scss +119 -125
  186. package/src/style/layout.scss +207 -233
  187. package/src/style/media-query.scss +29 -29
  188. package/src/style/reset.scss +135 -135
  189. package/src/style/text.scss +137 -124
  190. package/src/style/theme.scss +195 -195
  191. package/src/style/tooltip.scss +146 -146
  192. package/src/style/typography.scss +415 -415
  193. package/src/style/utils.scss +36 -36
  194. package/src/style.scss +1 -1
  195. package/dist/style.css +0 -1
@@ -1,40 +1,40 @@
1
- <script setup lang='ts'>
2
- import type { FlexProps } from '../Flex/Flex.vue'
3
- import type Radio from './Radio.vue'
4
- import { watchEffect } from 'vue'
5
- import Flex from '../Flex/Flex.vue'
6
-
7
- interface Props extends FlexProps {
8
- disabled?: boolean
9
- }
10
-
11
- const {
12
- disabled,
13
- ...flexProps
14
- } = defineProps<Props>()
15
-
16
- const slots = defineSlots<{
17
- default: () => Array<typeof Radio>
18
- }>()
19
-
20
- const checked = defineModel()
21
-
22
- watchEffect(() => {
23
- if (slots.default().some(s => s.type.__name !== 'Radio')) {
24
- console.error('You can only pass `<Radio />` components as children.')
25
- }
26
- })
27
- </script>
28
-
29
- <template>
30
- <Flex v-bind="flexProps">
31
- <Component
32
- :is="vnode"
33
- v-for="vnode of slots.default()"
34
- :key="vnode.props.value"
35
- v-bind="vnode.props"
36
- v-model="checked"
37
- :class="{ disabled: disabled || vnode.props.disabled }"
38
- />
39
- </Flex>
40
- </template>
1
+ <script setup lang='ts'>
2
+ import type { FlexProps } from '../Flex/Flex.vue'
3
+ import type Radio from './Radio.vue'
4
+ import { watchEffect } from 'vue'
5
+ import Flex from '../Flex/Flex.vue'
6
+
7
+ interface Props extends FlexProps {
8
+ disabled?: boolean
9
+ }
10
+
11
+ const {
12
+ disabled,
13
+ ...flexProps
14
+ } = defineProps<Props>()
15
+
16
+ const slots = defineSlots<{
17
+ default: () => Array<typeof Radio>
18
+ }>()
19
+
20
+ const checked = defineModel()
21
+
22
+ watchEffect(() => {
23
+ if (slots.default().some(s => s.type.__name !== 'Radio')) {
24
+ console.error('You can only pass `<Radio />` components as children.')
25
+ }
26
+ })
27
+ </script>
28
+
29
+ <template>
30
+ <Flex v-bind="flexProps">
31
+ <Component
32
+ :is="vnode"
33
+ v-for="vnode of slots.default()"
34
+ :key="vnode.props.value"
35
+ v-bind="vnode.props"
36
+ v-model="checked"
37
+ :class="{ disabled: disabled || vnode.props.disabled }"
38
+ />
39
+ </Flex>
40
+ </template>
@@ -1,78 +1,78 @@
1
- .vui-radio {
2
- --radio-size: 24px;
3
-
4
- &.checked {
5
- .vui-radio-icon svg {
6
- color: var(--color-text);
7
- }
8
-
9
- &.accent .vui-radio-icon svg {
10
- color: var(--color-accent);
11
- }
12
- }
13
-
14
- &.disabled {
15
- .vui-radio-icon {
16
- opacity: 0.25;
17
- cursor: not-allowed;
18
- }
19
-
20
- input + label .vui-radio-content {
21
- opacity: 0.4;
22
- cursor: not-allowed;
23
- }
24
- }
25
-
26
- .vui-radio-icon {
27
- cursor: pointer;
28
- width: var(--radio-size);
29
- height: var(--radio-size);
30
-
31
- svg {
32
- width: 100%;
33
- height: 100%;
34
- color: var(--color-text);
35
- }
36
- }
37
-
38
- input {
39
- width: 1px;
40
- height: 1px;
41
- position: absolute;
42
- overflow: hidden;
43
- outline: none !important;
44
- opacity: 0;
45
-
46
- &:focus-visible + label .vui-radio-icon {
47
- outline: 2px solid var(--color-text);
48
- border-radius: 999px;
49
- }
50
-
51
- & + label {
52
- display: grid;
53
- grid-template-columns: 20px 1fr;
54
- gap: 10px;
55
- font-size: var(--font-size-s);
56
- color: var(--color-text);
57
- user-select: none;
58
-
59
- .vui-radio-content {
60
- display: block;
61
-
62
- &:is(p) {
63
- display: flex;
64
- align-items: center;
65
- min-height: var(--radio-size);
66
- font-size: var(--font-size-m);
67
- // line-height: var(--radio-size);
68
- }
69
- }
70
- }
71
- }
72
- }
73
-
74
- :root.light {
75
- .vui-radio.checked.accent .vui-radio-icon svg {
76
- color: var(--color-accent);
77
- }
78
- }
1
+ .vui-radio {
2
+ --radio-size: 24px;
3
+
4
+ &.checked {
5
+ .vui-radio-icon svg {
6
+ color: var(--color-text);
7
+ }
8
+
9
+ &.accent .vui-radio-icon svg {
10
+ color: var(--color-accent);
11
+ }
12
+ }
13
+
14
+ &.disabled {
15
+ .vui-radio-icon {
16
+ opacity: 0.25;
17
+ cursor: not-allowed;
18
+ }
19
+
20
+ input + label .vui-radio-content {
21
+ opacity: 0.4;
22
+ cursor: not-allowed;
23
+ }
24
+ }
25
+
26
+ .vui-radio-icon {
27
+ cursor: pointer;
28
+ width: var(--radio-size);
29
+ height: var(--radio-size);
30
+
31
+ svg {
32
+ width: 100%;
33
+ height: 100%;
34
+ color: var(--color-text);
35
+ }
36
+ }
37
+
38
+ input {
39
+ width: 1px;
40
+ height: 1px;
41
+ position: absolute;
42
+ overflow: hidden;
43
+ outline: none !important;
44
+ opacity: 0;
45
+
46
+ &:focus-visible + label .vui-radio-icon {
47
+ outline: 2px solid var(--color-text);
48
+ border-radius: 999px;
49
+ }
50
+
51
+ & + label {
52
+ display: grid;
53
+ grid-template-columns: 20px 1fr;
54
+ gap: 10px;
55
+ font-size: var(--font-size-s);
56
+ color: var(--color-text);
57
+ user-select: none;
58
+
59
+ .vui-radio-content {
60
+ display: block;
61
+
62
+ &:is(p) {
63
+ display: flex;
64
+ align-items: center;
65
+ min-height: var(--radio-size);
66
+ font-size: var(--font-size-m);
67
+ // line-height: var(--radio-size);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ :root.light {
75
+ .vui-radio.checked.accent .vui-radio-icon svg {
76
+ color: var(--color-accent);
77
+ }
78
+ }
@@ -1,211 +1,211 @@
1
- <!-- eslint-disable ts/consistent-type-definitions -->
2
- <script setup lang='ts' generic="T">
3
- import { Icon } from '@iconify/vue'
4
- import { computed, onMounted, ref, useId, useTemplateRef } from 'vue'
5
- import { searchString } from '../../shared/helpers'
6
- import Button from '../Button/Button.vue'
7
- import Dropdown from '../Dropdown/Dropdown.vue'
8
- import DropdownItem from '../Dropdown/DropdownItem.vue'
9
- import DropdownTitle from '../Dropdown/DropdownTitle.vue'
10
- import Input from '../Input/Input.vue'
11
- import '../Input/input.scss'
12
- import './select.scss'
13
-
14
- export type SelectOption = {
15
- value: any
16
- label: string
17
- }
18
-
19
- type Props = {
20
- single?: boolean
21
- readonly?: boolean
22
- options: SelectOption[]
23
- label?: string
24
- placeholder?: string
25
- required?: boolean
26
- expand?: boolean
27
- hint?: string
28
- search?: boolean
29
- maxActiveOptions?: number
30
- showClear?: boolean
31
- disabled?: boolean
32
- errors?: string[]
33
- }
34
-
35
- const {
36
- expand,
37
- readonly,
38
- required,
39
- hint,
40
- placeholder,
41
- options,
42
- single = true,
43
- search,
44
- maxActiveOptions,
45
- showClear,
46
- disabled,
47
- errors = [] as string[],
48
- } = defineProps<Props>()
49
-
50
- const selected = defineModel<SelectOption[] | undefined>()
51
- const trigger = useTemplateRef('trigger')
52
-
53
- //
54
- function setValue(option: SelectOption) {
55
- if (single) {
56
- // Single
57
- if (selected.value?.some(o => o.value === option.value) && !required) {
58
- selected.value = undefined
59
- }
60
- else {
61
- selected.value = [option]
62
- }
63
- }
64
- else {
65
- if (selected.value && selected.value?.some(o => o.value === option.value)) {
66
- const values = selected.value.filter(o => o.value !== option.value)
67
- // Cant remove the last value if it's required
68
- if ((required && values.length > 0) || !required) {
69
- selected.value = values.length > 0 ? values : undefined
70
- }
71
- }
72
- else {
73
- if (!selected.value) {
74
- selected.value = [option]
75
- }
76
- else if (!maxActiveOptions || (selected.value.length < maxActiveOptions)) {
77
- selected.value?.push(option)
78
- }
79
- }
80
- }
81
- }
82
-
83
- // Search
84
- const searchStr = ref()
85
- const filteredOptions = computed(() => {
86
- if (!searchStr.value || searchStr.value.length === 0)
87
- return options
88
-
89
- return options.filter((option) => {
90
- return searchString(option.label, searchStr.value)
91
- })
92
- })
93
-
94
- // Render text inside the button
95
- const renderPlaceholder = computed(() => {
96
- // Render placeholder or a default select option
97
- if (!selected.value || selected.value.length === 0)
98
- return placeholder ?? 'Select an option'
99
-
100
- // Selected values
101
- if (single)
102
- return selected.value[0].label
103
-
104
- // If amount of selected exceeds the active capacity
105
- if (selected.value.length > 3) {
106
- return `${selected.value.length} selected`
107
- }
108
-
109
- // Just list ALL selected optionsx
110
- return selected.value.map(o => o.label).join(', ')
111
- })
112
-
113
- onMounted(() => {
114
- if (readonly && showClear) {
115
- console.warn('[Select] readonly and showClear are mutually exclusive props. The clear button will not show up if required is present.')
116
- }
117
- })
118
-
119
- const dropdownRef = useTemplateRef('dropdown')
120
-
121
- function clearValue() {
122
- selected.value = undefined
123
- dropdownRef.value?.close()
124
- }
125
-
126
- const id = useId()
127
- </script>
128
-
129
- <template>
130
- <div class="vui-input-container vui-select" :class="{ expand, required, readonly, disabled, 'has-errors': errors.length > 0 }">
131
- <Dropdown ref="dropdown" :expand @close="trigger?.focus({ preventScroll: true })">
132
- <template #trigger="{ toggle, isOpen }">
133
- <div class="vui-input vui-select-trigger-content">
134
- <label v-if="label" :for="id">{{ label }}</label>
135
- <p v-if="hint" class="vui-input-hint">
136
- {{ hint }}
137
- </p>
138
-
139
- <button
140
- :id
141
- ref="trigger"
142
- class="vui-input-style vui-select-trigger-container"
143
- :class="{ 'has-value': selected && selected.length > 0 }"
144
- :disabled
145
- @click="toggle"
146
- >
147
- <span>
148
- {{ renderPlaceholder }}
149
- </span>
150
- <template v-if="showClear && !required && selected">
151
- <div class="flex-1" />
152
- <Button
153
- plain
154
- icon="ph:x"
155
- square
156
- size="s"
157
-
158
- @click.stop="clearValue"
159
- />
160
- </template>
161
- <Icon :icon="isOpen ? 'ph:caret-up' : 'ph:caret-down'" />
162
- </button>
163
- </div>
164
- </template>
165
-
166
- <template #default="{ close, isOpen }">
167
- <DropdownTitle v-if="search" sticky>
168
- <Input
169
- v-model="searchStr"
170
- placeholder="Search..."
171
- :focus="isOpen"
172
- expand
173
- >
174
- <template #start>
175
- <Icon icon="ph:magnifying-glass" />
176
- </template>
177
- </Input>
178
- </DropdownTitle>
179
-
180
- <p v-if="filteredOptions.length === 0" class="vue-select-no-results">
181
- No results...
182
- </p>
183
-
184
- <DropdownItem
185
- v-for="option in filteredOptions"
186
- :key="option.value"
187
- :class="{ selected: selected?.find(v => v.value === option.value) }"
188
- :icon="selected?.find(v => v.value === option.value) ? 'ph:check-bold' : ''"
189
- @click="() => {
190
- setValue(option)
191
- // In single mode, close modal after clicking
192
- if (single && !(required && selected && selected[0].value === option.value)) {
193
- close()
194
- }
195
- }"
196
- >
197
- {{ option.label }}
198
- </DropdownItem>
199
- </template>
200
- </Dropdown>
201
-
202
- <p v-if="maxActiveOptions && !single" class="vui-input-limit">
203
- {{ `${selected ? selected.length : 0}/${maxActiveOptions}` }}
204
- </p>
205
- <ul v-if="errors.length > 0" class="vui-input-errors">
206
- <li v-for="err in errors" :key="err">
207
- {{ err }}
208
- </li>
209
- </ul>
210
- </div>
211
- </template>
1
+ <!-- eslint-disable ts/consistent-type-definitions -->
2
+ <script setup lang='ts' generic="T">
3
+ import { Icon } from '@iconify/vue'
4
+ import { computed, onMounted, ref, useId, useTemplateRef } from 'vue'
5
+ import { searchString } from '../../shared/helpers'
6
+ import Button from '../Button/Button.vue'
7
+ import Dropdown from '../Dropdown/Dropdown.vue'
8
+ import DropdownItem from '../Dropdown/DropdownItem.vue'
9
+ import DropdownTitle from '../Dropdown/DropdownTitle.vue'
10
+ import Input from '../Input/Input.vue'
11
+ import '../Input/input.scss'
12
+ import './select.scss'
13
+
14
+ export type SelectOption = {
15
+ value: any
16
+ label: string
17
+ }
18
+
19
+ type Props = {
20
+ single?: boolean
21
+ readonly?: boolean
22
+ options: SelectOption[]
23
+ label?: string
24
+ placeholder?: string
25
+ required?: boolean
26
+ expand?: boolean
27
+ hint?: string
28
+ search?: boolean
29
+ maxActiveOptions?: number
30
+ showClear?: boolean
31
+ disabled?: boolean
32
+ errors?: string[]
33
+ }
34
+
35
+ const {
36
+ expand,
37
+ readonly,
38
+ required,
39
+ hint,
40
+ placeholder,
41
+ options,
42
+ single = true,
43
+ search,
44
+ maxActiveOptions,
45
+ showClear,
46
+ disabled,
47
+ errors = [] as string[],
48
+ } = defineProps<Props>()
49
+
50
+ const selected = defineModel<SelectOption[] | undefined>()
51
+ const trigger = useTemplateRef('trigger')
52
+
53
+ //
54
+ function setValue(option: SelectOption) {
55
+ if (single) {
56
+ // Single
57
+ if (selected.value?.some(o => o.value === option.value) && !required) {
58
+ selected.value = undefined
59
+ }
60
+ else {
61
+ selected.value = [option]
62
+ }
63
+ }
64
+ else {
65
+ if (selected.value && selected.value?.some(o => o.value === option.value)) {
66
+ const values = selected.value.filter(o => o.value !== option.value)
67
+ // Cant remove the last value if it's required
68
+ if ((required && values.length > 0) || !required) {
69
+ selected.value = values.length > 0 ? values : undefined
70
+ }
71
+ }
72
+ else {
73
+ if (!selected.value) {
74
+ selected.value = [option]
75
+ }
76
+ else if (!maxActiveOptions || (selected.value.length < maxActiveOptions)) {
77
+ selected.value?.push(option)
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ // Search
84
+ const searchStr = ref()
85
+ const filteredOptions = computed(() => {
86
+ if (!searchStr.value || searchStr.value.length === 0)
87
+ return options
88
+
89
+ return options.filter((option) => {
90
+ return searchString(option.label, searchStr.value)
91
+ })
92
+ })
93
+
94
+ // Render text inside the button
95
+ const renderPlaceholder = computed(() => {
96
+ // Render placeholder or a default select option
97
+ if (!selected.value || selected.value.length === 0)
98
+ return placeholder ?? 'Select an option'
99
+
100
+ // Selected values
101
+ if (single)
102
+ return selected.value[0].label
103
+
104
+ // If amount of selected exceeds the active capacity
105
+ if (selected.value.length > 3) {
106
+ return `${selected.value.length} selected`
107
+ }
108
+
109
+ // Just list ALL selected optionsx
110
+ return selected.value.map(o => o.label).join(', ')
111
+ })
112
+
113
+ onMounted(() => {
114
+ if (readonly && showClear) {
115
+ console.warn('[Select] readonly and showClear are mutually exclusive props. The clear button will not show up if required is present.')
116
+ }
117
+ })
118
+
119
+ const dropdownRef = useTemplateRef('dropdown')
120
+
121
+ function clearValue() {
122
+ selected.value = undefined
123
+ dropdownRef.value?.close()
124
+ }
125
+
126
+ const id = useId()
127
+ </script>
128
+
129
+ <template>
130
+ <div class="vui-input-container vui-select" :class="{ expand, required, readonly, disabled, 'has-errors': errors.length > 0 }">
131
+ <Dropdown ref="dropdown" :expand @close="trigger?.focus({ preventScroll: true })">
132
+ <template #trigger="{ toggle, isOpen }">
133
+ <div class="vui-input vui-select-trigger-content">
134
+ <label v-if="label" :for="id">{{ label }}</label>
135
+ <p v-if="hint" class="vui-input-hint">
136
+ {{ hint }}
137
+ </p>
138
+
139
+ <button
140
+ :id
141
+ ref="trigger"
142
+ class="vui-input-style vui-select-trigger-container"
143
+ :class="{ 'has-value': selected && selected.length > 0 }"
144
+ :disabled
145
+ @click="toggle"
146
+ >
147
+ <span>
148
+ {{ renderPlaceholder }}
149
+ </span>
150
+ <template v-if="showClear && !required && selected">
151
+ <div class="flex-1" />
152
+ <Button
153
+ plain
154
+ icon="ph:x"
155
+ square
156
+ size="s"
157
+
158
+ @click.stop="clearValue"
159
+ />
160
+ </template>
161
+ <Icon :icon="isOpen ? 'ph:caret-up' : 'ph:caret-down'" />
162
+ </button>
163
+ </div>
164
+ </template>
165
+
166
+ <template #default="{ close, isOpen }">
167
+ <DropdownTitle v-if="search" sticky>
168
+ <Input
169
+ v-model="searchStr"
170
+ placeholder="Search..."
171
+ :focus="isOpen"
172
+ expand
173
+ >
174
+ <template #start>
175
+ <Icon icon="ph:magnifying-glass" />
176
+ </template>
177
+ </Input>
178
+ </DropdownTitle>
179
+
180
+ <p v-if="filteredOptions.length === 0" class="vue-select-no-results">
181
+ No results...
182
+ </p>
183
+
184
+ <DropdownItem
185
+ v-for="option in filteredOptions"
186
+ :key="option.value"
187
+ :class="{ selected: selected?.find(v => v.value === option.value) }"
188
+ :icon="selected?.find(v => v.value === option.value) ? 'ph:check-bold' : ''"
189
+ @click="() => {
190
+ setValue(option)
191
+ // In single mode, close modal after clicking
192
+ if (single && !(required && selected && selected[0].value === option.value)) {
193
+ close()
194
+ }
195
+ }"
196
+ >
197
+ {{ option.label }}
198
+ </DropdownItem>
199
+ </template>
200
+ </Dropdown>
201
+
202
+ <p v-if="maxActiveOptions && !single" class="vui-input-limit">
203
+ {{ `${selected ? selected.length : 0}/${maxActiveOptions}` }}
204
+ </p>
205
+ <ul v-if="errors.length > 0" class="vui-input-errors">
206
+ <li v-for="err in errors" :key="err">
207
+ {{ err }}
208
+ </li>
209
+ </ul>
210
+ </div>
211
+ </template>