@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.
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +3 -4
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +11 -10
- package/dist/core/blueprints/compile.d.ts +1 -1
- package/dist/core/blueprints/compile.d.ts.map +1 -1
- package/dist/core/blueprints/compile.js +3 -3
- package/dist/core/blueprints/mutations.js +2 -2
- package/dist/core/forms/rate-limit.d.ts +1 -1
- package/dist/core/forms/rate-limit.d.ts.map +1 -1
- package/dist/core/forms/rate-limit.js +3 -3
- package/dist/core/forms/unique.d.ts +1 -1
- package/dist/core/forms/unique.d.ts.map +1 -1
- package/dist/core/forms/unique.js +4 -4
- package/dist/core/globals/compile.d.ts +1 -1
- package/dist/core/globals/compile.d.ts.map +1 -1
- package/dist/core/globals/compile.js +2 -2
- package/dist/core/globals/definition.d.ts +1 -1
- package/dist/core/globals/definition.d.ts.map +1 -1
- package/dist/core/globals/definition.js +3 -3
- package/dist/core/repos/globals.js +3 -3
- package/dist/core/sha256.d.ts +3 -0
- package/dist/core/sha256.d.ts.map +1 -0
- package/dist/core/sha256.js +9 -0
- package/dist/integration/index.js +1 -1
- package/dist/integration/install-hook.d.ts.map +1 -1
- package/dist/integration/install-hook.js +6 -4
- package/dist/integration/wrangler-config.d.ts +12 -0
- package/dist/integration/wrangler-config.d.ts.map +1 -0
- package/dist/integration/wrangler-config.js +97 -0
- package/dist/integration/wrangler-patch.d.ts +1 -0
- package/dist/integration/wrangler-patch.d.ts.map +1 -1
- package/dist/integration/wrangler-patch.js +2 -1
- package/dist/server/routes/form-submit.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -3
- package/src/admin/assets/logo-mark.svg +5 -0
- package/src/admin/client/active-locale.ts +17 -0
- package/src/admin/client/api.ts +21 -0
- package/src/admin/client/form-from-zod.ts +7 -0
- package/src/admin/client/live-preview-enabled.ts +5 -0
- package/src/admin/components/AdminShell.astro +45 -0
- package/src/admin/components/AuthSettings.vue +60 -0
- package/src/admin/components/BlockEditor.vue +53 -0
- package/src/admin/components/BlueprintEditor.vue +1783 -0
- package/src/admin/components/CollectionKindIcon.vue +26 -0
- package/src/admin/components/CollectionTree.vue +220 -0
- package/src/admin/components/EntryEditorWithPreview.vue +130 -0
- package/src/admin/components/EntryForm.vue +411 -0
- package/src/admin/components/EntryList.vue +121 -0
- package/src/admin/components/EntryStatusBadge.vue +24 -0
- package/src/admin/components/FormEditor.vue +233 -0
- package/src/admin/components/FormList.vue +54 -0
- package/src/admin/components/GlobalSetEditor.vue +272 -0
- package/src/admin/components/GlobalSetList.vue +55 -0
- package/src/admin/components/LivePreviewPanel.vue +171 -0
- package/src/admin/components/LoginForm.vue +53 -0
- package/src/admin/components/MediaLibrary.vue +106 -0
- package/src/admin/components/MediaPicker.vue +49 -0
- package/src/admin/components/RevisionDiff.vue +11 -0
- package/src/admin/components/RevisionList.vue +134 -0
- package/src/admin/components/SeoFields.vue +113 -0
- package/src/admin/components/SetEditor.vue +137 -0
- package/src/admin/components/SetList.vue +32 -0
- package/src/admin/components/SettingsForm.vue +189 -0
- package/src/admin/components/SideNav.vue +152 -0
- package/src/admin/components/SubmissionDetail.vue +45 -0
- package/src/admin/components/SubmissionList.vue +89 -0
- package/src/admin/components/ToastHost.vue +33 -0
- package/src/admin/components/TreeRow.vue +163 -0
- package/src/admin/components/UserEditor.vue +186 -0
- package/src/admin/components/UserList.vue +46 -0
- package/src/admin/components/blocks/BlockItem.vue +32 -0
- package/src/admin/components/blocks/BlockToolbar.vue +12 -0
- package/src/admin/components/blocks/edit/CodeEdit.vue +18 -0
- package/src/admin/components/blocks/edit/EmbedEdit.vue +14 -0
- package/src/admin/components/blocks/edit/HeadingEdit.vue +19 -0
- package/src/admin/components/blocks/edit/ImageEdit.vue +40 -0
- package/src/admin/components/blocks/edit/ListEdit.vue +36 -0
- package/src/admin/components/blocks/edit/ParagraphEdit.vue +14 -0
- package/src/admin/components/blocks/edit/QuoteEdit.vue +18 -0
- package/src/admin/components/fields/BlocksField.vue +123 -0
- package/src/admin/components/fields/BlocksSetsPicker.vue +59 -0
- package/src/admin/components/fields/BoolField.vue +10 -0
- package/src/admin/components/fields/DateField.vue +22 -0
- package/src/admin/components/fields/EntriesField.vue +153 -0
- package/src/admin/components/fields/EntryField.vue +138 -0
- package/src/admin/components/fields/EnumField.vue +81 -0
- package/src/admin/components/fields/FieldRenderer.vue +87 -0
- package/src/admin/components/fields/GridField.vue +173 -0
- package/src/admin/components/fields/LinkField.vue +219 -0
- package/src/admin/components/fields/MediaField.vue +69 -0
- package/src/admin/components/fields/NumberField.vue +12 -0
- package/src/admin/components/fields/ObjectField.vue +18 -0
- package/src/admin/components/fields/RefField.vue +170 -0
- package/src/admin/components/fields/RepeaterField.vue +27 -0
- package/src/admin/components/fields/ReplicatorField.vue +121 -0
- package/src/admin/components/fields/TextField.vue +11 -0
- package/src/admin/components/fields/TextareaField.vue +11 -0
- package/src/admin/components/fields/VulseAccordionGroupNodeView.vue +82 -0
- package/src/admin/components/fields/VulseAccordionNodeView.vue +128 -0
- package/src/admin/components/fields/VulseCalloutNodeView.vue +81 -0
- package/src/admin/components/fields/VulseIframeNodeView.vue +112 -0
- package/src/admin/components/fields/VulseSetNodeView.vue +68 -0
- package/src/admin/components/fields/VulseVideoNodeView.vue +104 -0
- package/src/admin/components/fields/blocks-editor-extensions.ts +26 -0
- package/src/admin/components/fields/emoji-extension.ts +54 -0
- package/src/admin/components/fields/link-extension.ts +48 -0
- package/src/admin/components/fields/set-node-utils.ts +115 -0
- package/src/admin/components/fields/url-utils.ts +85 -0
- package/src/admin/components/fields/vulse-accordion-extension.ts +64 -0
- package/src/admin/components/fields/vulse-accordion-group-extension.ts +49 -0
- package/src/admin/components/fields/vulse-callout-extension.ts +53 -0
- package/src/admin/components/fields/vulse-iframe-extension.ts +96 -0
- package/src/admin/components/fields/vulse-set-extension.ts +66 -0
- package/src/admin/components/fields/vulse-video-extension.ts +65 -0
- package/src/admin/composables/toast.ts +35 -0
- package/src/admin/composables/useEntrySearch.ts +112 -0
- package/src/admin/composables/useSets.ts +31 -0
- package/src/admin/pages/collections/[name]/[id]/revisions.astro +27 -0
- package/src/admin/pages/collections/[name]/[id].astro +90 -0
- package/src/admin/pages/collections/[name]/index.astro +31 -0
- package/src/admin/pages/collections/[name]/new.astro +38 -0
- package/src/admin/pages/forms/[handle]/submissions/[id].astro +9 -0
- package/src/admin/pages/forms/[handle]/submissions/index.astro +9 -0
- package/src/admin/pages/forms/[handle].astro +9 -0
- package/src/admin/pages/forms/index.astro +7 -0
- package/src/admin/pages/forms/new.astro +7 -0
- package/src/admin/pages/index.astro +36 -0
- package/src/admin/pages/login.astro +14 -0
- package/src/admin/pages/media.astro +8 -0
- package/src/admin/pages/schema/[handle].astro +10 -0
- package/src/admin/pages/schema/new.astro +9 -0
- package/src/admin/pages/settings/auth.astro +9 -0
- package/src/admin/pages/settings/globals/[handle].astro +9 -0
- package/src/admin/pages/settings/globals/index.astro +7 -0
- package/src/admin/pages/settings/globals/new.astro +7 -0
- package/src/admin/pages/settings/index.astro +8 -0
- package/src/admin/pages/settings/sets/[handle].astro +9 -0
- package/src/admin/pages/settings/sets/index.astro +7 -0
- package/src/admin/pages/settings/sets/new.astro +7 -0
- package/src/admin/pages/users/[id].astro +10 -0
- package/src/admin/pages/users/index.astro +8 -0
- package/src/admin/styles/admin.css +166 -0
- package/src/core/access.ts +9 -0
- package/src/core/blocks/schema.ts +66 -0
- package/src/core/blueprints/code-to-definition.ts +156 -0
- package/src/core/blueprints/compile.ts +176 -0
- package/src/core/blueprints/define.ts +12 -0
- package/src/core/blueprints/definition.ts +185 -0
- package/src/core/blueprints/load.ts +144 -0
- package/src/core/blueprints/mutations.ts +236 -0
- package/src/core/blueprints/preview-path.ts +33 -0
- package/src/core/blueprints/reflect-fields.ts +305 -0
- package/src/core/blueprints/registry.ts +14 -0
- package/src/core/blueprints/seed.ts +20 -0
- package/src/core/blueprints/select-helpers.ts +30 -0
- package/src/core/blueprints/seo.ts +180 -0
- package/src/core/blueprints/types.ts +59 -0
- package/src/core/blueprints/zod-helpers.ts +86 -0
- package/src/core/db.ts +11 -0
- package/src/core/errors.ts +34 -0
- package/src/core/forms/compile.ts +84 -0
- package/src/core/forms/definition.ts +102 -0
- package/src/core/forms/rate-limit.ts +52 -0
- package/src/core/forms/unique.ts +38 -0
- package/src/core/globals/compile.ts +35 -0
- package/src/core/globals/definition.ts +27 -0
- package/src/core/locales.ts +45 -0
- package/src/core/migrations.ts +48 -0
- package/src/core/parse-content.ts +85 -0
- package/src/core/plugins/definition.ts +150 -0
- package/src/core/preview-content.ts +21 -0
- package/src/core/repos/entries.ts +504 -0
- package/src/core/repos/forms.ts +270 -0
- package/src/core/repos/globals.ts +179 -0
- package/src/core/repos/media.ts +106 -0
- package/src/core/repos/preview-sessions.ts +108 -0
- package/src/core/repos/revisions.ts +60 -0
- package/src/core/repos/settings.ts +23 -0
- package/src/core/schema.ts +244 -0
- package/src/core/sets/compile.ts +12 -0
- package/src/core/sets/definition.ts +10 -0
- package/src/core/sets/service.ts +82 -0
- package/src/core/sets/validate-tree.ts +57 -0
- package/src/core/sha256.ts +10 -0
- package/src/core/slug.ts +30 -0
- package/src/scaffold/collection-write.ts +83 -0
- package/src/scaffold/collection.ts +277 -0
- package/src/server/assets/live-preview-bridge.content.ts +2 -0
- package/src/server/assets/live-preview-bridge.js +535 -0
- package/src/server/better-auth.ts +82 -0
- package/src/server/cf-images.ts +34 -0
- package/src/server/cron.ts +37 -0
- package/src/server/email.ts +17 -0
- package/src/server/endpoints/api-auth.ts +10 -0
- package/src/server/endpoints/api-vulse-blueprints.ts +23 -0
- package/src/server/endpoints/api-vulse-entries-locales.ts +12 -0
- package/src/server/endpoints/api-vulse-entries-move.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-publish.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-tree.ts +7 -0
- package/src/server/endpoints/api-vulse-entries.ts +23 -0
- package/src/server/endpoints/api-vulse-form-handle.ts +30 -0
- package/src/server/endpoints/api-vulse-form-public.ts +7 -0
- package/src/server/endpoints/api-vulse-form-submit.ts +7 -0
- package/src/server/endpoints/api-vulse-form-upload.ts +7 -0
- package/src/server/endpoints/api-vulse-forms.ts +12 -0
- package/src/server/endpoints/api-vulse-globals-handle.ts +20 -0
- package/src/server/endpoints/api-vulse-globals-public-handle.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-public.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-value.ts +8 -0
- package/src/server/endpoints/api-vulse-globals.ts +12 -0
- package/src/server/endpoints/api-vulse-media-file.ts +7 -0
- package/src/server/endpoints/api-vulse-media-id.ts +12 -0
- package/src/server/endpoints/api-vulse-media.ts +12 -0
- package/src/server/endpoints/api-vulse-preview-bridge.ts +11 -0
- package/src/server/endpoints/api-vulse-preview-sessions-id.ts +10 -0
- package/src/server/endpoints/api-vulse-preview-sessions.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-start.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-stop.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions-restore.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions.ts +7 -0
- package/src/server/endpoints/api-vulse-search.ts +7 -0
- package/src/server/endpoints/api-vulse-sets.ts +23 -0
- package/src/server/endpoints/api-vulse-settings.ts +12 -0
- package/src/server/endpoints/api-vulse-users-id.ts +12 -0
- package/src/server/endpoints/api-vulse-users-reset-password.ts +7 -0
- package/src/server/endpoints/api-vulse-users-role.ts +9 -0
- package/src/server/endpoints/api-vulse-users.ts +7 -0
- package/src/server/endpoints/with-runtime.ts +11 -0
- package/src/server/env.ts +23 -0
- package/src/server/envelope.ts +21 -0
- package/src/server/forms/email.ts +11 -0
- package/src/server/forms/process-submission.ts +95 -0
- package/src/server/forms/queue.ts +25 -0
- package/src/server/forms/templates.ts +24 -0
- package/src/server/forms/webhook.ts +19 -0
- package/src/server/handler.ts +66 -0
- package/src/server/image-probe.ts +35 -0
- package/src/server/loader.ts +54 -0
- package/src/server/plugins.ts +214 -0
- package/src/server/preview.ts +25 -0
- package/src/server/r2.ts +13 -0
- package/src/server/routes/blueprints.ts +62 -0
- package/src/server/routes/entries.ts +255 -0
- package/src/server/routes/form-submit.ts +168 -0
- package/src/server/routes/form-upload.ts +100 -0
- package/src/server/routes/forms.ts +88 -0
- package/src/server/routes/globals-public.ts +30 -0
- package/src/server/routes/globals.ts +93 -0
- package/src/server/routes/media.ts +145 -0
- package/src/server/routes/preview-sessions.ts +76 -0
- package/src/server/routes/preview.ts +36 -0
- package/src/server/routes/revisions.ts +29 -0
- package/src/server/routes/search.ts +31 -0
- package/src/server/routes/sets.ts +40 -0
- package/src/server/routes/settings.ts +24 -0
- package/src/server/routes/users.ts +127 -0
- package/src/server/runtime.ts +99 -0
- package/src/server/sdk/collections.ts +98 -0
- package/src/server/sdk/index.ts +25 -0
- package/src/server/sdk/media.ts +11 -0
- 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="https://example.com/embed" title="Embedded content"></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>
|