pimelon-ui 0.0.57 → 0.0.84

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 (38) hide show
  1. package/package.json +5 -2
  2. package/src/components/Autocomplete.vue +51 -21
  3. package/src/components/Avatar.vue +1 -0
  4. package/src/components/Badge.vue +15 -19
  5. package/src/components/Button.vue +4 -0
  6. package/src/components/Dialog.vue +29 -4
  7. package/src/components/Dropdown.vue +15 -10
  8. package/src/components/ErrorMessage.vue +10 -1
  9. package/src/components/Input.vue +77 -47
  10. package/src/components/TextEditor/InsertImage.vue +8 -6
  11. package/src/components/TextEditor/InsertLink.vue +67 -0
  12. package/src/components/TextEditor/InsertVideo.vue +94 -0
  13. package/src/components/TextEditor/MentionList.vue +17 -15
  14. package/src/components/TextEditor/Menu.vue +31 -71
  15. package/src/components/TextEditor/TextEditor.vue +66 -156
  16. package/src/components/TextEditor/TextEditorBubbleMenu.vue +69 -0
  17. package/src/components/TextEditor/TextEditorFixedMenu.vue +71 -0
  18. package/src/components/TextEditor/TextEditorFloatingMenu.vue +55 -0
  19. package/src/components/TextEditor/commands.js +12 -2
  20. package/src/components/TextEditor/icons/video-add-line.vue +14 -0
  21. package/src/components/TextEditor/index.js +4 -0
  22. package/src/components/TextEditor/utils.js +11 -0
  23. package/src/components/TextEditor/video-extension.js +60 -0
  24. package/src/components/Toast.vue +1 -1
  25. package/src/directives/onOutsideClick.js +17 -11
  26. package/src/directives/visibility.js +24 -0
  27. package/src/index.js +2 -1
  28. package/src/resources/documentResource.js +192 -0
  29. package/src/resources/index.js +3 -0
  30. package/src/resources/listResource.js +300 -0
  31. package/src/resources/local.js +10 -0
  32. package/src/resources/plugin.js +111 -0
  33. package/src/resources/resources.js +194 -0
  34. package/src/utils/markdown.js +29 -0
  35. package/src/utils/plugin.js +2 -2
  36. package/src/utils/socketio.js +1 -1
  37. package/src/utils/tailwind.config.js +7 -0
  38. package/src/utils/resources.js +0 -661
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "pimelon-ui",
3
- "version": "0.0.57",
3
+ "version": "0.0.84",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
7
7
  "test": "npx prettier --check ./src",
8
8
  "prettier": "npx prettier -w ./src",
9
9
  "prepare": "husky install",
10
- "bump-and-release": "yarn version --patch && git push && git push --tags"
10
+ "bump-and-release": "git pull --rebase origin main && yarn version --patch && git push && git push --tags"
11
11
  },
