@edgedev/create-edge-app 1.1.25 → 1.1.27

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 (111) hide show
  1. package/README.md +55 -20
  2. package/{agent.md → agents.md} +2 -0
  3. package/bin/cli.js +6 -6
  4. package/edge/components/auth/login.vue +384 -0
  5. package/edge/components/auth/register.vue +396 -0
  6. package/edge/components/auth.vue +108 -0
  7. package/edge/components/autoFileUpload.vue +215 -0
  8. package/edge/components/billing.vue +8 -0
  9. package/edge/components/buttonDivider.vue +14 -0
  10. package/edge/components/chip.vue +34 -0
  11. package/edge/components/clipboardButton.vue +42 -0
  12. package/edge/components/cms/block.vue +529 -0
  13. package/edge/components/cms/blockApi.vue +212 -0
  14. package/edge/components/cms/blockEditor.vue +725 -0
  15. package/edge/components/cms/blockInput.vue +66 -0
  16. package/edge/components/cms/blockPicker.vue +486 -0
  17. package/edge/components/cms/blockRender.vue +78 -0
  18. package/edge/components/cms/blockSheetContent.vue +28 -0
  19. package/edge/components/cms/codeEditor.vue +466 -0
  20. package/edge/components/cms/fontUpload.vue +327 -0
  21. package/edge/components/cms/htmlContent.vue +807 -0
  22. package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
  23. package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
  24. package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
  25. package/edge/components/cms/init_blocks/carousel.html +103 -0
  26. package/edge/components/cms/init_blocks/contact_us.html +69 -0
  27. package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
  28. package/edge/components/cms/init_blocks/footer.html +24 -0
  29. package/edge/components/cms/init_blocks/header_divider.html +7 -0
  30. package/edge/components/cms/init_blocks/hero.html +35 -0
  31. package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
  32. package/edge/components/cms/init_blocks/newsletter.html +117 -0
  33. package/edge/components/cms/init_blocks/post_content.html +7 -0
  34. package/edge/components/cms/init_blocks/post_title_header.html +21 -0
  35. package/edge/components/cms/init_blocks/posts_list.html +20 -0
  36. package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
  37. package/edge/components/cms/init_blocks/property_carousel.html +59 -0
  38. package/edge/components/cms/init_blocks/property_detail.html +112 -0
  39. package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
  40. package/edge/components/cms/init_blocks/property_results.html +137 -0
  41. package/edge/components/cms/init_blocks/property_search.html +75 -0
  42. package/edge/components/cms/init_blocks/simple_array.html +7 -0
  43. package/edge/components/cms/mediaCard.vue +116 -0
  44. package/edge/components/cms/mediaManager.vue +386 -0
  45. package/edge/components/cms/menu.vue +1103 -0
  46. package/edge/components/cms/optionsSelect.vue +107 -0
  47. package/edge/components/cms/page.vue +1785 -0
  48. package/edge/components/cms/posts.vue +1083 -0
  49. package/edge/components/cms/site.vue +1475 -0
  50. package/edge/components/cms/themeDefaultMenu.vue +548 -0
  51. package/edge/components/cms/themeEditor.vue +429 -0
  52. package/edge/components/dashboard.vue +776 -0
  53. package/edge/components/editor.vue +671 -0
  54. package/edge/components/fileTree.vue +72 -0
  55. package/edge/components/files.vue +89 -0
  56. package/edge/components/formSubtypes/myOrgs.vue +214 -0
  57. package/edge/components/formSubtypes/users.vue +336 -0
  58. package/edge/components/functionChips.vue +57 -0
  59. package/edge/components/gError.vue +98 -0
  60. package/edge/components/gHelper.vue +67 -0
  61. package/edge/components/gInput.vue +1331 -0
  62. package/edge/components/loggingIn.vue +41 -0
  63. package/edge/components/menu.vue +137 -0
  64. package/edge/components/menuContent.vue +132 -0
  65. package/edge/components/myAccount.vue +317 -0
  66. package/edge/components/myOrganizations.vue +75 -0
  67. package/edge/components/myProfile.vue +122 -0
  68. package/edge/components/orgSwitcher.vue +25 -0
  69. package/edge/components/organizationMembers.vue +522 -0
  70. package/edge/components/organizationSettings.vue +271 -0
  71. package/edge/components/shad/breadcrumbs.vue +35 -0
  72. package/edge/components/shad/button.vue +43 -0
  73. package/edge/components/shad/checkbox.vue +73 -0
  74. package/edge/components/shad/combobox.vue +238 -0
  75. package/edge/components/shad/datepicker.vue +184 -0
  76. package/edge/components/shad/dialog.vue +32 -0
  77. package/edge/components/shad/dropdownMenu.vue +54 -0
  78. package/edge/components/shad/dropdownMenuItem.vue +21 -0
  79. package/edge/components/shad/form.vue +59 -0
  80. package/edge/components/shad/html.vue +877 -0
  81. package/edge/components/shad/input.vue +139 -0
  82. package/edge/components/shad/number.vue +109 -0
  83. package/edge/components/shad/select.vue +151 -0
  84. package/edge/components/shad/selectTags.vue +278 -0
  85. package/edge/components/shad/switch.vue +67 -0
  86. package/edge/components/shad/tags.vue +137 -0
  87. package/edge/components/shad/textarea.vue +102 -0
  88. package/edge/components/shad/typeMoney.vue +167 -0
  89. package/edge/components/sideBar.vue +288 -0
  90. package/edge/components/sideBarContent.vue +268 -0
  91. package/edge/components/sidebarProvider.vue +33 -0
  92. package/edge/components/tooltip.vue +16 -0
  93. package/edge/components/userMenu.vue +148 -0
  94. package/edge/components/v/alert.vue +59 -0
  95. package/edge/components/v/alertTitle.vue +18 -0
  96. package/edge/components/v/card.vue +53 -0
  97. package/edge/components/v/cardActions.vue +18 -0
  98. package/edge/components/v/cardText.vue +18 -0
  99. package/edge/components/v/cardTitle.vue +20 -0
  100. package/edge/components/v/col.vue +56 -0
  101. package/edge/components/v/list.vue +46 -0
  102. package/edge/components/v/listItem.vue +26 -0
  103. package/edge/components/v/listItemTitle.vue +18 -0
  104. package/edge/components/v/row.vue +42 -0
  105. package/edge/components/v/toolbar.vue +24 -0
  106. package/edge/composables/global.ts +519 -0
  107. package/edge-pull.sh +2 -0
  108. package/edge-push.sh +1 -0
  109. package/edge-status.sh +14 -0
  110. package/package.json +1 -1
  111. package/edge-components-install.sh +0 -1
