@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,121 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { ReplicatorSetDefinition } from '../../../core/blueprints/definition.js'
4
+ import { nestedFieldToDescriptor } from '../../../core/blueprints/code-to-definition.js'
5
+ import FieldRenderer from './FieldRenderer.vue'
6
+
7
+ interface ReplicatorItem {
8
+ set: string
9
+ content: Record<string, unknown>
10
+ }
11
+
12
+ const props = defineProps<{
13
+ label: string
14
+ modelValue: unknown
15
+ replicatorSets?: ReplicatorSetDefinition[]
16
+ }>()
17
+ const emit = defineEmits<{ (e: 'update:modelValue', v: ReplicatorItem[]): void }>()
18
+
19
+ const items = computed<ReplicatorItem[]>(() =>
20
+ Array.isArray(props.modelValue) ? (props.modelValue as ReplicatorItem[]) : [],
21
+ )
22
+ const setMap = computed(() => new Map((props.replicatorSets ?? []).map((set) => [set.name, set])))
23
+
24
+ function humanize(value: string): string {
25
+ return value.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')
26
+ .replace(/\b\w/g, (char) => char.toUpperCase())
27
+ }
28
+
29
+ function labelForSet(set: ReplicatorSetDefinition): string {
30
+ return set.label?.trim() || humanize(set.name)
31
+ }
32
+
33
+ function defaultForField(field: ReplicatorSetDefinition['fields'][number]): unknown {
34
+ if (field.default !== undefined) return field.default
35
+ switch (field.ui.kind) {
36
+ case 'boolean': return false
37
+ case 'blocks': return { type: 'doc', content: [{ type: 'paragraph' }] }
38
+ case 'date': return new Date().toISOString().slice(0, 16)
39
+ default: return ''
40
+ }
41
+ }
42
+
43
+ function emitItems(next: ReplicatorItem[]) {
44
+ emit('update:modelValue', next)
45
+ }
46
+
47
+ function addSet(set: ReplicatorSetDefinition) {
48
+ const content: Record<string, unknown> = {}
49
+ for (const field of set.fields) content[field.name] = defaultForField(field)
50
+ emitItems([...items.value, { set: set.name, content }])
51
+ }
52
+
53
+ function removeItem(index: number) {
54
+ emitItems(items.value.filter((_, current) => current !== index))
55
+ }
56
+
57
+ function moveItem(index: number, direction: -1 | 1) {
58
+ const target = index + direction
59
+ if (target < 0 || target >= items.value.length) return
60
+ const next = [...items.value]
61
+ const [moved] = next.splice(index, 1)
62
+ next.splice(target, 0, moved!)
63
+ emitItems(next)
64
+ }
65
+
66
+ function updateField(index: number, fieldName: string, value: unknown) {
67
+ const next = [...items.value]
68
+ const current = next[index]
69
+ if (!current) return
70
+ next[index] = { ...current, content: { ...current.content, [fieldName]: value } }
71
+ emitItems(next)
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <div class="space-y-3">
77
+ <div class="flex items-center justify-between">
78
+ <span class="block text-sm font-medium text-zinc-700">{{ label }}</span>
79
+ <div class="flex flex-wrap gap-2">
80
+ <button
81
+ v-for="set in replicatorSets ?? []"
82
+ :key="set.name"
83
+ type="button"
84
+ class="rounded border border-zinc-300 bg-white px-2.5 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-50"
85
+ @click="addSet(set)"
86
+ >
87
+ + {{ labelForSet(set) }}
88
+ </button>
89
+ </div>
90
+ </div>
91
+
92
+ <div v-if="items.length === 0" class="rounded border border-dashed border-zinc-300 bg-zinc-50 px-4 py-5 text-sm text-zinc-500">
93
+ No sets added yet.
94
+ </div>
95
+
96
+ <div v-for="(item, index) in items" :key="`${item.set}-${index}`" class="rounded-xl border border-zinc-200 bg-white">
97
+ <div class="flex items-center gap-2 border-b border-zinc-200 px-3 py-2">
98
+ <button type="button" class="px-2 text-zinc-400 hover:text-zinc-700" @click="moveItem(index, -1)">↑</button>
99
+ <button type="button" class="px-2 text-zinc-400 hover:text-zinc-700" @click="moveItem(index, 1)">↓</button>
100
+ <div class="flex-1">
101
+ <span class="rounded bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-700">
102
+ {{ setMap.get(item.set) ? labelForSet(setMap.get(item.set)!) : item.set }}
103
+ </span>
104
+ </div>
105
+ <button type="button" class="rounded px-2 py-1 text-xs text-red-600 hover:bg-red-50" @click="removeItem(index)">Remove</button>
106
+ </div>
107
+ <div v-if="setMap.get(item.set)" class="space-y-4 p-3">
108
+ <FieldRenderer
109
+ v-for="field in setMap.get(item.set)!.fields.map(nestedFieldToDescriptor)"
110
+ :key="`${item.set}-${field.path}`"
111
+ :field="field"
112
+ :model-value="item.content?.[field.path]"
113
+ @update:modelValue="(v: unknown) => updateField(index, field.path, v)"
114
+ />
115
+ </div>
116
+ <div v-else class="p-3 text-sm text-amber-700">
117
+ This set no longer exists in the schema.
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ modelValue: string; label: string; required?: boolean }>()
3
+ defineEmits<{ (e: 'update:modelValue', v: string): void }>()
4
+ </script>
5
+ <template>
6
+ <label class="block">
7
+ <span class="text-sm text-zinc-600">{{ label }}<span v-if="required" class="text-red-600">*</span></span>
8
+ <input :value="modelValue" @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
9
+ class="mt-1 w-full rounded border px-3 py-2" />
10
+ </label>
11
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ modelValue: string; label: string; required?: boolean }>()
3
+ defineEmits<{ (e: 'update:modelValue', v: string): void }>()
4
+ </script>
5
+ <template>
6
+ <label class="block">
7
+ <span class="text-sm text-zinc-600">{{ label }}</span>
8
+ <textarea :value="modelValue" @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
9
+ rows="6" class="mt-1 w-full rounded border px-3 py-2"></textarea>
10
+ </label>
11
+ </template>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import { NodeViewContent, NodeViewWrapper, type NodeViewProps } from '@tiptap/vue-3';
3
+ import { appendContentInside, deleteCurrentNode, insertParagraphAfter, insertParagraphBefore } from './set-node-utils.js';
4
+
5
+ const props = defineProps<NodeViewProps>();
6
+
7
+ function addItem() {
8
+ appendContentInside(props, {
9
+ type: 'vulseAccordion',
10
+ attrs: { summary: 'Accordion', open: false },
11
+ content: [{ type: 'paragraph' }],
12
+ });
13
+ }
14
+
15
+ function addTextAbove() {
16
+ insertParagraphBefore(props);
17
+ }
18
+
19
+ function addTextBelow() {
20
+ insertParagraphAfter(props);
21
+ }
22
+
23
+ function removeGroup() {
24
+ deleteCurrentNode(props);
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <NodeViewWrapper
30
+ class="my-4 overflow-hidden rounded-xl border border-zinc-300 bg-zinc-50"
31
+ data-testid="accordion-group-node-view"
32
+ >
33
+ <div
34
+ contenteditable="false"
35
+ class="flex flex-wrap items-center justify-between gap-3 border-b border-zinc-200 px-4 py-3"
36
+ >
37
+ <div>
38
+ <div class="text-xs font-medium uppercase tracking-wide text-zinc-500">
39
+ Accordion group
40
+ </div>
41
+ <div class="text-xs text-zinc-500">Items inside this block render together.</div>
42
+ </div>
43
+ <div class="flex flex-wrap items-center gap-2">
44
+ <button
45
+ type="button"
46
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
47
+ data-testid="accordion-group-add-item"
48
+ @click="addItem"
49
+ >
50
+ Add item
51
+ </button>
52
+ <button
53
+ type="button"
54
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
55
+ data-testid="accordion-group-add-above"
56
+ @click="addTextAbove"
57
+ >
58
+ Add text at top
59
+ </button>
60
+ <button
61
+ type="button"
62
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
63
+ data-testid="accordion-group-add-below"
64
+ @click="addTextBelow"
65
+ >
66
+ Add text below
67
+ </button>
68
+ <button
69
+ type="button"
70
+ class="rounded border border-red-200 bg-white px-2 py-1 text-xs text-red-600 hover:bg-red-50"
71
+ data-testid="accordion-group-delete"
72
+ @click="removeGroup"
73
+ >
74
+ Delete group
75
+ </button>
76
+ </div>
77
+ </div>
78
+ <div class="space-y-3 px-3 py-3">
79
+ <NodeViewContent class="space-y-3" />
80
+ </div>
81
+ </NodeViewWrapper>
82
+ </template>
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ import { NodeViewContent, NodeViewWrapper, type NodeViewProps } from '@tiptap/vue-3';
3
+ import { computed } from 'vue';
4
+ import {
5
+ deleteCurrentNode,
6
+ deleteCurrentNodeOrParentIfOnlyChild,
7
+ insertContentAfter,
8
+ insertParagraphAfter,
9
+ insertParagraphBefore,
10
+ parentNodeInfo,
11
+ } from './set-node-utils.js';
12
+
13
+ const props = defineProps<NodeViewProps>();
14
+
15
+ const summary = computed(() => String(props.node.attrs?.summary ?? 'Accordion'));
16
+ const open = computed(() => Boolean(props.node.attrs?.open));
17
+ const parent = computed(() => parentNodeInfo(props));
18
+ const isGrouped = computed(() => parent.value?.name === 'vulseAccordionGroup');
19
+ const itemNumber = computed(() => (isGrouped.value ? (parent.value?.index ?? 0) + 1 : null));
20
+
21
+ function onSummaryInput(event: Event) {
22
+ props.updateAttributes({ summary: (event.target as HTMLInputElement).value || 'Accordion' });
23
+ }
24
+
25
+ function onOpenChange(event: Event) {
26
+ props.updateAttributes({ open: (event.target as HTMLInputElement).checked });
27
+ }
28
+
29
+ function addAccordionBelow() {
30
+ insertContentAfter(props, {
31
+ type: 'vulseAccordion',
32
+ attrs: { summary: 'Accordion', open: false },
33
+ content: [{ type: 'paragraph' }],
34
+ });
35
+ }
36
+
37
+ function addTextAbove() {
38
+ insertParagraphBefore(props);
39
+ }
40
+
41
+ function addTextBelow() {
42
+ insertParagraphAfter(props);
43
+ }
44
+
45
+ function removeSet() {
46
+ if (isGrouped.value) {
47
+ deleteCurrentNodeOrParentIfOnlyChild(props, 'vulseAccordionGroup');
48
+ return;
49
+ }
50
+
51
+ deleteCurrentNode(props);
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <NodeViewWrapper
57
+ class="overflow-hidden rounded-lg border border-zinc-200 bg-white"
58
+ data-testid="accordion-node-view"
59
+ >
60
+ <div contenteditable="false" class="border-b border-zinc-200 px-3 py-2">
61
+ <div class="mb-2 flex items-center justify-between gap-3">
62
+ <div class="text-xs font-medium uppercase tracking-wide text-zinc-500">
63
+ {{ isGrouped ? `Item ${itemNumber}` : 'Accordion' }}
64
+ </div>
65
+ <div class="flex flex-wrap items-center gap-2">
66
+ <button
67
+ type="button"
68
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
69
+ data-testid="accordion-add-item"
70
+ @click="addAccordionBelow"
71
+ >
72
+ {{ isGrouped ? 'Add item below' : 'Add accordion below' }}
73
+ </button>
74
+ <button
75
+ v-if="!isGrouped"
76
+ type="button"
77
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
78
+ data-testid="accordion-add-above"
79
+ @click="addTextAbove"
80
+ >
81
+ Add text at top
82
+ </button>
83
+ <button
84
+ v-if="!isGrouped"
85
+ type="button"
86
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
87
+ data-testid="accordion-add-below"
88
+ @click="addTextBelow"
89
+ >
90
+ Add text below
91
+ </button>
92
+ <button
93
+ type="button"
94
+ class="rounded border border-red-200 bg-white px-2 py-1 text-xs text-red-600 hover:bg-red-50"
95
+ data-testid="accordion-delete"
96
+ @click="removeSet"
97
+ >
98
+ {{ isGrouped ? 'Delete item' : 'Delete' }}
99
+ </button>
100
+ </div>
101
+ </div>
102
+ <div class="grid gap-2 md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
103
+ <label class="grid gap-1 text-xs text-zinc-500">
104
+ <span>Title</span>
105
+ <input
106
+ class="rounded border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900"
107
+ :value="summary"
108
+ data-testid="accordion-summary"
109
+ @input="onSummaryInput"
110
+ />
111
+ </label>
112
+ <label class="mt-1 flex items-center gap-2 text-xs text-zinc-500 md:mt-5">
113
+ <input
114
+ type="checkbox"
115
+ class="rounded border-zinc-300"
116
+ :checked="open"
117
+ data-testid="accordion-open"
118
+ @change="onOpenChange"
119
+ />
120
+ <span>Open by default</span>
121
+ </label>
122
+ </div>
123
+ </div>
124
+ <div class="px-3 py-3">
125
+ <NodeViewContent class="min-h-10 rounded border border-zinc-200 bg-white px-3 py-2" />
126
+ </div>
127
+ </NodeViewWrapper>
128
+ </template>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import { NodeViewContent, NodeViewWrapper, type NodeViewProps } from '@tiptap/vue-3';
3
+ import { computed } from 'vue';
4
+ import { deleteCurrentNode, insertParagraphAfter, insertParagraphBefore } from './set-node-utils.js';
5
+
6
+ const props = defineProps<NodeViewProps>();
7
+
8
+ const tone = computed(() => String(props.node.attrs?.tone === 'warn' ? 'warn' : 'info'));
9
+
10
+ function onToneChange(event: Event) {
11
+ const next = (event.target as HTMLSelectElement).value === 'warn' ? 'warn' : 'info';
12
+ props.updateAttributes({ tone: next });
13
+ }
14
+
15
+ function addAbove() {
16
+ insertParagraphBefore(props);
17
+ }
18
+
19
+ function addBelow() {
20
+ insertParagraphAfter(props);
21
+ }
22
+
23
+ function removeSet() {
24
+ deleteCurrentNode(props);
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <NodeViewWrapper
30
+ class="my-3 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50"
31
+ data-testid="callout-node-view"
32
+ >
33
+ <div
34
+ contenteditable="false"
35
+ class="flex items-center justify-between gap-3 border-b border-zinc-200 px-3 py-2"
36
+ >
37
+ <div class="text-xs font-medium uppercase tracking-wide text-zinc-500">Callout</div>
38
+ <div class="flex items-center gap-2">
39
+ <label class="flex items-center gap-2 text-xs text-zinc-500">
40
+ <span>Tone</span>
41
+ <select
42
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-700"
43
+ :value="tone"
44
+ data-testid="callout-tone"
45
+ @change="onToneChange"
46
+ >
47
+ <option value="info">Info</option>
48
+ <option value="warn">Warn</option>
49
+ </select>
50
+ </label>
51
+ <button
52
+ type="button"
53
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
54
+ data-testid="callout-add-above"
55
+ @click="addAbove"
56
+ >
57
+ Add text at top
58
+ </button>
59
+ <button
60
+ type="button"
61
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
62
+ data-testid="callout-add-below"
63
+ @click="addBelow"
64
+ >
65
+ Add text below
66
+ </button>
67
+ <button
68
+ type="button"
69
+ class="rounded border border-red-200 bg-white px-2 py-1 text-xs text-red-600 hover:bg-red-50"
70
+ data-testid="callout-delete"
71
+ @click="removeSet"
72
+ >
73
+ Delete
74
+ </button>
75
+ </div>
76
+ </div>
77
+ <div class="px-3 py-3">
78
+ <NodeViewContent class="min-h-10 rounded border border-zinc-200 bg-white px-3 py-2" />
79
+ </div>
80
+ </NodeViewWrapper>
81
+ </template>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import { NodeViewWrapper, type NodeViewProps } from '@tiptap/vue-3';
3
+ import { ref, watch } from 'vue';
4
+ import { deleteCurrentNode, insertParagraphAfter, insertParagraphBefore } from './set-node-utils.js';
5
+ import { parseIframeCode } from './url-utils.js';
6
+
7
+ const props = defineProps<NodeViewProps>();
8
+
9
+ const codeDraft = ref(
10
+ String(
11
+ props.node.attrs?.code ??
12
+ (props.node.attrs?.src
13
+ ? `<iframe src="${String(props.node.attrs.src)}" title="${String(props.node.attrs?.title ?? 'Embedded content')}"></iframe>`
14
+ : ''),
15
+ ),
16
+ );
17
+ const invalidCode = ref(false);
18
+
19
+ watch(
20
+ () => props.node.attrs?.code,
21
+ (value) => {
22
+ codeDraft.value = String(value ?? '');
23
+ invalidCode.value = false;
24
+ },
25
+ );
26
+
27
+ function commitCode() {
28
+ if (!codeDraft.value.trim()) {
29
+ props.updateAttributes({ code: null, src: null, title: 'Embedded content' });
30
+ invalidCode.value = false;
31
+ return;
32
+ }
33
+
34
+ const parsed = parseIframeCode(codeDraft.value);
35
+ if (!parsed) {
36
+ invalidCode.value = true;
37
+ return;
38
+ }
39
+ invalidCode.value = false;
40
+ codeDraft.value = parsed.code;
41
+ props.updateAttributes(parsed);
42
+ }
43
+
44
+ function addAbove() {
45
+ insertParagraphBefore(props);
46
+ }
47
+
48
+ function addBelow() {
49
+ insertParagraphAfter(props);
50
+ }
51
+
52
+ function removeSet() {
53
+ deleteCurrentNode(props);
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <NodeViewWrapper
59
+ class="my-3 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50"
60
+ data-testid="iframe-node-view"
61
+ >
62
+ <div class="border-b border-zinc-200 px-3 py-2" contenteditable="false">
63
+ <div class="flex items-center justify-between gap-3">
64
+ <div class="text-xs font-medium uppercase tracking-wide text-zinc-500">Iframe</div>
65
+ <div class="flex items-center gap-2">
66
+ <button
67
+ type="button"
68
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
69
+ data-testid="iframe-add-above"
70
+ @click="addAbove"
71
+ >
72
+ Add text at top
73
+ </button>
74
+ <button
75
+ type="button"
76
+ class="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-100"
77
+ data-testid="iframe-add-below"
78
+ @click="addBelow"
79
+ >
80
+ Add text below
81
+ </button>
82
+ <button
83
+ type="button"
84
+ class="rounded border border-red-200 bg-white px-2 py-1 text-xs text-red-600 hover:bg-red-50"
85
+ data-testid="iframe-delete"
86
+ @click="removeSet"
87
+ >
88
+ Delete
89
+ </button>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ <div class="grid gap-3 px-3 py-3" contenteditable="false">
94
+ <label class="grid gap-1 text-xs text-zinc-500">
95
+ <span>Iframe code</span>
96
+ <textarea
97
+ v-model="codeDraft"
98
+ class="min-h-32 rounded border border-zinc-300 bg-white px-3 py-2 font-mono text-sm text-zinc-900"
99
+ placeholder="<iframe src=&quot;https://example.com/embed&quot; title=&quot;Embedded content&quot;></iframe>"
100
+ data-testid="iframe-code"
101
+ @blur="commitCode"
102
+ />
103
+ </label>
104
+ <div v-if="invalidCode" class="text-xs text-red-600">
105
+ Enter a valid iframe snippet with an `http` or `https` `src`.
106
+ </div>
107
+ <div class="text-xs text-zinc-500">
108
+ Paste the full iframe snippet. Vulse stores the code and renders a sanitized iframe on the frontend.
109
+ </div>
110
+ </div>
111
+ </NodeViewWrapper>
112
+ </template>
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ import type { NodeViewProps } from '@tiptap/vue-3'
3
+ import { NodeViewWrapper } from '@tiptap/vue-3'
4
+ import { computed, onMounted, ref } from 'vue'
5
+ import { nestedFieldToDescriptor } from '../../../core/blueprints/code-to-definition.js'
6
+ import { useSets } from '../../composables/useSets.js'
7
+ import FieldRenderer from './FieldRenderer.vue'
8
+
9
+ const props = defineProps<NodeViewProps>()
10
+ const { get, hydrate } = useSets()
11
+ onMounted(() => { void hydrate() })
12
+
13
+ const expanded = ref(false)
14
+
15
+ const setHandle = computed<string | null>(() => {
16
+ const s = (props.node.attrs as { set?: unknown }).set
17
+ return typeof s === 'string' ? s : null
18
+ })
19
+
20
+ const setDef = computed(() => (setHandle.value ? get(setHandle.value) : undefined))
21
+
22
+ const data = computed<Record<string, unknown>>(() => {
23
+ return ((props.node.attrs as { data?: unknown }).data as Record<string, unknown> | undefined) ?? {}
24
+ })
25
+
26
+ const fieldDescriptors = computed(() => (setDef.value?.fields ?? []).map(nestedFieldToDescriptor))
27
+
28
+ function updateField(name: string, value: unknown) {
29
+ props.updateAttributes({ data: { ...data.value, [name]: value } })
30
+ }
31
+
32
+ const summary = computed(() => {
33
+ const def = setDef.value
34
+ if (!def) return ''
35
+ const firstText = def.fields.find((f) => f.ui.kind === 'text' || f.ui.kind === 'textarea')
36
+ if (!firstText) return ''
37
+ const v = data.value[firstText.name]
38
+ return typeof v === 'string' && v ? v.slice(0, 80) : ''
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <NodeViewWrapper class="vulse-set my-2">
44
+ <div v-if="!setDef" class="rounded border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800">
45
+ <div class="font-medium">Missing set: {{ setHandle ?? '(unset)' }}</div>
46
+ <button type="button" class="mt-1 text-xs text-amber-900 underline" @click="props.deleteNode()">Remove</button>
47
+ </div>
48
+ <div v-else class="rounded border border-zinc-200 bg-white">
49
+ <div class="flex items-center justify-between gap-2 px-3 py-2">
50
+ <button type="button" class="flex flex-1 items-center gap-2 text-left text-sm" @click="expanded = !expanded">
51
+ <span class="text-zinc-400">{{ expanded ? '▾' : '▸' }}</span>
52
+ <span class="font-medium text-zinc-800">{{ setDef.label }}</span>
53
+ <span v-if="!expanded && summary" class="truncate text-zinc-500">— {{ summary }}</span>
54
+ </button>
55
+ <button type="button" class="text-xs text-zinc-500 hover:text-red-700" @click="props.deleteNode()">Remove</button>
56
+ </div>
57
+ <div v-if="expanded" class="space-y-2 border-t border-zinc-200 p-3">
58
+ <FieldRenderer
59
+ v-for="f in fieldDescriptors"
60
+ :key="f.path"
61
+ :field="f"
62
+ :model-value="data[f.path]"
63
+ @update:modelValue="(v: unknown) => updateField(f.path, v)"
64
+ />
65
+ </div>
66
+ </div>
67
+ </NodeViewWrapper>
68
+ </template>