12
12
  "files": [
13
13
  "src"
@@ -33,10 +33,13 @@
33
33
  "@tiptap/extension-table-row": "^2.0.0-beta.22",
34
34
  "@tiptap/extension-text-align": "^2.0.0-beta.31",
35
35
  "@tiptap/starter-kit": "^2.0.0-beta.191",
36
+ "@tiptap/suggestion": "^2.0.0-beta.195",
36
37
  "@tiptap/vue-3": "^2.0.0-beta.96",
37
38
  "autoprefixer": "^10.4.2",
38
39
  "feather-icons": "^4.28.0",
40
+ "idb-keyval": "^6.2.0",
39
41
  "postcss": "^8.4.5",
42
+ "showdown": "^2.1.0",
40
43
  "socket.io-client": "^4.5.1",
41
44
  "tailwindcss": "^3.0.12",
42
45
  "tippy.js": "^6.3.7"
@@ -31,7 +31,7 @@
31
31
  </template>
32
32
  <template #body>
33
33
  <ComboboxOptions
34
- class="max-h-[11rem] overflow-y-auto rounded-md rounded-t-none bg-white px-1.5 pb-1.5 shadow-md"
34
+ class="max-h-[15rem] overflow-y-auto rounded-md rounded-t-none bg-white px-1.5 pb-1.5 shadow-md"
35
35
  static
36
36
  v-show="isComboboxOpen"
37
37
  >
@@ -52,24 +52,36 @@
52
52
  />
53
53
  <Button icon="x" @click="selectedValue = null" />
54
54
  </div>
55
- <ComboboxOption
56
- as="template"
57
- v-for="option in filteredOptions"
58
- :key="option.value"
59
- :value="option"
60
- v-slot="{ active, selected }"
55
+ <div
56
+ v-for="group in groups"
57
+ :key="group.key"
58
+ v-show="group.items.length > 0"
61
59
  >
62
- <li
63
- :class="[
64
- 'rounded-md px-2.5 py-1.5 text-base',
65
- { 'bg-gray-100': active },
66
- ]"
60
+ <div
61
+ v-if="group.group && !group.hideLabel"
62
+ class="px-2 py-1 text-xs font-semibold uppercase tracking-wider text-gray-500"
63
+ >
64
+ {{ group.group }}
65
+ </div>
66
+ <ComboboxOption
67
+ as="template"
68
+ v-for="option in group.items"
69
+ :key="option.value"
70
+ :value="option"
71
+ v-slot="{ active, selected }"
67
72
  >
68
- {{ option.label }}
69
- </li>
70
- </ComboboxOption>
73
+ <li
74
+ :class="[
75
+ 'rounded-md px-2.5 py-1.5 text-base',
76
+ { 'bg-gray-100': active },
77
+ ]"
78
+ >
79
+ {{ option.label }}
80
+ </li>
81
+ </ComboboxOption>
82
+ </div>
71
83
  <li
72
- v-if="filteredOptions.length == 0"
84
+ v-if="groups.length == 0"
73
85
  class="rounded-md px-2.5 py-1.5 text-base text-gray-600"
74
86
  >
75
87
  No results found
@@ -119,11 +131,31 @@ export default {
119
131
  this.$emit(this.valuePropPassed ? 'change' : 'update:modelValue', val)
120
132
  },
121
133
  },
