@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,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useToast } from '../composables/toast.js'
|
|
3
|
+
|
|
4
|
+
const { toasts, dismiss } = useToast()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div
|
|
9
|
+
class="pointer-events-none fixed bottom-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2"
|
|
10
|
+
aria-live="polite"
|
|
11
|
+
aria-relevant="additions"
|
|
12
|
+
>
|
|
13
|
+
<div
|
|
14
|
+
v-for="toast in toasts"
|
|
15
|
+
:key="toast.id"
|
|
16
|
+
class="pointer-events-auto flex items-start gap-3 rounded-lg border px-4 py-3 text-sm shadow-lg"
|
|
17
|
+
:class="toast.kind === 'success'
|
|
18
|
+
? 'border-emerald-200 bg-emerald-50 text-emerald-900'
|
|
19
|
+
: 'border-red-200 bg-red-50 text-red-900'"
|
|
20
|
+
role="status"
|
|
21
|
+
>
|
|
22
|
+
<span class="flex-1">{{ toast.message }}</span>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="rounded px-1 text-xs opacity-70 hover:opacity-100"
|
|
26
|
+
aria-label="Dismiss notification"
|
|
27
|
+
@click="dismiss(toast.id)"
|
|
28
|
+
>
|
|
29
|
+
✕
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { EntryNode } from '../../core/repos/entries.js'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
node: EntryNode
|
|
7
|
+
handle: string
|
|
8
|
+
depth: number
|
|
9
|
+
expandedSet: Set<string>
|
|
10
|
+
draggingId: string | null
|
|
11
|
+
disabled: boolean
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
toggle: [id: string]
|
|
16
|
+
'move-up': [id: string]
|
|
17
|
+
'move-down': [id: string]
|
|
18
|
+
outdent: [id: string]
|
|
19
|
+
indent: [id: string]
|
|
20
|
+
'drag-start': [event: DragEvent, id: string]
|
|
21
|
+
'drag-over': [event: DragEvent]
|
|
22
|
+
'drop-onto': [event: DragEvent, id: string | null]
|
|
23
|
+
destroy: [id: string, label: string]
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const hasChildren = computed(() => props.node.children.length > 0)
|
|
27
|
+
const isOpen = computed(() => props.expandedSet.has(props.node.id))
|
|
28
|
+
const isDragging = computed(() => props.draggingId === props.node.id)
|
|
29
|
+
|
|
30
|
+
function label(): string {
|
|
31
|
+
const c = props.node.content as Record<string, unknown>
|
|
32
|
+
return (
|
|
33
|
+
(c.title as string | undefined) ??
|
|
34
|
+
(c.name as string | undefined) ??
|
|
35
|
+
(c.label as string | undefined) ??
|
|
36
|
+
props.node.slug ??
|
|
37
|
+
props.node.id
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<li
|
|
44
|
+
:data-testid="`tree-row-${node.id}`"
|
|
45
|
+
:draggable="!disabled"
|
|
46
|
+
:class="['flex flex-col', isDragging ? 'opacity-50' : '']"
|
|
47
|
+
@dragstart="emit('drag-start', $event, node.id)"
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
class="flex items-center gap-1 px-2 py-1.5 hover:bg-zinc-50"
|
|
51
|
+
:style="{ paddingLeft: `${depth * 1.25 + 0.5}rem` }"
|
|
52
|
+
@dragover="emit('drag-over', $event)"
|
|
53
|
+
@drop="emit('drop-onto', $event, node.id)"
|
|
54
|
+
>
|
|
55
|
+
<button
|
|
56
|
+
v-if="hasChildren"
|
|
57
|
+
type="button"
|
|
58
|
+
class="flex h-5 w-5 items-center justify-center rounded text-zinc-500 hover:bg-zinc-200"
|
|
59
|
+
:aria-expanded="isOpen"
|
|
60
|
+
:data-testid="`tree-toggle-${node.id}`"
|
|
61
|
+
@click="emit('toggle', node.id)"
|
|
62
|
+
>
|
|
63
|
+
{{ isOpen ? '▾' : '▸' }}
|
|
64
|
+
</button>
|
|
65
|
+
<span v-else class="inline-block h-5 w-5"></span>
|
|
66
|
+
<a
|
|
67
|
+
:href="`/admin/collections/${handle}/${node.id}`"
|
|
68
|
+
class="flex-1 truncate text-sm text-zinc-800 hover:underline"
|
|
69
|
+
:data-testid="`tree-link-${node.id}`"
|
|
70
|
+
>
|
|
71
|
+
{{ label() }}
|
|
72
|
+
</a>
|
|
73
|
+
<span
|
|
74
|
+
v-if="node.hasUnpublishedChanges"
|
|
75
|
+
class="rounded bg-amber-50 px-1.5 py-0.5 text-[10px] text-amber-800"
|
|
76
|
+
>
|
|
77
|
+
draft
|
|
78
|
+
</span>
|
|
79
|
+
<span class="hidden text-[10px] text-zinc-400 sm:inline">#{{ node.sortOrder }}</span>
|
|
80
|
+
<div class="flex shrink-0 items-center gap-0.5 text-zinc-400">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="rounded px-1.5 py-0.5 text-xs hover:bg-zinc-200 hover:text-zinc-700 disabled:opacity-40"
|
|
84
|
+
:disabled="disabled"
|
|
85
|
+
title="Move up"
|
|
86
|
+
:data-testid="`tree-up-${node.id}`"
|
|
87
|
+
@click="emit('move-up', node.id)"
|
|
88
|
+
>
|
|
89
|
+
↑
|
|
90
|
+
</button>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
class="rounded px-1.5 py-0.5 text-xs hover:bg-zinc-200 hover:text-zinc-700 disabled:opacity-40"
|
|
94
|
+
:disabled="disabled"
|
|
95
|
+
title="Move down"
|
|
96
|
+
:data-testid="`tree-down-${node.id}`"
|
|
97
|
+
@click="emit('move-down', node.id)"
|
|
98
|
+
>
|
|
99
|
+
↓
|
|
100
|
+
</button>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class="rounded px-1.5 py-0.5 text-xs hover:bg-zinc-200 hover:text-zinc-700 disabled:opacity-40"
|
|
104
|
+
:disabled="disabled || node.parentId === null"
|
|
105
|
+
title="Outdent"
|
|
106
|
+
:data-testid="`tree-outdent-${node.id}`"
|
|
107
|
+
@click="emit('outdent', node.id)"
|
|
108
|
+
>
|
|
109
|
+
⇤
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
class="rounded px-1.5 py-0.5 text-xs hover:bg-zinc-200 hover:text-zinc-700 disabled:opacity-40"
|
|
114
|
+
:disabled="disabled"
|
|
115
|
+
title="Indent"
|
|
116
|
+
:data-testid="`tree-indent-${node.id}`"
|
|
117
|
+
@click="emit('indent', node.id)"
|
|
118
|
+
>
|
|
119
|
+
⇥
|
|
120
|
+
</button>
|
|
121
|
+
<a
|
|
122
|
+
:href="`/admin/collections/${handle}/new?parent_id=${node.id}`"
|
|
123
|
+
class="rounded px-1.5 py-0.5 text-xs hover:bg-zinc-200 hover:text-zinc-700"
|
|
124
|
+
title="Add child"
|
|
125
|
+
:data-testid="`tree-add-child-${node.id}`"
|
|
126
|
+
>
|
|
127
|
+
+
|
|
128
|
+
</a>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
class="rounded px-1.5 py-0.5 text-xs text-red-500 hover:bg-red-50 disabled:opacity-40"
|
|
132
|
+
:disabled="disabled"
|
|
133
|
+
title="Delete"
|
|
134
|
+
:data-testid="`tree-delete-${node.id}`"
|
|
135
|
+
@click="emit('destroy', node.id, label())"
|
|
136
|
+
>
|
|
137
|
+
×
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<ul v-if="hasChildren && isOpen" class="border-t border-zinc-100">
|
|
142
|
+
<TreeRow
|
|
143
|
+
v-for="child in node.children"
|
|
144
|
+
:key="child.id"
|
|
145
|
+
:node="child"
|
|
146
|
+
:handle="handle"
|
|
147
|
+
:depth="depth + 1"
|
|
148
|
+
:expanded-set="expandedSet"
|
|
149
|
+
:dragging-id="draggingId"
|
|
150
|
+
:disabled="disabled"
|
|
151
|
+
@toggle="(id) => emit('toggle', id)"
|
|
152
|
+
@move-up="(id) => emit('move-up', id)"
|
|
153
|
+
@move-down="(id) => emit('move-down', id)"
|
|
154
|
+
@outdent="(id) => emit('outdent', id)"
|
|
155
|
+
@indent="(id) => emit('indent', id)"
|
|
156
|
+
@drag-start="(e, id) => emit('drag-start', e, id)"
|
|
157
|
+
@drag-over="(e) => emit('drag-over', e)"
|
|
158
|
+
@drop-onto="(e, id) => emit('drop-onto', e, id)"
|
|
159
|
+
@destroy="(id, lbl) => emit('destroy', id, lbl)"
|
|
160
|
+
/>
|
|
161
|
+
</ul>
|
|
162
|
+
</li>
|
|
163
|
+
</template>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, reactive, ref } from 'vue'
|
|
3
|
+
import { adminApi } from '../client/api.js'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ userId: string }>()
|
|
6
|
+
|
|
7
|
+
interface UserRecord {
|
|
8
|
+
id: string
|
|
9
|
+
email: string
|
|
10
|
+
name: string
|
|
11
|
+
role: 'admin' | 'editor' | 'member'
|
|
12
|
+
displayName: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const form = reactive({
|
|
16
|
+
name: '',
|
|
17
|
+
displayName: '',
|
|
18
|
+
role: 'member' as UserRecord['role'],
|
|
19
|
+
})
|
|
20
|
+
const email = ref('')
|
|
21
|
+
const loading = ref(true)
|
|
22
|
+
const saving = ref(false)
|
|
23
|
+
const resetSending = ref(false)
|
|
24
|
+
const settingPassword = ref(false)
|
|
25
|
+
const newPassword = ref('')
|
|
26
|
+
const error = ref<string | null>(null)
|
|
27
|
+
const notice = ref<string | null>(null)
|
|
28
|
+
|
|
29
|
+
async function load() {
|
|
30
|
+
loading.value = true
|
|
31
|
+
error.value = null
|
|
32
|
+
try {
|
|
33
|
+
const user = await adminApi.get<UserRecord>(`/api/vulse/users/${props.userId}`)
|
|
34
|
+
email.value = user.email
|
|
35
|
+
form.name = user.name
|
|
36
|
+
form.displayName = user.displayName ?? ''
|
|
37
|
+
form.role = user.role
|
|
38
|
+
} catch (e) {
|
|
39
|
+
error.value = e instanceof Error ? e.message : 'Failed to load user'
|
|
40
|
+
} finally {
|
|
41
|
+
loading.value = false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onMounted(load)
|
|
46
|
+
|
|
47
|
+
async function save() {
|
|
48
|
+
saving.value = true
|
|
49
|
+
error.value = null
|
|
50
|
+
notice.value = null
|
|
51
|
+
try {
|
|
52
|
+
await adminApi.patch(`/api/vulse/users/${props.userId}`, {
|
|
53
|
+
name: form.name,
|
|
54
|
+
displayName: form.displayName || null,
|
|
55
|
+
role: form.role,
|
|
56
|
+
})
|
|
57
|
+
notice.value = 'User saved.'
|
|
58
|
+
} catch (e) {
|
|
59
|
+
error.value = e instanceof Error ? e.message : 'Save failed'
|
|
60
|
+
} finally {
|
|
61
|
+
saving.value = false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function sendResetEmail() {
|
|
66
|
+
resetSending.value = true
|
|
67
|
+
error.value = null
|
|
68
|
+
notice.value = null
|
|
69
|
+
try {
|
|
70
|
+
await adminApi.post(`/api/vulse/users/${props.userId}/reset-password`, { action: 'email' })
|
|
71
|
+
notice.value = 'Password reset email sent (or logged in development).'
|
|
72
|
+
} catch (e) {
|
|
73
|
+
error.value = e instanceof Error ? e.message : 'Could not send reset email'
|
|
74
|
+
} finally {
|
|
75
|
+
resetSending.value = false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function setPassword() {
|
|
80
|
+
if (newPassword.value.length < 8) {
|
|
81
|
+
error.value = 'Password must be at least 8 characters.'
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
settingPassword.value = true
|
|
85
|
+
error.value = null
|
|
86
|
+
notice.value = null
|
|
87
|
+
try {
|
|
88
|
+
await adminApi.post(`/api/vulse/users/${props.userId}/reset-password`, {
|
|
89
|
+
action: 'set',
|
|
90
|
+
password: newPassword.value,
|
|
91
|
+
})
|
|
92
|
+
newPassword.value = ''
|
|
93
|
+
notice.value = 'Password updated.'
|
|
94
|
+
} catch (e) {
|
|
95
|
+
error.value = e instanceof Error ? e.message : 'Could not set password'
|
|
96
|
+
} finally {
|
|
97
|
+
settingPassword.value = false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<div>
|
|
104
|
+
<div class="mb-6 flex items-center gap-3">
|
|
105
|
+
<a href="/admin/users" class="text-sm text-zinc-500 hover:text-zinc-800">← Users</a>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div v-if="loading" class="text-sm text-zinc-500">Loading…</div>
|
|
109
|
+
|
|
110
|
+
<div v-else class="max-w-xl space-y-6">
|
|
111
|
+
<h1 class="text-2xl font-semibold">Edit user</h1>
|
|
112
|
+
|
|
113
|
+
<div class="space-y-4 rounded-xl border border-zinc-200 bg-white p-4">
|
|
114
|
+
<label class="block">
|
|
115
|
+
<span class="text-sm font-medium text-zinc-700">Email</span>
|
|
116
|
+
<input :value="email" disabled class="mt-1 w-full rounded-lg border border-zinc-300 bg-zinc-50 px-3 py-2 text-sm" />
|
|
117
|
+
</label>
|
|
118
|
+
<label class="block">
|
|
119
|
+
<span class="text-sm font-medium text-zinc-700">Name</span>
|
|
120
|
+
<input v-model="form.name" required class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm" />
|
|
121
|
+
</label>
|
|
122
|
+
<label class="block">
|
|
123
|
+
<span class="text-sm font-medium text-zinc-700">Display name</span>
|
|
124
|
+
<input v-model="form.displayName" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm" />
|
|
125
|
+
<span class="mt-1 block text-xs text-zinc-500">Optional public-facing name.</span>
|
|
126
|
+
</label>
|
|
127
|
+
<label class="block">
|
|
128
|
+
<span class="text-sm font-medium text-zinc-700">Role</span>
|
|
129
|
+
<select v-model="form.role" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm">
|
|
130
|
+
<option value="admin">admin</option>
|
|
131
|
+
<option value="editor">editor</option>
|
|
132
|
+
<option value="member">member</option>
|
|
133
|
+
</select>
|
|
134
|
+
</label>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="space-y-4 rounded-xl border border-zinc-200 bg-white p-4">
|
|
138
|
+
<h2 class="text-sm font-semibold text-zinc-700">Password</h2>
|
|
139
|
+
<p class="text-sm text-zinc-500">Send a reset link to the user's email, or set a new password directly.</p>
|
|
140
|
+
<div class="flex flex-wrap gap-2">
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
class="rounded-lg border border-zinc-300 px-4 py-2 text-sm"
|
|
144
|
+
:disabled="resetSending"
|
|
145
|
+
@click="sendResetEmail"
|
|
146
|
+
>
|
|
147
|
+
{{ resetSending ? 'Sending…' : 'Send reset email' }}
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="flex flex-wrap items-end gap-2">
|
|
151
|
+
<label class="block flex-1 min-w-[12rem]">
|
|
152
|
+
<span class="text-sm font-medium text-zinc-700">Set new password</span>
|
|
153
|
+
<input
|
|
154
|
+
v-model="newPassword"
|
|
155
|
+
type="password"
|
|
156
|
+
minlength="8"
|
|
157
|
+
autocomplete="new-password"
|
|
158
|
+
class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm"
|
|
159
|
+
placeholder="At least 8 characters"
|
|
160
|
+
/>
|
|
161
|
+
</label>
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
class="rounded-lg border border-zinc-300 px-4 py-2 text-sm"
|
|
165
|
+
:disabled="settingPassword || !newPassword"
|
|
166
|
+
@click="setPassword"
|
|
167
|
+
>
|
|
168
|
+
{{ settingPassword ? 'Updating…' : 'Set password' }}
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div v-if="error" class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700">{{ error }}</div>
|
|
174
|
+
<div v-if="notice" class="rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">{{ notice }}</div>
|
|
175
|
+
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
class="vulse-button-primary rounded-lg px-4 py-2 text-sm font-medium"
|
|
179
|
+
:disabled="saving"
|
|
180
|
+
@click="save"
|
|
181
|
+
>
|
|
182
|
+
{{ saving ? 'Saving…' : 'Save changes' }}
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue'
|
|
3
|
+
import { adminApi } from '../client/api'
|
|
4
|
+
|
|
5
|
+
const users = ref<{ id: string; email: string; name: string; role: string }[]>([])
|
|
6
|
+
|
|
7
|
+
onMounted(async () => { users.value = await adminApi.get('/api/vulse/users') })
|
|
8
|
+
|
|
9
|
+
async function setRole(id: string, role: string) {
|
|
10
|
+
await adminApi.post(`/api/vulse/users/${id}/role`, { role })
|
|
11
|
+
users.value = users.value.map((u) => (u.id === id ? { ...u, role } : u))
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<table class="w-full bg-white border rounded text-sm">
|
|
17
|
+
<thead>
|
|
18
|
+
<tr class="border-b text-left">
|
|
19
|
+
<th class="p-3">Email</th>
|
|
20
|
+
<th class="p-3">Name</th>
|
|
21
|
+
<th class="p-3">Role</th>
|
|
22
|
+
<th class="p-3 w-24" />
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<tr v-for="u in users" :key="u.id" class="border-b">
|
|
27
|
+
<td class="p-3">{{ u.email }}</td>
|
|
28
|
+
<td class="p-3">{{ u.name }}</td>
|
|
29
|
+
<td class="p-3">
|
|
30
|
+
<select
|
|
31
|
+
:value="u.role"
|
|
32
|
+
class="rounded border px-2 py-1"
|
|
33
|
+
@change="setRole(u.id, ($event.target as HTMLSelectElement).value)"
|
|
34
|
+
>
|
|
35
|
+
<option>admin</option>
|
|
36
|
+
<option>editor</option>
|
|
37
|
+
<option>member</option>
|
|
38
|
+
</select>
|
|
39
|
+
</td>
|
|
40
|
+
<td class="p-3 text-right">
|
|
41
|
+
<a :href="`/admin/users/${u.id}`" class="text-brand hover:underline">Edit</a>
|
|
42
|
+
</td>
|
|
43
|
+
</tr>
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '../../../core/blocks/schema'
|
|
3
|
+
import HeadingEdit from './edit/HeadingEdit.vue'
|
|
4
|
+
import ParagraphEdit from './edit/ParagraphEdit.vue'
|
|
5
|
+
import ImageEdit from './edit/ImageEdit.vue'
|
|
6
|
+
import CodeEdit from './edit/CodeEdit.vue'
|
|
7
|
+
import EmbedEdit from './edit/EmbedEdit.vue'
|
|
8
|
+
import QuoteEdit from './edit/QuoteEdit.vue'
|
|
9
|
+
import ListEdit from './edit/ListEdit.vue'
|
|
10
|
+
|
|
11
|
+
defineProps<{ block: Block; index: number; total: number }>()
|
|
12
|
+
defineEmits<{ (e: 'update', b: Block): void; (e: 'remove'): void; (e: 'move', dir: -1 | 1): void }>()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div class="p-4 flex gap-3 group">
|
|
17
|
+
<div class="flex flex-col gap-1 text-zinc-400">
|
|
18
|
+
<button type="button" @click="$emit('move', -1)" :disabled="index === 0" class="text-sm">↑</button>
|
|
19
|
+
<button type="button" @click="$emit('move', 1)" :disabled="index === total - 1" class="text-sm">↓</button>
|
|
20
|
+
<button type="button" @click="$emit('remove')" class="text-sm text-red-600 opacity-0 group-hover:opacity-100">×</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="flex-1">
|
|
23
|
+
<HeadingEdit v-if="block.type === 'heading'" :model-value="block" @update:modelValue="$emit('update', $event)" />
|
|
24
|
+
<ParagraphEdit v-else-if="block.type === 'paragraph'" :model-value="block" @update:modelValue="$emit('update', $event)" />
|
|
25
|
+
<ImageEdit v-else-if="block.type === 'image'" :model-value="block" @update:modelValue="$emit('update', $event)" />
|
|
26
|
+
<CodeEdit v-else-if="block.type === 'code'" :model-value="block" @update:modelValue="$emit('update', $event)" />
|
|
27
|
+
<EmbedEdit v-else-if="block.type === 'embed'" :model-value="block" @update:modelValue="$emit('update', $event)" />
|
|
28
|
+
<QuoteEdit v-else-if="block.type === 'quote'" :model-value="block" @update:modelValue="$emit('update', $event)" />
|
|
29
|
+
<ListEdit v-else-if="block.type === 'list'" :model-value="block" @update:modelValue="$emit('update', $event)" />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BlockType } from '../../../core/blocks/schema'
|
|
3
|
+
import { BUILT_IN_BLOCK_TYPES } from '../../../core/blocks/schema'
|
|
4
|
+
defineEmits<{ (e: 'add', t: BlockType): void }>()
|
|
5
|
+
</script>
|
|
6
|
+
<template>
|
|
7
|
+
<div class="p-2 flex flex-wrap gap-1 border-t bg-zinc-50">
|
|
8
|
+
<button v-for="t in BUILT_IN_BLOCK_TYPES" :key="t" type="button"
|
|
9
|
+
@click="$emit('add', t)"
|
|
10
|
+
class="px-3 py-1 rounded border bg-white text-sm hover:bg-zinc-100">+ {{ t }}</button>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { z } from 'astro/zod'
|
|
3
|
+
import { codeBlock } from '../../../../core/blocks/schema'
|
|
4
|
+
type Block = z.infer<typeof codeBlock>
|
|
5
|
+
const props = defineProps<{ modelValue: Block }>()
|
|
6
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: Block): void }>()
|
|
7
|
+
function update<K extends keyof Block>(k: K, v: Block[K]) {
|
|
8
|
+
emit('update:modelValue', { ...props.modelValue, [k]: v })
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<div class="space-y-2">
|
|
13
|
+
<input :value="modelValue.language" @input="update('language', ($event.target as HTMLInputElement).value)"
|
|
14
|
+
placeholder="Language" class="w-full rounded border px-3 py-2 text-sm" />
|
|
15
|
+
<textarea :value="modelValue.code" @input="update('code', ($event.target as HTMLTextAreaElement).value)"
|
|
16
|
+
rows="6" placeholder="Code…" class="w-full rounded border px-3 py-2 font-mono text-sm"></textarea>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { z } from 'astro/zod'
|
|
3
|
+
import { embedBlock } from '../../../../core/blocks/schema'
|
|
4
|
+
type Block = z.infer<typeof embedBlock>
|
|
5
|
+
const props = defineProps<{ modelValue: Block }>()
|
|
6
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: Block): void }>()
|
|
7
|
+
function update<K extends keyof Block>(k: K, v: Block[K]) {
|
|
8
|
+
emit('update:modelValue', { ...props.modelValue, [k]: v })
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<input :value="modelValue.url" @input="update('url', ($event.target as HTMLInputElement).value)"
|
|
13
|
+
type="url" placeholder="https://…" class="w-full rounded border px-3 py-2" />
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { z } from 'astro/zod'
|
|
3
|
+
import { headingBlock } from '../../../../core/blocks/schema'
|
|
4
|
+
type Block = z.infer<typeof headingBlock>
|
|
5
|
+
const props = defineProps<{ modelValue: Block }>()
|
|
6
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: Block): void }>()
|
|
7
|
+
function update<K extends keyof Block>(k: K, v: Block[K]) {
|
|
8
|
+
emit('update:modelValue', { ...props.modelValue, [k]: v })
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<div class="space-y-2">
|
|
13
|
+
<select :value="modelValue.level" @change="update('level', Number(($event.target as HTMLSelectElement).value) as Block['level'])" class="rounded border px-2 py-1 text-sm">
|
|
14
|
+
<option :value="1">H1</option><option :value="2">H2</option><option :value="3">H3</option><option :value="4">H4</option>
|
|
15
|
+
</select>
|
|
16
|
+
<input :value="modelValue.text" @input="update('text', ($event.target as HTMLInputElement).value)"
|
|
17
|
+
placeholder="Heading…" class="w-full rounded border px-3 py-2" />
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { z } from 'astro/zod'
|
|
3
|
+
import { imageBlock } from '../../../../core/blocks/schema'
|
|
4
|
+
import MediaField from '../../fields/MediaField.vue'
|
|
5
|
+
|
|
6
|
+
type Block = z.infer<typeof imageBlock>
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ modelValue: Block }>()
|
|
9
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: Block): void }>()
|
|
10
|
+
|
|
11
|
+
function update<K extends keyof Block>(k: K, v: Block[K]) {
|
|
12
|
+
emit('update:modelValue', { ...props.modelValue, [k]: v })
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div class="space-y-2">
|
|
18
|
+
<MediaField
|
|
19
|
+
:model-value="modelValue.mediaId || null"
|
|
20
|
+
label="Image"
|
|
21
|
+
@update:modelValue="update('mediaId', $event ?? '')"
|
|
22
|
+
/>
|
|
23
|
+
<label class="block">
|
|
24
|
+
<span class="vulse-label">Alt text</span>
|
|
25
|
+
<input
|
|
26
|
+
:value="modelValue.alt"
|
|
27
|
+
class="vulse-input mt-1"
|
|
28
|
+
@input="update('alt', ($event.target as HTMLInputElement).value)"
|
|
29
|
+
/>
|
|
30
|
+
</label>
|
|
31
|
+
<label class="block">
|
|
32
|
+
<span class="vulse-label">Caption</span>
|
|
33
|
+
<input
|
|
34
|
+
:value="modelValue.caption ?? ''"
|
|
35
|
+
class="vulse-input mt-1"
|
|
36
|
+
@input="update('caption', ($event.target as HTMLInputElement).value)"
|
|
37
|
+
/>
|
|
38
|
+
</label>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { z } from 'astro/zod'
|
|
3
|
+
import { listBlock } from '../../../../core/blocks/schema'
|
|
4
|
+
type Block = z.infer<typeof listBlock>
|
|
5
|
+
const props = defineProps<{ modelValue: Block }>()
|
|
6
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: Block): void }>()
|
|
7
|
+
function updateItems(items: string[]) {
|
|
8
|
+
emit('update:modelValue', { ...props.modelValue, items })
|
|
9
|
+
}
|
|
10
|
+
function updateItem(i: number, v: string) {
|
|
11
|
+
const next = [...props.modelValue.items]
|
|
12
|
+
next[i] = v
|
|
13
|
+
updateItems(next)
|
|
14
|
+
}
|
|
15
|
+
function addItem() { updateItems([...props.modelValue.items, '']) }
|
|
16
|
+
function removeItem(i: number) {
|
|
17
|
+
const next = [...props.modelValue.items]
|
|
18
|
+
next.splice(i, 1)
|
|
19
|
+
updateItems(next.length ? next : [''])
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
<template>
|
|
23
|
+
<div class="space-y-2">
|
|
24
|
+
<label class="flex items-center gap-2 text-sm">
|
|
25
|
+
<input type="checkbox" :checked="modelValue.ordered"
|
|
26
|
+
@change="emit('update:modelValue', { ...modelValue, ordered: ($event.target as HTMLInputElement).checked })" />
|
|
27
|
+
Ordered list
|
|
28
|
+
</label>
|
|
29
|
+
<div v-for="(item, i) in modelValue.items" :key="i" class="flex gap-2">
|
|
30
|
+
<input :value="item" @input="updateItem(i, ($event.target as HTMLInputElement).value)"
|
|
31
|
+
class="flex-1 rounded border px-3 py-2" />
|
|
32
|
+
<button type="button" @click="removeItem(i)" class="text-sm text-red-600">×</button>
|
|
33
|
+
</div>
|
|
34
|
+
<button type="button" @click="addItem" class="text-sm rounded border px-3 py-1">Add item</button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { z } from 'astro/zod'
|
|
3
|
+
import { paragraphBlock } from '../../../../core/blocks/schema'
|
|
4
|
+
type Block = z.infer<typeof paragraphBlock>
|
|
5
|
+
const props = defineProps<{ modelValue: Block }>()
|
|
6
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: Block): void }>()
|
|
7
|
+
function update<K extends keyof Block>(k: K, v: Block[K]) {
|
|
8
|
+
emit('update:modelValue', { ...props.modelValue, [k]: v })
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<textarea :value="modelValue.text" @input="update('text', ($event.target as HTMLTextAreaElement).value)"
|
|
13
|
+
rows="4" placeholder="Paragraph…" class="w-full rounded border px-3 py-2"></textarea>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { z } from 'astro/zod'
|
|
3
|
+
import { quoteBlock } from '../../../../core/blocks/schema'
|
|
4
|
+
type Block = z.infer<typeof quoteBlock>
|
|
5
|
+
const props = defineProps<{ modelValue: Block }>()
|
|
6
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: Block): void }>()
|
|
7
|
+
function update<K extends keyof Block>(k: K, v: Block[K]) {
|
|
8
|
+
emit('update:modelValue', { ...props.modelValue, [k]: v })
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<div class="space-y-2">
|
|
13
|
+
<textarea :value="modelValue.text" @input="update('text', ($event.target as HTMLTextAreaElement).value)"
|
|
14
|
+
rows="3" placeholder="Quote…" class="w-full rounded border px-3 py-2"></textarea>
|
|
15
|
+
<input :value="modelValue.cite ?? ''" @input="update('cite', ($event.target as HTMLInputElement).value)"
|
|
16
|
+
placeholder="Citation" class="w-full rounded border px-3 py-2 text-sm" />
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|