pimelon-ui 0.1.0-alpha.9 → 0.1.13

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 (66) hide show
  1. package/package.json +7 -13
  2. package/src/components/Autocomplete.story.vue +25 -0
  3. package/src/components/Autocomplete.vue +38 -22
  4. package/src/components/Avatar.story.vue +28 -0
  5. package/src/components/Badge.story.vue +26 -0
  6. package/src/components/Badge.vue +6 -2
  7. package/src/components/Breadcrumbs.story.vue +73 -0
  8. package/src/components/Breadcrumbs.vue +108 -0
  9. package/src/components/Button.story.vue +31 -0
  10. package/src/components/Checkbox.story.vue +22 -0
  11. package/src/components/Checkbox.vue +4 -1
  12. package/src/components/CommandPalette/CommandPalette.vue +110 -0
  13. package/src/components/CommandPalette/CommandPaletteItem.vue +28 -0
  14. package/src/components/Dialog.story.vue +39 -0
  15. package/src/components/Dialog.vue +1 -1
  16. package/src/components/Dropdown.story.vue +98 -0
  17. package/src/components/ErrorMessage.story.vue +28 -0
  18. package/src/components/FileUploader.story.vue +34 -0
  19. package/src/components/FormControl.story.vue +101 -0
  20. package/src/components/ListFilter/FilterIcon.vue +31 -0
  21. package/src/components/ListFilter/ListFilter.vue +292 -0
  22. package/src/components/ListFilter/NestedPopover.vue +60 -0
  23. package/src/components/ListFilter/SearchComplete.vue +85 -0
  24. package/src/components/ListView/ListHeader.vue +34 -0
  25. package/src/components/ListView/ListHeaderItem.vue +22 -0
  26. package/src/components/ListView/ListRow.vue +72 -0
  27. package/src/components/ListView/ListRowItem.vue +45 -0
  28. package/src/components/ListView/ListRows.vue +14 -0
  29. package/src/components/ListView/ListSelectBanner.vue +86 -0
  30. package/src/components/ListView/ListView.vue +96 -0
  31. package/src/components/ListView/utils.js +22 -0
  32. package/src/components/ListView.story.md +110 -0
  33. package/src/components/ListView.story.vue +208 -0
  34. package/src/components/Popover.story.vue +29 -0
  35. package/src/components/Progress.story.vue +34 -0
  36. package/src/components/Select.story.vue +21 -0
  37. package/src/components/Select.vue +1 -0
  38. package/src/components/Spinner.story.vue +14 -0
  39. package/src/components/Switch.story.vue +28 -0
  40. package/src/components/Switch.vue +11 -15
  41. package/src/components/TabButtons.vue +54 -0
  42. package/src/components/Tabs.story.vue +72 -0
  43. package/src/components/Tabs.vue +97 -0
  44. package/src/components/TextInput.story.vue +81 -0
  45. package/src/components/TextInput.vue +8 -2
  46. package/src/components/Textarea.vue +1 -0
  47. package/src/components/Tooltip.story.vue +11 -0
  48. package/src/env.d.ts +2 -0
  49. package/src/index.js +13 -0
  50. package/src/resources/documentResource.js +45 -5
  51. package/src/resources/listResource.js +16 -5
  52. package/src/style.css +6 -0
  53. package/src/utils/fileUploadHandler.ts +62 -57
  54. package/src/utils/{melonRequest.js → frappeRequest.js} +2 -9
  55. package/src/components/Avatar.stories.ts +0 -110
  56. package/src/components/Badge.stories.js +0 -149
  57. package/src/components/Button.stories.js +0 -173
  58. package/src/components/Divider.stories.ts +0 -110
  59. package/src/components/Dropdown.stories.ts +0 -73
  60. package/src/components/Progress.stories.js +0 -80
  61. package/src/components/Spinner.stories.ts +0 -13
  62. package/src/components/Switch.stories.js +0 -52
  63. package/src/components/TextInput.stories.ts +0 -143
  64. package/src/tokens/Color.vue +0 -194
  65. package/src/utils/tailwind.config.stories.js +0 -8
  66. package/src/vite-env.d.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pimelon-ui",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.1.13",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -14,8 +14,9 @@
14
14
  "dev": "vite",
15
15
  "build": "vite build",
16
16
  "preview": "vite preview",
17
- "storybook": "storybook dev -p 6006",
18
- "build-storybook": "storybook build"
17
+ "story:dev": "histoire dev",
18
+ "story:build": "histoire build",
19
+ "story:preview": "histoire preview"
19
20
  },