@@ -0,0 +1,271 @@
1
+ <script setup>
2
+ import { computed, defineProps, inject, nextTick, onBeforeMount, reactive, watch } from 'vue'
3
+ import CardContent from '~/components/ui/card/CardContent.vue'
4
+
5
+ import { useToast } from '@/components/ui/toast/use-toast'
6
+ const props = defineProps({
7
+ subscribeOptions: {
8
+ type: Array,
9
+ default: () => [],
10
+ },
11
+ orgFields: {
12
+ type: Object,
13
+ required: true,
14
+ },
15
+ title: {
16
+ type: String,
17
+ default: 'Organization Settings',
18
+ },
19
+ hideUniqueIdentifier: {
20
+ type: Boolean,
21
+ default: false,
22
+ },
23
+ formSchema: {
24
+ type: Object,
25
+ required: true,
26
+ },
27
+ })
28
+ const { toast } = useToast()
29
+
30
+ const edgeFirebase = inject('edgeFirebase')
31
+ // const edgeGlobal = inject('edgeGlobal')
32
+
33
+ const state = reactive({
34
+ data: {},
35
+ org: '',
36
+ form: false,
37
+ loaded: false,
38
+ showSnack: false,
39
+ successMessage: '',
40
+ snackColor: 'success',
41
+ loading: false,
42
+ })
43
+
44
+ const onSubmit = async () => {
45
+ state.loading = true
46
+ state.showSnack = false
47
+ const result = await edgeFirebase.changeDoc('organizations', edgeGlobal.edgeState.currentOrganization, state.data)
48
+ edgeGlobal.getOrganizations(edgeFirebase)
49
+
50
+ edgeGlobal.edgeState.changeTracker = {}
51
+ state.loaded = false
52
+ let message = 'Updated Successfully'
53
+ if (!result.success) {
54
+ message = 'You do not have permission'
55
+ }
56
+ toast({
57
+ title: '',
58
+ description: message,
59
+ duration: 1000,
60
+ })
61
+ await nextTick()
62
+ state.loaded = true
63
+ state.loading = false
64
+ }
65
+
66
+ const currentOrgData = computed(() => {
67
+ if (edgeGlobal.objHas(edgeFirebase.data, edgeGlobal.edgeState.organizationDocPath) === false) {
68
+ return ''
69
+ }
70
+ return edgeFirebase.data[edgeGlobal.edgeState.organizationDocPath]
71
+ })
72
+
73
+ const loadStateData = () => {
74
+ state.data = edgeGlobal.dupObject(currentOrgData.value)
75
+ for (const field of props.orgFields) {
76
+ if (edgeGlobal.objHas(state.data, field.field) === false) {
77
+ if (field.type === 'section') {
78
+ state.data[field.field] = {}
79
+ for (const subField of field.fields) {
80
+ if (edgeGlobal.objHas(state.data[field.field], subField.field) === false) {
81
+ state.data[field.field][subField.field] = subField.value
82
+ }
83
+ }
84
+ }
85
+ else {
86
+ state.data[field.field] = field.value
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ onBeforeMount(() => {
93
+ loadStateData()
94
+ state.loaded = true
95
+ })
96
+ watch(currentOrgData, async () => {
97
+ edgeGlobal.edgeState.changeTracker = {}
98
+ loadStateData()
99
+ state.loaded = false
100
+ await nextTick()
101
+ state.loaded = true
102
+ })
103
+
104
+ const navigateToBilling = async () => {
105
+ state.loading = true
106
+ const billingLink = await edgeFirebase.runFunction('stripe-redirectToBilling', {})
107
+ window.location.href = billingLink.data.url
108
+ state.loading = false
109
+ }
110
+
111
+ const gotoSubscription = async (url) => {
112
+ state.loading = true
113
+ window.location.href = url
114
+ state.loading = false
115
+ }
116
+
117
+ const route = useRoute()
118
+ </script>
119
+
120
+ <template>
121
+ <Card class="w-full flex-1 bg-muted/50 mx-auto w-full border-none shadow-none pt-2">
122
+ <slot
123
+ name="subscription"
124
+ :subscribed-status="edgeGlobal.edgeState.subscribedStatus"
125
+ :subscribe-options="props.subscribeOptions"
126
+ :goto-subscription="gotoSubscription"
127
+ :navigate-to-billing="navigateToBilling"
128
+ :current-organization="edgeGlobal.edgeState.currentOrganization"
129
+ >
130
+ <Alert v-if="edgeGlobal.edgeState.subscribedStatus && props.subscribeOptions.length > 0" class="mt-0" :class="edgeGlobal.edgeState.subscribedStatus.color">
131
+ <component :is="edgeGlobal.edgeState.subscribedStatus.icon" />
132
+ <AlertTitle>
133
+ {{ edgeGlobal.edgeState.subscribedStatus.status }}
134
+ </AlertTitle>
135
+ <AlertDescription>
136
+ {{ edgeGlobal.edgeState.subscribedStatus.description }}
137
+ <template v-if="!edgeGlobal.edgeState.subscribedStatus.isSubscribed">
138
+ <div class="text-center w-full mb-4">
139
+ <slot name="subscribeTitle">
140
+ <span class="text-2xl">Subscribe now with 7 day free trial!</span>
141
+ </slot>
142
+ </div>
143
+ <slot name="subscribeOptions">
144
+ <div class="flex justify-center space-x-4">
145
+ <edge-shad-button
146
+ v-for="option in props.subscribeOptions"
147
+ :key="option.buttonText"
148
+ class="text-white w-100 bg-slate-800 hover:bg-slate-400"
149
+ @click="gotoSubscription(`${option.stripeSubscriptionLink}?client_reference_id=${edgeGlobal.edgeState.currentOrganization}`)"
150
+ >
151
+ {{ option.buttonText }}
152
+ </edge-shad-button>
153
+ </div>
154
+ </slot>
155
+ </template>
156
+ <div v-else class="flex flex-col sm:flex-row">
157
+ <div>
158
+ <edge-shad-button
159
+ class="text-white w-100 bg-slate-800 hover:bg-slate-400"
160
+ @click="navigateToBilling"
161
+ >
162
+ Manage Subscription
163
+ </edge-shad-button>
164
+ </div>
165
+ </div>
166
+ </AlertDescription>
167
+ </Alert>
168
+ </slot>
169
+
170
+ <edge-shad-form
171
+ :schema="props.formSchema"
172
+ @submit="onSubmit"
173
+ >
174
+ <slot name="header">
175
+ <edge-menu class="bg-secondary text-foreground rounded-none sticky top-0 py-6">
176
+ <template #start>
177
+ <slot name="header-start">
178
+ <component :is="edgeGlobal.iconFromMenu(route)" class="mr-2" />
179
+ <span class="capitalize">Organization Settings</span>
180
+ </slot>
181
+ </template>
182
+ <template #center>
183
+ <slot name="header-center">
184
+ <div v-if="!props.hideSearch" class="w-full px-6" />
185
+ </slot>
186
+ </template>
187
+ <template #end>
188
+ <slot name="header-end">
189
+ <div />
190
+ </slot>
191
+ </template>
192
+ </edge-menu>
193
+ </slot>
194
+ <CardContent v-if="state.loaded" class="p-3 w-full overflow-y-auto scroll-area">
195
+ <template v-for="field in props.orgFields" :key="field.field">
196
+ <Card v-if="field.type === 'section'" class="mb-2">
197
+ <CardHeader>
198
+ <CardTitle>
199
+ {{ field.label }}
200
+ </CardTitle>
201
+ </CardHeader>
202
+ <CardContent>
203
+ <div class="grid gap-2">
204
+ <template v-for="subField in field.fields" :key="subField.field">
205
+ <edge-g-input
206
+ v-model="state.data[field.field][subField.field]"
207
+ :name="`${field.field}.${subField.field}`"
208
+ :label="subField.label"
209
+ :field-type="subField.type"
210
+ :hint="subField.hint"
211
+ persistent-hint
212
+ />
213
+ </template>
214
+ </div>
215
+ </CardContent>
216
+ </Card>
217
+ <edge-g-input
218
+ v-if="edgeGlobal.objHas(field, 'bindings')"
219
+ v-model="state.data[field.field]"
220
+ :name="field.field"
221
+ v-bind="field.bindings"
222
+ :parent-tracker-id="`org-settings-${field.field}`"
223
+ />
224
+ <edge-g-input
225
+ v-else
226
+ v-model="state.data[field.field]"
227
+ :name="field.field"
228
+ :field-type="field.type"
229
+ :label="field.label"
230
+ parent-tracker-id="org-settings"
231
+ :hint="field.hint"
232
+ persistent-hint
233
+ />
234
+ </template>
235
+ <edge-chip class="mt-3">
236
+ ID: {{ edgeGlobal.edgeState.currentOrganization }}
237
+ <edge-clipboard-button :text="edgeGlobal.edgeState.currentOrganization" />
238
+ </edge-chip>
239
+ <Alert v-if="state.showSnack" class="bg-success mt-4 py-2 flex">
240
+ <div>
241
+ {{ state.successMessage }}
242
+ </div>
243
+ <div class="grow text-right">
244
+ <edge-shad-button
245
+
246
+ class="mx-2 h-6 text-xs text-white bg-slate-800"
247
+
248
+ @click="state.showSnack = false"
249
+ >
250
+ Close
251
+ </edge-shad-button>
252
+ </div>
253
+ </Alert>
254
+ </CardContent>
255
+ <CardFooter>
256
+ <edge-shad-button
257
+ type="submit"
258
+ :disabled="state.loading"
259
+ class="text-white w-100 bg-slate-800 hover:bg-slate-400"
260
+ >
261
+ <Loader2 v-if="state.loading" class="w-4 h-4 mr-2 animate-spin" />
262
+ Save
263
+ </edge-shad-button>
264
+ </CardFooter>
265
+ </edge-shad-form>
266
+ </Card>
267
+ </template>
268
+
269
+ <style lang="scss" scoped>
270
+
271
+ </style>
@@ -0,0 +1,35 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ items: {
4
+ type: Array,
5
+ required: true,
6
+ },
7
+ itemClass: {
8
+ type: String,
9
+ default: 'text-secondary text-2xl sm:text-4xl font-[700]',
10
+ },
11
+ })
12
+ </script>
13
+
14
+ <template>
15
+ <Breadcrumb>
16
+ <BreadcrumbList>
17
+ <template v-for="(item, index) in props.items" :key="index">
18
+ <BreadcrumbItem>
19
+ <BreadcrumbLink v-if="item.to" class="border-b-2 border-dashed" :class="props.itemClass">
20
+ <NuxtLink :to="item.to">
21
+ {{ item.text }}
22
+ </NuxtLink>
23
+ </BreadcrumbLink>
24
+ <BreadcrumbPage v-else :class="props.itemClass">
25
+ {{ item.text }}
26
+ </BreadcrumbPage>
27
+ </BreadcrumbItem>
28
+ <BreadcrumbSeparator v-if="index < (props.items.length - 1)" />
29
+ </template>
30
+ </BreadcrumbList>
31
+ </Breadcrumb>
32
+ </template>
33
+
34
+ <style>
35
+ </style>
@@ -0,0 +1,43 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ type: {
4
+ type: String,
5
+ required: false,
6
+ default: 'button',
7
+ },
8
+ to: {
9
+ type: String,
10
+ required: false,
11
+ },
12
+ target: {
13
+ type: String,
14
+ required: false,
15
+ },
16
+ })
17
+ const router = useRouter()
18
+ function handleNavigation() {
19
+ if (props.target === '_blank') {
20
+ window.open(props.to, '_blank')
21
+ }
22
+ else {
23
+ router.push(props.to)
24
+ }
25
+ }
26
+ </script>
27
+
28
+ <script setup>
29
+
30
+ </script>
31
+
32
+ <template>
33
+ <Button v-if="props.to" :type="props.type" @click.stop.prevent="handleNavigation">
34
+ <slot />
35
+ </Button>
36
+ <Button v-else :type="props.type">
37
+ <slot />
38
+ </Button>
39
+ </template>
40
+
41
+ <style lang="scss" scoped>
42
+
43
+ </style>
@@ -0,0 +1,73 @@
1
+ <script setup>
2
+ import { useVModel } from '@vueuse/core'
3
+ const props = defineProps({
4
+ name: {
5
+ type: String,
6
+ required: true,
7
+ },
8
+ modelValue: {
9
+ type: Boolean,
10
+ default: false,
11
+ },
12
+ class: {
13
+ type: null,
14
+ required: false,
15
+ },
16
+ placeholder: {
17
+ type: String,
18
+ required: false,
19
+ },
20
+ label: {
21
+ type: String,
22
+ required: false,
23
+ },
24
+ description: {
25
+ type: String,
26
+ required: false,
27
+ },
28
+ })
29
+
30
+ const emits = defineEmits(['update:modelValue'])
31
+
32
+ const modelValue = useVModel(props, 'modelValue', emits, {
33
+ passive: true,
34
+ defaultValue: props.modelValue,
35
+ })
36
+
37
+ const formHandleChange = ref(() => {})
38
+
39
+ watch(modelValue, (val) => {
40
+ formHandleChange.value(val)
41
+ })
42
+ </script>
43
+
44
+ <template>
45
+ <div>
46
+ <FormField v-slot="{ handleChange }" type="checkbox" :name="props.name">
47
+ <FormItem class="flex flex-row items-start gap-x-3 space-y-0 rounded-md border p-3 mt-3" @click.capture.once="formHandleChange(modelValue)">
48
+ <FormControl>
49
+ <Checkbox
50
+ :id="props.name"
51
+ :ref="() => { formHandleChange = handleChange }"
52
+ v-model="modelValue"
53
+ :class="props.class"
54
+ class="bg-slate-200"
55
+ />
56
+ </FormControl>
57
+ <div class="space-y-1 leading-none w-full">
58
+ <div class="relative w-full items-center text-left">
59
+ <FormLabel><slot /></FormLabel>
60
+ <span class="absolute end-0 inset-y-0 flex items-center justify-center pl-2 pr-0">
61
+ <slot name="icon" /></span>
62
+ </div>
63
+ <FormDescription />
64
+ <FormMessage />
65
+ </div>
66
+ </FormItem>
67
+ </FormField>
68
+ </div>
69
+ </template>
70
+
71
+ <style lang="scss" scoped>
72
+
73
+ </style>
@@ -0,0 +1,238 @@
1
+ <script setup>
2
+ import { useVModel } from '@vueuse/core'
3
+ import { useField } from 'vee-validate'
4
+ import { cn } from '@/lib/utils'
5
+ const props = defineProps({
6
+ name: {
7
+ type: String,
8
+ required: true,
9
+ },
10
+ modelValue: {
11
+ type: [String, Array],
12
+ required: false,
13
+ },
14
+ class: {
15
+ type: null,
16
+ required: false,
17
+ default: 'w-100',
18
+ },
19
+ placeholder: {
20
+ type: String,
21
+ default: 'Make a selection',
22
+ required: false,
23
+ },
24
+ label: {
25
+ type: String,
26
+ required: false,
27
+ },
28
+ description: {
29
+ type: String,
30
+ required: false,
31
+ },
32
+ disabled: {
33
+ type: Boolean,
34
+ required: false,
35
+ default: false,
36
+ },
37
+ items: {
38
+ type: Array,
39
+ required: false,
40
+ default: () => [],
41
+ },
42
+ itemTitle: {
43
+ type: String,
44
+ required: false,
45
+ default: 'title',
46
+ },
47
+ itemValue: {
48
+ type: String,
49
+ required: false,
50
+ default: 'name',
51
+ },
52
+ multiple: {
53
+ type: Boolean,
54
+ required: false,
55
+ default: false,
56
+ },
57
+ })
58
+
59
+ const emits = defineEmits(['update:modelValue'])
60
+
61
+ const state = reactive({
62
+ width: 255,
63
+ })
64
+
65
+ const open = ref(false)
66
+
67
+ const computedItems = computed(() => {
68
+ return props.items.map((item) => {
69
+ if (typeof item === 'string') {
70
+ return { [props.itemTitle]: item, [props.itemValue]: item }
71
+ }
72
+ const getNestedValue = (obj, path) => path.split('.').reduce((acc, key) => acc && acc[key], obj)
73
+ return {
74
+ [props.itemTitle]: getNestedValue(item, props.itemTitle),
75
+ [props.itemValue]: getNestedValue(item, props.itemValue),
76
+ }
77
+ })
78
+ })
79
+
80
+ const modelValue = useVModel(props, 'modelValue', emits, {
81
+ passive: false,
82
+ prop: 'modelValue',
83
+ })
84
+
85
+ // Ensure model is an array when multiple
86
+ if (props.multiple && !Array.isArray(modelValue.value)) {
87
+ modelValue.value = modelValue.value ? [modelValue.value] : []
88
+ }
89
+
90
+ const triggerButton = ref(null)
91
+
92
+ watch(open, (isOpen) => {
93
+ if (isOpen) {
94
+ nextTick(() => {
95
+ if (triggerButton.value) {
96
+ state.width = triggerButton.value.$el.offsetWidth
97
+ }
98
+ })
99
+ }
100
+ })
101
+
102
+ const updateCheck = ref(true)
103
+ watch(() => modelValue.value, () => {
104
+ updateCheck.value = false
105
+ nextTick(() => {
106
+ updateCheck.value = true
107
+ })
108
+ })
109
+
110
+ const { setValue } = useField(props.name)
111
+
112
+ const handleItemSelect = (item) => {
113
+ const selectedValue = item[props.itemValue] ?? ''
114
+
115
+ if (props.multiple) {
116
+ // Start from an array
117
+ const current = Array.isArray(modelValue.value) ? [...modelValue.value] : []
118
+ const idx = current.indexOf(selectedValue)
119
+ if (idx === -1) {
120
+ current.push(selectedValue)
121
+ }
122
+ else {
123
+ current.splice(idx, 1)
124
+ }
125
+ modelValue.value = current
126
+ emits('update:modelValue', current)
127
+ setValue(current)
128
+ // keep popover open for multi-select
129
+ }
130
+ else {
131
+ modelValue.value = selectedValue
132
+ emits('update:modelValue', selectedValue)
133
+ setValue(selectedValue)
134
+ open.value = false
135
+ }
136
+ }
137
+
138
+ watch(() => modelValue.value, (newValue) => {
139
+ emits('update:modelValue', newValue)
140
+ })
141
+
142
+ const isSelected = (item) => {
143
+ const val = item[props.itemValue] ?? ''
144
+ if (props.multiple) {
145
+ return Array.isArray(modelValue.value) && modelValue.value.includes(val)
146
+ }
147
+ return modelValue.value === val
148
+ }
149
+
150
+ const triggerTitle = computed(() => {
151
+ const placeholder = props.placeholder
152
+ const items = computedItems.value
153
+
154
+ if (props.multiple) {
155
+ const values = Array.isArray(modelValue.value) ? modelValue.value : []
156
+ if (values.length === 0)
157
+ return placeholder
158
+ const titles = values
159
+ .map(v => items.find(i => (i[props.itemValue] ?? '') === v))
160
+ .filter(Boolean)
161
+ .map(i => i[props.itemTitle])
162
+ if (titles.length === 0)
163
+ return placeholder
164
+ if (titles.length <= 3)
165
+ return titles.join(', ')
166
+ return `${titles.length} selected`
167
+ }
168
+ else {
169
+ if (modelValue.value) {
170
+ const item = items.find(i => (i[props.itemValue] ?? '') === modelValue.value)
171
+ if (item && edgeGlobal.objHas(item, props.itemTitle))
172
+ return item[props.itemTitle]
173
+ }
174
+ return placeholder
175
+ }
176
+ })
177
+ </script>
178
+
179
+ <template>
180
+ <FormField :name="props.name">
181
+ <FormItem class="flex flex-col space-y-1">
182
+ <FormLabel>{{ props.label }}</FormLabel>
183
+ <Popover v-model:open="open">
184
+ <PopoverTrigger as-child>
185
+ <FormControl>
186
+ <Button
187
+ ref="triggerButton"
188
+ variant="outline"
189
+ role="combobox"
190
+ :aria-expanded="open"
191
+ class="w-[200px] justify-between text-foreground"
192
+ :class="props.class"
193
+ :disabled="props.disabled"
194
+ >
195
+ {{ triggerTitle }}
196
+ <ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
197
+ </Button>
198
+ </FormControl>
199
+ </PopoverTrigger>
200
+ <PopoverContent class="p-0" :style="`width: ${state.width}px !important;`">
201
+ <Command>
202
+ <CommandInput class="h-9" placeholder="Search..." />
203
+ <CommandEmpty>Not found.</CommandEmpty>
204
+ <CommandList>
205
+ <CommandGroup>
206
+ <CommandItem
207
+ v-for="item in computedItems"
208
+ :key="item[props.itemTitle]"
209
+ :value="item[props.itemTitle]"
210
+ @select="() => handleItemSelect(item)"
211
+ >
212
+ <Check
213
+ :class="cn(
214
+ 'h-4 w-4',
215
+ isSelected(item) ? 'opacity-100' : 'opacity-0',
216
+ )"
217
+ />
218
+ {{ item[props.itemTitle] }}
219
+ </CommandItem>
220
+ </CommandGroup>
221
+ </CommandList>
222
+ </Command>
223
+ </PopoverContent>
224
+ </Popover>
225
+ <FormDescription>
226
+ {{ props.description }}
227
+ </FormDescription>
228
+ <FormMessage />
229
+ </FormItem>
230
+ </FormField>
231
+ </template>
232
+
233
+ <style lang="scss" scoped>
234
+ .PopoverContent {
235
+ width: 100vw;
236
+ max-height: var(--radix-popover-content-available-height);
237
+ }
238
+ </style>