pimelon-ui 0.0.19

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 (39) hide show
  1. package/license.md +0 -0
  2. package/package.json +33 -0
  3. package/readme.md +0 -0
  4. package/src/components/Alert.vue +57 -0
  5. package/src/components/Avatar.vue +61 -0
  6. package/src/components/Badge.vue +40 -0
  7. package/src/components/Button.vue +134 -0
  8. package/src/components/Card.vue +37 -0
  9. package/src/components/Dialog.vue +195 -0
  10. package/src/components/Dropdown.vue +113 -0
  11. package/src/components/ErrorMessage.vue +15 -0
  12. package/src/components/FeatherIcon.vue +55 -0
  13. package/src/components/FileUploader.vue +220 -0
  14. package/src/components/GreenCheckIcon.vue +16 -0
  15. package/src/components/Input.vue +169 -0
  16. package/src/components/Link.vue +28 -0
  17. package/src/components/ListItem.vue +28 -0
  18. package/src/components/LoadingIndicator.vue +12 -0
  19. package/src/components/LoadingText.vue +21 -0
  20. package/src/components/Modal.vue +67 -0
  21. package/src/components/Popover.vue +192 -0
  22. package/src/components/Resource.vue +21 -0
  23. package/src/components/Spinner.vue +27 -0
  24. package/src/components/SuccessMessage.vue +15 -0
  25. package/src/components/TextEditor/Menu.vue +32 -0
  26. package/src/components/TextEditor/TextEditor.vue +193 -0
  27. package/src/components/TextEditor/commands.js +79 -0
  28. package/src/components/TextEditor/index.js +1 -0
  29. package/src/components/Toast.vue +167 -0
  30. package/src/directives/onOutsideClick.js +28 -0
  31. package/src/index.js +33 -0
  32. package/src/style.css +15 -0
  33. package/src/utils/call.js +98 -0
  34. package/src/utils/debounce.js +15 -0
  35. package/src/utils/plugin.js +24 -0
  36. package/src/utils/resources.js +510 -0
  37. package/src/utils/socketio.js +9 -0
  38. package/src/utils/tailwind.config.js +110 -0
  39. package/src/utils/vite-dev-server.js +14 -0