20
21
  "files": [
21
22
  "src",
@@ -49,6 +50,7 @@
49
50
  "@tiptap/starter-kit": "^2.0.3",
50
51
  "@tiptap/suggestion": "^2.0.3",
51
52
  "@tiptap/vue-3": "^2.0.3",
53
+ "@vueuse/core": "^10.4.1",
52
54
  "feather-icons": "^4.28.0",
53
55
  "idb-keyval": "^6.2.0",
54
56
  "showdown": "^2.1.0",
@@ -60,24 +62,16 @@
60
62
  "vue-router": "^4.1.6"
61
63
  },
62
64
  "devDependencies": {
63
- "@storybook/addon-essentials": "^7.0.0-beta.61",
64
- "@storybook/addon-interactions": "^7.0.0-beta.61",
65
- "@storybook/addon-links": "^7.0.0-beta.61",
66
- "@storybook/blocks": "^7.0.0-alpha.8",
67
- "@storybook/testing-library": "^0.0.14-next.1",
68
- "@storybook/vue3": "^7.0.0-beta.61",
69
- "@storybook/vue3-vite": "^7.0.0-beta.61",
65
+ "@histoire/plugin-vue": "^0.16.1",
70
66
  "@vitejs/plugin-vue": "^4.0.0",
71
67
  "autoprefixer": "^10.4.13",
72
68
  "cross-fetch": "^3.1.5",
69
+ "histoire": "^0.16.2",
73
70
  "husky": "^8.0.3",
74
71
  "lint-staged": ">=10",
75
72
  "postcss": "^8.4.21",
76
73
  "prettier": "2.7.1",
77
74
  "prettier-plugin-tailwindcss": "^0.1.13",
78
- "react": "^18.2.0",
79
- "react-dom": "^18.2.0",
80
- "storybook": "^7.0.0-beta.61",
81
75
  "tailwindcss": "^3.2.7",
82
76
  "typescript": "^5.0.2",
83
77
  "vite": "^4.1.0",
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import Autocomplete from './Autocomplete.vue'
4
+
5
+ const value = ref('')
6
+ const options = [
7
+ { label: 'John Doe', value: 'john-doe' },
8
+ { label: 'Jane Doe', value: 'jane-doe' },
9
+ { label: 'John Smith', value: 'john-smith' },
10
+ { label: 'Jane Smith', value: 'jane-smith' },
11
+ { label: 'John Wayne', value: 'john-wayne' },
12
+ { label: 'Jane Wayne', value: 'jane-wayne' },
13
+ ]
14
+ </script>
15
+ <template>
16
+ <Story :layout="{ width: 500, type: 'grid' }" autoPropsDisabled>
17
+ <div class="p-2">
18
+ <Autocomplete
19
+ :options="options"
20
+ v-model="value"
21
+ placeholder="Select person"
22
+ />
23
+ </div>
24
+ </Story>
25
+ </template>
@@ -2,28 +2,33 @@
2
2
  <Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
3
3
  <Popover class="w-full" v-model:show="showOptions">
4
4
  <template #target="{ open: openPopover, togglePopover }">
5
- <div class="w-full">
6
- <button
7
- class="flex h-7 w-full items-center justify-between rounded bg-gray-100 py-1 pl-3 pr-2 transition-colors hover:bg-gray-200 focus:ring-2 focus:ring-gray-400"
8
- :class="{ 'bg-gray-200': isComboboxOpen }"
9
- @click="() => togglePopover()"
10
- >
11
- <span
12
- class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
13
- v-if="selectedValue"
5
+ <slot name="target" v-bind="{ open: openPopover, togglePopover }">
6
+ <div class="w-full">
7
+ <button
8
+ class="flex h-7 w-full items-center justify-between gap-2 rounded bg-gray-100 px-2 py-1 transition-colors hover:bg-gray-200 focus:ring-2 focus:ring-gray-400"
9
+ :class="{ 'bg-gray-200': isComboboxOpen }"
10
+ @click="() => togglePopover()"
14
11
  >
15
- {{ displayValue(selectedValue) }}
16
- </span>
17
- <span class="text-base leading-5 text-gray-500" v-else>
18
- {{ placeholder || '' }}
19
- </span>
20
- <FeatherIcon
21
- name="chevron-down"
22
- class="h-4 w-4 text-gray-500"
23
- aria-hidden="true"
24
- />
25
- </button>
26
- </div>
12
+ <div class="flex items-center">
13
+ <slot name="prefix" />
14
+ <span
15
+ class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
16
+ v-if="selectedValue"
17
+ >
18
+ {{ displayValue(selectedValue) }}
19
+ </span>
20
+ <span class="text-base leading-5 text-gray-500" v-else>
21
+ {{ placeholder || '' }}
22
+ </span>
23
+ </div>
24
+ <FeatherIcon
25
+ name="chevron-down"
26
+ class="h-4 w-4 text-gray-600"
27
+ aria-hidden="true"
28
+ />
29
+ </button>
30
+ </div>
31
+ </slot>
27
32
  </template>
28
33
  <template #body="{ isOpen }">
29
34
  <div v-show="isOpen">
@@ -36,6 +41,7 @@
36
41
  >
37
42
  <div class="relative w-full">
38
43
  <ComboboxInput
44
+ ref="search"
39
45
  class="form-input w-full"
40
46
  type="text"
41
47
  @change="
@@ -80,7 +86,10 @@
80
86
  { 'bg-gray-100': active },
81
87
  ]"
