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
@@ -0,0 +1,55 @@
1
+ <script>
2
+ import { h, mergeProps } from 'vue'
3
+ import feather from 'feather-icons'
4
+
5
+ const validIcons = Object.keys(feather.icons)
6
+
7
+ export default {
8
+ props: {
9
+ name: {
10
+ type: String,
11
+ required: true,
12
+ validator(value) {
13
+ const valid = validIcons.includes(value)
14
+ if (!valid) {
15
+ console.warn(
16
+ `name property for feather-icon must be one of `,
17
+ validIcons
18
+ )
19
+ }
20
+ return valid
21
+ },
22
+ },
23
+ color: {
24
+ type: String,
25
+ default: null,
26
+ },
27
+ strokeWidth: {
28
+ type: Number,
29
+ default: 1.5,
30
+ },
31
+ },
32
+ render() {
33
+ let icon = feather.icons[this.name]
34
+ return h(
35
+ 'svg',
36
+ mergeProps(
37
+ icon.attrs,
38
+ {
39
+ fill: 'none',
40
+ stroke: 'currentColor',
41
+ color: this.color,
42
+ 'stroke-linecap': 'round',
43
+ 'stroke-linejoin': 'round',
44
+ 'stroke-width': this.strokeWidth,
45
+ width: null,
46
+ height: null,
47
+ class: [icon.attrs.class],
48
+ innerHTML: icon.contents,
49
+ },
50
+ this.$attrs
51
+ )
52
+ )
53
+ },
54
+ }
55
+ </script>
@@ -0,0 +1,220 @@
1
+ <template>
2
+ <div>
3
+ <input
4
+ ref="input"
5
+ type="file"
6
+ :accept="fileTypes"
7
+ class="hidden"
8
+ @change="onFileAdd"
9
+ />
10
+ <slot
11
+ v-bind="{
12
+ file,
13
+ uploading,
14
+ progress,
15
+ uploaded,
16
+ message,
17
+ error,
18
+ total,
19
+ success,
20
+ openFileSelector,
21
+ }"
22
+ />
23
+ </div>
24
+ </template>
25
+
26
+ <script>
27
+ class FileUploader {
28
+ constructor() {
29
+ this.listeners = {}
30
+ }
31
+
32
+ on(event, handler) {
33
+ this.listeners[event] = this.listeners[event] || []
34
+ this.listeners[event].push(handler)
35
+ }
36
+
37
+ trigger(event, data) {
38
+ let handlers = this.listeners[event] || []
39
+ handlers.forEach((handler) => {
40
+ handler.call(this, data)
41
+ })
42
+ }
43
+
44
+ upload(file, options) {
45
+ return new Promise((resolve, reject) => {
46
+ let xhr = new XMLHttpRequest()
47
+ xhr.upload.addEventListener('loadstart', () => {
48
+ this.trigger('start')
49
+ })
50
+ xhr.upload.addEventListener('progress', (e) => {
51
+ if (e.lengthComputable) {
52
+ this.trigger('progress', {
53
+ uploaded: e.loaded,
54
+ total: e.total,
55
+ })
56
+ }
57
+ })
58
+ xhr.upload.addEventListener('load', () => {
59
+ this.trigger('finish')
60
+ })
61
+ xhr.addEventListener('error', () => {
62
+ this.trigger('error')
63
+ reject()
64
+ })
65
+ xhr.onreadystatechange = () => {
66
+ if (xhr.readyState == XMLHttpRequest.DONE) {
67
+ let error
68
+ if (xhr.status === 200) {
69
+ let r = null
70
+ try {
71
+ r = JSON.parse(xhr.responseText)
72
+ } catch (e) {
73
+ r = xhr.responseText
74
+ }
75
+ let out = r.message || r
76
+ resolve(out)
77
+ } else if (xhr.status === 403) {
78
+ error = JSON.parse(xhr.responseText)
79
+ } else {
80
+ this.failed = true
81
+ try {
82
+ error = JSON.parse(xhr.responseText)
83
+ } catch (e) {
84
+ // pass
85
+ }
86
+ }
87
+ if (error && error.exc) {
88
+ console.error(JSON.parse(error.exc)[0])
89
+ }
90
+ reject(error)
91
+ }
92
+ }
93
+ xhr.open('POST', '/api/method/upload_file', true)
94
+ xhr.setRequestHeader('Accept', 'application/json')
95
+ if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') {
96
+ xhr.setRequestHeader('X-Melon-CSRF-Token', window.csrf_token)
97
+ }
98
+
99
+ let form_data = new FormData()
100
+ if (file) {
101
+ form_data.append('file', file, file.name)
102
+ }
103
+ form_data.append('is_private', +(options.private || 0))
104
+ form_data.append('folder', options.folder || 'Home')
105
+
106
+ if (options.file_url) {
107
+ form_data.append('file_url', options.file_url)
108
+ }
109
+
110
+ if (options.doctype && options.docname) {
111
+ form_data.append('doctype', options.doctype)
112
+ form_data.append('docname', options.docname)
113
+ if (options.fieldname) {
114
+ form_data.append('fieldname', options.fieldname)
115
+ }
116
+ }
117
+
118
+ if (options.method) {
119
+ form_data.append('method', options.method)
120
+ }
121
+
122
+ if (options.type) {
123
+ form_data.append('type', options.type)
124
+ }
125
+
126
+ xhr.send(form_data)
127
+ })
128
+ }
129
+ }
130
+
131
+ export default {
132
+ name: 'FileUploader',
133
+ props: ['fileTypes', 'uploadArgs', 'type', 'validateFile'],
134
+ data() {
135
+ return {
136
+ uploader: null,
137
+ uploading: false,
138
+ uploaded: 0,
139
+ error: null,
140
+ message: '',
141
+ total: 0,
142
+ file: null,
143
+ finishedUploading: false,
144
+ }
145
+ },
146
+ computed: {
147
+ progress() {
148
+ let value = Math.floor((this.uploaded / this.total) * 100)
149
+ return isNaN(value) ? 0 : value
150
+ },
151
+ success() {
152
+ return this.finishedUploading && !this.error
153
+ },
154
+ },
155
+ methods: {
156
+ openFileSelector() {
157
+ this.$refs['input'].click()
158
+ },
159
+ async onFileAdd(e) {
160
+ this.error = null
161
+ this.file = e.target.files[0]
162
+
163
+ if (this.file && this.validateFile) {
164
+ try {
165
+ let message = await this.validateFile(this.file)
166
+ if (message) {
167
+ this.error = message
168
+ }
169
+ } catch (error) {
170
+ this.error = error
171
+ }
172
+ }
173
+
174
+ if (!this.error) {
175
+ this.uploadFile(this.file)
176
+ }
177
+ },
178
+ async uploadFile(file) {
179
+ this.error = null
180
+ this.uploaded = 0
181
+ this.total = 0
182
+
183
+ this.uploader = new FileUploader()
184
+ this.uploader.on('start', () => {
185
+ this.uploading = true
186
+ })
187
+ this.uploader.on('progress', (data) => {
188
+ this.uploaded = data.uploaded
189
+ this.total = data.total
190
+ })
191
+ this.uploader.on('error', () => {
192
+ this.uploading = false
193
+ this.error = 'Error Uploading File'
194
+ })
195
+ this.uploader.on('finish', () => {
196
+ this.uploading = false
197
+ this.finishedUploading = true
198
+ })
199
+ this.uploader
200
+ .upload(file, this.uploadArgs || {})
201
+ .then((data) => {
202
+ this.$emit('success', data)
203
+ })
204
+ .catch((error) => {
205
+ this.uploading = false
206
+ let errorMessage = 'Error Uploading File'
207
+ if (error._server_messages) {
208
+ errorMessage = JSON.parse(
209
+ JSON.parse(error._server_messages)[0]
210
+ ).message
211
+ } else if (error.exc) {
212
+ errorMessage = JSON.parse(error.exc)[0].split('\n').slice(-2, -1)[0]
213
+ }
214
+ this.error = errorMessage
215
+ this.$emit('failure', error)
216
+ })
217
+ },
218
+ },
219
+ }
220
+ </script>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path
4
+ d="M16 32c8.837 0 16-7.163 16-16S24.837 0 16 0 0 7.163 0 16s7.163 16 16 16z"
5
+ fill="#59B179"
6
+ />
7
+ <path
8
+ d="M9.333 17.227l1.333 1.333 2.667 2.667 5.333-5.333 2.667-2.667 1.333-1.333"
9
+ stroke="#fff"
10
+ stroke-width="2"
11
+ stroke-miterlimit="10"
12
+ stroke-linecap="round"
13
+ stroke-linejoin="round"
14
+ />
15
+ </svg>
16
+ </template>
@@ -0,0 +1,169 @@
1
+ <template>
2
+ <label :class="[type == 'checkbox' ? 'flex' : 'block', $attrs.class]">
3
+ <span
4
+ v-if="label && type != 'checkbox'"
5
+ class="block mb-2 text-sm leading-4 text-gray-700"
6
+ >
7
+ {{ label }}
8
+ </span>
9
+ <input
10
+ v-if="
11
+ ['text', 'number', 'checkbox', 'email', 'password', 'date'].includes(
12
+ type
13
+ )
14
+ "
15
+ v-bind="inputAttributes"
16
+ class="placeholder-gray-500"
17
+ ref="input"
18
+ :class="[
19
+ {
20
+ 'block w-full form-input': type != 'checkbox',
21
+ 'form-checkbox': type == 'checkbox',
22
+ },
23
+ inputClass,
24
+ ]"
25
+ :type="type || 'text'"
26
+ :disabled="disabled"
27
+ :placeholder="placeholder"
28
+ :value="passedInputValue"
29
+ />
30
+ <textarea
31
+ v-if="type === 'textarea'"
32
+ v-bind="inputAttributes"
33
+ :class="['block w-full resize-none form-textarea', inputClass]"
34
+ ref="input"
35
+ :value="passedInputValue"
36
+ :disabled="disabled"
37
+ :rows="rows || 3"
38
+ @blur="$emit('blur', $event)"
39
+ ></textarea>
40
+ <select
41
+ v-bind="inputAttributes"
42
+ class="block w-full form-select"
43
+ ref="input"
44
+ v-if="type === 'select'"
45
+ :disabled="disabled"
46
+ >
47
+ <option
48
+ v-for="option in selectOptions"
49
+ :key="option.value"
50
+ :value="option.value"
51
+ :selected="passedInputValue === option.value"
52
+ >
53
+ {{ option.label }}
54
+ </option>
55
+ </select>
56
+ <span
57
+ v-if="label && type == 'checkbox'"
58
+ class="inline-block ml-2 text-base leading-4"
59
+ >
60
+ {{ label }}
61
+ </span>
62
+ </label>
63
+ </template>
64
+
65
+ <script>
66
+ import { debounce } from 'pimelon-ui'
67
+
68
+ export default {
69
+ name: 'Input',
70
+ inheritAttrs: false,
71
+ expose: ['getInputValue'],
72
+ props: {
73
+ label: {
74
+ type: String,
75
+ },
76
+ type: {
77
+ type: String,
78
+ validator(value) {
79
+ let isValid = [
80
+ 'text',
81
+ 'number',
82
+ 'checkbox',
83
+ 'textarea',
84
+ 'select',
85
+ 'email',
86
+ 'password',
87
+ 'date',
88
+ ].includes(value)
89
+ if (!isValid) {
90
+ console.warn(`Invalid value "${value}" for "type" prop for Input`)
91
+ }
92
+ return isValid
93
+ },
94
+ },
95
+ modelValue: {
96
+ type: [String, Number, Boolean, Object, Array],
97
+ },
98
+ inputClass: {
99
+ type: [String, Array, Object],
100
+ },
101
+ debounce: {
102
+ type: Number,
103
+ },
104
+ options: {
105
+ type: Array,
106
+ },
107
+ disabled: {
108
+ type: Boolean,
109
+ },
110
+ rows: {
111
+ type: Number,
112
+ },
113
+ placeholder: {
114
+ type: String,
115
+ },
116
+ },
117
+ emits: ['blur', 'input', 'change', 'update:modelValue'],
118
+ methods: {
119
+ focus() {
120
+ this.$refs.input.focus()
121
+ },
122
+ blur() {
123
+ this.$refs.input.blur()
124
+ },
125
+ getInputValue(e) {
126
+ let $input = e ? e.target : this.$refs.input
127
+ let value = $input.value
128
+ if (this.type == 'checkbox') {
129
+ value = $input.checked
130
+ }
131
+ return value
132
+ },
133
+ },
134
+ computed: {
135
+ passedInputValue() {
136
+ if ('value' in this.$attrs) {
137
+ return this.$attrs.value
138
+ }
139
+ return this.modelValue || null
140
+ },
141
+ inputAttributes() {
142
+ let onInput = (e) => {
143
+ this.$emit('input', this.getInputValue(e))
144
+ }
145
+ if (this.debounce) {
146
+ onInput = debounce(onInput, this.debounce)
147
+ }
148
+ return Object.assign({}, this.$attrs, {
149
+ onInput,
150
+ onChange: (e) => {
151
+ this.$emit('change', this.getInputValue(e))
152
+ this.$emit('update:modelValue', this.getInputValue(e))
153
+ },
154
+ })
155
+ },
156
+ selectOptions() {
157
+ return this.options.map((option) => {
158
+ if (typeof option === 'string') {
159
+ return {
160
+ label: option,
161
+ value: option,
162
+ }
163
+ }
164
+ return option
165
+ })
166
+ },
167
+ },
168
+ }
169
+ </script>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <component
3
+ :is="isExternal ? 'a' : 'router-link'"
4
+ v-bind="attributes"
5
+ class="text-blue-500 cursor-pointer hover:text-blue-600"
6
+ >
7
+ <slot></slot>
8
+ </component>
9
+ </template>
10
+
11
+ <script>
12
+ export default {
13
+ props: ['to'],
14
+ computed: {
15
+ attributes() {
16
+ return {
17
+ ...this.$attrs,
18
+ target: this.isExternal ? '_blank' : null,
19
+ to: !this.isExternal ? this.to : undefined,
20
+ href: this.isExternal ? this.to : undefined,
21
+ }
22
+ },
23
+ isExternal() {
24
+ return this.to.startsWith('http')
25
+ },
26
+ },
27
+ }
28
+ </script>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <div class="flex items-center justify-between py-3">
3
+ <div>
4
+ <h3 class="text-base font-medium text-gray-900">
5
+ {{ title }}
6
+ </h3>
7
+ <div class="mt-1" v-if="secondaryText || $slots.subtitle">
8
+ <template v-if="secondaryText">
9
+ <span class="text-base text-gray-600" v-html="secondaryText" />
10
+ </template>
11
+ <slot v-if="$slots.subtitle" name="subtitle" />
12
+ </div>
13
+ </div>
14
+ <slot name="actions"></slot>
15
+ </div>
16
+ </template>
17
+ <script>
18
+ export default {
19
+ name: 'ListItem',
20
+ props: ['title', 'subtitle'],
21
+ computed: {
22
+ secondaryText() {
23
+ let text = this.subtitle || ''
24
+ return text.replace('\n', '<br>')
25
+ },
26
+ },
27
+ }
28
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <Spinner class="max-w-xs" />
3
+ </template>
4
+ <script>
5
+ import Spinner from './Spinner.vue'
6
+ export default {
7
+ name: 'LoadingIndicator',
8
+ components: {
9
+ Spinner,
10
+ },
11
+ }
12
+ </script>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <div class="flex items-center text-base text-gray-500">
3
+ <LoadingIndicator /> {{ text }}
4
+ </div>
5
+ </template>
6
+ <script>
7
+ import LoadingIndicator from './LoadingIndicator.vue'
8
+
9
+ export default {
10
+ name: 'Loading',
11
+ props: {
12
+ text: {
13
+ type: String,
14
+ default: 'Loading...',
15
+ },
16
+ },
17
+ components: {
18
+ LoadingIndicator,
19
+ },
20
+ }
21
+ </script>
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <teleport to="#modals">
3
+ <div
4
+ v-show="show"
5
+ class="fixed inset-0 flex items-center justify-center px-4 py-4"
6
+ >
7
+ <div
8
+ v-show="show"
9
+ class="fixed inset-0 transition-opacity"
10
+ @click="onBackdropClick"
11
+ >
12
+ <div class="absolute inset-0 bg-gray-900 opacity-75"></div>
13
+ </div>
14
+
15
+ <div
16
+ v-show="show"
17
+ class="w-full overflow-auto transition-all transform bg-white rounded-lg shadow-xl"
18
+ :class="!full ? 'sm:max-w-lg' : ''"
19
+ style="max-height: 95vh"
20
+ >
21
+ <slot></slot>
22
+ </div>
23
+ </div>
24
+ </teleport>
25
+ </template>
26
+
27
+ <script>
28
+ export default {
29
+ name: 'Modal',
30
+ props: {
31
+ show: {
32
+ type: Boolean,
33
+ default: false,
34
+ },
35
+ dismissable: {
36
+ type: Boolean,
37
+ default: true,
38
+ },
39
+ full: {
40
+ type: Boolean,
41
+ default: false,
42
+ },
43
+ },
44
+ emits: ['change'],
45
+ created() {
46
+ if (!this.dismissable) return
47
+ this.escapeListener = (e) => {
48
+ if (e.key === 'Escape') {
49
+ this.hide()
50
+ }
51
+ }
52
+ document.addEventListener('keydown', this.escapeListener)
53
+ },
54
+ unmounted() {
55
+ document.removeEventListener('keydown', this.escapeListener)
56
+ },
57
+ methods: {
58
+ onBackdropClick() {
59
+ if (!this.dismissable) return
60
+ this.hide()
61
+ },
62
+ hide() {
63
+ this.$emit('change', false)
64
+ },
65
+ },
66
+ }
67
+ </script>