@edgedev/create-edge-app 1.1.25 → 1.1.26
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/README.md +55 -20
- package/{agent.md → agents.md} +2 -0
- package/bin/cli.js +6 -6
- package/edge/components/auth/login.vue +384 -0
- package/edge/components/auth/register.vue +396 -0
- package/edge/components/auth.vue +108 -0
- package/edge/components/autoFileUpload.vue +215 -0
- package/edge/components/billing.vue +8 -0
- package/edge/components/buttonDivider.vue +14 -0
- package/edge/components/chip.vue +34 -0
- package/edge/components/clipboardButton.vue +42 -0
- package/edge/components/cms/block.vue +529 -0
- package/edge/components/cms/blockApi.vue +212 -0
- package/edge/components/cms/blockEditor.vue +725 -0
- package/edge/components/cms/blockInput.vue +66 -0
- package/edge/components/cms/blockPicker.vue +486 -0
- package/edge/components/cms/blockRender.vue +78 -0
- package/edge/components/cms/blockSheetContent.vue +28 -0
- package/edge/components/cms/codeEditor.vue +466 -0
- package/edge/components/cms/fontUpload.vue +327 -0
- package/edge/components/cms/htmlContent.vue +807 -0
- package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
- package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
- package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
- package/edge/components/cms/init_blocks/carousel.html +103 -0
- package/edge/components/cms/init_blocks/contact_us.html +69 -0
- package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
- package/edge/components/cms/init_blocks/footer.html +24 -0
- package/edge/components/cms/init_blocks/header_divider.html +7 -0
- package/edge/components/cms/init_blocks/hero.html +35 -0
- package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
- package/edge/components/cms/init_blocks/newsletter.html +117 -0
- package/edge/components/cms/init_blocks/post_content.html +7 -0
- package/edge/components/cms/init_blocks/post_title_header.html +21 -0
- package/edge/components/cms/init_blocks/posts_list.html +20 -0
- package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
- package/edge/components/cms/init_blocks/property_carousel.html +59 -0
- package/edge/components/cms/init_blocks/property_detail.html +112 -0
- package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
- package/edge/components/cms/init_blocks/property_results.html +137 -0
- package/edge/components/cms/init_blocks/property_search.html +75 -0
- package/edge/components/cms/init_blocks/simple_array.html +7 -0
- package/edge/components/cms/mediaCard.vue +116 -0
- package/edge/components/cms/mediaManager.vue +386 -0
- package/edge/components/cms/menu.vue +1103 -0
- package/edge/components/cms/optionsSelect.vue +107 -0
- package/edge/components/cms/page.vue +1785 -0
- package/edge/components/cms/posts.vue +1083 -0
- package/edge/components/cms/site.vue +1298 -0
- package/edge/components/cms/themeDefaultMenu.vue +548 -0
- package/edge/components/cms/themeEditor.vue +426 -0
- package/edge/components/dashboard.vue +776 -0
- package/edge/components/editor.vue +671 -0
- package/edge/components/fileTree.vue +72 -0
- package/edge/components/files.vue +89 -0
- package/edge/components/formSubtypes/myOrgs.vue +214 -0
- package/edge/components/formSubtypes/users.vue +336 -0
- package/edge/components/functionChips.vue +57 -0
- package/edge/components/gError.vue +98 -0
- package/edge/components/gHelper.vue +67 -0
- package/edge/components/gInput.vue +1331 -0
- package/edge/components/loggingIn.vue +41 -0
- package/edge/components/menu.vue +137 -0
- package/edge/components/menuContent.vue +132 -0
- package/edge/components/myAccount.vue +317 -0
- package/edge/components/myOrganizations.vue +75 -0
- package/edge/components/myProfile.vue +122 -0
- package/edge/components/orgSwitcher.vue +25 -0
- package/edge/components/organizationMembers.vue +522 -0
- package/edge/components/organizationSettings.vue +271 -0
- package/edge/components/shad/breadcrumbs.vue +35 -0
- package/edge/components/shad/button.vue +43 -0
- package/edge/components/shad/checkbox.vue +73 -0
- package/edge/components/shad/combobox.vue +238 -0
- package/edge/components/shad/datepicker.vue +184 -0
- package/edge/components/shad/dialog.vue +32 -0
- package/edge/components/shad/dropdownMenu.vue +54 -0
- package/edge/components/shad/dropdownMenuItem.vue +21 -0
- package/edge/components/shad/form.vue +59 -0
- package/edge/components/shad/html.vue +877 -0
- package/edge/components/shad/input.vue +139 -0
- package/edge/components/shad/number.vue +109 -0
- package/edge/components/shad/select.vue +151 -0
- package/edge/components/shad/selectTags.vue +278 -0
- package/edge/components/shad/switch.vue +67 -0
- package/edge/components/shad/tags.vue +137 -0
- package/edge/components/shad/textarea.vue +102 -0
- package/edge/components/shad/typeMoney.vue +167 -0
- package/edge/components/sideBar.vue +288 -0
- package/edge/components/sideBarContent.vue +268 -0
- package/edge/components/sidebarProvider.vue +33 -0
- package/edge/components/tooltip.vue +16 -0
- package/edge/components/userMenu.vue +148 -0
- package/edge/components/v/alert.vue +59 -0
- package/edge/components/v/alertTitle.vue +18 -0
- package/edge/components/v/card.vue +53 -0
- package/edge/components/v/cardActions.vue +18 -0
- package/edge/components/v/cardText.vue +18 -0
- package/edge/components/v/cardTitle.vue +20 -0
- package/edge/components/v/col.vue +56 -0
- package/edge/components/v/list.vue +46 -0
- package/edge/components/v/listItem.vue +26 -0
- package/edge/components/v/listItemTitle.vue +18 -0
- package/edge/components/v/row.vue +42 -0
- package/edge/components/v/toolbar.vue +24 -0
- package/edge/composables/global.ts +519 -0
- package/edge-pull.sh +2 -0
- package/edge-push.sh +1 -0
- package/edge-status.sh +14 -0
- package/package.json +1 -1
- package/edge-components-install.sh +0 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVModel } from '@vueuse/core'
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
name: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
},
|
|
8
|
+
type: {
|
|
9
|
+
type: String,
|
|
10
|
+
required: false,
|
|
11
|
+
default: 'text',
|
|
12
|
+
},
|
|
13
|
+
defaultValue: {
|
|
14
|
+
type: [String, Number],
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
modelValue: {
|
|
18
|
+
type: [String, Number],
|
|
19
|
+
required: false,
|
|
20
|
+
},
|
|
21
|
+
class: {
|
|
22
|
+
type: null,
|
|
23
|
+
required: false,
|
|
24
|
+
},
|
|
25
|
+
placeholder: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: false,
|
|
28
|
+
},
|
|
29
|
+
label: {
|
|
30
|
+
type: String,
|
|
31
|
+
required: false,
|
|
32
|
+
},
|
|
33
|
+
description: {
|
|
34
|
+
type: String,
|
|
35
|
+
required: false,
|
|
36
|
+
},
|
|
37
|
+
maskOptions: {
|
|
38
|
+
type: [Object],
|
|
39
|
+
required: false,
|
|
40
|
+
default: null,
|
|
41
|
+
},
|
|
42
|
+
disabled: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
required: false,
|
|
45
|
+
default: false,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const emits = defineEmits(['update:modelValue', 'blur'])
|
|
50
|
+
|
|
51
|
+
const state = reactive({
|
|
52
|
+
showPassword: false,
|
|
53
|
+
type: '',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
onBeforeMount(() => {
|
|
57
|
+
state.type = props.type
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const modelValue = useVModel(props, 'modelValue', emits, {
|
|
61
|
+
passive: false,
|
|
62
|
+
emits,
|
|
63
|
+
prop: 'modelValue',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const classComputed = computed(() => {
|
|
67
|
+
if (props.type === 'password') {
|
|
68
|
+
return `${props.class} pr-10`
|
|
69
|
+
}
|
|
70
|
+
return props.class
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const inputRef = ref(null) // Reference to the input field
|
|
74
|
+
|
|
75
|
+
const onInputChange = (e) => {
|
|
76
|
+
if (e.target.value !== modelValue.value) {
|
|
77
|
+
modelValue.value = e.target.value // Sync value only if changed
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check for pre-filled (autofilled) value on mount
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
nextTick(() => {
|
|
84
|
+
if (inputRef.value?.value && inputRef.value.value !== modelValue.value) {
|
|
85
|
+
modelValue.value = inputRef.value.value
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<div>
|
|
93
|
+
<FormField v-slot="{ componentField }" :name="props.name">
|
|
94
|
+
<FormItem>
|
|
95
|
+
<FormLabel class="flex">
|
|
96
|
+
{{ props.label }}
|
|
97
|
+
<div class="ml-auto inline-block">
|
|
98
|
+
<slot />
|
|
99
|
+
</div>
|
|
100
|
+
</FormLabel>
|
|
101
|
+
<FormControl>
|
|
102
|
+
<div class="relative w-full items-center">
|
|
103
|
+
<Input
|
|
104
|
+
:id="props.name"
|
|
105
|
+
ref="inputRef"
|
|
106
|
+
v-model="modelValue"
|
|
107
|
+
v-maska:[props.maskOptions]
|
|
108
|
+
:name="props.name"
|
|
109
|
+
:default-value="props.modelValue"
|
|
110
|
+
:class="classComputed"
|
|
111
|
+
:type="state.type"
|
|
112
|
+
v-bind="componentField"
|
|
113
|
+
:placeholder="props.placeholder"
|
|
114
|
+
:disabled="props.disabled"
|
|
115
|
+
@change="onInputChange"
|
|
116
|
+
@blur="$emit('blur', $event)"
|
|
117
|
+
/>
|
|
118
|
+
<span class="absolute end-0 inset-y-0 flex items-center justify-center px-2">
|
|
119
|
+
<slot name="icon" />
|
|
120
|
+
<template v-if="props.type === 'password'">
|
|
121
|
+
<Eye v-if="state.type === 'text'" class="size-6 text-muted-foreground cursor-pointer" @click="state.type = 'password'" />
|
|
122
|
+
<EyeOff v-else class="size-6 text-muted-foreground cursor-pointer" @click="state.type = 'text'" />
|
|
123
|
+
</template>
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
</FormControl>
|
|
127
|
+
<FormDescription>
|
|
128
|
+
{{ props.description }}
|
|
129
|
+
<slot name="description" />
|
|
130
|
+
</FormDescription>
|
|
131
|
+
<FormMessage />
|
|
132
|
+
</FormItem>
|
|
133
|
+
</FormField>
|
|
134
|
+
</div>
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<style lang="scss" scoped>
|
|
138
|
+
|
|
139
|
+
</style>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVModel } from '@vueuse/core'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
name: { type: String, required: true },
|
|
6
|
+
type: { type: String, default: 'text' },
|
|
7
|
+
defaultValue: { type: [String, Number], required: false },
|
|
8
|
+
modelValue: { type: [String, Number], required: false },
|
|
9
|
+
class: { type: null, required: false },
|
|
10
|
+
placeholder: { type: String, required: false },
|
|
11
|
+
label: { type: String, required: false },
|
|
12
|
+
description: { type: String, required: false },
|
|
13
|
+
maskOptions: { type: Object, required: false, default: null },
|
|
14
|
+
disabled: { type: Boolean, default: false },
|
|
15
|
+
min: { type: Number, required: false },
|
|
16
|
+
max: { type: Number, required: false },
|
|
17
|
+
formatOptions: { type: Object, required: false, default: () => ({}) },
|
|
18
|
+
step: { type: Number, default: 1 },
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const emits = defineEmits(['update:modelValue'])
|
|
22
|
+
|
|
23
|
+
const state = reactive({
|
|
24
|
+
showPassword: false,
|
|
25
|
+
type: '',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
onBeforeMount(() => {
|
|
29
|
+
state.type = props.type
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Base v-model (can be string or number)
|
|
33
|
+
const rawModel = useVModel(props, 'modelValue', emits, {
|
|
34
|
+
passive: false,
|
|
35
|
+
defaultValue: props.defaultValue,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Coercion helper: returns number | null
|
|
39
|
+
function toNumberOrNull(v) {
|
|
40
|
+
if (v === '' || v === undefined || v === null)
|
|
41
|
+
return null
|
|
42
|
+
if (typeof v === 'number')
|
|
43
|
+
return Number.isNaN(v) ? null : v
|
|
44
|
+
// Strip common formatting (e.g., "1,234.56")
|
|
45
|
+
const cleaned = String(v).replace(/,/g, '').trim()
|
|
46
|
+
const n = Number(cleaned)
|
|
47
|
+
return Number.isNaN(n) ? null : n
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Internal numeric ref exposed to NumberField
|
|
51
|
+
const numericValue = computed({
|
|
52
|
+
get() {
|
|
53
|
+
// Prefer current raw value; fall back to default
|
|
54
|
+
const base = rawModel.value ?? props.defaultValue ?? null
|
|
55
|
+
return toNumberOrNull(base)
|
|
56
|
+
},
|
|
57
|
+
set(val) {
|
|
58
|
+
// NumberField may pass string or number; normalize to number | null
|
|
59
|
+
const n = toNumberOrNull(val)
|
|
60
|
+
emits('update:modelValue', n)
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<div>
|
|
67
|
+
<FormField v-slot="{ componentField }" :name="props.name">
|
|
68
|
+
<FormItem>
|
|
69
|
+
<FormLabel class="flex">
|
|
70
|
+
{{ props.label }}
|
|
71
|
+
<div class="ml-auto inline-block">
|
|
72
|
+
<slot />
|
|
73
|
+
</div>
|
|
74
|
+
</FormLabel>
|
|
75
|
+
|
|
76
|
+
<NumberField
|
|
77
|
+
v-model="numericValue"
|
|
78
|
+
:default-value="numericValue ?? undefined"
|
|
79
|
+
:class="props.class"
|
|
80
|
+
:min="props.min"
|
|
81
|
+
:max="props.max"
|
|
82
|
+
:format-options="props.formatOptions"
|
|
83
|
+
:step="props.step"
|
|
84
|
+
:disabled="props.disabled"
|
|
85
|
+
>
|
|
86
|
+
<NumberFieldContent>
|
|
87
|
+
<NumberFieldDecrement class="mt-5" />
|
|
88
|
+
<FormControl>
|
|
89
|
+
<NumberFieldInput />
|
|
90
|
+
</FormControl>
|
|
91
|
+
<NumberFieldIncrement class="mt-5" />
|
|
92
|
+
</NumberFieldContent>
|
|
93
|
+
</NumberField>
|
|
94
|
+
|
|
95
|
+
<!-- keep validation/form integration without polluting NumberField props -->
|
|
96
|
+
<input type="hidden" v-bind="componentField" :value="numericValue ?? ''">
|
|
97
|
+
|
|
98
|
+
<span class="absolute end-0 inset-y-0 flex items-center justify-center px-2">
|
|
99
|
+
<slot name="icon" />
|
|
100
|
+
</span>
|
|
101
|
+
|
|
102
|
+
<FormDescription>
|
|
103
|
+
{{ props.description }} <slot name="description" />
|
|
104
|
+
</FormDescription>
|
|
105
|
+
<FormMessage />
|
|
106
|
+
</FormItem>
|
|
107
|
+
</FormField>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVModel } from '@vueuse/core'
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
name: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: false,
|
|
7
|
+
},
|
|
8
|
+
modelValue: {
|
|
9
|
+
type: [String, Boolean, Number, Array],
|
|
10
|
+
required: false,
|
|
11
|
+
},
|
|
12
|
+
class: {
|
|
13
|
+
type: null,
|
|
14
|
+
required: false,
|
|
15
|
+
default: 'w-full',
|
|
16
|
+
},
|
|
17
|
+
placeholder: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: false,
|
|
20
|
+
},
|
|
21
|
+
label: {
|
|
22
|
+
type: String,
|
|
23
|
+
required: false,
|
|
24
|
+
},
|
|
25
|
+
description: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: false,
|
|
28
|
+
},
|
|
29
|
+
disabled: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
required: false,
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
items: {
|
|
35
|
+
type: Array,
|
|
36
|
+
required: false,
|
|
37
|
+
default: () => [],
|
|
38
|
+
},
|
|
39
|
+
itemTitle: {
|
|
40
|
+
type: String,
|
|
41
|
+
required: false,
|
|
42
|
+
default: 'title',
|
|
43
|
+
},
|
|
44
|
+
itemValue: {
|
|
45
|
+
type: String,
|
|
46
|
+
required: false,
|
|
47
|
+
default: 'name',
|
|
48
|
+
},
|
|
49
|
+
multiple: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: false,
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const emits = defineEmits(['update:modelValue'])
|
|
56
|
+
|
|
57
|
+
const computedItems = computed(() => {
|
|
58
|
+
return props.items.map((item) => {
|
|
59
|
+
if (typeof item === 'string') {
|
|
60
|
+
return { [props.itemTitle]: item, [props.itemValue]: item }
|
|
61
|
+
}
|
|
62
|
+
const getNestedValue = (obj, path) => path.split('.').reduce((acc, key) => acc && acc[key], obj)
|
|
63
|
+
return {
|
|
64
|
+
[props.itemTitle]: getNestedValue(item, props.itemTitle),
|
|
65
|
+
[props.itemValue]: getNestedValue(item, props.itemValue),
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const modelValue = useVModel(props, 'modelValue', emits, {
|
|
71
|
+
passive: false,
|
|
72
|
+
prop: 'modelValue',
|
|
73
|
+
})
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<template v-if="props.name">
|
|
78
|
+
<FormField v-slot="{ componentField }" :name="props.name">
|
|
79
|
+
<FormItem>
|
|
80
|
+
<FormLabel class="flex">
|
|
81
|
+
{{ props.label }}
|
|
82
|
+
<div class="ml-auto inline-block">
|
|
83
|
+
<slot />
|
|
84
|
+
</div>
|
|
85
|
+
</FormLabel>
|
|
86
|
+
<div class="relative w-full items-center">
|
|
87
|
+
<Select v-model="modelValue" :disabled="props.disabled" :multiple="props.multiple" :default-value="modelValue" v-bind="componentField">
|
|
88
|
+
<FormControl>
|
|
89
|
+
<SelectTrigger class="text-foreground" :class="[$slots.icon ? 'pr-8' : '', props.class]">
|
|
90
|
+
<SelectValue :placeholder="props.placeholder" />
|
|
91
|
+
</SelectTrigger>
|
|
92
|
+
</FormControl>
|
|
93
|
+
<SelectContent>
|
|
94
|
+
<SelectGroup>
|
|
95
|
+
<SelectItem
|
|
96
|
+
v-for="item in computedItems"
|
|
97
|
+
:key="item[props.itemTitle]"
|
|
98
|
+
:value="item[props.itemValue]"
|
|
99
|
+
>
|
|
100
|
+
{{ item[props.itemTitle] }}
|
|
101
|
+
</SelectItem>
|
|
102
|
+
</SelectGroup>
|
|
103
|
+
</SelectContent>
|
|
104
|
+
</Select>
|
|
105
|
+
<span class="absolute end-0 inset-y-0 flex items-center justify-center pl-2 pr-2">
|
|
106
|
+
<slot name="icon" />
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
<FormDescription>
|
|
110
|
+
{{ props.description }}
|
|
111
|
+
</FormDescription>
|
|
112
|
+
<FormMessage />
|
|
113
|
+
</FormItem>
|
|
114
|
+
</FormField>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<template v-else>
|
|
118
|
+
<div class="w-full">
|
|
119
|
+
<label class="flex mb-1">
|
|
120
|
+
{{ props.label }}
|
|
121
|
+
</label>
|
|
122
|
+
<div class="relative w-full items-center">
|
|
123
|
+
<Select v-model="modelValue" :disabled="props.disabled" :default-value="modelValue">
|
|
124
|
+
<SelectTrigger class="text-foreground" :class="[$slots.icon ? 'pr-8' : '', props.class]">
|
|
125
|
+
<SelectValue />
|
|
126
|
+
</SelectTrigger>
|
|
127
|
+
<SelectContent>
|
|
128
|
+
<SelectGroup>
|
|
129
|
+
<SelectItem
|
|
130
|
+
v-for="item in computedItems"
|
|
131
|
+
:key="item[props.itemTitle]"
|
|
132
|
+
:value="item[props.itemValue]"
|
|
133
|
+
>
|
|
134
|
+
{{ item[props.itemTitle] }}
|
|
135
|
+
</SelectItem>
|
|
136
|
+
</SelectGroup>
|
|
137
|
+
</SelectContent>
|
|
138
|
+
</Select>
|
|
139
|
+
<span class="absolute end-0 inset-y-0 flex items-center justify-center pl-2 pr-2">
|
|
140
|
+
<slot name="icon" />
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
144
|
+
{{ props.description }}
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
</template>
|
|
149
|
+
|
|
150
|
+
<style lang="scss" scoped>
|
|
151
|
+
</style>
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useFilter } from 'reka-ui'
|
|
3
|
+
import { useVModel } from '@vueuse/core'
|
|
4
|
+
import { cn } from '~/lib/utils'
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
name: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: false,
|
|
9
|
+
},
|
|
10
|
+
modelValue: {
|
|
11
|
+
type: Array,
|
|
12
|
+
required: false,
|
|
13
|
+
},
|
|
14
|
+
class: {
|
|
15
|
+
type: null,
|
|
16
|
+
required: false,
|
|
17
|
+
default: 'w-full',
|
|
18
|
+
},
|
|
19
|
+
placeholder: {
|
|
20
|
+
type: String,
|
|
21
|
+
required: false,
|
|
22
|
+
},
|
|
23
|
+
label: {
|
|
24
|
+
type: String,
|
|
25
|
+
required: false,
|
|
26
|
+
},
|
|
27
|
+
description: {
|
|
28
|
+
type: String,
|
|
29
|
+
required: false,
|
|
30
|
+
},
|
|
31
|
+
disabled: {
|
|
32
|
+
type: Boolean,
|
|
33
|
+
required: false,
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
36
|
+
items: {
|
|
37
|
+
type: Array,
|
|
38
|
+
required: false,
|
|
39
|
+
default: () => [],
|
|
40
|
+
},
|
|
41
|
+
itemTitle: {
|
|
42
|
+
type: String,
|
|
43
|
+
required: false,
|
|
44
|
+
default: 'title',
|
|
45
|
+
},
|
|
46
|
+
itemValue: {
|
|
47
|
+
type: String,
|
|
48
|
+
required: false,
|
|
49
|
+
default: 'name',
|
|
50
|
+
},
|
|
51
|
+
allowAdditions: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const emits = defineEmits(['update:modelValue', 'add'])
|
|
58
|
+
|
|
59
|
+
const computedItems = computed(() => {
|
|
60
|
+
return props.items.map((item) => {
|
|
61
|
+
if (typeof item === 'string') {
|
|
62
|
+
return { [props.itemTitle]: item, [props.itemValue]: item }
|
|
63
|
+
}
|
|
64
|
+
const getNestedValue = (obj, path) => path.split('.').reduce((acc, key) => acc && acc[key], obj)
|
|
65
|
+
return {
|
|
66
|
+
[props.itemTitle]: getNestedValue(item, props.itemTitle),
|
|
67
|
+
[props.itemValue]: getNestedValue(item, props.itemValue),
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const modelValue = useVModel(props, 'modelValue', emits, {
|
|
73
|
+
passive: false,
|
|
74
|
+
prop: 'modelValue',
|
|
75
|
+
})
|
|
76
|
+
const open = ref(false)
|
|
77
|
+
const searchTerm = ref('')
|
|
78
|
+
|
|
79
|
+
const { contains } = useFilter({ sensitivity: 'base' })
|
|
80
|
+
|
|
81
|
+
const forceOpen = () => {
|
|
82
|
+
if (!props.disabled)
|
|
83
|
+
open.value = true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const filteredItems = computed(() => {
|
|
87
|
+
const options = computedItems.value.filter(i => !modelValue.value.includes(i[props.itemValue]))
|
|
88
|
+
return searchTerm.value ? options.filter(option => contains(option[props.itemTitle], searchTerm.value)) : options
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const valueToTitle = computed(() => {
|
|
92
|
+
const map = {}
|
|
93
|
+
for (const it of computedItems.value) {
|
|
94
|
+
map[it[props.itemValue]] = it[props.itemTitle]
|
|
95
|
+
}
|
|
96
|
+
return map
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const existingValueMap = computed(() => {
|
|
100
|
+
const map = {}
|
|
101
|
+
for (const item of computedItems.value) {
|
|
102
|
+
const value = String(item[props.itemValue])
|
|
103
|
+
map[value.toLowerCase()] = value
|
|
104
|
+
}
|
|
105
|
+
return map
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const normalize = value => String(value || '').trim()
|
|
109
|
+
|
|
110
|
+
const addValueToModel = (value) => {
|
|
111
|
+
const normalized = normalize(value)
|
|
112
|
+
if (!normalized)
|
|
113
|
+
return
|
|
114
|
+
if (!modelValue.value.includes(normalized))
|
|
115
|
+
modelValue.value = [...modelValue.value, normalized]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const addFromSearch = () => {
|
|
119
|
+
if (!props.allowAdditions)
|
|
120
|
+
return
|
|
121
|
+
const raw = normalize(searchTerm.value)
|
|
122
|
+
if (!raw)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
const existingValue = existingValueMap.value[raw.toLowerCase()]
|
|
126
|
+
if (existingValue) {
|
|
127
|
+
addValueToModel(existingValue)
|
|
128
|
+
searchTerm.value = ''
|
|
129
|
+
open.value = false
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
addValueToModel(raw)
|
|
134
|
+
emits('add', raw)
|
|
135
|
+
searchTerm.value = ''
|
|
136
|
+
open.value = false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const onEnter = () => {
|
|
140
|
+
if (!props.allowAdditions)
|
|
141
|
+
return
|
|
142
|
+
addFromSearch()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const onSelectItem = (ev) => {
|
|
146
|
+
const value = ev?.detail?.value
|
|
147
|
+
if (typeof value === 'string') {
|
|
148
|
+
searchTerm.value = ''
|
|
149
|
+
addValueToModel(value)
|
|
150
|
+
}
|
|
151
|
+
if (filteredItems.value.length === 0)
|
|
152
|
+
open.value = false
|
|
153
|
+
}
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
<template>
|
|
157
|
+
<template v-if="props.name">
|
|
158
|
+
<FormField :name="props.name">
|
|
159
|
+
<FormItem>
|
|
160
|
+
<FormLabel class="flex">
|
|
161
|
+
{{ props.label }}
|
|
162
|
+
<div class="ml-auto inline-block">
|
|
163
|
+
<slot />
|
|
164
|
+
</div>
|
|
165
|
+
</FormLabel>
|
|
166
|
+
<div class="relative w-full items-center">
|
|
167
|
+
<FormControl>
|
|
168
|
+
<Combobox v-model="modelValue" v-model:open="open" :ignore-filter="true" :disabled="props.disabled">
|
|
169
|
+
<ComboboxAnchor as-child>
|
|
170
|
+
<TagsInput
|
|
171
|
+
v-model="modelValue"
|
|
172
|
+
:class="cn('relative flex items-center gap-1 flex-nowrap pl-2 pr-1 pt-[7px] pb-[7px] w-80', props.class)"
|
|
173
|
+
:disabled="props.disabled"
|
|
174
|
+
@click="forceOpen"
|
|
175
|
+
>
|
|
176
|
+
<!-- Wrapping area for tags + input -->
|
|
177
|
+
<div class="flex flex-wrap items-center gap-2 flex-1 min-w-0">
|
|
178
|
+
<TagsInputItem v-for="val in modelValue" :key="val" class="h-6" :value="val">
|
|
179
|
+
<span class="px-1">{{ valueToTitle[val] ?? val }}</span>
|
|
180
|
+
<TagsInputItemDelete v-if="!props.disabled" />
|
|
181
|
+
</TagsInputItem>
|
|
182
|
+
|
|
183
|
+
<ComboboxInput v-model="searchTerm" as-child>
|
|
184
|
+
<TagsInputInput
|
|
185
|
+
:disabled="props.disabled"
|
|
186
|
+
:placeholder="props.placeholder"
|
|
187
|
+
class="p-0 border-none shadow-none focus-visible:ring-0 h-auto grow min-w-[6rem] w-auto"
|
|
188
|
+
@input="forceOpen"
|
|
189
|
+
@keydown.enter.prevent="onEnter"
|
|
190
|
+
/>
|
|
191
|
+
</ComboboxInput>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<!-- Non-wrapping trigger -->
|
|
195
|
+
<ComboboxTrigger as-child>
|
|
196
|
+
<Button variant="ghost" size="icon" class="h-6 w-6 z-10">
|
|
197
|
+
<ChevronsUpDown class="h-4 w-4 shrink-0 opacity-50" />
|
|
198
|
+
</Button>
|
|
199
|
+
</ComboboxTrigger>
|
|
200
|
+
</TagsInput>
|
|
201
|
+
|
|
202
|
+
<ComboboxList class="w-[--reka-popper-anchor-width]">
|
|
203
|
+
<ComboboxEmpty>
|
|
204
|
+
<button
|
|
205
|
+
v-if="props.allowAdditions && searchTerm && searchTerm.trim() && !computedItems.some(item => String(item[props.itemValue]).toLowerCase() === searchTerm.trim().toLowerCase())"
|
|
206
|
+
type="button"
|
|
207
|
+
class="w-full rounded-sm px-2 py-1 text-left text-sm hover:bg-muted"
|
|
208
|
+
@click.prevent="addFromSearch()"
|
|
209
|
+
>
|
|
210
|
+
Create "{{ searchTerm.trim() }}"
|
|
211
|
+
</button>
|
|
212
|
+
<span v-else-if="searchTerm && searchTerm.trim()" class="block px-2 py-1 text-sm text-muted-foreground">
|
|
213
|
+
No results found
|
|
214
|
+
</span>
|
|
215
|
+
<span v-else class="block px-2 py-1 text-sm text-muted-foreground">
|
|
216
|
+
Start typing to search
|
|
217
|
+
</span>
|
|
218
|
+
</ComboboxEmpty>
|
|
219
|
+
<ComboboxGroup>
|
|
220
|
+
<ComboboxItem
|
|
221
|
+
v-for="item in filteredItems" :key="item[props.itemValue]" :value="item[props.itemValue]"
|
|
222
|
+
@select.prevent="onSelectItem"
|
|
223
|
+
>
|
|
224
|
+
{{ item[props.itemTitle] }}
|
|
225
|
+
</ComboboxItem>
|
|
226
|
+
</ComboboxGroup>
|
|
227
|
+
</ComboboxList>
|
|
228
|
+
</ComboboxAnchor>
|
|
229
|
+
</Combobox>
|
|
230
|
+
</FormControl>
|
|
231
|
+
|
|
232
|
+
<span class="absolute end-0 inset-y-0 flex items-center justify-center pl-2 pr-2">
|
|
233
|
+
<slot name="icon" />
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
<FormDescription>
|
|
237
|
+
{{ props.description }}
|
|
238
|
+
</FormDescription>
|
|
239
|
+
<FormMessage />
|
|
240
|
+
</FormItem>
|
|
241
|
+
</FormField>
|
|
242
|
+
</template>
|
|
243
|
+
|
|
244
|
+
<template v-else>
|
|
245
|
+
<div class="w-full">
|
|
246
|
+
<label class="flex mb-1">
|
|
247
|
+
{{ props.label }}
|
|
248
|
+
</label>
|
|
249
|
+
<div class="relative w-full items-center">
|
|
250
|
+
<Select v-model="modelValue" :disabled="props.disabled" :default-value="modelValue">
|
|
251
|
+
<SelectTrigger class="text-foreground" :class="[$slots.icon ? 'pr-8' : '', props.class]">
|
|
252
|
+
<SelectValue />
|
|
253
|
+
</SelectTrigger>
|
|
254
|
+
<SelectContent>
|
|
255
|
+
<SelectGroup>
|
|
256
|
+
<SelectItem
|
|
257
|
+
v-for="item in computedItems"
|
|
258
|
+
:key="item[props.itemTitle]"
|
|
259
|
+
:value="item[props.itemValue]"
|
|
260
|
+
>
|
|
261
|
+
{{ item[props.itemTitle] }}
|
|
262
|
+
</SelectItem>
|
|
263
|
+
</SelectGroup>
|
|
264
|
+
</SelectContent>
|
|
265
|
+
</Select>
|
|
266
|
+
<span class="absolute end-0 inset-y-0 flex items-center justify-center pl-2 pr-2">
|
|
267
|
+
<slot name="icon" />
|
|
268
|
+
</span>
|
|
269
|
+
</div>
|
|
270
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
271
|
+
{{ props.description }}
|
|
272
|
+
</p>
|
|
273
|
+
</div>
|
|
274
|
+
</template>
|
|
275
|
+
</template>
|
|
276
|
+
|
|
277
|
+
<style lang="scss" scoped>
|
|
278
|
+
</style>
|