package/license.md ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "pimelon-ui",
3
+ "version": "0.0.19",
4
+ "description": "A set of components and utilities for rapid UI development",
5
+ "main": "./src/index.js",
6
+ "scripts": {
7
+ "test": "npx prettier --check ./src",
8
+ "prettier": "npx prettier -w ./src"
9
+ },
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/amonak/pimelon-ui.git"
16
+ },
17
+ "author": "Alphamonak Solutions",
18
+ "dependencies": {
19
+ "@headlessui/vue": "^1.5.0",
20
+ "@popperjs/core": "^2.11.2",
21
+ "@tailwindcss/forms": "^0.4.0",
22
+ "@tailwindcss/typography": "^0.5.0",
23
+ "@tiptap/extension-image": "^2.0.0-beta.27",
24
+ "@tiptap/extension-placeholder": "^2.0.0-beta.48",
25
+ "@tiptap/starter-kit": "^2.0.0-beta.183",
26
+ "@tiptap/vue-3": "^2.0.0-beta.90",
27
+ "autoprefixer": "^10.4.2",
28
+ "feather-icons": "^4.28.0",
29
+ "postcss": "^8.4.5",
30
+ "socket.io-client": "^2.4.0",
31
+ "tailwindcss": "^3.0.12"
32
+ }
33
+ }
package/readme.md ADDED
File without changes
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div class="block w-full">
3
+ <div
4
+ class="items-start px-4 md:px-5 py-3.5 text-base rounded-md flex"
5
+ :class="classes"
6
+ >
7
+ <svg
8
+ width="24"
9
+ height="24"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ >
14
+ <path
15
+ opacity="0.8"
16
+ fill-rule="evenodd"
17
+ clip-rule="evenodd"
18
+ d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2ZM12 10.5C12.5523 10.5 13 10.9477 13 11.5V17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17V11.5C11 10.9477 11.4477 10.5 12 10.5ZM13 7.99976C13 7.44747 12.5523 6.99976 12 6.99976C11.4477 6.99976 11 7.44747 11 7.99976V8.1C11 8.65228 11.4477 9.1 12 9.1C12.5523 9.1 13 8.65228 13 8.1V7.99976Z"
19
+ fill="#318AD8"
20
+ />
21
+ </svg>
22
+ <div class="w-full ml-2">
23
+ <div class="flex flex-col md:items-baseline md:flex-row">
24
+ <h3 class="text-lg font-medium text-gray-900" v-if="title">
25
+ {{ title }}
26
+ </h3>
27
+ <div class="mt-1 md:mt-0 md:ml-2">
28
+ <slot></slot>
29
+ </div>
30
+ <div class="mt-3 md:mt-0 md:ml-auto">
31
+ <slot name="actions"></slot>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <script>
40
+ export default {
41
+ name: 'Alert',
42
+ props: {
43
+ title: String,
44
+ type: {
45
+ type: String,
46
+ default: 'warning',
47
+ },
48
+ },
49
+ computed: {
50
+ classes() {
51
+ return {
52
+ warning: 'text-gray-700 bg-blue-50',
53
+ }[this.type]
54
+ },
55
+ },
56
+ }
57
+ </script>
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <div class="overflow-hidden" :class="styleClasses">
3
+ <img
4
+ v-if="imageURL"
5
+ :src="imageURL"
6
+ class="object-cover"
7
+ :class="styleClasses"
8
+ />
9
+ <div
10
+ v-else
11
+ class="flex items-center justify-center w-full h-full text-gray-600 uppercase bg-gray-200"
12
+ :class="{ sm: 'text-xs', md: 'text-base', lg: 'text-lg' }[size]"
13
+ >
14
+ {{ label && label[0] }}
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script>
20
+ const validShapes = ['square', 'circle']
21
+
22
+ export default {
23
+ name: 'Avatar',
24
+ props: {
25
+ imageURL: String,
26
+ label: String,
27
+ size: {
28
+ default: 'md',
29
+ },
30
+ shape: {
31
+ default: 'circle',
32
+ validator(value) {
33
+ const valid = validShapes.includes(value)
34
+ if (!valid) {
35
+ console.warn(
36
+ `shape property for <Avatar /> must be one of `,
37
+ validShapes
38
+ )
39
+ }
40
+ return valid
41
+ },
42
+ },
43
+ },
44
+ computed: {
45
+ styleClasses() {
46
+ const sizeClasses = {
47
+ sm: 'w-5 h-5',
48
+ md: 'w-8 h-8',
49
+ lg: 'w-12 h-12',
50
+ }[this.size]
51
+
52
+ const shapeClass = {
53
+ circle: 'rounded-full',
54
+ square: 'rounded-lg',
55
+ }[this.shape]
56
+
57
+ return `${shapeClass} ${sizeClasses}`
58
+ },
59
+ },
60
+ }
61
+ </script>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <span
3
+ class="inline-block px-3 py-1 text-xs font-medium rounded-md cursor-default"
4
+ :class="classes"
5
+ >
6
+ <slot>{{ status }}</slot>
7
+ </span>
8
+ </template>
9
+ <script>
10
+ export default {
11
+ name: 'Badge',
12
+ props: ['color', 'status'],
13
+ computed: {
14
+ classes() {
15
+ let color = this.color
16
+ if (!color && this.status) {
17
+ color = {
18
+ Pending: 'yellow',
19
+ Running: 'yellow',
20
+ Success: 'green',
21
+ Failure: 'red',
22
+ Active: 'green',
23
+ Broken: 'red',
24
+ Updating: 'blue',
25
+ Rejected: 'red',
26
+ Published: 'green',
27
+ Approved: 'green',
28
+ }[this.status]
29
+ }
30
+ return {
31
+ gray: 'text-gray-700 bg-gray-50',
32
+ green: 'text-green-700 bg-green-50',
33
+ red: 'text-red-700 bg-red-50',
34
+ yellow: 'text-yellow-700 bg-yellow-50',
35
+ blue: 'text-blue-700 bg-blue-50',
36
+ }[color || 'gray']
37
+ },
38
+ },
39
+ }
40
+ </script>
@@ -0,0 +1,134 @@
1
+ <template>
2
+ <button
3
+ v-bind="$attrs"
4
+ :class="buttonClasses"
5
+ @click="handleClick"
6
+ :disabled="isDisabled"
7
+ >
8
+ <LoadingIndicator
9
+ v-if="loading"
10
+ :class="{
11
+ 'text-white': appearance == 'primary',
12
+ 'text-gray-600': appearance == 'secondary',
13
+ 'text-red-200': appearance == 'danger',
14
+ }"
15
+ />
16
+ <FeatherIcon
17
+ v-else-if="iconLeft"
18
+ :name="iconLeft"
19
+ class="w-4 h-4 mr-1.5"
20
+ aria-hidden="true"
21
+ />
22
+ <template v-if="loading && loadingText">{{ loadingText }}</template>
23
+ <template v-else-if="icon">
24
+ <FeatherIcon :name="icon" class="w-4 h-4" :aria-label="label" />
25
+ </template>
26
+ <span :class="icon ? 'sr-only' : ''">
27
+ <slot>
28
+ {{ label }}
29
+ </slot>
30
+ </span>
31
+ <FeatherIcon
32
+ v-if="iconRight"
33
+ :name="iconRight"
34
+ class="w-4 h-4 ml-2"
35
+ aria-hidden="true"
36
+ />
37
+ </button>
38
+ </template>
39
+ <script>
40
+ import FeatherIcon from './FeatherIcon.vue'
41
+ import LoadingIndicator from './LoadingIndicator.vue'
42
+
43
+ const ValidAppearances = ['primary', 'secondary', 'danger', 'white', 'minimal']
44
+
45
+ export default {
46
+ name: 'Button',
47
+ components: {
48
+ FeatherIcon,
49
+ LoadingIndicator,
50
+ },
51
+ props: {
52
+ label: {
53
+ type: String,
54
+ default: null,
55
+ },
56
+ appearance: {
57
+ type: String,
58
+ default: 'secondary',
59
+ validator: (value) => {
60
+ return ValidAppearances.includes(value)
61
+ },
62
+ },
63
+ disabled: {
64
+ type: Boolean,
65
+ default: false,
66
+ },
67
+ active: {
68
+ type: Boolean,
69
+ default: false,
70
+ },
71
+ iconLeft: {
72
+ type: String,
73
+ default: null,
74
+ },
75
+ iconRight: {
76
+ type: String,
77
+ default: null,
78
+ },
79
+ icon: {
80
+ type: String,
81
+ default: null,
82
+ },
83
+ loading: {
84
+ type: Boolean,
85
+ default: false,
86
+ },
87
+ loadingText: {
88
+ type: String,
89
+ default: null,
90
+ },
91
+ route: {},
92
+ link: {
93
+ type: String,
94
+ default: null,
95
+ },
96
+ },
97
+ computed: {
98
+ buttonClasses() {
99
+ let appearanceClasses = {
100
+ primary:
101
+ 'bg-blue-500 hover:bg-blue-600 text-white focus:ring-2 focus:ring-offset-2 focus:ring-blue-500',
102
+ secondary:
103
+ 'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:ring-2 focus:ring-offset-2 focus:ring-gray-500',
104
+ danger:
105
+ 'bg-red-500 hover:bg-red-400 text-white focus:ring-2 focus:ring-offset-2 focus:ring-red-500',
106
+ white:
107
+ 'bg-white text-gray-900 border hover:bg-gray-50 focus:ring-2 focus:ring-offset-2 focus:ring-gray-400',
108
+ minimal: `active:bg-gray-200 focus:bg-gray-200 text-gray-900 ${
109
+ this.active ? 'bg-gray-200' : 'bg-transparent hover:bg-gray-200'
110
+ }`,
111
+ }
112
+ return [
113
+ 'inline-flex items-center justify-center text-base leading-5 rounded-md transition-colors focus:outline-none',
114
+ this.icon ? 'p-1.5' : 'px-3 py-1',
115
+ this.isDisabled
116
+ ? 'opacity-50 cursor-not-allowed pointer-events-none'
117
+ : '',
118
+ appearanceClasses[this.appearance],
119
+ ]
120
+ },
121
+ isDisabled() {
122
+ return this.disabled || this.loading
123
+ },
124
+ },
125
+ methods: {
126
+ handleClick() {
127
+ if (this.route && this.$router) {
128
+ this.route && this.$router.push(this.route)
129
+ }
130
+ this.link ? window.open(this.link, '_blank') : null
131
+ },
132
+ },
133
+ }
134
+ </script>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <div class="flex flex-col px-6 py-5 bg-white border rounded-lg shadow">
3
+ <div class="flex items-baseline justify-between">
4
+ <div class="flex items-baseline space-x-2">
5
+ <div class="flex items-center space-x-2" v-if="$slots['actions-left']">
6
+ <slot name="actions-left"></slot>
7
+ </div>
8
+ <h2 class="text-xl font-semibold">{{ title }}</h2>
9
+ </div>
10
+ <div class="flex items-center space-x-2" v-if="$slots['actions']">
11
+ <slot name="actions"></slot>
12
+ </div>
13
+ </div>
14
+ <p class="text-base text-gray-600 mt-1.5" v-if="subtitle">
15
+ {{ subtitle }}
16
+ </p>
17
+ <div
18
+ v-if="loading"
19
+ class="flex flex-col items-center justify-center flex-auto mt-4 rounded-md"
20
+ >
21
+ <LoadingText />
22
+ </div>
23
+ <div class="flex-auto mt-4 overflow-auto" v-else-if="$slots['default']">
24
+ <slot></slot>
25
+ </div>
26
+ </div>
27
+ </template>
28
+ <script>
29
+ import LoadingText from './LoadingText.vue'
30
+ export default {
31
+ name: 'Card',
32
+ props: ['title', 'subtitle', 'loading'],
33
+ components: {
34
+ LoadingText,
35
+ },
36
+ }
37
+ </script>
@@ -0,0 +1,195 @@
1
+ <template>
2
+ <TransitionRoot as="template" :show="open">
3
+ <HDialog
4
+ as="div"
5
+ class="fixed inset-0 z-10 overflow-y-auto"
6
+ @close="open = false"
7
+ >
8
+ <div
9
+ class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0"
10
+ >
11
+ <TransitionChild
12
+ as="template"
13
+ enter="ease-out duration-300"
14
+ enter-from="opacity-0"
15
+ enter-to="opacity-100"
16
+ leave="ease-in duration-200"
17
+ leave-from="opacity-100"
18
+ leave-to="opacity-0"
19
+ >
20
+ <DialogOverlay
21
+ class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
22
+ />
23
+ </TransitionChild>
24
+
25
+ <!-- This element is to trick the browser into centering the modal contents. -->
26
+ <span
27
+ class="hidden sm:inline-block sm:align-middle sm:h-screen"
28
+ aria-hidden="true"
29
+ >
30
+ &#8203;
31
+ </span>
32
+ <TransitionChild
33
+ as="template"
34
+ enter="ease-out duration-300"
35
+ enter-from="opacity-0 translate-y-4 sm:-translate-y-12 sm:scale-95"
36
+ enter-to="opacity-100 translate-y-0 sm:scale-100"
37
+ leave="ease-in duration-200"
38
+ leave-from="opacity-100 translate-y-0 sm:scale-100"
39
+ leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
40
+ >
41
+ <div
42
+ class="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
43
+ >
44
+ <slot name="body">
45
+ <slot name="body-main">
46
+ <div class="px-4 py-5 bg-white sm:p-6">
47
+ <div class="flex flex-col sm:flex-row">
48
+ <div
49
+ v-if="icon"
50
+ class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto mb-3 rounded-full sm:mx-0 sm:h-9 sm:w-9 sm:mb-0 sm:mr-4"
51
+ :class="{
52
+ 'bg-yellow-100': icon.appearance === 'warning',
53
+ 'bg-blue-100': icon.appearance === 'info',
54
+ 'bg-red-100': icon.appearance === 'danger',
55
+ 'bg-green-100': icon.appearance === 'success',
56
+ }"
57
+ >
58
+ <FeatherIcon
59
+ :name="icon.name"
60
+ class="w-6 h-6 text-red-600 sm:w-5 sm:h-5"
61
+ :class="{
62
+ 'text-yellow-600': icon.appearance === 'warning',
63
+ 'text-blue-600': icon.appearance === 'info',
64
+ 'text-red-600': icon.appearance === 'danger',
65
+ 'text-green-600': icon.appearance === 'success',
66
+ }"
67
+ aria-hidden="true"
68
+ />
69
+ </div>
70
+ <div class="flex-1 text-center sm:text-left">
71
+ <DialogTitle as="header">
72
+ <slot name="body-title">
73
+ <h3
74
+ class="mb-2 text-lg font-medium leading-6 text-gray-900"
75
+ >
76
+ {{ options.title || 'Untitled' }}
77
+ </h3>
78
+ </slot>
79
+ </DialogTitle>
80
+
81
+ <slot name="body-content">
82
+ <p class="text-sm text-gray-600" v-if="options.message">
83
+ {{ options.message }}
84
+ </p>
85
+ </slot>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </slot>
90
+ <div
91
+ class="px-4 py-3 space-y-2 sm:space-x-reverse sm:space-x-3 sm:space-y-0 bg-gray-50 sm:px-6 sm:flex sm:flex-row-reverse"
92
+ v-if="options?.actions || $slots.actions"
93
+ >
94
+ <slot name="actions" v-bind="{ close: () => (open = false) }">
95
+ <Button
96
+ class="w-full sm:w-max"
97
+ v-for="action in options.actions"
98
+ :key="action.label"
99
+ :loading="action.loading"
100
+ v-bind="action"
101
+ @click="handleAction(action)"
102
+ >
103
+ {{ action.label }}
104
+ </Button>
105
+ </slot>
106
+ </div>
107
+ </slot>
108
+ </div>
109
+ </TransitionChild>
110
+ </div>
111
+ </HDialog>
112
+ </TransitionRoot>
113
+ </template>
114
+
115
+ <script>
116
+ import { computed } from 'vue'
117
+ import {
118
+ Dialog as HDialog,
119
+ DialogOverlay,
120
+ DialogTitle,
121
+ TransitionChild,
122
+ TransitionRoot,
123
+ } from '@headlessui/vue'
124
+ import { Button, FeatherIcon } from 'pimelon-ui'
125
+
126
+ export default {
127
+ name: 'Dialog',
128
+ props: {
129
+ modelValue: {
130
+ type: Boolean,
131
+ required: true,
132
+ },
133
+ options: {
134
+ type: Object,
135
+ default() {
136
+ return {}
137
+ },
138
+ },
139
+ },
140
+ emits: ['update:modelValue', 'close'],
141
+ components: {
142
+ HDialog,
143
+ DialogOverlay,
144
+ DialogTitle,
145
+ TransitionChild,
146
+ TransitionRoot,
147
+ Button,
148
+ FeatherIcon,
149
+ },
150
+ setup(props, { emit }) {
151
+ let open = computed({
152
+ get: () => props.modelValue,
153
+ set: (val) => {
154
+ emit('update:modelValue', val)
155
+ if (!val) {
156
+ emit('close')
157
+ }
158
+ },
159
+ })
160
+ return {
161
+ open,
162
+ }
163
+ },
164
+ methods: {
165
+ handleAction(action) {
166
+ let close = () => (this.open = false)
167
+ if (action.handler && typeof action.handler === 'function') {
168
+ action.loading = true
169
+ let result = action.handler({ close })
170
+ if (result && result.then) {
171
+ result.then(() => (action.loading = false))
172
+ } else {
173
+ action.loading = false
174
+ }
175
+ } else {
176
+ close()
177
+ }
178
+ },
179
+ },
180
+ computed: {
181
+ icon() {
182
+ if (!this.options?.icon) return null
183
+
184
+ let icon = this.options.icon
185
+ if (typeof icon === 'string') {
186
+ icon = {
187
+ name: icon,
188
+ type: 'info',
189
+ }
190
+ }
191
+ return icon
192
+ },
193
+ },
194
+ }
195
+ </script>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <Menu as="div" class="relative inline-block text-left" v-slot="{ open }">
3
+ <MenuButton as="div">
4
+ <slot v-if="$slots.default" v-bind="{ open }"></slot>
5
+ <Button v-else v-bind="button" :active="open">
6
+ {{ button ? button?.label || null : 'Options' }}
7
+ </Button>
8
+ </MenuButton>
9
+
10
+ <transition
11
+ enter-active-class="transition duration-100 ease-out"
12
+ enter-from-class="transform scale-95 opacity-0"
13
+ enter-to-class="transform scale-100 opacity-100"
14
+ leave-active-class="transition duration-75 ease-in"
15
+ leave-from-class="transform scale-100 opacity-100"
16
+ leave-to-class="transform scale-95 opacity-0"
17
+ >
18
+ <MenuItems
19
+ class="absolute z-10 mt-2 bg-white divide-y divide-gray-100 rounded-md shadow-lg min-w-40 ring-1 ring-black ring-opacity-5 focus:outline-none"
20
+ :class="
21
+ placement === 'left'
22
+ ? 'left-0 origin-top-left'
23
+ : 'right-0 origin-top-right'
24
+ "
25
+ >
26
+ <div v-for="group in groups" :key="group.key" class="px-1 py-1">
27
+ <div
28
+ v-if="group.group && !group.hideLabel"
29
+ class="px-2 py-1 text-xs font-semibold tracking-wider text-gray-500 uppercase"
30
+ >
31
+ {{ group.group }}
32
+ </div>
33
+ <MenuItem
34
+ v-for="item in group.items"
35
+ :key="item.label"
36
+ v-slot="{ active }"
37
+ >
38
+ <button
39
+ :class="[
40
+ active ? 'bg-gray-100' : 'text-gray-900',
41
+ 'group flex rounded-md items-center w-full px-2 py-2 text-sm',
42
+ ]"
43
+ @click="item.onClick"
44
+ >
45
+ <FeatherIcon
46
+ v-if="item.icon"
47
+ :name="item.icon"
48
+ class="flex-shrink-0 w-4 h-4 mr-2 text-gray-500"
49
+ aria-hidden="true"
50
+ />
51
+ <span class="whitespace-nowrap">
52
+ {{ item.label }}
53
+ </span>
54
+ </button>
55
+ </MenuItem>
56
+ </div>
57
+ </MenuItems>
58
+ </transition>
59
+ </Menu>
60
+ </template>
61
+
62
+ <script>
63
+ import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
64
+ import { FeatherIcon } from 'pimelon-ui'
65
+
66
+ export default {
67
+ name: 'NewDropdown',
68
+ props: ['button', 'options', 'placement'],
69
+ components: {
70
+ Menu,
71
+ MenuButton,
72
+ MenuItems,
73
+ MenuItem,
74
+ FeatherIcon,
75
+ },
76
+ methods: {
77
+ normalizeDropdownItem(option) {
78
+ let onClick = option.handler || null
79
+ if (!onClick && option.route && this.$router) {
80
+ onClick = () => this.$router.push(option.route)
81
+ }
82
+ return {
83
+ label: option.label,
84
+ icon: option.icon,
85
+ group: option.group,
86
+ onClick,
87
+ }
88
+ },
89
+ filterOptions(options) {
90
+ return (options || [])
91
+ .filter(Boolean)
92
+ .filter((option) => (option.condition ? option.condition() : true))
93
+ .map((option) => this.normalizeDropdownItem(option))
94
+ },
95
+ },
96
+ computed: {
97
+ groups() {
98
+ let groups = this.options[0]?.group
99
+ ? this.options
100
+ : [{ group: '', items: this.options }]
101
+
102
+ return groups.map((group, i) => {
103
+ return {
104
+ key: i,
105
+ group: group.group,
106
+ hideLabel: group.hideLabel || false,
107
+ items: this.filterOptions(group.items),
108
+ }
109
+ })
110
+ },
111
+ },
112
+ }
113
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div
3
+ v-if="message"
4
+ class="text-sm text-red-600 whitespace-pre-line"
5
+ role="alert"
6
+ v-html="message"
7
+ ></div>
8
+ </template>
9
+
10
+ <script>
11
+ export default {
12
+ name: 'ErrorMessage',
13
+ props: ['message'],
14
+ }
15
+ </script>