@edgedev/create-edge-app 1.1.23 → 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/.env +1 -0
- package/.env.dev +1 -0
- 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/firebase.json +5 -2
- package/firebase_init.sh +21 -6
- package/package.json +1 -1
- package/plugins/firebase.client.ts +1 -0
- package/edge-components-install.sh +0 -1
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, reactive, watchEffect } from 'vue'
|
|
3
|
+
import { useVModel } from '@vueuse/core'
|
|
4
|
+
import { File as FileIcon, FileMinus2, FilePen, Folder, FolderMinus, FolderPen, FolderPlus, GripVertical, Plus } from 'lucide-vue-next'
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
modelValue: {
|
|
8
|
+
type: Object,
|
|
9
|
+
required: true,
|
|
10
|
+
},
|
|
11
|
+
templateOptions: {
|
|
12
|
+
type: Array,
|
|
13
|
+
default: () => [],
|
|
14
|
+
},
|
|
15
|
+
templatePages: {
|
|
16
|
+
type: Object,
|
|
17
|
+
default: () => ({}),
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits(['update:modelValue'])
|
|
22
|
+
|
|
23
|
+
const modelValue = useVModel(props, 'modelValue', emit)
|
|
24
|
+
|
|
25
|
+
const ROOT_MENUS = ['Site Root', 'Not In Menu']
|
|
26
|
+
const dragGroup = { name: 'theme-default-menus', pull: true, put: true }
|
|
27
|
+
|
|
28
|
+
const state = reactive({
|
|
29
|
+
folderDialog: {
|
|
30
|
+
open: false,
|
|
31
|
+
menu: '',
|
|
32
|
+
value: '',
|
|
33
|
+
},
|
|
34
|
+
renameDialog: {
|
|
35
|
+
open: false,
|
|
36
|
+
type: 'page',
|
|
37
|
+
menu: '',
|
|
38
|
+
folder: '',
|
|
39
|
+
index: -1,
|
|
40
|
+
value: '',
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const ensureMenuStructure = () => {
|
|
45
|
+
if (!modelValue.value || typeof modelValue.value !== 'object' || Array.isArray(modelValue.value))
|
|
46
|
+
modelValue.value = {}
|
|
47
|
+
for (const key of ROOT_MENUS) {
|
|
48
|
+
if (!Array.isArray(modelValue.value[key]))
|
|
49
|
+
modelValue.value[key] = []
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
watchEffect(ensureMenuStructure)
|
|
54
|
+
|
|
55
|
+
const slugify = (value) => {
|
|
56
|
+
return String(value || '')
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
60
|
+
.replace(/(^-|-$)+/g, '') || 'page'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const uniqueSlug = (value, siblings = [], current = '', options = {}) => {
|
|
64
|
+
const { forceSuffix = false } = options
|
|
65
|
+
const base = slugify(value) || 'page'
|
|
66
|
+
const siblingSet = new Set(siblings.filter(Boolean))
|
|
67
|
+
if (current)
|
|
68
|
+
siblingSet.delete(current)
|
|
69
|
+
if (!forceSuffix) {
|
|
70
|
+
let candidate = base
|
|
71
|
+
let suffix = 1
|
|
72
|
+
while (siblingSet.has(candidate)) {
|
|
73
|
+
candidate = `${base}-${suffix}`
|
|
74
|
+
suffix += 1
|
|
75
|
+
}
|
|
76
|
+
return candidate
|
|
77
|
+
}
|
|
78
|
+
let suffix = 1
|
|
79
|
+
let candidate = `${base}-${suffix}`
|
|
80
|
+
while (siblingSet.has(candidate)) {
|
|
81
|
+
suffix += 1
|
|
82
|
+
candidate = `${base}-${suffix}`
|
|
83
|
+
}
|
|
84
|
+
return candidate
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isFolder = entry => entry && typeof entry.item === 'object'
|
|
88
|
+
|
|
89
|
+
const getFolderName = (entry) => {
|
|
90
|
+
if (!isFolder(entry))
|
|
91
|
+
return ''
|
|
92
|
+
return Object.keys(entry.item || {})[0] || ''
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const getFolderList = (menuName, folderName) => {
|
|
96
|
+
const targetMenu = modelValue.value[menuName] || []
|
|
97
|
+
const folderEntry = targetMenu.find(entry => isFolder(entry) && getFolderName(entry) === folderName)
|
|
98
|
+
if (!folderEntry)
|
|
99
|
+
return null
|
|
100
|
+
return folderEntry.item[folderName]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const getParentList = (menuName, folderName = null) => {
|
|
104
|
+
if (!folderName)
|
|
105
|
+
return modelValue.value[menuName]
|
|
106
|
+
return getFolderList(menuName, folderName)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const resolveTemplateTitle = (pageId) => {
|
|
110
|
+
return props.templatePages?.[pageId]?.name || 'Untitled Page'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const siblingSlugs = (list = [], excludeIndex = -1) => {
|
|
114
|
+
return list
|
|
115
|
+
.map((entry, idx) => (idx === excludeIndex || typeof entry?.item !== 'string') ? null : entry.name)
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const addPageToList = (list, pageId, nameHint) => {
|
|
120
|
+
if (!Array.isArray(list))
|
|
121
|
+
return
|
|
122
|
+
const siblings = siblingSlugs(list)
|
|
123
|
+
const slug = uniqueSlug(nameHint, siblings, '', { forceSuffix: true })
|
|
124
|
+
list.push({
|
|
125
|
+
name: slug,
|
|
126
|
+
item: pageId,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const addPageToMenu = (menuName, pageId, folderName = null) => {
|
|
131
|
+
if (!pageId)
|
|
132
|
+
return
|
|
133
|
+
const targetList = getParentList(menuName, folderName)
|
|
134
|
+
if (!targetList)
|
|
135
|
+
return
|
|
136
|
+
addPageToList(targetList, pageId, resolveTemplateTitle(pageId))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const removePage = (menuName, index, folderName = null) => {
|
|
140
|
+
const targetList = getParentList(menuName, folderName)
|
|
141
|
+
if (!targetList)
|
|
142
|
+
return
|
|
143
|
+
targetList.splice(index, 1)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const folderNamesInMenu = (menuName) => {
|
|
147
|
+
return (modelValue.value[menuName] || [])
|
|
148
|
+
.filter(entry => isFolder(entry))
|
|
149
|
+
.map(entry => getFolderName(entry))
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const uniqueFolderSlug = (value, menuName, current = '') => {
|
|
154
|
+
const names = new Set(folderNamesInMenu(menuName))
|
|
155
|
+
if (current)
|
|
156
|
+
names.delete(current)
|
|
157
|
+
const base = slugify(value) || 'folder'
|
|
158
|
+
let candidate = base
|
|
159
|
+
let suffix = 1
|
|
160
|
+
while (names.has(candidate)) {
|
|
161
|
+
candidate = `${base}-${suffix}`
|
|
162
|
+
suffix += 1
|
|
163
|
+
}
|
|
164
|
+
return candidate
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const openAddFolderDialog = (menuName) => {
|
|
168
|
+
state.folderDialog.menu = menuName
|
|
169
|
+
state.folderDialog.value = ''
|
|
170
|
+
state.folderDialog.open = true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const submitFolderDialog = () => {
|
|
174
|
+
if (!state.folderDialog.menu)
|
|
175
|
+
return
|
|
176
|
+
const value = state.folderDialog.value?.trim()
|
|
177
|
+
if (!value)
|
|
178
|
+
return
|
|
179
|
+
const slug = uniqueFolderSlug(value, state.folderDialog.menu)
|
|
180
|
+
modelValue.value[state.folderDialog.menu].push({
|
|
181
|
+
item: { [slug]: [] },
|
|
182
|
+
})
|
|
183
|
+
state.folderDialog.open = false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const canDeleteFolder = (entry) => {
|
|
187
|
+
if (!isFolder(entry))
|
|
188
|
+
return false
|
|
189
|
+
const folderName = getFolderName(entry)
|
|
190
|
+
return !(entry.item?.[folderName]?.length)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const deleteFolder = (menuName, index) => {
|
|
194
|
+
const target = modelValue.value[menuName]?.[index]
|
|
195
|
+
if (!target || !canDeleteFolder(target))
|
|
196
|
+
return
|
|
197
|
+
modelValue.value[menuName].splice(index, 1)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const openRenameDialogForPage = (menuName, index, folderName = null) => {
|
|
201
|
+
const parentList = getParentList(menuName, folderName)
|
|
202
|
+
if (!parentList?.[index])
|
|
203
|
+
return
|
|
204
|
+
state.renameDialog = {
|
|
205
|
+
open: true,
|
|
206
|
+
type: 'page',
|
|
207
|
+
menu: menuName,
|
|
208
|
+
folder: folderName,
|
|
209
|
+
index,
|
|
210
|
+
value: parentList[index].name || '',
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const openRenameDialogForFolder = (menuName, folderName, index) => {
|
|
215
|
+
state.renameDialog = {
|
|
216
|
+
open: true,
|
|
217
|
+
type: 'folder',
|
|
218
|
+
menu: menuName,
|
|
219
|
+
folder: folderName,
|
|
220
|
+
index,
|
|
221
|
+
value: folderName,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const submitRenameDialog = () => {
|
|
226
|
+
if (!state.renameDialog.open)
|
|
227
|
+
return
|
|
228
|
+
const value = state.renameDialog.value?.trim()
|
|
229
|
+
if (state.renameDialog.type === 'page') {
|
|
230
|
+
const parentList = getParentList(state.renameDialog.menu, state.renameDialog.folder)
|
|
231
|
+
const target = parentList?.[state.renameDialog.index]
|
|
232
|
+
if (!parentList || !target)
|
|
233
|
+
return
|
|
234
|
+
const siblings = siblingSlugs(parentList, state.renameDialog.index)
|
|
235
|
+
target.name = uniqueSlug(value || target.name, siblings, target.name)
|
|
236
|
+
}
|
|
237
|
+
else if (state.renameDialog.type === 'folder') {
|
|
238
|
+
const folderList = modelValue.value[state.renameDialog.menu] || []
|
|
239
|
+
const target = folderList[state.renameDialog.index]
|
|
240
|
+
if (!isFolder(target))
|
|
241
|
+
return
|
|
242
|
+
const currentName = getFolderName(target)
|
|
243
|
+
const slug = uniqueFolderSlug(value || currentName, state.renameDialog.menu, currentName)
|
|
244
|
+
if (slug !== currentName) {
|
|
245
|
+
target.item[slug] = target.item[currentName]
|
|
246
|
+
delete target.item[currentName]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
state.renameDialog.open = false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const displayMenuName = menuName => (menuName === 'Site Root' ? 'Site Menu' : menuName)
|
|
253
|
+
|
|
254
|
+
const hasEntries = computed(() => {
|
|
255
|
+
return ROOT_MENUS.some(menu => (modelValue.value[menu] || []).length)
|
|
256
|
+
})
|
|
257
|
+
</script>
|
|
258
|
+
|
|
259
|
+
<template>
|
|
260
|
+
<div class="space-y-6">
|
|
261
|
+
<div
|
|
262
|
+
v-for="menuName in ROOT_MENUS"
|
|
263
|
+
:key="menuName"
|
|
264
|
+
class="rounded-lg border border-border bg-card/30 p-4 space-y-3"
|
|
265
|
+
>
|
|
266
|
+
<div class="flex items-start justify-between gap-3">
|
|
267
|
+
<div>
|
|
268
|
+
<p class="text-sm font-semibold">
|
|
269
|
+
{{ displayMenuName(menuName) }}
|
|
270
|
+
</p>
|
|
271
|
+
<p class="text-xs text-muted-foreground">
|
|
272
|
+
{{ menuName === 'Site Root' ? 'Visible navigation for new sites.' : 'Pages that are created but hidden from navigation.' }}
|
|
273
|
+
</p>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="flex gap-1">
|
|
276
|
+
<DropdownMenu>
|
|
277
|
+
<DropdownMenuTrigger as-child>
|
|
278
|
+
<edge-shad-button
|
|
279
|
+
size="icon"
|
|
280
|
+
variant="ghost"
|
|
281
|
+
class="h-8 w-8"
|
|
282
|
+
:disabled="!templateOptions.length"
|
|
283
|
+
aria-label="Add page"
|
|
284
|
+
>
|
|
285
|
+
<Plus class="w-4 h-4" />
|
|
286
|
+
</edge-shad-button>
|
|
287
|
+
</DropdownMenuTrigger>
|
|
288
|
+
<DropdownMenuContent class="w-64">
|
|
289
|
+
<template v-if="templateOptions.length">
|
|
290
|
+
<DropdownMenuItem
|
|
291
|
+
v-for="option in templateOptions"
|
|
292
|
+
:key="option.value"
|
|
293
|
+
@click="addPageToMenu(menuName, option.value)"
|
|
294
|
+
>
|
|
295
|
+
{{ option.label }}
|
|
296
|
+
</DropdownMenuItem>
|
|
297
|
+
</template>
|
|
298
|
+
<div v-else class="px-3 py-2 text-xs text-muted-foreground">
|
|
299
|
+
All template pages are already assigned.
|
|
300
|
+
</div>
|
|
301
|
+
</DropdownMenuContent>
|
|
302
|
+
</DropdownMenu>
|
|
303
|
+
<edge-shad-button
|
|
304
|
+
size="icon"
|
|
305
|
+
variant="ghost"
|
|
306
|
+
class="h-8 w-8"
|
|
307
|
+
aria-label="Add folder"
|
|
308
|
+
@click="openAddFolderDialog(menuName)"
|
|
309
|
+
>
|
|
310
|
+
<FolderPlus class="w-4 h-4" />
|
|
311
|
+
</edge-shad-button>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<draggable
|
|
316
|
+
:list="modelValue[menuName]"
|
|
317
|
+
handle=".drag-handle"
|
|
318
|
+
item-key="name"
|
|
319
|
+
class="space-y-3"
|
|
320
|
+
:group="dragGroup"
|
|
321
|
+
>
|
|
322
|
+
<template #item="{ element, index }">
|
|
323
|
+
<div v-if="isFolder(element)" class="rounded-md border border-border/60 bg-background/60">
|
|
324
|
+
<div class="flex items-center justify-between gap-2 border-b border-border/60 px-3 py-2">
|
|
325
|
+
<div class="flex items-center gap-2">
|
|
326
|
+
<GripVertical class="w-4 h-4 text-muted-foreground drag-handle" />
|
|
327
|
+
<div>
|
|
328
|
+
<div class="text-sm font-semibold flex items-center gap-1">
|
|
329
|
+
<Folder class="w-4 h-4" />
|
|
330
|
+
{{ getFolderName(element) }}
|
|
331
|
+
</div>
|
|
332
|
+
<div class="text-[11px] text-muted-foreground">
|
|
333
|
+
Folder
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="flex gap-1">
|
|
338
|
+
<edge-shad-button
|
|
339
|
+
variant="ghost"
|
|
340
|
+
size="icon"
|
|
341
|
+
class="h-7 w-7"
|
|
342
|
+
@click="openRenameDialogForFolder(menuName, getFolderName(element), index)"
|
|
343
|
+
>
|
|
344
|
+
<FolderPen class="w-3.5 h-3.5" />
|
|
345
|
+
</edge-shad-button>
|
|
346
|
+
<edge-shad-button
|
|
347
|
+
variant="ghost"
|
|
348
|
+
size="icon"
|
|
349
|
+
class="h-7 w-7"
|
|
350
|
+
:disabled="!canDeleteFolder(element)"
|
|
351
|
+
@click="deleteFolder(menuName, index)"
|
|
352
|
+
>
|
|
353
|
+
<FolderMinus class="w-3.5 h-3.5" />
|
|
354
|
+
</edge-shad-button>
|
|
355
|
+
<DropdownMenu>
|
|
356
|
+
<DropdownMenuTrigger as-child>
|
|
357
|
+
<edge-shad-button
|
|
358
|
+
size="icon"
|
|
359
|
+
class="h-7 w-7"
|
|
360
|
+
variant="ghost"
|
|
361
|
+
|
|
362
|
+
:disabled="!templateOptions.length"
|
|
363
|
+
>
|
|
364
|
+
<Plus class="w-3.5 h-3.5" />
|
|
365
|
+
</edge-shad-button>
|
|
366
|
+
</DropdownMenuTrigger>
|
|
367
|
+
<DropdownMenuContent class="w-64">
|
|
368
|
+
<template v-if="templateOptions.length">
|
|
369
|
+
<DropdownMenuItem
|
|
370
|
+
v-for="option in templateOptions"
|
|
371
|
+
:key="`${getFolderName(element)}-${option.value}`"
|
|
372
|
+
@click="addPageToMenu(menuName, option.value, getFolderName(element))"
|
|
373
|
+
>
|
|
374
|
+
{{ option.label }}
|
|
375
|
+
</DropdownMenuItem>
|
|
376
|
+
</template>
|
|
377
|
+
<div v-else class="px-3 py-2 text-xs text-muted-foreground">
|
|
378
|
+
All template pages are already assigned.
|
|
379
|
+
</div>
|
|
380
|
+
</DropdownMenuContent>
|
|
381
|
+
</DropdownMenu>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="space-y-3 border-l border-dashed border-border/80 px-3 py-3 ml-4">
|
|
385
|
+
<div class="flex justify-end" />
|
|
386
|
+
<draggable
|
|
387
|
+
:list="element.item[getFolderName(element)]"
|
|
388
|
+
handle=".drag-handle"
|
|
389
|
+
item-key="name"
|
|
390
|
+
class="space-y-2"
|
|
391
|
+
:group="dragGroup"
|
|
392
|
+
>
|
|
393
|
+
<template #item="{ element: child, index: childIndex }">
|
|
394
|
+
<div class="flex items-center justify-between gap-3 rounded-md border border-border/70 bg-card px-3 py-2">
|
|
395
|
+
<div class="flex items-center gap-2">
|
|
396
|
+
<GripVertical class="w-4 h-4 text-muted-foreground drag-handle" />
|
|
397
|
+
<div>
|
|
398
|
+
<div class="text-sm font-semibold flex items-center gap-1">
|
|
399
|
+
<FileIcon class="w-4 h-4" />
|
|
400
|
+
{{ element.item[getFolderName(element)][childIndex].name }}
|
|
401
|
+
</div>
|
|
402
|
+
<div class="text-[11px] text-muted-foreground">
|
|
403
|
+
Template: {{ resolveTemplateTitle(element.item[getFolderName(element)][childIndex].item) }}
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
<div class="flex gap-1">
|
|
408
|
+
<edge-shad-button
|
|
409
|
+
variant="ghost"
|
|
410
|
+
size="icon"
|
|
411
|
+
class="h-7 w-7"
|
|
412
|
+
@click="openRenameDialogForPage(menuName, childIndex, getFolderName(element))"
|
|
413
|
+
>
|
|
414
|
+
<FilePen class="w-3.5 h-3.5" />
|
|
415
|
+
</edge-shad-button>
|
|
416
|
+
<edge-shad-button
|
|
417
|
+
variant="ghost"
|
|
418
|
+
size="icon"
|
|
419
|
+
class="h-7 w-7 text-destructive"
|
|
420
|
+
@click="removePage(menuName, childIndex, getFolderName(element))"
|
|
421
|
+
>
|
|
422
|
+
<FileMinus2 class="w-3.5 h-3.5" />
|
|
423
|
+
</edge-shad-button>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</template>
|
|
427
|
+
<template #footer>
|
|
428
|
+
<p
|
|
429
|
+
v-if="!(element.item[getFolderName(element)] || []).length"
|
|
430
|
+
class="text-xs text-muted-foreground italic pl-1"
|
|
431
|
+
>
|
|
432
|
+
No pages in this folder yet.
|
|
433
|
+
</p>
|
|
434
|
+
</template>
|
|
435
|
+
</draggable>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
<div v-else class="flex items-center justify-between gap-3 rounded-md border border-border/70 bg-card px-3 py-2">
|
|
439
|
+
<div class="flex items-center gap-2">
|
|
440
|
+
<GripVertical class="w-4 h-4 text-muted-foreground drag-handle" />
|
|
441
|
+
<div>
|
|
442
|
+
<div class="text-sm font-semibold flex items-center gap-1">
|
|
443
|
+
<FileIcon class="w-4 h-4" />
|
|
444
|
+
{{ element.name }}
|
|
445
|
+
</div>
|
|
446
|
+
<div class="text-[11px] text-muted-foreground">
|
|
447
|
+
Template: {{ resolveTemplateTitle(element.item) }}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
<div class="flex gap-1">
|
|
452
|
+
<edge-shad-button
|
|
453
|
+
variant="ghost"
|
|
454
|
+
size="icon"
|
|
455
|
+
class="h-7 w-7"
|
|
456
|
+
@click="openRenameDialogForPage(menuName, index)"
|
|
457
|
+
>
|
|
458
|
+
<FilePen class="w-3.5 h-3.5" />
|
|
459
|
+
</edge-shad-button>
|
|
460
|
+
<edge-shad-button
|
|
461
|
+
variant="ghost"
|
|
462
|
+
size="icon"
|
|
463
|
+
class="h-7 w-7 text-destructive"
|
|
464
|
+
@click="removePage(menuName, index)"
|
|
465
|
+
>
|
|
466
|
+
<FileMinus2 class="w-3.5 h-3.5" />
|
|
467
|
+
</edge-shad-button>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</template>
|
|
471
|
+
<template #footer>
|
|
472
|
+
<div
|
|
473
|
+
v-if="!(modelValue[menuName] || []).length"
|
|
474
|
+
class="rounded-md border border-dashed border-border/80 px-3 py-2 text-xs text-muted-foreground italic"
|
|
475
|
+
>
|
|
476
|
+
No entries yet. Use the buttons above to add pages or folders.
|
|
477
|
+
</div>
|
|
478
|
+
</template>
|
|
479
|
+
</draggable>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<div v-if="!hasEntries" class="text-xs text-muted-foreground">
|
|
483
|
+
Start by selecting template pages to include. Each page can only be used once across all menus.
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<edge-shad-dialog v-model="state.folderDialog.open">
|
|
488
|
+
<DialogContent class="pt-10">
|
|
489
|
+
<DialogHeader>
|
|
490
|
+
<DialogTitle>Create Folder</DialogTitle>
|
|
491
|
+
</DialogHeader>
|
|
492
|
+
<edge-shad-input
|
|
493
|
+
v-model="state.folderDialog.value"
|
|
494
|
+
name="folderName"
|
|
495
|
+
label="Folder Name"
|
|
496
|
+
placeholder="Enter folder name"
|
|
497
|
+
/>
|
|
498
|
+
<DialogFooter class="pt-2">
|
|
499
|
+
<edge-shad-button
|
|
500
|
+
variant="destructive"
|
|
501
|
+
@click="state.folderDialog.open = false"
|
|
502
|
+
>
|
|
503
|
+
Cancel
|
|
504
|
+
</edge-shad-button>
|
|
505
|
+
<edge-shad-button
|
|
506
|
+
type="button"
|
|
507
|
+
class="bg-slate-800 hover:bg-slate-500 text-white"
|
|
508
|
+
:disabled="!state.folderDialog.value?.trim()?.length"
|
|
509
|
+
@click="submitFolderDialog"
|
|
510
|
+
>
|
|
511
|
+
Create
|
|
512
|
+
</edge-shad-button>
|
|
513
|
+
</DialogFooter>
|
|
514
|
+
</DialogContent>
|
|
515
|
+
</edge-shad-dialog>
|
|
516
|
+
|
|
517
|
+
<edge-shad-dialog v-model="state.renameDialog.open">
|
|
518
|
+
<DialogContent class="pt-10">
|
|
519
|
+
<DialogHeader>
|
|
520
|
+
<DialogTitle>
|
|
521
|
+
{{ state.renameDialog.type === 'folder' ? 'Rename Folder' : 'Rename Page Slug' }}
|
|
522
|
+
</DialogTitle>
|
|
523
|
+
</DialogHeader>
|
|
524
|
+
<edge-shad-input
|
|
525
|
+
v-model="state.renameDialog.value"
|
|
526
|
+
name="renameValue"
|
|
527
|
+
label="New Name"
|
|
528
|
+
placeholder="Enter new name"
|
|
529
|
+
/>
|
|
530
|
+
<DialogFooter class="pt-2">
|
|
531
|
+
<edge-shad-button
|
|
532
|
+
variant="destructive"
|
|
533
|
+
@click="state.renameDialog.open = false"
|
|
534
|
+
>
|
|
535
|
+
Cancel
|
|
536
|
+
</edge-shad-button>
|
|
537
|
+
<edge-shad-button
|
|
538
|
+
type="button"
|
|
539
|
+
class="bg-slate-800 hover:bg-slate-500 text-white"
|
|
540
|
+
:disabled="!state.renameDialog.value?.trim()?.length"
|
|
541
|
+
@click="submitRenameDialog"
|
|
542
|
+
>
|
|
543
|
+
Save
|
|
544
|
+
</edge-shad-button>
|
|
545
|
+
</DialogFooter>
|
|
546
|
+
</DialogContent>
|
|
547
|
+
</edge-shad-dialog>
|
|
548
|
+
</template>
|