pimelon-ui 0.1.91 → 0.1.92

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pimelon-ui",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -51,6 +51,7 @@
51
51
  "feather-icons": "^4.28.0",
52
52
  "husky": "^9.1.7",
53
53
  "idb-keyval": "^6.2.0",
54
+ "ora": "5.4.1",
54
55
  "prettier": "^3.3.2",
55
56
  "radix-vue": "^1.5.3",
56
57
  "showdown": "^2.1.0",
@@ -2,7 +2,7 @@
2
2
  import { ref } from 'vue'
3
3
  import Autocomplete from './Autocomplete.vue'
4
4
 
5
- const single = ref('')
5
+ const single = ref()
6
6
  const people = ref(null)
7
7
  const options = [
8
8
  {
@@ -74,7 +74,7 @@ const options = [
74
74
  :options="options"
75
75
  v-model="single"
76
76
  placeholder="Select person"
77
- hide-search="true"
77
+ :hideSearch="true"
78
78
  />
79
79
  </div>
80
80
  </Variant>
@@ -84,7 +84,7 @@ const options = [
84
84
  :options="options"
85
85
  v-model="people"
86
86
  placeholder="Select people"
87
- multiple="true"
87
+ :multiple="true"
88
88
  />
89
89
  </div>
90
90
  </Variant>
@@ -94,8 +94,8 @@ const options = [
94
94
  :options="options"
95
95
  v-model="people"
96
96
  placeholder="Select people"
97
- multiple="true"
98
- hide-search="true"
97
+ :multiple="true"
98
+ :hideSearch="true"
99
99
  />
100
100
  </div>
101
101
  </Variant>
@@ -18,9 +18,9 @@
18
18
  <slot name="prefix" />
19
19
  <span
20
20
  class="truncate text-base leading-5 text-ink-gray-8"
21
- v-if="selectedValue"
21
+ v-if="displayValue"
22
22
  >
23
- {{ displayValue(selectedValue) }}
23
+ {{ displayValue }}
24
24
  </span>
25
25
  <span class="text-base leading-5 text-ink-gray-4" v-else>
26
26
  {{ placeholder || '' }}
@@ -55,18 +55,14 @@
55
55
  ref="searchInput"
56
56
  class="form-input w-full focus:bg-surface-gray-3 hover:bg-surface-gray-4 text-ink-gray-8"
57
57
  type="text"
58
- @change="
59
- (e) => {
60
- query = e.target.value
61
- }
62
- "
63
58
  :value="query"
59
+ @change="query = $event.target.value"
64
60
  autocomplete="off"
65
61
  placeholder="Search"
66
62
  />
67
63
  <button
68
64
  class="absolute right-0 inline-flex h-7 w-7 items-center justify-center"
69
- @click="selectedValue = null"
65
+ @click="clearAll"
70
66
  >
71
67
  <FeatherIcon name="x" class="w-4 text-ink-gray-8" />
72
68
  </button>
@@ -86,7 +82,7 @@
86
82
  <ComboboxOption
87
83
  as="template"
88
84
  v-for="(option, idx) in group.items.slice(0, 50)"
89
- :key="option?.value || idx"
85
+ :key="idx"
90
86
  :value="option"
91
87
  v-slot="{ active, selected }"
92
88
  >
@@ -167,185 +163,222 @@
167
163
  </Combobox>
168
164
  </template>
169
165
 
170
- <script>
166
+ <script setup lang="ts">
171
167
  import {
172
168
  Combobox,
173
- ComboboxButton,
174
169
  ComboboxInput,
175
170
  ComboboxOption,
176
171
  ComboboxOptions,
177
172
  } from '@headlessui/vue'
178
- import { nextTick } from 'vue'
173
+ import { computed, nextTick, ref, watch } from 'vue'
179
174
  import Popover from './Popover.vue'
180
175
  import { Button } from './Button'
181
176
  import FeatherIcon from './FeatherIcon.vue'
182
177
 
183
- export default {
184
- name: 'Autocomplete',
185
- props: {
186
- options: {
187
- type: Array,
188
- default: () => [],
189
- },
190
- modelValue: {
191
- type: [String, Object, Array],
192
- },
193
- placeholder: {
194
- type: String,
195
- },
196
- bodyClasses: {
197
- type: [String, Array, Object],
198
- },
199
- multiple: {
200
- type: Boolean,
201
- default: false,
202
- },
203
- hideSearch: {
204
- type: Boolean,
205
- default: false,
206
- },
207
- },
208
- emits: ['update:modelValue', 'update:query', 'change'],
209
- components: {
210
- Popover,
211
- Button,
212
- FeatherIcon,
213
- Combobox,
214
- ComboboxInput,
215
- ComboboxOptions,
216
- ComboboxOption,
217
- ComboboxButton,
218
- },
219
- expose: ['togglePopover', 'rootRef'],
220
- data() {
221
- return {
222
- query: '',
223
- showOptions: false,
178
+ type Option = {
179
+ label: string
180
+ value: OptionValue
181
+ description?: string
182
+ [key: string]: any
183
+ }
184
+
185
+ type OptionValue = string | number | boolean
186
+
187
+ type AutocompleteOption = OptionValue | Option
188
+
189
+ type AutocompleteOptionGroup = {
190
+ group: string
191
+ items: AutocompleteOption[]
192
+ hideLabel?: boolean
193
+ }
194
+
195
+ type AutocompleteOptions = AutocompleteOption[] | AutocompleteOptionGroup[]
196
+
197
+ type AutocompleteProps = {
198
+ options: AutocompleteOptions
199
+ hideSearch?: boolean
200
+ placeholder?: string
201
+ bodyClasses?: string | string[]
202
+ } & (
203
+ | {
204
+ multiple: true
205
+ modelValue?: AutocompleteOption[] | null
224
206
  }
225
- },
226
- computed: {
227
- selectedValue: {
228
- get() {
229
- if (!this.multiple) {
230
- return this.findOption(this.modelValue)
231
- }
232
- // in case of `multiple`, modelValue is an array of values
233
- // if the modelValue is a list of values, convert them to options
234
- return isOptionOrValue(this.modelValue?.[0]) === 'value'
235
- ? this.modelValue?.map((v) => this.findOption(v))
236
- : this.modelValue
237
- },
238
- set(val) {
239
- this.query = ''
240
- if (val && !this.multiple) this.showOptions = false
241
- if (!this.multiple) {
242
- this.$emit('update:modelValue', val)
243
- return
244
- }
245
- this.$emit('update:modelValue', val)
207
+ | {
208
+ multiple?: false
209
+ modelValue?: AutocompleteOption | null
210
+ }
211
+ )
212
+
213
+ const props = withDefaults(defineProps<AutocompleteProps>(), {
214
+ multiple: false,
215
+ hideSearch: false,
216
+ })
217
+ const emit = defineEmits(['update:modelValue', 'update:query', 'change'])
218
+
219
+ const searchInput = ref()
220
+ const showOptions = ref(false)
221
+ const query = ref('')
222
+
223
+ const groups = computed(() => {
224
+ if (!props.options?.length) return []
225
+
226
+ let groups: AutocompleteOptionGroup[]
227
+ if (isOptionGroup(props.options[0])) {
228
+ groups = props.options as AutocompleteOptionGroup[]
229
+ } else {
230
+ groups = [
231
+ {
232
+ group: '',
233
+ items: sanitizeOptions(props.options as AutocompleteOption[]),
234
+ hideLabel: false,
246
235
  },
247
- },
248
- groups() {
249
- if (!this.options || this.options.length == 0) return []
250
-
251
- let groups = this.options[0]?.group
252
- ? this.options
253
- : [{ group: '', items: this.sanitizeOptions(this.options) }]
254
-
255
- return groups
256
- .map((group, i) => {
257
- return {
258
- key: i,
259
- group: group.group,
260
- hideLabel: group.hideLabel || false,
261
- items: this.filterOptions(this.sanitizeOptions(group.items)),
262
- }
263
- })
264
- .filter((group) => group.items.length > 0)
265
- },
266
- allOptions() {
267
- return this.groups.flatMap((group) => group.items)
268
- },
269
- areAllOptionsSelected() {
270
- if (!this.multiple) return false
271
- return this.allOptions.length === this.selectedValue?.length
272
- },
273
- },
274
- watch: {
275
- query(q) {
276
- this.$emit('update:query', q)
277
- },
278
- showOptions(val) {
279
- if (val) nextTick(() => this.$refs.searchInput?.$el?.focus())
280
- },
281
- },
282
- methods: {
283
- rootRef() {
284
- return this.$refs['rootRef']
285
- },
286
- togglePopover(val) {
287
- this.showOptions = val ?? !this.showOptions
288
- },
289
- findOption(option) {
290
- if (!option) return option
291
- const value = isOptionOrValue(option) === 'value' ? option : option.value
292
- return this.allOptions.find((o) => o.value === value)
293
- },
294
- filterOptions(options) {
295
- if (!this.query) return options
296
- return options.filter((option) => {
297
- return (
298
- option.label
299
- .toLowerCase()
300
- .includes(this.query.trim().toLowerCase()) ||
301
- option.value.toLowerCase().includes(this.query.trim().toLowerCase())
302
- )
303
- })
304
- },
305
- displayValue(option) {
306
- if (!option) return ''
307
-
308
- if (!this.multiple) {
309
- return this.getLabel(this.findOption(option))
310
- }
236
+ ]
237
+ }
311
238
 
312
- if (!Array.isArray(option)) return ''
313
-
314
- // in case of `multiple`, option is an array of values
315
- // so the display value should be comma separated labels
316
- return option.map((v) => this.getLabel(this.findOption(v))).join(', ')
317
- },
318
- getLabel(option) {
319
- if (isOptionOrValue(option) === 'value') return option
320
- return option?.label || option?.value || 'No label'
321
- },
322
- sanitizeOptions(options) {
323
- if (!options) return []
324
- // in case the options are just values, convert them to objects
325
- return options.map((option) => {
326
- return isOptionOrValue(option) === 'option'
327
- ? option
328
- : { label: option, value: option }
329
- })
330
- },
331
- isOptionSelected(option) {
332
- if (!this.selectedValue) return false
333
- const value = isOptionOrValue(option) === 'value' ? option : option.value
334
- if (!this.multiple) {
335
- return this.selectedValue?.value === value
239
+ return groups
240
+ .map((group, i) => {
241
+ return {
242
+ key: i,
243
+ group: group.group,
244
+ hideLabel: group.hideLabel,
245
+ items: filterOptions(sanitizeOptions(group.items || [])),
336
246
  }
337
- return this.selectedValue?.find((v) => v && v.value === value)
338
- },
339
- selectAll() {
340
- this.selectedValue = this.allOptions
341
- },
342
- clearAll() {
343
- this.selectedValue = []
344
- },
247
+ })
248
+ .filter((group) => group.items.length > 0)
249
+ })
250
+
251
+ const allOptions = computed(() => {
252
+ return groups.value.flatMap((group) => group.items)
253
+ })
254
+
255
+ const sanitizeOptions = (options: AutocompleteOption[]) => {
256
+ if (!options) return []
257
+ // in case the options are just values, convert them to objects
258
+ return options.map((option) => {
259
+ return isOption(option)
260
+ ? option
261
+ : { label: option.toString(), value: option }
262
+ })
263
+ }
264
+
265
+ const filterOptions = (options: Option[]) => {
266
+ if (!query.value) return options
267
+ return options.filter((option) => {
268
+ return (
269
+ option.label.toLowerCase().includes(query.value.trim().toLowerCase()) ||
270
+ option.value
271
+ .toString()
272
+ .toLowerCase()
273
+ .includes(query.value.trim().toLowerCase())
274
+ )
275
+ })
276
+ }
277
+
278
+ const selectedValue = computed({
279
+ get() {
280
+ if (!props.multiple) {
281
+ return findOption(props.modelValue as AutocompleteOption)
282
+ }
283
+ // in case of `multiple`, modelValue is an array of values
284
+ // if the modelValue is a list of values, convert them to options
285
+ let values = props.modelValue as AutocompleteOption[]
286
+ if (!values) return []
287
+ return isOption(values[0]) ? values : values.map((v) => findOption(v))
288
+ },
289
+ set(val) {
290
+ query.value = ''
291
+ if (val && !props.multiple) showOptions.value = false
292
+ if (!props.multiple) {
293
+ emit('update:modelValue', val)
294
+ return
295
+ }
296
+ emit('update:modelValue', val)
345
297
  },
298
+ })
299
+
300
+ const findOption = (option: AutocompleteOption) => {
301
+ if (!option) return option
302
+ const value = isOption(option) ? option.value : option
303
+ return allOptions.value.find((o) => o.value === value)
346
304
  }
347
305
 
348
- function isOptionOrValue(optionOrValue) {
349
- return typeof optionOrValue === 'object' ? 'option' : 'value'
306
+ const getLabel = (option: AutocompleteOption) => {
307
+ if (isOption(option)) {
308
+ return option?.label || option?.value || 'No label'
309
+ }
310
+ return option
311
+ }
312
+
313
+ const displayValue = computed(() => {
314
+ if (!selectedValue.value) return ''
315
+ if (!props.multiple) {
316
+ return getLabel(selectedValue.value as AutocompleteOption)
317
+ }
318
+ return (selectedValue.value as AutocompleteOption[])
319
+ .map((v) => getLabel(v))
320
+ .join(', ')
321
+ })
322
+
323
+ const isOptionSelected = (option: AutocompleteOption) => {
324
+ if (!selectedValue.value) return false
325
+ const value = isOption(option) ? option.value : option
326
+ if (!props.multiple) {
327
+ return selectedValue.value === value
328
+ }
329
+ return (selectedValue.value as AutocompleteOption[]).find((v) =>
330
+ isOption(v) ? v.value === value : v === value,
331
+ )
350
332
  }
333
+
334
+ const areAllOptionsSelected = computed(() => {
335
+ if (!props.multiple) return false
336
+ return (
337
+ allOptions.value.length ===
338
+ (selectedValue.value as AutocompleteOption[])?.length
339
+ )
340
+ })
341
+
342
+ const selectAll = () => {
343
+ selectedValue.value = allOptions.value
344
+ }
345
+
346
+ const clearAll = () => {
347
+ selectedValue.value = props.multiple ? [] : undefined
348
+ }
349
+
350
+ const isOption = (option: AutocompleteOption) => {
351
+ return typeof option === 'object'
352
+ }
353
+
354
+ const isOptionGroup = (option: any) => {
355
+ return typeof option === 'object' && 'items' in option && 'group' in option
356
+ }
357
+
358
+ watch(
359
+ () => query.value,
360
+ () => {
361
+ emit('update:query', query.value)
362
+ },
363
+ )
364
+
365
+ watch(
366
+ () => showOptions.value,
367
+ () => {
368
+ if (showOptions.value) {
369
+ nextTick(() => searchInput.value?.$el.focus())
370
+ }
371
+ },
372
+ )
373
+
374
+ const rootRef = ref()
375
+
376
+ const togglePopover = () => {
377
+ showOptions.value = !showOptions.value
378
+ }
379
+
380
+ defineExpose({
381
+ rootRef,
382
+ togglePopover,
383
+ })
351
384
  </script>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { EditorContent } from '@tiptap/vue-3'
4
+ import TextEditor from './TextEditor.vue'
5
+ import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
6
+ import { Button } from '../Button'
7
+
8
+ const value = ref('')
9
+ const customValue = ref('')
10
+ const customButtons = [
11
+ 'Paragraph',
12
+ ['Heading 2', 'Heading 3', 'Heading 4'],
13
+ 'Separator',
14
+ 'Bold',
15
+ 'Italic',
16
+ 'Separator',
17
+ 'Bullet List',
18
+ 'Numbered List',
19
+ 'Separator',
20
+ 'Link',
21
+ 'Image',
22
+ ]
23
+ </script>
24
+ <template>
25
+ <Story :layout="{ width: 600, type: 'grid' }" autoPropsDisabled>
26
+ <Variant title="Basic">
27
+ <div class="p-2">
28
+ <TextEditor
29
+ editor-class="prose-sm min-h-[4rem] border rounded-b-lg border-t-0 p-2"
30
+ :content="value"
31
+ placeholder="Type something..."
32
+ @change="(val) => (value = val)"
33
+ :bubbleMenu="true"
34
+ :fixed-menu="true"
35
+ />
36
+ </div>
37
+ </Variant>
38
+ <Variant title="Comment Editor">
39
+ <div class="p-2">
40
+ <TextEditor
41
+ ref="textEditor"
42
+ editor-class="prose-sm max-w-none min-h-[4rem]"
43
+ :content="customValue"
44
+ @change="(val) => (customValue = val)"
45
+ :starterkit-options="{ heading: { levels: [2, 3, 4] } }"
46
+ placeholder="Write something amazing..."
47
+ >
48
+ <template v-slot:editor="{ editor }">
49
+ <EditorContent
50
+ class="max-h-[50vh] overflow-y-auto border rounded-lg p-4"
51
+ :editor="editor"
52
+ />
53
+ </template>
54
+ <template v-slot:bottom>
55
+ <div
56
+ class="mt-2 flex flex-col justify-between sm:flex-row sm:items-center"
57
+ >
58
+ <TextEditorFixedMenu
59
+ class="-ml-1 overflow-x-auto"
60
+ :buttons="customButtons"
61
+ />
62
+ <div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0">
63
+ <Button>Cancel</Button>
64
+ <Button variant="solid">Submit</Button>
65
+ </div>
66
+ </div>
67
+ </template>
68
+ </TextEditor>
69
+ </div>
70
+ </Variant>
71
+ </Story>
72
+ </template>
@@ -15,6 +15,15 @@ export function detectMarkdown(text) {
15
15
  const lines = text.split('\n')
16
16
  const markdown = lines.filter(
17
17
  (line) =>
18
+ // check for inline markdown content like images, links, italic, bold, etc.
19
+ /!\[.*\]\(.*\)/.test(line) ||
20
+ /\[.*\]\(.*\)/.test(line) ||
21
+ /(^|\s)\*.*\*(\s|$)/.test(line) ||
22
+ /(^|\s)_.*_(\s|$)/.test(line) ||
23
+ /(^|\s)\*\*.*\*\*(\s|$)/.test(line) ||
24
+ /(^|\s)__.*__(\s|$)/.test(line) ||
25
+ /(^|\s)~~.*~~(\s|$)/.test(line) ||
26
+ // check for block markdown content like headings, code blocks, lists, etc.
18
27
  line.startsWith('![') ||
19
28
  line.startsWith('#') ||
20
29
  line.startsWith('> ') ||
package/vite.js CHANGED
@@ -1,13 +1,16 @@
1
1
  const path = require('path')
2
2
  const fs = require('fs')
3
+ const DocTypeInterfaceGenerator = require('./scripts/generateInterface')
3
4
 
4
5
  module.exports = function proxyOptions({
5
6
  port = 8080,
6
7
  source = '^/(app|login|api|assets|files|private)',
7
8
  } = {}) {
8
- const config = getCommonSiteConfig()
9
- const webserver_port = config ? config.webserver_port : 8000
10
- if (!config) {
9
+ const commonSiteConfig = getCommonSiteConfig()
10
+ const webserver_port = commonSiteConfig
11
+ ? commonSiteConfig.webserver_port
12
+ : 8000
13
+ if (!commonSiteConfig) {
11
14
  console.log('No common_site_config.json found, using default port 8000')
12
15
  }
13
16
  let proxy = {}
@@ -19,15 +22,44 @@ module.exports = function proxyOptions({
19
22
  return `http://${site_name}:${webserver_port}`
20
23
  },
21
24
  }
25
+
22
26
  return {
23
27
  name: 'melonui-vite-plugin',
24
- config: () => ({
25
- server: {
26
- port: port,
27
- proxy: proxy,
28
- },
29
- }),
28
+ config: async () => {
29
+ await generateDocTypeInterfaces()
30
+
31
+ return {
32
+ server: {
33
+ port: port,
34
+ proxy: proxy,
35
+ },
36
+ }
37
+ },
38
+ }
39
+ }
40
+
41
+ async function generateDocTypeInterfaces() {
42
+ const config = getConfig()
43
+ if (!(config && config.typeGeneration && config.typeGeneration.input)) return
44
+
45
+ const frontendFolder = process.cwd()
46
+ let outputPath = config.typeGeneration.output || 'src/types/doctypes.ts'
47
+ if (!path.isAbsolute(outputPath)) {
48
+ outputPath = path.join(frontendFolder, outputPath)
49
+ }
50
+
51
+ const appsFolder = findAppsFolder()
52
+ if (!appsFolder) {
53
+ console.error('Could not find pimelon/apps folder')
54
+ return
30
55
  }
56
+
57
+ const generator = new DocTypeInterfaceGenerator(
58
+ appsFolder,
59
+ config.typeGeneration.input,
60
+ outputPath,
61
+ )
62
+ await generator.generate()
31
63
  }
32
64
 
33
65
  function getCommonSiteConfig() {
@@ -48,3 +80,24 @@ function getCommonSiteConfig() {
48
80
  }
49
81
  return null
50
82
  }
83
+
84
+ function findAppsFolder() {
85
+ let currentDir = process.cwd()
86
+ while (currentDir !== '/') {
87
+ if (
88
+ fs.existsSync(path.join(currentDir, 'apps')) &&
89
+ fs.existsSync(path.join(currentDir, 'sites'))
90
+ ) {
91
+ return path.join(currentDir, 'apps')
92
+ }
93
+ currentDir = path.resolve(currentDir, '..')
94
+ }
95
+ return null
96
+ }
97
+
98
+ function getConfig() {
99
+ let configPath = path.join(process.cwd(), 'melonui.json')
100
+ if (fs.existsSync(configPath)) {
101
+ return JSON.parse(fs.readFileSync(configPath))
102
+ }
103
+ }