122
- filteredOptions() {
134
+ groups() {
135
+ if (!this.options || this.options.length == 0) return []
136
+
137
+ let groups = this.options[0]?.group
138
+ ? this.options
139
+ : [{ group: '', items: this.options }]
140
+
141
+ return groups
142
+ .map((group, i) => {
143
+ return {
144
+ key: i,
145
+ group: group.group,
146
+ hideLabel: group.hideLabel || false,
147
+ items: this.filterOptions(group.items),
148
+ }
149
+ })
150
+ .filter((group) => group.items.length > 0)
151
+ },
152
+ },
153
+ methods: {
154
+ filterOptions(options) {
123
155
  if (!this.query) {
124
- return this.options
156
+ return options
125
157
  }
126
- return this.options.filter((option) => {
158
+ return options.filter((option) => {
127
159
  let searchTexts = [option.label, option.value]
128
160
  return searchTexts.some((text) =>
129
161
  (text || '')
@@ -133,8 +165,6 @@ export default {
133
165
  )
134
166
  })
135
167
  },
136
- },
137
- methods: {
138
168
  displayValue(option) {
139
169
  if (typeof option === 'string') {
140
170
  return option
@@ -5,6 +5,7 @@
5
5
  :src="imageURL"
6
6
  class="object-cover"
7
7
  :class="styleClasses"
8
+ loading="lazy"
8
9
  />
9
10
  <div
10
11
  v-else
@@ -7,29 +7,28 @@
7
7
  </span>
8
8
  </template>
9
9
  <script>
10
+ const DEFAULT_COLOR_MAP = {
11
+ Pending: 'yellow',
12
+ Running: 'yellow',
13
+ Success: 'green',
14
+ Failure: 'red',
15
+ Active: 'green',
16
+ Broken: 'red',
17
+ Updating: 'blue',
18
+ Rejected: 'red',
19
+ Published: 'green',
20
+ Approved: 'green',
21
+ }
22
+
10
23
  export default {
11
24
  name: 'Badge',
12
25
  props: ['color', 'status', 'colorMap'],
13
- data: {
14
- defaultColorMap: {
15
- Pending: 'yellow',
16
- Running: 'yellow',
17
- Success: 'green',
18
- Failure: 'red',
19
- Active: 'green',
20
- Broken: 'red',
21
- Updating: 'blue',
22
- Rejected: 'red',
23
- Published: 'green',
24
- Approved: 'green',
25
- },
26
- },
27
26
  computed: {
28
27
  classes() {
29
28
  let color = this.getBadgeColor()
30
29
 
31
30
  let cssClasses = {
32
- gray: 'text-gray-700 bg-gray-50',
31
+ gray: 'text-gray-700 bg-gray-100',
33
32
  green: 'text-green-700 bg-green-50',
34
33
  red: 'text-red-700 bg-red-50',
35
34
  yellow: 'text-yellow-700 bg-yellow-50',
@@ -46,10 +45,7 @@ export default {
46
45
  return color
47
46
  }
48
47
 
49
- let statusColorMap = Object.assign(
50
- this.defaultColorMap,
51
- this.colorMap || {}
52
- )
48
+ let statusColorMap = Object.assign(DEFAULT_COLOR_MAP, this.colorMap || {})
53
49
  color = statusColorMap[this.status] || 'gray'
54
50
 
55
51
  return color
@@ -4,6 +4,7 @@
4
4
  :class="buttonClasses"
5
5
  @click="handleClick"
6
6
  :disabled="isDisabled"
7
+ :aria-label="ariaLabel"
7
8
  >
8
9
  <LoadingIndicator
9
10
  v-if="loading"
@@ -136,6 +137,9 @@ export default {
136
137
  isDisabled() {
137
138
  return this.disabled || this.loading
138
139
  },
140
+ ariaLabel() {
141
+ return this.icon ? this.label : null
142
+ },
139
143
  },
140
144
  methods: {
141
145
  handleClick() {
@@ -10,7 +10,8 @@
10
10
  @close="open = false"
11
11
  >
12
12
  <div
13
- class="flex min-h-screen flex-col items-center justify-center px-4 pt-4 pb-20 text-center"
13
+ class="flex min-h-screen flex-col items-center px-4 pt-4 pb-20 text-center"
14
+ :class="dialogPositionClasses"
14
15
  >
15
16
  <TransitionChild
16
17
  as="template"
@@ -36,7 +37,20 @@
36
37
  leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
37
38
  >
38
39
  <div
39
- class="my-8 inline-block w-full max-w-lg transform overflow-hidden rounded-lg bg-white text-left align-middle shadow-xl transition-all"
40
+ class="my-8 inline-block w-full transform overflow-hidden rounded-lg bg-white text-left align-middle shadow-xl transition-all"
41
+ :class="{
42
+ 'max-w-7xl': options.size === '7xl',
43
+ 'max-w-6xl': options.size === '6xl',
44
+ 'max-w-5xl': options.size === '5xl',
45
+ 'max-w-4xl': options.size === '4xl',
46
+ 'max-w-3xl': options.size === '3xl',
47
+ 'max-w-2xl': options.size === '2xl',
48
+ 'max-w-xl': options.size === 'xl',
49
+ 'max-w-md': options.size === 'md',
50
+ 'max-w-lg': options.size === 'lg' || !options.size,
51
+ 'max-w-sm': options.size === 'sm',
52
+ 'max-w-xs': options.size === 'xs',
53
+ }"
40
54
  >
41
55
  <slot name="body">
42
56
  <slot name="body-main">
@@ -76,7 +90,10 @@
76
90
  </DialogTitle>
77
91
 
78
92
  <slot name="body-content">
79
- <p class="text-sm text-gray-600" v-if="options.message">
93
+ <p
94
+ class="text-base text-gray-600"
95
+ v-if="options.message"
96
+ >
80
97
  {{ options.message }}
81
98
  </p>
82
99
  </slot>
@@ -118,7 +135,8 @@ import {
118
135
  TransitionChild,
119
136
  TransitionRoot,
120
137
  } from '@headlessui/vue'
121
- import { Button, FeatherIcon } from 'pimelon-ui'
138
+ import Button from './Button.vue'
139
+ import FeatherIcon from './FeatherIcon.vue'
122
140
 
123
141
  export default {
124
142
  name: 'Dialog',
@@ -187,6 +205,13 @@ export default {
187
205
  }
188
206
  return icon
189
207
  },
208
+ dialogPositionClasses() {
209
+ let position = this.options?.position || 'center'
210
+ return {
211
+ 'justify-center': position === 'center',
212
+ 'pt-[20vh]': position === 'top',
213
+ }
214
+ },
190
215
  },
191
216
  }
192
217
  </script>
@@ -42,15 +42,18 @@
42
42
  ]"
43
43
  @click="item.onClick"
44
44
  >
45
- <FeatherIcon
46
- v-if="item.icon"
47
- :name="item.icon"
48
- class="mr-2 h-4 w-4 flex-shrink-0 text-gray-500"
49
- aria-hidden="true"
50
- />
51
- <span class="whitespace-nowrap">
52
- {{ item.label }}
53
- </span>
45
+ <component :is="item.component" v-if="item.component" />
46
+ <template v-else>
47
+ <FeatherIcon
48
+ v-if="item.icon"
49
+ :name="item.icon"
50
+ class="mr-2 h-4 w-4 flex-shrink-0 text-gray-500"
51
+ aria-hidden="true"
52
+ />
53
+ <span class="whitespace-nowrap">
54
+ {{ item.label }}
55
+ </span>
56
+ </template>
54
57
  </button>
55
58
  </MenuItem>
56
59
  </div>
@@ -61,7 +64,7 @@
61
64
 
62
65
  <script>
63
66
  import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
64
- import { FeatherIcon } from 'pimelon-ui'
67
+ import FeatherIcon from './FeatherIcon.vue'
65
68
 
66
69
  export default {
67
70
  name: 'NewDropdown',
@@ -79,10 +82,12 @@ export default {
79
82
  if (!onClick && option.route && this.$router) {
80
83
  onClick = () => this.$router.push(option.route)
81
84
  }
85
+
82
86
  return {
83
87
  label: option.label,
84
88
  icon: option.icon,
85
89
  group: option.group,
90
+ component: option.component,
86
91
  onClick,
87
92
  }
88
93
  },
@@ -3,7 +3,7 @@
3
3
  v-if="message"
4
4
  class="whitespace-pre-line text-sm text-red-600"
5
5
  role="alert"
6
- v-html="message"
6
+ v-html="errorMessage"
7
7
  ></div>
8
8
  </template>
9
9
 
@@ -11,5 +11,14 @@
11
11
  export default {
12
12
  name: 'ErrorMessage',
13
13
  props: ['message'],
14
+ computed: {
15
+ errorMessage() {
16
+ if (!this.message) return ''
17
+ if (this.message instanceof Error) {
18
+ return this.message.messages || this.message.message
19
+ }
20
+ return this.message
21
+ },
22
+ },
14
23
  }
15
24
  </script>
@@ -6,55 +6,70 @@
6
6
  >
7
7
  {{ label }}
8
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="border-gray-400 placeholder-gray-500"
17
- ref="input"
18
- :class="[
19
- {
20
- 'form-input block w-full': 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
- :placeholder="placeholder"
34
- class="placeholder-gray-500"
35
- :class="['form-textarea block w-full resize-none', inputClass]"
36
- ref="input"
37
- :value="passedInputValue"
38
- :disabled="disabled"
39
- :rows="rows || 3"
40
- ></textarea>
41
- <select
42
- v-bind="inputAttributes"
43
- class="form-select block w-full"
44
- ref="input"
45
- v-if="type === 'select'"
46
- :disabled="disabled"
9
+ <div
10
+ class="relative flex"
11
+ :class="{ 'items-center': isNormalInput || type == 'select' }"
47
12
  >
48
- <option
49
- v-for="option in selectOptions"
50
- :key="option.value"
51
- :value="option.value"
52
- :disabled="option.disabled || false"
53
- :selected="passedInputValue === option.value"
13
+ <FeatherIcon
14
+ v-if="iconLeft && type != 'checkbox'"
15
+ :name="iconLeft"
16
+ class="absolute mx-2 h-4 w-4 text-gray-600"
17
+ :class="{ 'mt-2': type == 'textarea' }"
18
+ />
19
+ <input
20
+ v-if="isNormalInput"
21
+ v-bind="inputAttributes"
22
+ class="border-gray-400 placeholder-gray-500"
23
+ ref="input"
24
+ :class="[
25
+ {
26
+ 'form-input block w-full': type != 'checkbox',
27
+ 'form-checkbox': type == 'checkbox',
28
+ 'pl-8': iconLeft && type != 'checkbox',
29
+ },
30
+ inputClass,
31
+ ]"
32
+ :type="type || 'text'"
33
+ :disabled="disabled"
34
+ :placeholder="placeholder"
35
+ :value="passedInputValue"
36
+ />
37
+ <textarea
38
+ v-if="type === 'textarea'"
39
+ v-bind="inputAttributes"
40
+ :placeholder="placeholder"
41
+ class="placeholder-gray-500"
42
+ :class="[
43
+ 'form-textarea block w-full resize-none',
44
+ inputClass,
45
+ {
46
+ 'pl-8': iconLeft,
47
+ },
48
+ ]"
49
+ ref="input"
50
+ :value="passedInputValue"
51
+ :disabled="disabled"
52
+ :rows="rows || 3"
53
+ ></textarea>
54
+ <select
55
+ v-if="type === 'select'"
56
+ v-bind="inputAttributes"
57
+ class="form-select block w-full"
58
+ :class="{ 'pl-8': iconLeft }"
59
+ ref="input"
60
+ :disabled="disabled"
54
61
  >
55
- {{ option.label }}
56
- </option>
57
- </select>
62
+ <option
63
+ v-for="option in selectOptions"
64
+ :key="option.value"
65
+ :value="option.value"
66
+ :disabled="option.disabled || false"
67
+ :selected="passedInputValue === option.value"
68
+ >
69
+ {{ option.label }}
70
+ </option>
71
+ </select>
72
+ </div>
58
73
  <span
59
74
  v-if="label && type == 'checkbox'"
60
75
  class="ml-2 inline-block text-base leading-4"
@@ -66,11 +81,13 @@
66
81
 
67
82
  <script>
68
83
  import { debounce } from 'pimelon-ui'
84
+ import FeatherIcon from './FeatherIcon.vue'
69
85
 
70
86
  export default {
71
87
  name: 'Input',
72
88
  inheritAttrs: false,
73
89
  expose: ['getInputValue'],
90
+ components: { FeatherIcon },
74
91
  props: {
75
92
  label: {
76
93
  type: String,
@@ -116,6 +133,9 @@ export default {
116
133
  placeholder: {
117
134
  type: String,
118
135
  },
136
+ iconLeft: {
137
+ type: String,
138
+ },
119
139
  },
120
140
  emits: ['input', 'change', 'update:modelValue'],
121
141
  methods: {
@@ -173,6 +193,16 @@ export default {
173
193
  })
174
194
  .filter(Boolean)
175
195
  },
196
+ isNormalInput() {
197
+ return [
198
+ 'text',
199
+ 'number',
200
+ 'checkbox',
201
+ 'email',
202
+ 'password',
203
+ 'date',
204
+ ].includes(this.type)
205
+ },
176
206
  },
177
207
  }
178
208
  </script>
@@ -1,9 +1,9 @@
1
1
  <template>
2
- <slot v-bind="{ openDialog, resetAddImage }"></slot>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
3
  <Dialog
4
4
  :options="{ title: 'Add Image' }"
5
5
  v-model="addImageDialog.show"
6
- @after-leave="resetAddImage"
6
+ @after-leave="reset"
7
7
  >
8
8
  <template #body-content>
9
9
  <label
@@ -29,12 +29,14 @@
29
29
  <Button appearance="primary" @click="addImage(addImageDialog.url)">
30
30
  Insert Image
31
31
  </Button>
32
+ <Button @click="reset"> Cancel </Button>
32
33
  </template>
33
34
  </Dialog>
34
35
  </template>
35
36
  <script>
36
37
  import fileToBase64 from '../../utils/file-to-base64'
37
38
  import Dialog from '../Dialog.vue'
39
+ import Button from '../Button.vue'
38
40
 
39
41
  export default {
40
42
  name: 'InsertImage',
@@ -45,7 +47,7 @@ export default {
45
47
  addImageDialog: { url: '', file: null, show: false },
46
48
  }
47
49
  },
48
- components: { Dialog },
50
+ components: { Button, Dialog },
49
51
  methods: {
50
52
  openDialog() {
51
53
  this.addImageDialog.show = true
@@ -62,10 +64,10 @@ export default {
62
64
  },
63
65
  addImage(src) {
64
66
  this.editor.chain().focus().setImage({ src }).run()
65
- this.resetAddImage()
67
+ this.reset()
66
68
  },
67
- resetAddImage() {
68
- this.addImageDialog = { show: false, url: null, file: null }
69
+ reset() {
70
+ this.addImageDialog = this.$options.data().addImageDialog
69
71
  },
70
72
  },
71
73
  }
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
+ <Dialog
4
+ :options="{ title: 'Set Link' }"
5
+ v-model="setLinkDialog.show"
6
+ @after-leave="reset"
7
+ >
8
+ <template #body-content>
9
+ <Input
10
+ type="text"
11
+ label="URL"
12
+ v-model="setLinkDialog.url"
13
+ @keydown.enter="(e) => setLink(e.target.value)"
14
+ />
15
+ </template>
16
+ <template #actions>
17
+ <Button appearance="primary" @click="setLink(setLinkDialog.url)">
18
+ Save
19
+ </Button>
20
+ </template>
21
+ </Dialog>
22
+ </template>
23
+ <script>
24
+ import Dialog from '../Dialog.vue'
25
+ import Button from '../Button.vue'
26
+ import Input from '../Input.vue'
27
+
28
+ export default {
29
+ name: 'InsertLink',
30
+ props: ['editor'],
31
+ components: { Button, Input, Dialog },
32
+ data() {
33
+ return {
34
+ setLinkDialog: { url: '', show: false },
35
+ }
36
+ },
37
+ methods: {
38
+ openDialog() {
39
+ let existingURL = this.editor.getAttributes('link').href
40
+ if (existingURL) {
41
+ this.setLinkDialog.url = existingURL
42
+ }
43
+ this.setLinkDialog.show = true
44
+ },
45
+ setLink(url) {
46
+ // empty
47
+ if (url === '') {
48
+ this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
49
+ } else {
50
+ // update link
51
+ this.editor
52
+ .chain()
53
+ .focus()
54
+ .extendMarkRange('link')
55
+ .setLink({ href: url })
56
+ .run()
57
+ }
58
+
59
+ this.setLinkDialog.show = false
60
+ this.setLinkDialog.url = ''
61
+ },
62
+ reset() {
63
+ this.setLinkDialog = this.$options.data().setLinkDialog
64
+ },
65
+ },
66
+ }
67
+ </script>