82
88
  >
83
- <slot name="prefix" v-bind="{ active, selected, option }" />
89
+ <slot
90
+ name="item-prefix"
91
+ v-bind="{ active, selected, option }"
92
+ />
84
93
  {{ option.label }}
85
94
  </li>
86
95
  </ComboboxOption>
@@ -168,6 +177,13 @@ export default {
168
177
  query(q) {
169
178
  this.$emit('update:query', q)
170
179
  },
180
+ showOptions(val) {
181
+ if (val) {
182
+ this.$nextTick(() => {
183
+ this.$refs.search.el.focus()
184
+ })
185
+ }
186
+ },
171
187
  },
172
188
  methods: {
173
189
  filterOptions(options) {
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { reactive } from 'vue'
3
+ import Avatar from './Avatar.vue'
4
+ const state = reactive({
5
+ image: 'https://avatars.githubusercontent.com/u/499550?s=60&v=4',
6
+ label: 'EY',
7
+ size: 'md',
8
+ })
9
+
10
+ const shapes = ['circle', 'square']
11
+ const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl']
12
+ </script>
13
+
14
+ <template>
15
+ <Story :layout="{ type: 'grid', width: 300 }">
16
+ <Variant v-for="shape in shapes" :key="shape" :title="shape">
17
+ <Avatar :shape="shape" v-bind="state" />
18
+ </Variant>
19
+ <Variant v-for="shape in shapes" :key="shape" :title="shape">
20
+ <Avatar :shape="shape" v-bind="state" :image="null" />
21
+ </Variant>
22
+
23
+ <template #controls>
24
+ <HstText v-model="state.label" title="Label" />
25
+ <HstSelect v-model="state.size" :options="sizes" title="Size" />
26
+ </template>
27
+ </Story>
28
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import { reactive } from 'vue'
3
+ import Badge from './Badge.vue'
4
+ const state = reactive({
5
+ theme: 'gray',
6
+ size: 'sm',
7
+ label: 'Badge',
8
+ })
9
+ const variants = ['solid', 'subtle', 'outline', 'ghost']
10
+ const themes = ['gray', 'blue', 'green', 'orange', 'red']
11
+ const sizes = ['sm', 'md', 'lg']
12
+ </script>
13
+
14
+ <template>
15
+ <Story :layout="{ type: 'grid', width: 300 }">
16
+ <Variant v-for="variant in variants" :key="variant" :title="variant">
17
+ <Badge :variant="variant" v-bind="state">{{ state.label }}</Badge>
18
+ </Variant>
19
+
20
+ <template #controls>
21
+ <HstText v-model="state.label" title="Content" />
22
+ <HstSelect v-model="state.theme" :options="themes" title="Theme" />
23
+ <HstSelect v-model="state.size" :options="sizes" title="Size" />
24
+ </template>
25
+ </Story>
26
+ </template>
@@ -9,7 +9,7 @@
9
9
  >
10
10
  <slot name="prefix"></slot>
11
11
  </div>
12
- <slot>{{ props.label }}</slot>
12
+ <slot>{{ props.label?.toString() }}</slot>
13
13
  <div
14
14
  :class="[props.size == 'lg' ? 'max-h-6' : 'max-h-4']"
15
15
  v-if="$slots.suffix"
@@ -22,11 +22,15 @@
22
22
  <script lang="ts" setup>
23
23
  import { computed } from 'vue'
24
24
 
25
+ interface Label {
26
+ toString(): string
27
+ }
28
+
25
29
  interface BadgeProps {
26
30
  theme?: 'gray' | 'blue' | 'green' | 'orange' | 'red'
27
31
  size?: 'sm' | 'md' | 'lg'
28
32
  variant?: 'solid' | 'subtle' | 'outline' | 'ghost'
29
- label?: string
33
+ label?: Label | string | number
30
34
  }
31
35
 
32
36
  const props = withDefaults(defineProps<BadgeProps>(), {
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ import { logEvent } from 'histoire/client'
3
+ import Breadcrumbs from './Breadcrumbs.vue'
4
+ </script>
5
+
6
+ <template>
7
+ <Story :layout="{ type: 'grid', width: 500 }">
8
+ <Variant title="With route option">
9
+ <Breadcrumbs
10
+ :items="[
11
+ {
12
+ label: 'Home',
13
+ route: { name: 'Home' },
14
+ },
15
+ {
16
+ label: 'Views',
17
+ route: '/components',
18
+ },
19
+ {
20
+ label: 'List',
21
+ route: '/components/breadcrumbs',
22
+ },
23
+ ]"
24
+ />
25
+ </Variant>
26
+ <Variant title="With onClick option">
27
+ <Breadcrumbs
28
+ :items="[
29
+ {
30
+ label: 'Home',
31
+ onClick: () => logEvent('onClick', 'Home'),
32
+ },
33
+ {
34
+ label: 'Views',
35
+ onClick: () => logEvent('onClick', 'Home'),
36
+ },
37
+ {
38
+ label: 'Kanban',
39
+ onClick: () => logEvent('onClick', 'Home'),
40
+ },
41
+ ]"
42
+ />
43
+ </Variant>
44
+
45
+ <Variant title="With prefix slot">
46
+ <Breadcrumbs
47
+ :items="[
48
+ {
49
+ label: 'Home',
50
+ icon: '🏡',
51
+ route: { name: 'Home' },
52
+ },
53
+ {
54
+ label: 'Views',
55
+ icon: '🏞️',
56
+ route: '/components',
57
+ },
58
+ {
59
+ label: 'List',
60
+ icon: '📃',
61
+ route: '/components/breadcrumbs',
62
+ },
63
+ ]"
64
+ >
65
+ <template #prefix="{ item }">
66
+ <span class="mr-1">
67
+ {{ item.icon }}
68
+ </span>
69
+ </template>
70
+ </Breadcrumbs>
71
+ </Variant>
72
+ </Story>
73
+ </template>
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <div class="flex min-w-0 items-center">
3
+ <template v-if="dropdownItems.length">
4
+ <Dropdown class="h-7" :options="dropdownItems">
5
+ <Button variant="ghost">
6
+ <template #icon>
7
+ <svg
8
+ class="w-4 text-gray-600"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ >
19
+ <circle cx="12" cy="12" r="1" />
20
+ <circle cx="19" cy="12" r="1" />
21
+ <circle cx="5" cy="12" r="1" />
22
+ </svg>
23
+ </template>
24
+ </Button>
25
+ </Dropdown>
26
+ <span class="ml-1 mr-0.5 text-base text-gray-500" aria-hidden="true">
27
+ /
28
+ </span>
29
+ </template>
30
+ <div
31
+ class="flex min-w-0 items-center overflow-hidden text-ellipsis whitespace-nowrap"
32
+ >
33
+ <template v-for="(item, i) in crumbs" :key="item.label">
34
+ <component
35
+ :is="item.route ? 'router-link' : 'button'"
36
+ class="flex items-center rounded px-0.5 py-1 text-lg font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400"
37
+ :class="[
38
+ i == crumbs.length - 1
39
+ ? 'text-gray-900'
40
+ : 'text-gray-600 hover:text-gray-700',
41
+ ]"
42
+ v-bind="item.route ? { to: item.route } : { onClick: item.onClick }"
43
+ >
44
+ <slot name="prefix" :item="item" />
45
+ <span>
46
+ {{ item.label }}
47
+ </span>
48
+ </component>
49
+ <span
50
+ v-if="i != crumbs.length - 1"
51
+ class="mx-0.5 text-base text-gray-500"
52
+ aria-hidden="true"
53
+ >
54
+ /
55
+ </span>
56
+ </template>
57
+ </div>
58
+ </div>
59
+ </template>
60
+ <script setup lang="ts">
61
+ import { useWindowSize } from '@vueuse/core'
62
+ import { computed } from 'vue'
63
+ import { RouterLinkProps, useRouter } from 'vue-router'
64
+ import Dropdown from '../components/Dropdown.vue'
65
+ import Button from '../components/Button.vue'
66
+
67
+ interface BreadcrumbItem {
68
+ label: string
69
+ route?: RouterLinkProps['to']
70
+ onClick?: () => void
71
+ [key: string]: any
72
+ }
73
+
74
+ interface BreadcrumbsProps {
75
+ items: BreadcrumbItem[]
76
+ }
77
+
78
+ const props = defineProps<BreadcrumbsProps>()
79
+
80
+ const router = useRouter()
81
+ const { width } = useWindowSize()
82
+
83
+ const items = computed(() => {
84
+ return (props.items || []).filter(Boolean)
85
+ })
86
+
87
+ const dropdownItems = computed(() => {
88
+ if (width.value > 640) return []
89
+
90
+ let allExceptLastTwo = items.value.slice(0, -2)
91
+ return allExceptLastTwo.map((item) => {
92
+ let onClick = item.onClick ? item.onClick : () => router.push(item.route)
93
+ return {
94
+ ...item,
95
+ icon: null,
96
+ label: item.label,
97
+ onClick,
98
+ }
99
+ })
100
+ })
101
+
102
+ const crumbs = computed(() => {
103
+ if (width.value > 640) return items.value
104
+
105
+ let lastTwo = items.value.slice(-2)
106
+ return lastTwo
107
+ })
108
+ </script>
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import { reactive } from 'vue'
3
+ import Button from './Button.vue'
4
+ const state = reactive({
5
+ theme: 'gray',
6
+ size: 'sm',
7
+ label: 'Button',
8
+ loading: false,
9
+ loadingText: null,
10
+ disabled: false,
11
+ link: null,
12
+ })
13
+ const variants = ['solid', 'subtle', 'outline', 'ghost']
14
+ const themes = ['gray', 'blue', 'green', 'red']
15
+ const sizes = ['sm', 'md', 'lg', 'xl', '2xl']
16
+ </script>
17
+
18
+ <template>
19
+ <Story :layout="{ type: 'grid', width: 300 }">
20
+ <Variant v-for="variant in variants" :key="variant" :title="variant">
21
+ <Button :variant="variant" v-bind="state">{{ state.label }}</Button>
22
+ </Variant>
23
+
24
+ <template #controls>
25
+ <HstText v-model="state.label" title="Content" />
26
+ <HstCheckbox v-model="state.disabled" title="Disabled" />
27
+ <HstSelect v-model="state.theme" :options="themes" title="Theme" />
28
+ <HstSelect v-model="state.size" :options="sizes" title="Size" />
29
+ </template>
30
+ </Story>
31
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { reactive } from 'vue'
3
+ import Checkbox from './Checkbox.vue'
4
+
5
+ const state = reactive({
6
+ size: 'sm',
7
+ value: false,
8
+ })
9
+
10
+ const sizes = ['sm', 'md']
11
+ </script>
12
+ <template>
13
+ <Story :layout="{ width: 500, type: 'grid' }">
14
+ <div class="p-2">
15
+ <Checkbox v-bind="state" v-model="state.value" label="Enable feature" />
16
+ </div>
17
+
18
+ <template #controls>
19
+ <HstSelect v-model="state.size" :options="sizes" title="Size" />
20
+ </template>
21
+ </Story>
22
+ </template>
@@ -16,6 +16,7 @@
16
16
  :id="htmlId"
