@ekrist1/vulse 0.1.6-alpha.3 → 0.1.7-alpha.4

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 (264) hide show
  1. package/dist/cli/migrate.d.ts.map +1 -1
  2. package/dist/cli/migrate.js +3 -4
  3. package/dist/cli/setup.d.ts.map +1 -1
  4. package/dist/cli/setup.js +11 -10
  5. package/dist/core/blueprints/compile.d.ts +1 -1
  6. package/dist/core/blueprints/compile.d.ts.map +1 -1
  7. package/dist/core/blueprints/compile.js +3 -3
  8. package/dist/core/blueprints/mutations.js +2 -2
  9. package/dist/core/forms/rate-limit.d.ts +1 -1
  10. package/dist/core/forms/rate-limit.d.ts.map +1 -1
  11. package/dist/core/forms/rate-limit.js +3 -3
  12. package/dist/core/forms/unique.d.ts +1 -1
  13. package/dist/core/forms/unique.d.ts.map +1 -1
  14. package/dist/core/forms/unique.js +4 -4
  15. package/dist/core/globals/compile.d.ts +1 -1
  16. package/dist/core/globals/compile.d.ts.map +1 -1
  17. package/dist/core/globals/compile.js +2 -2
  18. package/dist/core/globals/definition.d.ts +1 -1
  19. package/dist/core/globals/definition.d.ts.map +1 -1
  20. package/dist/core/globals/definition.js +3 -3
  21. package/dist/core/repos/globals.js +3 -3
  22. package/dist/core/sha256.d.ts +3 -0
  23. package/dist/core/sha256.d.ts.map +1 -0
  24. package/dist/core/sha256.js +9 -0
  25. package/dist/integration/index.js +1 -1
  26. package/dist/integration/install-hook.d.ts.map +1 -1
  27. package/dist/integration/install-hook.js +6 -4
  28. package/dist/integration/wrangler-config.d.ts +12 -0
  29. package/dist/integration/wrangler-config.d.ts.map +1 -0
  30. package/dist/integration/wrangler-config.js +97 -0
  31. package/dist/integration/wrangler-patch.d.ts +1 -0
  32. package/dist/integration/wrangler-patch.d.ts.map +1 -1
  33. package/dist/integration/wrangler-patch.js +2 -1
  34. package/dist/server/routes/form-submit.js +1 -1
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/package.json +11 -3
  38. package/src/admin/assets/logo-mark.svg +5 -0
  39. package/src/admin/client/active-locale.ts +17 -0
  40. package/src/admin/client/api.ts +21 -0
  41. package/src/admin/client/form-from-zod.ts +7 -0
  42. package/src/admin/client/live-preview-enabled.ts +5 -0
  43. package/src/admin/components/AdminShell.astro +45 -0
  44. package/src/admin/components/AuthSettings.vue +60 -0
  45. package/src/admin/components/BlockEditor.vue +53 -0
  46. package/src/admin/components/BlueprintEditor.vue +1783 -0
  47. package/src/admin/components/CollectionKindIcon.vue +26 -0
  48. package/src/admin/components/CollectionTree.vue +220 -0
  49. package/src/admin/components/EntryEditorWithPreview.vue +130 -0
  50. package/src/admin/components/EntryForm.vue +411 -0
  51. package/src/admin/components/EntryList.vue +121 -0
  52. package/src/admin/components/EntryStatusBadge.vue +24 -0
  53. package/src/admin/components/FormEditor.vue +233 -0
  54. package/src/admin/components/FormList.vue +54 -0
  55. package/src/admin/components/GlobalSetEditor.vue +272 -0
  56. package/src/admin/components/GlobalSetList.vue +55 -0
  57. package/src/admin/components/LivePreviewPanel.vue +171 -0
  58. package/src/admin/components/LoginForm.vue +53 -0
  59. package/src/admin/components/MediaLibrary.vue +106 -0
  60. package/src/admin/components/MediaPicker.vue +49 -0
  61. package/src/admin/components/RevisionDiff.vue +11 -0
  62. package/src/admin/components/RevisionList.vue +134 -0
  63. package/src/admin/components/SeoFields.vue +113 -0
  64. package/src/admin/components/SetEditor.vue +137 -0
  65. package/src/admin/components/SetList.vue +32 -0
  66. package/src/admin/components/SettingsForm.vue +189 -0
  67. package/src/admin/components/SideNav.vue +152 -0
  68. package/src/admin/components/SubmissionDetail.vue +45 -0
  69. package/src/admin/components/SubmissionList.vue +89 -0
  70. package/src/admin/components/ToastHost.vue +33 -0
  71. package/src/admin/components/TreeRow.vue +163 -0
  72. package/src/admin/components/UserEditor.vue +186 -0
  73. package/src/admin/components/UserList.vue +46 -0
  74. package/src/admin/components/blocks/BlockItem.vue +32 -0
  75. package/src/admin/components/blocks/BlockToolbar.vue +12 -0
  76. package/src/admin/components/blocks/edit/CodeEdit.vue +18 -0
  77. package/src/admin/components/blocks/edit/EmbedEdit.vue +14 -0
  78. package/src/admin/components/blocks/edit/HeadingEdit.vue +19 -0
  79. package/src/admin/components/blocks/edit/ImageEdit.vue +40 -0
  80. package/src/admin/components/blocks/edit/ListEdit.vue +36 -0
  81. package/src/admin/components/blocks/edit/ParagraphEdit.vue +14 -0
  82. package/src/admin/components/blocks/edit/QuoteEdit.vue +18 -0
  83. package/src/admin/components/fields/BlocksField.vue +123 -0
  84. package/src/admin/components/fields/BlocksSetsPicker.vue +59 -0
  85. package/src/admin/components/fields/BoolField.vue +10 -0
  86. package/src/admin/components/fields/DateField.vue +22 -0
  87. package/src/admin/components/fields/EntriesField.vue +153 -0
  88. package/src/admin/components/fields/EntryField.vue +138 -0
  89. package/src/admin/components/fields/EnumField.vue +81 -0
  90. package/src/admin/components/fields/FieldRenderer.vue +87 -0
  91. package/src/admin/components/fields/GridField.vue +173 -0
  92. package/src/admin/components/fields/LinkField.vue +219 -0
  93. package/src/admin/components/fields/MediaField.vue +69 -0
  94. package/src/admin/components/fields/NumberField.vue +12 -0
  95. package/src/admin/components/fields/ObjectField.vue +18 -0
  96. package/src/admin/components/fields/RefField.vue +170 -0
  97. package/src/admin/components/fields/RepeaterField.vue +27 -0
  98. package/src/admin/components/fields/ReplicatorField.vue +121 -0
  99. package/src/admin/components/fields/TextField.vue +11 -0
  100. package/src/admin/components/fields/TextareaField.vue +11 -0
  101. package/src/admin/components/fields/VulseAccordionGroupNodeView.vue +82 -0
  102. package/src/admin/components/fields/VulseAccordionNodeView.vue +128 -0
  103. package/src/admin/components/fields/VulseCalloutNodeView.vue +81 -0
  104. package/src/admin/components/fields/VulseIframeNodeView.vue +112 -0
  105. package/src/admin/components/fields/VulseSetNodeView.vue +68 -0
  106. package/src/admin/components/fields/VulseVideoNodeView.vue +104 -0
  107. package/src/admin/components/fields/blocks-editor-extensions.ts +26 -0
  108. package/src/admin/components/fields/emoji-extension.ts +54 -0
  109. package/src/admin/components/fields/link-extension.ts +48 -0
  110. package/src/admin/components/fields/set-node-utils.ts +115 -0
  111. package/src/admin/components/fields/url-utils.ts +85 -0
  112. package/src/admin/components/fields/vulse-accordion-extension.ts +64 -0
  113. package/src/admin/components/fields/vulse-accordion-group-extension.ts +49 -0
  114. package/src/admin/components/fields/vulse-callout-extension.ts +53 -0
  115. package/src/admin/components/fields/vulse-iframe-extension.ts +96 -0
  116. package/src/admin/components/fields/vulse-set-extension.ts +66 -0
  117. package/src/admin/components/fields/vulse-video-extension.ts +65 -0
  118. package/src/admin/composables/toast.ts +35 -0
  119. package/src/admin/composables/useEntrySearch.ts +112 -0
  120. package/src/admin/composables/useSets.ts +31 -0
  121. package/src/admin/pages/collections/[name]/[id]/revisions.astro +27 -0
  122. package/src/admin/pages/collections/[name]/[id].astro +90 -0
  123. package/src/admin/pages/collections/[name]/index.astro +31 -0
  124. package/src/admin/pages/collections/[name]/new.astro +38 -0
  125. package/src/admin/pages/forms/[handle]/submissions/[id].astro +9 -0
  126. package/src/admin/pages/forms/[handle]/submissions/index.astro +9 -0
  127. package/src/admin/pages/forms/[handle].astro +9 -0
  128. package/src/admin/pages/forms/index.astro +7 -0
  129. package/src/admin/pages/forms/new.astro +7 -0
  130. package/src/admin/pages/index.astro +36 -0
  131. package/src/admin/pages/login.astro +14 -0
  132. package/src/admin/pages/media.astro +8 -0
  133. package/src/admin/pages/schema/[handle].astro +10 -0
  134. package/src/admin/pages/schema/new.astro +9 -0
  135. package/src/admin/pages/settings/auth.astro +9 -0
  136. package/src/admin/pages/settings/globals/[handle].astro +9 -0
  137. package/src/admin/pages/settings/globals/index.astro +7 -0
  138. package/src/admin/pages/settings/globals/new.astro +7 -0
  139. package/src/admin/pages/settings/index.astro +8 -0
  140. package/src/admin/pages/settings/sets/[handle].astro +9 -0
  141. package/src/admin/pages/settings/sets/index.astro +7 -0
  142. package/src/admin/pages/settings/sets/new.astro +7 -0
  143. package/src/admin/pages/users/[id].astro +10 -0
  144. package/src/admin/pages/users/index.astro +8 -0
  145. package/src/admin/styles/admin.css +166 -0
  146. package/src/core/access.ts +9 -0
  147. package/src/core/blocks/schema.ts +66 -0
  148. package/src/core/blueprints/code-to-definition.ts +156 -0
  149. package/src/core/blueprints/compile.ts +176 -0
  150. package/src/core/blueprints/define.ts +12 -0
  151. package/src/core/blueprints/definition.ts +185 -0
  152. package/src/core/blueprints/load.ts +144 -0
  153. package/src/core/blueprints/mutations.ts +236 -0
  154. package/src/core/blueprints/preview-path.ts +33 -0
  155. package/src/core/blueprints/reflect-fields.ts +305 -0
  156. package/src/core/blueprints/registry.ts +14 -0
  157. package/src/core/blueprints/seed.ts +20 -0
  158. package/src/core/blueprints/select-helpers.ts +30 -0
  159. package/src/core/blueprints/seo.ts +180 -0
  160. package/src/core/blueprints/types.ts +59 -0
  161. package/src/core/blueprints/zod-helpers.ts +86 -0
  162. package/src/core/db.ts +11 -0
  163. package/src/core/errors.ts +34 -0
  164. package/src/core/forms/compile.ts +84 -0
  165. package/src/core/forms/definition.ts +102 -0
  166. package/src/core/forms/rate-limit.ts +52 -0
  167. package/src/core/forms/unique.ts +38 -0
  168. package/src/core/globals/compile.ts +35 -0
  169. package/src/core/globals/definition.ts +27 -0
  170. package/src/core/locales.ts +45 -0
  171. package/src/core/migrations.ts +48 -0
  172. package/src/core/parse-content.ts +85 -0
  173. package/src/core/plugins/definition.ts +150 -0
  174. package/src/core/preview-content.ts +21 -0
  175. package/src/core/repos/entries.ts +504 -0
  176. package/src/core/repos/forms.ts +270 -0
  177. package/src/core/repos/globals.ts +179 -0
  178. package/src/core/repos/media.ts +106 -0
  179. package/src/core/repos/preview-sessions.ts +108 -0
  180. package/src/core/repos/revisions.ts +60 -0
  181. package/src/core/repos/settings.ts +23 -0
  182. package/src/core/schema.ts +244 -0
  183. package/src/core/sets/compile.ts +12 -0
  184. package/src/core/sets/definition.ts +10 -0
  185. package/src/core/sets/service.ts +82 -0
  186. package/src/core/sets/validate-tree.ts +57 -0
  187. package/src/core/sha256.ts +10 -0
  188. package/src/core/slug.ts +30 -0
  189. package/src/scaffold/collection-write.ts +83 -0
  190. package/src/scaffold/collection.ts +277 -0
  191. package/src/server/assets/live-preview-bridge.content.ts +2 -0
  192. package/src/server/assets/live-preview-bridge.js +535 -0
  193. package/src/server/better-auth.ts +82 -0
  194. package/src/server/cf-images.ts +34 -0
  195. package/src/server/cron.ts +37 -0
  196. package/src/server/email.ts +17 -0
  197. package/src/server/endpoints/api-auth.ts +10 -0
  198. package/src/server/endpoints/api-vulse-blueprints.ts +23 -0
  199. package/src/server/endpoints/api-vulse-entries-locales.ts +12 -0
  200. package/src/server/endpoints/api-vulse-entries-move.ts +7 -0
  201. package/src/server/endpoints/api-vulse-entries-publish.ts +7 -0
  202. package/src/server/endpoints/api-vulse-entries-tree.ts +7 -0
  203. package/src/server/endpoints/api-vulse-entries.ts +23 -0
  204. package/src/server/endpoints/api-vulse-form-handle.ts +30 -0
  205. package/src/server/endpoints/api-vulse-form-public.ts +7 -0
  206. package/src/server/endpoints/api-vulse-form-submit.ts +7 -0
  207. package/src/server/endpoints/api-vulse-form-upload.ts +7 -0
  208. package/src/server/endpoints/api-vulse-forms.ts +12 -0
  209. package/src/server/endpoints/api-vulse-globals-handle.ts +20 -0
  210. package/src/server/endpoints/api-vulse-globals-public-handle.ts +7 -0
  211. package/src/server/endpoints/api-vulse-globals-public.ts +7 -0
  212. package/src/server/endpoints/api-vulse-globals-value.ts +8 -0
  213. package/src/server/endpoints/api-vulse-globals.ts +12 -0
  214. package/src/server/endpoints/api-vulse-media-file.ts +7 -0
  215. package/src/server/endpoints/api-vulse-media-id.ts +12 -0
  216. package/src/server/endpoints/api-vulse-media.ts +12 -0
  217. package/src/server/endpoints/api-vulse-preview-bridge.ts +11 -0
  218. package/src/server/endpoints/api-vulse-preview-sessions-id.ts +10 -0
  219. package/src/server/endpoints/api-vulse-preview-sessions.ts +7 -0
  220. package/src/server/endpoints/api-vulse-preview-start.ts +7 -0
  221. package/src/server/endpoints/api-vulse-preview-stop.ts +7 -0
  222. package/src/server/endpoints/api-vulse-revisions-restore.ts +7 -0
  223. package/src/server/endpoints/api-vulse-revisions.ts +7 -0
  224. package/src/server/endpoints/api-vulse-search.ts +7 -0
  225. package/src/server/endpoints/api-vulse-sets.ts +23 -0
  226. package/src/server/endpoints/api-vulse-settings.ts +12 -0
  227. package/src/server/endpoints/api-vulse-users-id.ts +12 -0
  228. package/src/server/endpoints/api-vulse-users-reset-password.ts +7 -0
  229. package/src/server/endpoints/api-vulse-users-role.ts +9 -0
  230. package/src/server/endpoints/api-vulse-users.ts +7 -0
  231. package/src/server/endpoints/with-runtime.ts +11 -0
  232. package/src/server/env.ts +23 -0
  233. package/src/server/envelope.ts +21 -0
  234. package/src/server/forms/email.ts +11 -0
  235. package/src/server/forms/process-submission.ts +95 -0
  236. package/src/server/forms/queue.ts +25 -0
  237. package/src/server/forms/templates.ts +24 -0
  238. package/src/server/forms/webhook.ts +19 -0
  239. package/src/server/handler.ts +66 -0
  240. package/src/server/image-probe.ts +35 -0
  241. package/src/server/loader.ts +54 -0
  242. package/src/server/plugins.ts +214 -0
  243. package/src/server/preview.ts +25 -0
  244. package/src/server/r2.ts +13 -0
  245. package/src/server/routes/blueprints.ts +62 -0
  246. package/src/server/routes/entries.ts +255 -0
  247. package/src/server/routes/form-submit.ts +168 -0
  248. package/src/server/routes/form-upload.ts +100 -0
  249. package/src/server/routes/forms.ts +88 -0
  250. package/src/server/routes/globals-public.ts +30 -0
  251. package/src/server/routes/globals.ts +93 -0
  252. package/src/server/routes/media.ts +145 -0
  253. package/src/server/routes/preview-sessions.ts +76 -0
  254. package/src/server/routes/preview.ts +36 -0
  255. package/src/server/routes/revisions.ts +29 -0
  256. package/src/server/routes/search.ts +31 -0
  257. package/src/server/routes/sets.ts +40 -0
  258. package/src/server/routes/settings.ts +24 -0
  259. package/src/server/routes/users.ts +127 -0
  260. package/src/server/runtime.ts +99 -0
  261. package/src/server/sdk/collections.ts +98 -0
  262. package/src/server/sdk/index.ts +25 -0
  263. package/src/server/sdk/media.ts +11 -0
  264. package/src/server/sdk/search.ts +90 -0
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ singleton?: boolean; class?: string }>()
3
+ </script>
4
+
5
+ <template>
6
+ <svg
7
+ class="h-4 w-4 shrink-0 text-zinc-500"
8
+ :class="$props.class"
9
+ viewBox="0 0 20 20"
10
+ fill="currentColor"
11
+ aria-hidden="true"
12
+ >
13
+ <path
14
+ v-if="singleton"
15
+ fill-rule="evenodd"
16
+ d="M10 2a1 1 0 0 1 1 1v1.323l3.954 1.582 1.599-.8a1 1 0 0 1 .894 1.79l-1.233.616 1.738 5.42A1 1 0 0 1 17 13H3a1 1 0 0 1-.948-1.316l1.738-5.42-1.233-.617a1 1 0 0 1 .894-1.788l1.599.799L9 4.323V3a1 1 0 0 1 1-1Z"
17
+ clip-rule="evenodd"
18
+ />
19
+ <path
20
+ v-else
21
+ fill-rule="evenodd"
22
+ d="M2 4.75A2.75 2.75 0 0 1 4.75 2h10.5A2.75 2.75 0 0 1 18 4.75v10.5A2.75 2.75 0 0 1 15.25 18H4.75A2.75 2.75 0 0 1 2 15.25V4.75ZM4.75 3.5c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25V4.75c0-.69-.56-1.25-1.25-1.25H4.75Z"
23
+ clip-rule="evenodd"
24
+ />
25
+ </svg>
26
+ </template>
@@ -0,0 +1,220 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, ref, watch } from 'vue'
3
+ import { adminApi } from '../client/api.js'
4
+ import type { EntryNode } from '../../core/repos/entries.js'
5
+ import TreeRow from './TreeRow.vue'
6
+
7
+ const props = defineProps<{ handle: string; entryLocale?: string }>()
8
+
9
+ const tree = ref<EntryNode[]>([])
10
+ const loading = ref(false)
11
+ const moving = ref(false)
12
+ const expanded = ref<Set<string>>(new Set())
13
+ const dragId = ref<string | null>(null)
14
+ const error = ref<string | null>(null)
15
+
16
+ async function load() {
17
+ loading.value = true
18
+ error.value = null
19
+ try {
20
+ const qs = props.entryLocale ? `?locale=${encodeURIComponent(props.entryLocale)}` : ''
21
+ tree.value = await adminApi.get<EntryNode[]>(`/api/vulse/entries/${props.handle}/tree${qs}`)
22
+ for (const root of tree.value) {
23
+ expanded.value.add(root.id)
24
+ for (const child of root.children) expanded.value.add(child.id)
25
+ }
26
+ } catch (e) {
27
+ error.value = e instanceof Error ? e.message : 'Could not load tree'
28
+ } finally {
29
+ loading.value = false
30
+ }
31
+ }
32
+
33
+ onMounted(load)
34
+
35
+ watch(() => props.entryLocale, () => { void load() })
36
+
37
+ function toggle(id: string) {
38
+ if (expanded.value.has(id)) expanded.value.delete(id)
39
+ else expanded.value.add(id)
40
+ expanded.value = new Set(expanded.value)
41
+ }
42
+
43
+ function descendantIds(node: EntryNode): Set<string> {
44
+ const out = new Set<string>([node.id])
45
+ function walk(n: EntryNode) {
46
+ for (const c of n.children) {
47
+ out.add(c.id)
48
+ walk(c)
49
+ }
50
+ }
51
+ walk(node)
52
+ return out
53
+ }
54
+
55
+ function findNode(nodes: EntryNode[], id: string): EntryNode | null {
56
+ for (const n of nodes) {
57
+ if (n.id === id) return n
58
+ const found = findNode(n.children, id)
59
+ if (found) return found
60
+ }
61
+ return null
62
+ }
63
+
64
+ function siblingsOf(id: string): EntryNode[] {
65
+ function walk(nodes: EntryNode[], parent: EntryNode[]): EntryNode[] | null {
66
+ for (const n of nodes) {
67
+ if (n.id === id) return parent
68
+ const found = walk(n.children, n.children)
69
+ if (found) return found
70
+ }
71
+ return null
72
+ }
73
+ return walk(tree.value, tree.value) ?? []
74
+ }
75
+
76
+ async function moveEntry(id: string, body: { parentId: string | null; sortOrder?: number }) {
77
+ await adminApi.patch(`/api/vulse/entries/${props.handle}/${id}/move`, body)
78
+ }
79
+
80
+ async function moveUp(id: string) {
81
+ const siblings = siblingsOf(id)
82
+ const idx = siblings.findIndex((s) => s.id === id)
83
+ if (idx <= 0) return
84
+ moving.value = true
85
+ try {
86
+ const node = siblings[idx]!
87
+ await moveEntry(id, { parentId: node.parentId, sortOrder: idx })
88
+ await load()
89
+ } finally {
90
+ moving.value = false
91
+ }
92
+ }
93
+
94
+ async function moveDown(id: string) {
95
+ const siblings = siblingsOf(id)
96
+ const idx = siblings.findIndex((s) => s.id === id)
97
+ if (idx < 0 || idx >= siblings.length - 1) return
98
+ moving.value = true
99
+ try {
100
+ const node = siblings[idx]!
101
+ await moveEntry(id, { parentId: node.parentId, sortOrder: idx + 2 })
102
+ await load()
103
+ } finally {
104
+ moving.value = false
105
+ }
106
+ }
107
+
108
+ async function outdent(id: string) {
109
+ const node = findNode(tree.value, id)
110
+ if (!node || node.parentId === null) return
111
+ const parent = findNode(tree.value, node.parentId)
112
+ moving.value = true
113
+ try {
114
+ await moveEntry(id, { parentId: parent?.parentId ?? null })
115
+ await load()
116
+ } finally {
117
+ moving.value = false
118
+ }
119
+ }
120
+
121
+ async function indent(id: string) {
122
+ const siblings = siblingsOf(id)
123
+ const idx = siblings.findIndex((s) => s.id === id)
124
+ if (idx <= 0) return
125
+ const newParent = siblings[idx - 1]!
126
+ moving.value = true
127
+ try {
128
+ await moveEntry(id, { parentId: newParent.id })
129
+ expanded.value.add(newParent.id)
130
+ expanded.value = new Set(expanded.value)
131
+ await load()
132
+ } finally {
133
+ moving.value = false
134
+ }
135
+ }
136
+
137
+ function onDragStart(event: DragEvent, id: string) {
138
+ dragId.value = id
139
+ event.dataTransfer?.setData('text/plain', id)
140
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
141
+ }
142
+
143
+ function onDragOver(event: DragEvent) {
144
+ if (dragId.value) event.preventDefault()
145
+ }
146
+
147
+ async function onDropOnto(event: DragEvent, targetId: string | null) {
148
+ event.preventDefault()
149
+ const src = dragId.value
150
+ dragId.value = null
151
+ if (!src || src === targetId) return
152
+ const srcNode = findNode(tree.value, src)
153
+ if (srcNode && targetId !== null && descendantIds(srcNode).has(targetId)) {
154
+ error.value = "Can't drop a page onto one of its descendants."
155
+ return
156
+ }
157
+ moving.value = true
158
+ try {
159
+ await moveEntry(src, { parentId: targetId })
160
+ if (targetId) {
161
+ expanded.value.add(targetId)
162
+ expanded.value = new Set(expanded.value)
163
+ }
164
+ await load()
165
+ } catch (e) {
166
+ error.value = e instanceof Error ? e.message : 'Move failed'
167
+ } finally {
168
+ moving.value = false
169
+ }
170
+ }
171
+
172
+ async function destroy(id: string, label: string) {
173
+ if (!confirm(`Delete "${label}" and any nested children? This cannot be undone.`)) return
174
+ try {
175
+ await adminApi.delete(`/api/vulse/entries/${props.handle}/${id}`)
176
+ await load()
177
+ } catch (e) {
178
+ error.value = e instanceof Error ? e.message : 'Could not delete'
179
+ }
180
+ }
181
+ </script>
182
+
183
+ <template>
184
+ <div data-testid="collection-tree">
185
+ <p v-if="error" class="mb-3 rounded bg-red-50 px-3 py-2 text-sm text-red-700">{{ error }}</p>
186
+ <div v-if="loading" class="text-sm text-zinc-500">Loading…</div>
187
+ <div
188
+ v-else-if="tree.length === 0"
189
+ class="rounded border border-dashed border-zinc-300 bg-white p-6 text-sm text-zinc-500"
190
+ >
191
+ No pages yet. Use the “+ New” button to create your first entry.
192
+ </div>
193
+ <ul
194
+ v-else
195
+ class="divide-y divide-zinc-100 overflow-hidden rounded border border-zinc-200 bg-white"
196
+ @dragover="onDragOver"
197
+ @drop="(e) => onDropOnto(e, null)"
198
+ >
199
+ <TreeRow
200
+ v-for="node in tree"
201
+ :key="node.id"
202
+ :node="node"
203
+ :handle="handle"
204
+ :depth="0"
205
+ :expanded-set="expanded"
206
+ :dragging-id="dragId"
207
+ :disabled="moving"
208
+ @toggle="toggle"
209
+ @move-up="moveUp"
210
+ @move-down="moveDown"
211
+ @outdent="outdent"
212
+ @indent="indent"
213
+ @drag-start="onDragStart"
214
+ @drag-over="onDragOver"
215
+ @drop-onto="onDropOnto"
216
+ @destroy="destroy"
217
+ />
218
+ </ul>
219
+ </div>
220
+ </template>
@@ -0,0 +1,130 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref } from 'vue'
3
+ import type { FieldDescriptor } from '../client/form-from-zod.js'
4
+ import { resolveActiveLocale } from '../client/active-locale.js'
5
+ import { isLivePreviewEnabled } from '../client/live-preview-enabled.js'
6
+ import EntryForm from './EntryForm.vue'
7
+ import LivePreviewPanel from './LivePreviewPanel.vue'
8
+
9
+ const PREVIEW_VISIBLE_KEY = 'vulse:live-preview-visible'
10
+
11
+ const props = defineProps<{
12
+ collection: string
13
+ entryId?: string
14
+ fields: FieldDescriptor[]
15
+ initial: Record<string, unknown>
16
+ titleField?: string
17
+ draftsEnabled?: boolean
18
+ seoEnabled?: boolean
19
+ seoMapping?: import('../../core/blueprints/seo.js').SeoFieldMapping
20
+ tree?: boolean
21
+ parentId?: string | null
22
+ hasUnpublishedChanges?: boolean
23
+ previewPath: string
24
+ /** When false, hides live preview for this collection. Avoid prop name `enabled` (Astro/HTML coercion). */
25
+ livePreviewAllowed?: boolean
26
+ /** Active locale from the server. Avoid prop name `locale` (Astro/HTML coercion). */
27
+ entryLocale?: string
28
+ defaultLocale?: string
29
+ supportedLocales?: string[]
30
+ existingLocales?: string[]
31
+ }>()
32
+
33
+ const allowLivePreview = computed(() => isLivePreviewEnabled(props.livePreviewAllowed))
34
+
35
+ const activeLocale = computed(() =>
36
+ resolveActiveLocale(props.supportedLocales, props.entryLocale, props.defaultLocale),
37
+ )
38
+
39
+ const previewContent = ref<Record<string, unknown>>({ ...props.initial })
40
+ delete previewContent.value.slug
41
+ delete previewContent.value.status
42
+ delete previewContent.value.hasUnpublishedChanges
43
+
44
+ const previewSlug = ref(String(props.initial?.slug ?? ''))
45
+ const previewVisible = ref(true)
46
+
47
+ onMounted(() => {
48
+ if (!allowLivePreview.value) return
49
+ try {
50
+ const stored = localStorage.getItem(PREVIEW_VISIBLE_KEY)
51
+ if (stored === 'false') previewVisible.value = false
52
+ } catch {
53
+ // ignore storage errors
54
+ }
55
+ })
56
+
57
+ function onPreviewChange(payload: { content: Record<string, unknown>; slug: string }) {
58
+ previewContent.value = payload.content
59
+ previewSlug.value = payload.slug
60
+ }
61
+
62
+ function togglePreview() {
63
+ previewVisible.value = !previewVisible.value
64
+ try {
65
+ localStorage.setItem(PREVIEW_VISIBLE_KEY, String(previewVisible.value))
66
+ } catch {
67
+ // ignore storage errors
68
+ }
69
+ }
70
+ </script>
71
+
72
+ <template>
73
+ <div class="space-y-4">
74
+ <div
75
+ v-if="allowLivePreview"
76
+ class="flex items-center justify-end gap-2"
77
+ >
78
+ <button
79
+ type="button"
80
+ class="rounded border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-700 hover:bg-zinc-50"
81
+ @click="togglePreview"
82
+ >
83
+ {{ previewVisible ? 'Hide live preview' : 'Show live preview' }}
84
+ </button>
85
+ </div>
86
+
87
+ <p
88
+ v-else
89
+ class="rounded border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-600"
90
+ >
91
+ Live preview is disabled for this collection. Use the Preview button above to view saved drafts in a new tab.
92
+ </p>
93
+
94
+ <div
95
+ class="grid gap-8"
96
+ :class="previewVisible && allowLivePreview ? 'grid-cols-1 xl:grid-cols-2' : 'grid-cols-1'"
97
+ >
98
+ <EntryForm
99
+ :key="`${entryId ?? 'new'}:${activeLocale}`"
100
+ :collection="collection"
101
+ :entry-id="entryId"
102
+ :fields="fields"
103
+ :initial="initial"
104
+ :title-field="titleField"
105
+ :drafts-enabled="draftsEnabled"
106
+ :seo-enabled="seoEnabled"
107
+ :seo-mapping="seoMapping"
108
+ :tree="tree"
109
+ :parent-id="parentId"
110
+ :has-unpublished-changes="hasUnpublishedChanges"
111
+ :wide="!previewVisible || !allowLivePreview"
112
+ :entry-locale="entryLocale"
113
+ :default-locale="defaultLocale"
114
+ :supported-locales="supportedLocales"
115
+ :existing-locales="existingLocales"
116
+ @preview-change="onPreviewChange"
117
+ />
118
+ <LivePreviewPanel
119
+ v-if="previewVisible && allowLivePreview"
120
+ :key="(entryId ?? previewSlug) + ':' + activeLocale"
121
+ :collection="collection"
122
+ :entry-id="entryId"
123
+ :preview-path="previewPath"
124
+ :slug="previewSlug"
125
+ :content="previewContent"
126
+ :entry-locale="activeLocale"
127
+ />
128
+ </div>
129
+ </div>
130
+ </template>