@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,66 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core';
2
+ import { VueNodeViewRenderer } from '@tiptap/vue-3';
3
+ import VulseSetNodeView from './VulseSetNodeView.vue';
4
+
5
+ declare module '@tiptap/core' {
6
+ interface Commands<ReturnType> {
7
+ vulseSet: {
8
+ insertVulseSet: (setHandle: string) => ReturnType;
9
+ };
10
+ }
11
+ }
12
+
13
+ export const VulseSetExtension = Node.create({
14
+ name: 'vulseSet',
15
+ group: 'block',
16
+ atom: true,
17
+ draggable: true,
18
+
19
+ addAttributes() {
20
+ return {
21
+ set: {
22
+ default: null,
23
+ parseHTML: (el: HTMLElement) => el.getAttribute('data-vulse-set'),
24
+ renderHTML: (attrs: { set: string | null }) => ({ 'data-vulse-set': attrs.set ?? '' }),
25
+ },
26
+ data: {
27
+ default: {} as Record<string, unknown>,
28
+ parseHTML: (el: HTMLElement) => {
29
+ const raw = el.getAttribute('data-vulse-data');
30
+ try {
31
+ return raw ? JSON.parse(raw) : {};
32
+ } catch {
33
+ return {};
34
+ }
35
+ },
36
+ renderHTML: (attrs: { data: Record<string, unknown> }) => ({
37
+ 'data-vulse-data': JSON.stringify(attrs.data ?? {}),
38
+ }),
39
+ },
40
+ };
41
+ },
42
+
43
+ parseHTML() {
44
+ return [{ tag: 'div[data-vulse-set]' }];
45
+ },
46
+
47
+ renderHTML({ HTMLAttributes }) {
48
+ return ['div', mergeAttributes(HTMLAttributes, { 'data-vulse-set': '' })];
49
+ },
50
+
51
+ addNodeView() {
52
+ return VueNodeViewRenderer(VulseSetNodeView);
53
+ },
54
+
55
+ addCommands() {
56
+ return {
57
+ insertVulseSet:
58
+ (setHandle: string) =>
59
+ ({ commands }) =>
60
+ commands.insertContent({
61
+ type: this.name,
62
+ attrs: { set: setHandle, data: {} },
63
+ }),
64
+ };
65
+ },
66
+ });
@@ -0,0 +1,65 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core';
2
+ import { VueNodeViewRenderer } from '@tiptap/vue-3';
3
+ import { sanitizeMediaSrc } from './url-utils.js';
4
+ import VulseVideoNodeView from './VulseVideoNodeView.vue';
5
+
6
+ declare module '@tiptap/core' {
7
+ interface Commands<ReturnType> {
8
+ vulseVideo: {
9
+ insertVulseVideo: (src?: string) => ReturnType;
10
+ };
11
+ }
12
+ }
13
+
14
+ export const VulseVideoExtension = Node.create({
15
+ name: 'vulseVideo',
16
+ group: 'block',
17
+ atom: true,
18
+ selectable: true,
19
+ draggable: true,
20
+
21
+ addAttributes() {
22
+ return {
23
+ src: {
24
+ default: null,
25
+ parseHTML: (element: HTMLElement) =>
26
+ sanitizeMediaSrc(element.getAttribute('src') ?? '') ?? null,
27
+ renderHTML: (attrs: { src?: string | null }) => (attrs.src ? { src: attrs.src } : {}),
28
+ },
29
+ };
30
+ },
31
+
32
+ parseHTML() {
33
+ return [{ tag: 'video[data-vulse-embed="video"]' }, { tag: 'video[src]' }];
34
+ },
35
+
36
+ renderHTML({ HTMLAttributes }) {
37
+ return [
38
+ 'video',
39
+ mergeAttributes(
40
+ {
41
+ 'data-vulse-embed': 'video',
42
+ controls: 'true',
43
+ preload: 'metadata',
44
+ },
45
+ HTMLAttributes,
46
+ ),
47
+ ];
48
+ },
49
+
50
+ addNodeView() {
51
+ return VueNodeViewRenderer(VulseVideoNodeView);
52
+ },
53
+
54
+ addCommands() {
55
+ return {
56
+ insertVulseVideo:
57
+ (src = '') =>
58
+ ({ commands }) =>
59
+ commands.insertContent({
60
+ type: this.name,
61
+ attrs: { src: sanitizeMediaSrc(src) },
62
+ }),
63
+ };
64
+ },
65
+ });
@@ -0,0 +1,35 @@
1
+ import { ref } from 'vue'
2
+
3
+ export type ToastKind = 'success' | 'error'
4
+
5
+ export interface ToastMessage {
6
+ id: number
7
+ message: string
8
+ kind: ToastKind
9
+ }
10
+
11
+ const toasts = ref<ToastMessage[]>([])
12
+ let nextId = 1
13
+
14
+ function push(message: string, kind: ToastKind, durationMs: number) {
15
+ const id = nextId++
16
+ toasts.value = [...toasts.value, { id, message, kind }]
17
+ window.setTimeout(() => {
18
+ toasts.value = toasts.value.filter((t) => t.id !== id)
19
+ }, durationMs)
20
+ }
21
+
22
+ export function useToast() {
23
+ return {
24
+ toasts,
25
+ success(message: string, durationMs = 2800) {
26
+ push(message, 'success', durationMs)
27
+ },
28
+ error(message: string, durationMs = 4000) {
29
+ push(message, 'error', durationMs)
30
+ },
31
+ dismiss(id: number) {
32
+ toasts.value = toasts.value.filter((t) => t.id !== id)
33
+ },
34
+ }
35
+ }
@@ -0,0 +1,112 @@
1
+ import { ref, watch } from 'vue'
2
+ import { adminApi } from '../client/api.js'
3
+
4
+ export interface EntryOption {
5
+ id: string
6
+ collection: string
7
+ title?: string
8
+ email?: string
9
+ }
10
+
11
+ export function entryOptionLabel(option: EntryOption): string {
12
+ return option.title ?? option.email ?? option.id
13
+ }
14
+
15
+ export function useEntrySearch(collections: () => string[]) {
16
+ const open = ref(false)
17
+ const query = ref('')
18
+ const options = ref<EntryOption[]>([])
19
+ const loading = ref(false)
20
+
21
+ async function loadOptions(search = '') {
22
+ const cols = collections().filter(Boolean)
23
+ if (cols.length === 0) {
24
+ options.value = []
25
+ return
26
+ }
27
+
28
+ loading.value = true
29
+ try {
30
+ const needle = search.trim().toLowerCase()
31
+ const merged: EntryOption[] = []
32
+
33
+ for (const collection of cols) {
34
+ if (collection === 'user') {
35
+ const users = await adminApi.get<{ id: string; email?: string }[]>(
36
+ `/api/vulse/users?q=${encodeURIComponent(search)}`,
37
+ )
38
+ for (const user of users) {
39
+ merged.push({ id: user.id, collection: 'user', email: user.email })
40
+ }
41
+ continue
42
+ }
43
+
44
+ const rows = await adminApi.get<
45
+ { id: string; content?: { title?: string }; slug?: string }[]
46
+ >(`/api/vulse/entries/${collection}`)
47
+ for (const row of rows) {
48
+ merged.push({
49
+ id: row.id,
50
+ collection,
51
+ title: row.content?.title ?? row.slug ?? row.id,
52
+ })
53
+ }
54
+ }
55
+
56
+ options.value = merged.filter((row) => {
57
+ if (!needle) return true
58
+ return entryOptionLabel(row).toLowerCase().includes(needle)
59
+ })
60
+ } finally {
61
+ loading.value = false
62
+ }
63
+ }
64
+
65
+ async function resolveLabel(entryId: string, collection: string): Promise<string> {
66
+ if (!entryId) return ''
67
+ if (collection === 'user') {
68
+ const users = await adminApi.get<{ id: string; email?: string }[]>(
69
+ `/api/vulse/users?q=${encodeURIComponent(entryId)}`,
70
+ )
71
+ const match = users.find((user) => user.id === entryId)
72
+ return match ? entryOptionLabel({ id: match.id, collection: 'user', email: match.email }) : entryId
73
+ }
74
+
75
+ const row = await adminApi.get<{ id: string; content?: { title?: string }; slug?: string }>(
76
+ `/api/vulse/entries/${collection}/${entryId}`,
77
+ )
78
+ return row.content?.title ?? row.slug ?? row.id
79
+ }
80
+
81
+ function openDropdown() {
82
+ open.value = true
83
+ void loadOptions(query.value)
84
+ }
85
+
86
+ function closeDropdown() {
87
+ open.value = false
88
+ }
89
+
90
+ function onBlur(event: FocusEvent) {
91
+ const next = event.relatedTarget as Node | null
92
+ if (next && (event.currentTarget as HTMLElement).contains(next)) return
93
+ closeDropdown()
94
+ }
95
+
96
+ watch(query, (value) => {
97
+ if (!open.value) return
98
+ void loadOptions(value)
99
+ })
100
+
101
+ return {
102
+ open,
103
+ query,
104
+ options,
105
+ loading,
106
+ loadOptions,
107
+ resolveLabel,
108
+ openDropdown,
109
+ closeDropdown,
110
+ onBlur,
111
+ }
112
+ }
@@ -0,0 +1,31 @@
1
+ import { onMounted, ref } from 'vue'
2
+ import { adminApi } from '../client/api.js'
3
+ import type { SetDefinition } from '../../core/sets/definition.js'
4
+
5
+ let cache: Map<string, SetDefinition> | null = null
6
+ let loading: Promise<Map<string, SetDefinition>> | null = null
7
+
8
+ export async function hydrateSets(): Promise<Map<string, SetDefinition>> {
9
+ if (cache) return cache
10
+ if (!loading) {
11
+ loading = adminApi.get<SetDefinition[]>('/api/vulse/sets').then((list) => {
12
+ cache = new Map(list.map((s) => [s.handle, s]))
13
+ return cache
14
+ })
15
+ }
16
+ return loading
17
+ }
18
+
19
+ export function useSets() {
20
+ const sets = ref<Map<string, SetDefinition>>(cache ?? new Map())
21
+
22
+ onMounted(async () => {
23
+ sets.value = await hydrateSets()
24
+ })
25
+
26
+ function get(handle: string): SetDefinition | undefined {
27
+ return sets.value.get(handle)
28
+ }
29
+
30
+ return { sets, get, hydrate: hydrateSets }
31
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ import AdminShell from '../../../../components/AdminShell.astro'
3
+ import RevisionList from '../../../../components/RevisionList.vue'
4
+ import { createDb } from '../../../../../core/db.js'
5
+ import { getRuntimeEnv } from '../../../../../server/env.js'
6
+ import { readLocalesConfig } from '../../../../../core/locales.js'
7
+
8
+ const name = Astro.params.name!
9
+ const id = Astro.params.id!
10
+
11
+ const env = getRuntimeEnv()
12
+ const db = createDb(env.DB)
13
+ const localesCfg = await readLocalesConfig(db)
14
+ const requested = Astro.url.searchParams.get('locale')
15
+ const activeLocale = requested && localesCfg.locales.includes(requested) ? requested : localesCfg.defaultLocale
16
+ ---
17
+ <AdminShell title="Revisions" activePath={`/admin/collections/${name}`}>
18
+ <h1 class="text-2xl font-semibold mb-4">Revisions</h1>
19
+ <RevisionList
20
+ client:load
21
+ collection={name}
22
+ entryId={id}
23
+ entryLocale={activeLocale}
24
+ supportedLocales={localesCfg.locales}
25
+ defaultLocale={localesCfg.defaultLocale}
26
+ />
27
+ </AdminShell>
@@ -0,0 +1,90 @@
1
+ ---
2
+ import AdminShell from '../../../components/AdminShell.astro'
3
+ import EntryEditorWithPreview from '../../../components/EntryEditorWithPreview.vue'
4
+ import { createDb } from '../../../../core/db.js'
5
+ import { registryForRequest } from '../../../../core/blueprints/load.js'
6
+ import { fieldDescriptorsFromBlueprint } from '../../../client/form-from-zod.js'
7
+ import { getRuntime } from '../../../../server/runtime.js'
8
+ import { getRuntimeEnv } from '../../../../server/env.js'
9
+ import { readLocalesConfig } from '../../../../core/locales.js'
10
+ import { resolvePreviewConfig } from '../../../../core/blueprints/preview-path.js'
11
+ import { EntriesRepo } from '../../../../core/repos/entries.js'
12
+
13
+ const env = getRuntimeEnv()
14
+ const db = createDb(env.DB)
15
+ const reg = await registryForRequest(db)
16
+ const bp = reg.get(Astro.params.name!)
17
+ if (!bp) return Astro.redirect('/admin')
18
+ const fields = fieldDescriptorsFromBlueprint(bp)
19
+
20
+ const localesCfg = await readLocalesConfig(db)
21
+ const requested = Astro.url.searchParams.get('locale')
22
+ const activeLocale = requested && localesCfg.locales.includes(requested) ? requested : localesCfg.defaultLocale
23
+
24
+ const entryId = Astro.params.id!
25
+ const entriesRepo = new EntriesRepo(db)
26
+ const existingLocales = (await entriesRepo.listLocales(entryId)).map((l) => l.locale)
27
+
28
+ const rt = await getRuntime(env, reg, new URL(Astro.request.url).origin)
29
+ const findUrl = `${Astro.request.url.split('?')[0]}?locale=${encodeURIComponent(activeLocale)}`
30
+ const res = await rt.routes.entries.findById(new Request(findUrl, { headers: Astro.request.headers }), { collection: bp.name, id: entryId })
31
+ const body = await res.json() as {
32
+ ok: true
33
+ data: {
34
+ content: Record<string, unknown>
35
+ draftContent: Record<string, unknown> | null
36
+ slug: string
37
+ status: 'draft' | 'published'
38
+ hasUnpublishedChanges: boolean
39
+ }
40
+ } | { ok: false }
41
+
42
+ // If the entry has no rows at all (none of its locales exist), 404. Otherwise,
43
+ // the requested locale just doesn't have a translation yet — show an empty form
44
+ // so the editor can author one.
45
+ if (!body.ok && existingLocales.length === 0) return new Response('Not found', { status: 404 })
46
+
47
+ const hasTranslation = body.ok
48
+ const row = hasTranslation ? body.data : null
49
+ const editContent = bp.drafts && row?.draftContent ? row.draftContent : (row?.content ?? {})
50
+ const initial = hasTranslation
51
+ ? {
52
+ ...(editContent as Record<string, unknown>),
53
+ slug: row!.slug,
54
+ status: row!.status,
55
+ hasUnpublishedChanges: row!.hasUnpublishedChanges,
56
+ }
57
+ : { status: 'draft' }
58
+
59
+ const previewConfig = resolvePreviewConfig(bp)
60
+ const previewTemplate = previewConfig.path
61
+ const livePreviewEnabled = previewConfig.live !== false
62
+ const previewPath = previewTemplate.replace('{slug}', encodeURIComponent(row?.slug ?? ''))
63
+ ---
64
+ <AdminShell title={`Edit ${bp.label}`} activePath={`/admin/collections/${bp.name}`}>
65
+ <div class="mb-4 flex items-center gap-4">
66
+ <a href={`/admin/collections/${bp.name}/${entryId}/revisions?locale=${encodeURIComponent(activeLocale)}`} class="text-sm text-zinc-600 underline">View history</a>
67
+ {hasTranslation && (
68
+ <a href={`/api/vulse/preview/start?to=${encodeURIComponent(previewPath)}`} class="rounded border border-zinc-300 px-3 py-1 text-sm hover:bg-zinc-50">Preview</a>
69
+ )}
70
+ </div>
71
+ <EntryEditorWithPreview
72
+ client:load
73
+ collection={bp.name}
74
+ entryId={entryId}
75
+ fields={fields}
76
+ initial={initial}
77
+ draftsEnabled={bp.drafts === true}
78
+ seoEnabled={bp.seo === true}
79
+ seoMapping={bp.admin.seoMapping}
80
+ tree={bp.tree === true}
81
+ hasUnpublishedChanges={row?.hasUnpublishedChanges ?? false}
82
+ titleField={bp.admin.titleField}
83
+ previewPath={previewTemplate}
84
+ livePreviewAllowed={livePreviewEnabled}
85
+ entryLocale={activeLocale}
86
+ defaultLocale={localesCfg.defaultLocale}
87
+ supportedLocales={localesCfg.locales}
88
+ existingLocales={existingLocales}
89
+ />
90
+ </AdminShell>
@@ -0,0 +1,31 @@
1
+ ---
2
+ import AdminShell from '../../../components/AdminShell.astro'
3
+ import EntryList from '../../../components/EntryList.vue'
4
+ import { createDb } from '../../../../core/db.js'
5
+ import { getRuntimeEnv } from '../../../../server/env.js'
6
+ import { registryForRequest } from '../../../../core/blueprints/load.js'
7
+ import { readLocalesConfig } from '../../../../core/locales.js'
8
+
9
+ const env = getRuntimeEnv()
10
+ const db = createDb(env.DB)
11
+ const reg = await registryForRequest(db)
12
+ const bp = reg.get(Astro.params.name!)
13
+ if (!bp) return Astro.redirect('/admin')
14
+ const columns = bp.admin.listColumns ?? [bp.admin.titleField]
15
+
16
+ const localesCfg = await readLocalesConfig(db)
17
+ const requested = Astro.url.searchParams.get('locale')
18
+ const activeLocale = requested && localesCfg.locales.includes(requested) ? requested : localesCfg.defaultLocale
19
+ ---
20
+ <AdminShell title={bp.label} activePath={`/admin/collections/${bp.name}`}>
21
+ <EntryList
22
+ client:load
23
+ collection={bp.name}
24
+ label={bp.label}
25
+ columns={columns}
26
+ tree={bp.tree === true}
27
+ entryLocale={activeLocale}
28
+ supportedLocales={localesCfg.locales}
29
+ defaultLocale={localesCfg.defaultLocale}
30
+ />
31
+ </AdminShell>
@@ -0,0 +1,38 @@
1
+ ---
2
+ import AdminShell from '../../../components/AdminShell.astro'
3
+ import EntryForm from '../../../components/EntryForm.vue'
4
+ import { createDb } from '../../../../core/db.js'
5
+ import { getRuntimeEnv } from '../../../../server/env.js'
6
+ import { registryForRequest } from '../../../../core/blueprints/load.js'
7
+ import { fieldDescriptorsFromBlueprint } from '../../../client/form-from-zod.js'
8
+ import { readLocalesConfig } from '../../../../core/locales.js'
9
+
10
+ const env = getRuntimeEnv()
11
+ const db = createDb(env.DB)
12
+ const reg = await registryForRequest(db)
13
+ const bp = reg.get(Astro.params.name!)
14
+ if (!bp) return Astro.redirect('/admin')
15
+ const fields = fieldDescriptorsFromBlueprint(bp)
16
+ const parentId = Astro.url.searchParams.get('parent_id')
17
+
18
+ const localesCfg = await readLocalesConfig(db)
19
+ const requested = Astro.url.searchParams.get('locale')
20
+ const activeLocale = requested && localesCfg.locales.includes(requested) ? requested : localesCfg.defaultLocale
21
+ ---
22
+ <AdminShell title={`New ${bp.label}`} activePath={`/admin/collections/${bp.name}`}>
23
+ <EntryForm
24
+ client:load
25
+ collection={bp.name}
26
+ fields={fields}
27
+ initial={{}}
28
+ draftsEnabled={bp.drafts === true}
29
+ seoEnabled={bp.seo === true}
30
+ seoMapping={bp.admin.seoMapping}
31
+ tree={bp.tree === true}
32
+ parentId={parentId}
33
+ titleField={bp.admin.titleField}
34
+ entryLocale={activeLocale}
35
+ defaultLocale={localesCfg.defaultLocale}
36
+ supportedLocales={localesCfg.locales}
37
+ />
38
+ </AdminShell>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import AdminShell from '../../../../components/AdminShell.astro'
3
+ import SubmissionDetail from '../../../../components/SubmissionDetail.vue'
4
+
5
+ const { handle, id } = Astro.params
6
+ ---
7
+ <AdminShell title="Submission" activePath="/admin/forms">
8
+ <SubmissionDetail client:load formHandle={handle!} submissionId={id!} />
9
+ </AdminShell>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import AdminShell from '../../../../components/AdminShell.astro'
3
+ import SubmissionList from '../../../../components/SubmissionList.vue'
4
+
5
+ const { handle } = Astro.params
6
+ ---
7
+ <AdminShell title="Submissions" activePath="/admin/forms">
8
+ <SubmissionList client:load formHandle={handle!} />
9
+ </AdminShell>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import AdminShell from '../../components/AdminShell.astro'
3
+ import FormEditor from '../../components/FormEditor.vue'
4
+
5
+ const { handle } = Astro.params
6
+ ---
7
+ <AdminShell title={handle ? `Form: ${handle}` : 'Form'} activePath="/admin/forms">
8
+ <FormEditor client:load handle={handle ?? null} />
9
+ </AdminShell>
@@ -0,0 +1,7 @@
1
+ ---
2
+ import AdminShell from '../../components/AdminShell.astro'
3
+ import FormList from '../../components/FormList.vue'
4
+ ---
5
+ <AdminShell title="Forms" activePath="/admin/forms">
6
+ <FormList client:load />
7
+ </AdminShell>
@@ -0,0 +1,7 @@
1
+ ---
2
+ import AdminShell from '../../components/AdminShell.astro'
3
+ import FormEditor from '../../components/FormEditor.vue'
4
+ ---
5
+ <AdminShell title="New form" activePath="/admin/forms">
6
+ <FormEditor client:load handle={null} />
7
+ </AdminShell>
@@ -0,0 +1,36 @@
1
+ ---
2
+ import AdminShell from '../components/AdminShell.astro'
3
+ import { getRuntime } from '../../server/runtime.js'
4
+ import { getRuntimeEnv } from '../../server/env.js'
5
+ import { createDb } from '../../core/db.js'
6
+ import { registryForRequest } from '../../core/blueprints/load.js'
7
+
8
+ const env = getRuntimeEnv()
9
+ const db = createDb(env.DB)
10
+ const rt = await getRuntime(env, await registryForRequest(db), new URL(Astro.request.url).origin)
11
+ const reg = rt.registry
12
+
13
+ const counts: { name: string; label: string; total: number; published: number }[] = []
14
+ for (const bp of reg.list()) {
15
+ const all = await rt.routes.entries.list(new Request(Astro.request.url, { headers: Astro.request.headers }), { collection: bp.name })
16
+ const body = await all.json() as { ok: true; data: { status: string }[] } | { ok: false }
17
+ const list = body.ok ? body.data : []
18
+ counts.push({
19
+ name: bp.name, label: bp.label,
20
+ total: list.length,
21
+ published: list.filter((e) => e.status === 'published').length,
22
+ })
23
+ }
24
+ ---
25
+ <AdminShell title="Dashboard" activePath="/admin">
26
+ <h1 class="text-2xl font-semibold mb-6">Dashboard</h1>
27
+ <div class="grid grid-cols-3 gap-4">
28
+ {counts.map((c) => (
29
+ <a href={`/admin/collections/${c.name}`} class="block p-4 rounded-xl border bg-white hover:shadow-sm">
30
+ <div class="text-sm text-zinc-500">{c.label}</div>
31
+ <div class="text-3xl font-semibold mt-1">{c.total}</div>
32
+ <div class="text-xs text-zinc-400 mt-1">{c.published} published</div>
33
+ </a>
34
+ ))}
35
+ </div>
36
+ </AdminShell>
@@ -0,0 +1,14 @@
1
+ ---
2
+ import '../styles/admin.css'
3
+ import LoginForm from '../components/LoginForm.vue'
4
+ const next = new URL(Astro.request.url).searchParams.get('next') ?? '/admin'
5
+ ---
6
+ <!doctype html>
7
+ <html lang="en" class="vulse-admin">
8
+ <head>
9
+ <meta charset="utf-8" /><title>Sign in — Vulse</title>
10
+ </head>
11
+ <body class="min-h-screen grid place-items-center bg-zinc-50 vulse-admin">
12
+ <LoginForm client:load next={next} />
13
+ </body>
14
+ </html>
@@ -0,0 +1,8 @@
1
+ ---
2
+ import AdminShell from '../components/AdminShell.astro'
3
+ import MediaLibrary from '../components/MediaLibrary.vue'
4
+ ---
5
+ <AdminShell title="Media" activePath="/admin/media">
6
+ <h1 class="mb-4 text-2xl font-semibold text-zinc-900">Assets</h1>
7
+ <MediaLibrary client:load />
8
+ </AdminShell>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import AdminShell from '../../components/AdminShell.astro'
3
+ import BlueprintEditor from '../../components/BlueprintEditor.vue'
4
+
5
+ const { handle } = Astro.params
6
+ const isAdmin = (Astro.locals as { vulseUser?: { role?: string } }).vulseUser?.role === 'admin'
7
+ ---
8
+ <AdminShell title={handle ? `Schema: ${handle}` : 'New collection'} activePath={handle ? `/admin/schema/${handle}` : '/admin/schema/new'}>
9
+ <BlueprintEditor client:only="vue" handle={handle ?? null} is-admin={isAdmin} />
10
+ </AdminShell>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import AdminShell from '../../components/AdminShell.astro'
3
+ import BlueprintEditor from '../../components/BlueprintEditor.vue'
4
+
5
+ const isAdmin = (Astro.locals as { vulseUser?: { role?: string } }).vulseUser?.role === 'admin'
6
+ ---
7
+ <AdminShell title="New collection" activePath="/admin/schema/new">
8
+ <BlueprintEditor client:only="vue" handle={null} is-admin={isAdmin} />
9
+ </AdminShell>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import AdminShell from '../../components/AdminShell.astro'
3
+ import AuthSettings from '../../components/AuthSettings.vue'
4
+ ---
5
+ <AdminShell title="Auth settings" activePath="/admin/settings/auth">
6
+ <h1 class="mb-4 text-2xl font-semibold text-zinc-900">Authentication</h1>
7
+ <p class="mb-4 text-sm text-zinc-600">Control public member sign-up on your site. End-user sign-in forms are headless — you style them in your Astro pages.</p>
8
+ <AuthSettings client:load />
9
+ </AdminShell>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import AdminShell from '../../../components/AdminShell.astro'
3
+ import GlobalSetEditor from '../../../components/GlobalSetEditor.vue'
4
+
5
+ const { handle } = Astro.params
6
+ ---
7
+ <AdminShell title={handle ? `Global: ${handle}` : 'Global set'} activePath="/admin/settings/globals">
8
+ <GlobalSetEditor client:load handle={handle ?? null} />
9
+ </AdminShell>
@@ -0,0 +1,7 @@
1
+ ---
2
+ import AdminShell from '../../../components/AdminShell.astro'
3
+ import GlobalSetList from '../../../components/GlobalSetList.vue'
4
+ ---
5
+ <AdminShell title="Globals" activePath="/admin/settings/globals">
6
+ <GlobalSetList client:load />
7
+ </AdminShell>