17
17
  :checked="Boolean(modelValue)"
18
18
  @change="(e) => $emit('update:modelValue', (e.target as HTMLInputElement).checked)"
19
+ v-bind="attrs"
19
20
  />
20
21
  <label class="block" :class="labelClasses" v-if="label" :for="htmlId">
21
22
  {{ label }}
@@ -23,7 +24,7 @@
23
24
  </div>
24
25
  </template>
25
26
  <script lang="ts" setup>
26
- import { computed } from 'vue'
27
+ import { computed, useAttrs } from 'vue'
27
28
  import { useId } from '../utils/useId'
28
29
 
29
30
  interface CheckboxProps {
@@ -40,6 +41,8 @@ const props = withDefaults(defineProps<CheckboxProps>(), {
40
41
  padding: false,
41
42
  })
42
43
 
44
+ const attrs = useAttrs()
45
+
43
46
  const htmlId = props.id ?? useId()
44
47
 
45
48
  const labelClasses = computed(() => {
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <template>
3
+ <Dialog
4
+ v-model="show"
5
+ :options="{ size: 'xl', position: 'top' }"
6
+ @after-leave="searchQuery = ''"
7
+ >
8
+ <template #body>
9
+ <div>
10
+ <Combobox nullable @update:model-value="select">
11
+ <div class="relative">
12
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4.5">
13
+ <FeatherIcon name="search" class="h-4 w-4" />
14
+ </div>
15
+ <ComboboxInput
16
+ placeholder="Search"
17
+ class="w-full border-none bg-transparent py-3 pl-11.5 pr-4.5 text-base text-gray-800 placeholder-gray-500 focus:ring-0"
18
+ v-model="searchQuery"
19
+ autocomplete="off"
20
+ />
21
+ </div>
22
+ <ComboboxOptions
23
+ class="max-h-96 overflow-auto border-t border-gray-100"
24
+ static
25
+ :hold="true"
26
+ >
27
+ <div
28
+ class="mb-2 mt-4.5 first:mt-3"
29
+ v-for="(group, index) in groups"
30
+ :key="group.title"
31
+ >
32
+ <div
33
+ class="mb-2.5 px-4.5 text-base text-gray-600"
34
+ v-if="!group.hideTitle"
35
+ >
36
+ {{ group.title }}
37
+ </div>
38
+ <ComboboxOption
39
+ v-for="item in group.items"
40
+ :key="item.name"
41
+ v-slot="{ active }"
42
+ :value="item"
43
+ class="px-2.5"
44
+ :disabled="item.disabled"
45
+ >
46
+ <component
47
+ :is="group.component"
48
+ :item="item"
49
+ :active="active"
50
+ />
51
+ </ComboboxOption>
52
+ </div>
53
+ </ComboboxOptions>
54
+ </Combobox>
55
+ </div>
56
+ </template>
57
+ </Dialog>
58
+ </template>
59
+ </template>
60
+
61
+ <script setup>
62
+ import {
63
+ Combobox,
64
+ ComboboxInput,
65
+ ComboboxOption,
66
+ ComboboxOptions,
67
+ } from '@headlessui/vue'
68
+ import { computed, onBeforeUnmount, onMounted } from 'vue'
69
+
70
+ const emit = defineEmits(['update:show', 'update:searchQuery', 'select'])
71
+ const props = defineProps({
72
+ show: { type: Boolean, default: false },
73
+ searchQuery: { type: String, default: '' },
74
+ groups: { type: Array, default: () => [] },
75
+ })
76
+
77
+ const show = computed({
78
+ get: () => props.show,
79
+ set: (value) => emit('update:show', value),
80
+ })
81
+
82
+ const searchQuery = computed({
83
+ get: () => props.searchQuery,
84
+ set: (value) => emit('update:searchQuery', value),
85
+ })
86
+
87
+ function select(item) {
88
+ emit('select', item)
89
+ show.value = false
90
+ }
91
+
92
+ function keydownWatcher(e) {
93
+ if (e.key === 'Escape' && show.value) {
94
+ show.value = false
95
+ e.preventDefault()
96
+ }
97
+
98
+ if (
99
+ e.key === 'k' &&
100
+ (e.ctrlKey || e.metaKey) &&
101
+ !e.target.classList.contains('ProseMirror')
102
+ ) {
103
+ show.value = true
104
+ e.preventDefault()
105
+ }
106
+ }
107
+
108
+ onMounted(() => window.addEventListener('keydown', keydownWatcher))
109
+ onBeforeUnmount(() => window.removeEventListener('keydown', keydownWatcher))
110
+ </script>
@@ -0,0 +1,28 @@
1
+ <script setup>
2
+ defineProps({
3
+ item: { type: Object, required: true },
4
+ active: { type: Boolean, default: false },
5
+ })
6
+ </script>
7
+
8
+ <template>
9
+ <div
10
+ class="flex w-full min-w-0 items-center rounded px-2 py-2 text-base font-medium text-gray-800"
11
+ :class="{ 'bg-gray-200': active }"
12
+ >
13
+ <component
14
+ :is="item.icon"
15
+ v-if="item.icon"
16
+ class="mr-3 h-4 w-4 text-gray-700"
17
+ />
18
+ <span class="overflow-hidden text-ellipsis whitespace-nowrap">
19
+ {{ item.title }}
20
+ </span>
21
+ <span
22
+ v-if="item.description"
23
+ class="ml-auto whitespace-nowrap pl-2 text-gray-600"
24
+ >
25
+ {{ item.description }}
26
+ </span>
27
+ </div>
28
+ </template>