@xleddyl/nuxt-cms 0.1.9
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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/module.d.mts +36 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +765 -0
- package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +15 -0
- package/dist/runtime/app/components/cms/BlocksField.vue +110 -0
- package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +15 -0
- package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +18 -0
- package/dist/runtime/app/components/cms/EmptyState.vue +20 -0
- package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +18 -0
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +23 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +78 -0
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +23 -0
- package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +85 -0
- package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/JsonField.d.vue.ts +10 -0
- package/dist/runtime/app/components/cms/JsonField.vue +51 -0
- package/dist/runtime/app/components/cms/JsonField.vue.d.ts +10 -0
- package/dist/runtime/app/components/cms/MediaField.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/MediaField.vue +81 -0
- package/dist/runtime/app/components/cms/MediaField.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +19 -0
- package/dist/runtime/app/components/cms/MediaGallery.vue +309 -0
- package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +19 -0
- package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +14 -0
- package/dist/runtime/app/components/cms/MediaUpload.vue +132 -0
- package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +14 -0
- package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +19 -0
- package/dist/runtime/app/components/cms/PageHeader.vue +23 -0
- package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +19 -0
- package/dist/runtime/app/components/cms/RelationField.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/RelationField.vue +87 -0
- package/dist/runtime/app/components/cms/RelationField.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/RichTextField.d.vue.ts +10 -0
- package/dist/runtime/app/components/cms/RichTextField.vue +123 -0
- package/dist/runtime/app/components/cms/RichTextField.vue.d.ts +10 -0
- package/dist/runtime/app/components/cms/SlugField.d.vue.ts +14 -0
- package/dist/runtime/app/components/cms/SlugField.vue +44 -0
- package/dist/runtime/app/components/cms/SlugField.vue.d.ts +14 -0
- package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +15 -0
- package/dist/runtime/app/components/cms/TranslatableField.vue +64 -0
- package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +15 -0
- package/dist/runtime/app/composables/cms-query.d.ts +4 -0
- package/dist/runtime/app/composables/cms-query.js +18 -0
- package/dist/runtime/app/composables/cms-runtime.d.ts +5 -0
- package/dist/runtime/app/composables/cms-runtime.js +4 -0
- package/dist/runtime/app/i18n/en.json +93 -0
- package/dist/runtime/app/i18n/it.json +93 -0
- package/dist/runtime/app/layouts/cms-admin.d.vue.ts +13 -0
- package/dist/runtime/app/layouts/cms-admin.vue +97 -0
- package/dist/runtime/app/layouts/cms-admin.vue.d.ts +13 -0
- package/dist/runtime/app/middleware/cms-auth.d.ts +2 -0
- package/dist/runtime/app/middleware/cms-auth.js +7 -0
- package/dist/runtime/app/pages/admin-collection.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-collection.vue +273 -0
- package/dist/runtime/app/pages/admin-collection.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-entry.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-entry.vue +156 -0
- package/dist/runtime/app/pages/admin-entry.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-index.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-index.vue +11 -0
- package/dist/runtime/app/pages/admin-index.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-login.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-login.vue +87 -0
- package/dist/runtime/app/pages/admin-login.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-media.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-media.vue +13 -0
- package/dist/runtime/app/pages/admin-media.vue.d.ts +3 -0
- package/dist/runtime/app/utils/api.d.ts +7 -0
- package/dist/runtime/app/utils/api.js +3 -0
- package/dist/runtime/app/utils/ui.d.ts +8 -0
- package/dist/runtime/app/utils/ui.js +9 -0
- package/dist/runtime/assets/main.css +1 -0
- package/dist/runtime/server/api/collection.get.d.ts +6 -0
- package/dist/runtime/server/api/collection.get.js +19 -0
- package/dist/runtime/server/api/collection.post.d.ts +2 -0
- package/dist/runtime/server/api/collection.post.js +30 -0
- package/dist/runtime/server/api/collection.put.d.ts +2 -0
- package/dist/runtime/server/api/collection.put.js +28 -0
- package/dist/runtime/server/api/item.delete.d.ts +4 -0
- package/dist/runtime/server/api/item.delete.js +19 -0
- package/dist/runtime/server/api/item.get.d.ts +2 -0
- package/dist/runtime/server/api/item.get.js +18 -0
- package/dist/runtime/server/api/item.put.d.ts +2 -0
- package/dist/runtime/server/api/item.put.js +40 -0
- package/dist/runtime/server/api/media-presign.post.d.ts +11 -0
- package/dist/runtime/server/api/media-presign.post.js +51 -0
- package/dist/runtime/server/api/media.delete.d.ts +4 -0
- package/dist/runtime/server/api/media.delete.js +25 -0
- package/dist/runtime/server/api/media.get.d.ts +5 -0
- package/dist/runtime/server/api/media.get.js +13 -0
- package/dist/runtime/server/api/media.post.d.ts +3 -0
- package/dist/runtime/server/api/media.post.js +32 -0
- package/dist/runtime/server/api/media.put.d.ts +3 -0
- package/dist/runtime/server/api/media.put.js +28 -0
- package/dist/runtime/server/plugins/migrate-postgres.d.ts +2 -0
- package/dist/runtime/server/plugins/migrate-postgres.js +9 -0
- package/dist/runtime/server/plugins/migrate-sqlite.d.ts +2 -0
- package/dist/runtime/server/plugins/migrate-sqlite.js +9 -0
- package/dist/runtime/server/plugins/session-check.d.ts +2 -0
- package/dist/runtime/server/plugins/session-check.js +9 -0
- package/dist/runtime/server/routes/auth/login.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/login.post.js +62 -0
- package/dist/runtime/server/routes/graphql.d.ts +2 -0
- package/dist/runtime/server/routes/graphql.js +34 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/custom-id.d.ts +2 -0
- package/dist/runtime/server/utils/custom-id.js +10 -0
- package/dist/runtime/server/utils/db-errors.d.ts +1 -0
- package/dist/runtime/server/utils/db-errors.js +31 -0
- package/dist/runtime/server/utils/db-postgres.d.ts +3 -0
- package/dist/runtime/server/utils/db-postgres.js +16 -0
- package/dist/runtime/server/utils/db-sqlite.d.ts +4 -0
- package/dist/runtime/server/utils/db-sqlite.js +17 -0
- package/dist/runtime/server/utils/graphql-depth.d.ts +4 -0
- package/dist/runtime/server/utils/graphql-depth.js +72 -0
- package/dist/runtime/server/utils/graphql.d.ts +14 -0
- package/dist/runtime/server/utils/graphql.js +317 -0
- package/dist/runtime/server/utils/media.d.ts +24 -0
- package/dist/runtime/server/utils/media.js +38 -0
- package/dist/runtime/server/utils/registry.d.ts +20 -0
- package/dist/runtime/server/utils/registry.js +47 -0
- package/dist/runtime/server/utils/relations.d.ts +12 -0
- package/dist/runtime/server/utils/relations.js +80 -0
- package/dist/runtime/server/utils/require-admin.d.ts +2 -0
- package/dist/runtime/server/utils/require-admin.js +13 -0
- package/dist/runtime/shared/empty-config.d.ts +2 -0
- package/dist/runtime/shared/empty-config.js +1 -0
- package/dist/runtime/shared/graphql-sdl.d.ts +5 -0
- package/dist/runtime/shared/graphql-sdl.js +147 -0
- package/dist/runtime/shared/index.d.ts +62 -0
- package/dist/runtime/shared/index.js +55 -0
- package/dist/runtime/shared/validation.d.ts +16 -0
- package/dist/runtime/shared/validation.js +82 -0
- package/dist/types.d.mts +3 -0
- package/package.json +93 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-rise flex flex-col gap-7">
|
|
3
|
+
<CmsPageHeader :kicker="kicker" :title="config.label">
|
|
4
|
+
<UButton
|
|
5
|
+
v-if="config.kind === 'single'"
|
|
6
|
+
type="submit"
|
|
7
|
+
:form="FORM_ID"
|
|
8
|
+
:label="t('cms.form.save')"
|
|
9
|
+
:loading="saving"
|
|
10
|
+
class="rounded-full px-6"
|
|
11
|
+
/>
|
|
12
|
+
<div v-if="config.kind === 'collection'" class="flex items-center gap-2">
|
|
13
|
+
<UDropdownMenu
|
|
14
|
+
v-if="rows.length"
|
|
15
|
+
:items="columnItems"
|
|
16
|
+
:content="{ align: 'end' }"
|
|
17
|
+
:ui="{ content: 'w-48' }"
|
|
18
|
+
>
|
|
19
|
+
<UButton
|
|
20
|
+
:label="t('cms.collection.columns')"
|
|
21
|
+
icon="i-lucide-columns-3"
|
|
22
|
+
variant="subtle"
|
|
23
|
+
class="rounded-full px-4"
|
|
24
|
+
/>
|
|
25
|
+
</UDropdownMenu>
|
|
26
|
+
<UButton
|
|
27
|
+
:label="t('cms.collection.new')"
|
|
28
|
+
icon="i-lucide-plus"
|
|
29
|
+
class="rounded-full px-4"
|
|
30
|
+
@click="openCreate"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
</CmsPageHeader>
|
|
34
|
+
|
|
35
|
+
<div v-if="config.kind === 'single'" class="cms-card p-7">
|
|
36
|
+
<CmsEntryForm
|
|
37
|
+
v-model="formState"
|
|
38
|
+
:fields="config.fields"
|
|
39
|
+
:form-id="FORM_ID"
|
|
40
|
+
:loading="saving"
|
|
41
|
+
@submit="saveSingle"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<template v-else>
|
|
46
|
+
<div v-if="rows.length" class="cms-card overflow-hidden">
|
|
47
|
+
<UTable
|
|
48
|
+
v-model:column-visibility="columnVisibility"
|
|
49
|
+
:data="rows"
|
|
50
|
+
:columns="columns"
|
|
51
|
+
:ui="tableUi"
|
|
52
|
+
@select="onSelect"
|
|
53
|
+
>
|
|
54
|
+
<template v-if="drafts" #status-cell="{ row }">
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
class="cms-badge"
|
|
58
|
+
:class="row.original.status === 'published' ? 'is-published' : 'is-draft'"
|
|
59
|
+
:title="
|
|
60
|
+
row.original.status === 'published' ? t('cms.status.unpublish') : t('cms.status.publish')
|
|
61
|
+
"
|
|
62
|
+
@click.stop="toggleStatus(row.original)"
|
|
63
|
+
>
|
|
64
|
+
{{
|
|
65
|
+
row.original.status === "published" ? t("cms.status.published") : t("cms.status.draft")
|
|
66
|
+
}}
|
|
67
|
+
</button>
|
|
68
|
+
</template>
|
|
69
|
+
<template #actions-cell="{ row }">
|
|
70
|
+
<div class="flex justify-end">
|
|
71
|
+
<UButton
|
|
72
|
+
icon="i-lucide-trash-2"
|
|
73
|
+
variant="ghost"
|
|
74
|
+
color="error"
|
|
75
|
+
size="xs"
|
|
76
|
+
class="rounded-full"
|
|
77
|
+
@click.stop="deleteRow(row.original)"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
</UTable>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<CmsEmptyState
|
|
85
|
+
v-else
|
|
86
|
+
icon="i-lucide-sprout"
|
|
87
|
+
:title="t('cms.collection.emptyTitle')"
|
|
88
|
+
:body="t('cms.collection.emptyBody')"
|
|
89
|
+
>
|
|
90
|
+
<UButton
|
|
91
|
+
:label="t('cms.collection.new')"
|
|
92
|
+
icon="i-lucide-plus"
|
|
93
|
+
variant="subtle"
|
|
94
|
+
class="mt-3 rounded-full px-4"
|
|
95
|
+
@click="openCreate"
|
|
96
|
+
/>
|
|
97
|
+
</CmsEmptyState>
|
|
98
|
+
</template>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup>
|
|
103
|
+
import { isTranslatableField } from "#nuxt-cms";
|
|
104
|
+
import {
|
|
105
|
+
computed,
|
|
106
|
+
createError,
|
|
107
|
+
definePageMeta,
|
|
108
|
+
navigateTo,
|
|
109
|
+
onMounted,
|
|
110
|
+
ref,
|
|
111
|
+
useFetch,
|
|
112
|
+
useI18n,
|
|
113
|
+
useRoute,
|
|
114
|
+
useToast,
|
|
115
|
+
watch
|
|
116
|
+
} from "#imports";
|
|
117
|
+
import cmsConfig from "#cms-config";
|
|
118
|
+
import { useCmsRuntime } from "../composables/cms-runtime";
|
|
119
|
+
import { cmsApi } from "../utils/api";
|
|
120
|
+
import { errorMessage } from "../utils/ui";
|
|
121
|
+
definePageMeta({
|
|
122
|
+
layout: "cms-admin",
|
|
123
|
+
middleware: "cms-auth",
|
|
124
|
+
validate: (route2) => Object.hasOwn(cmsConfig, route2.params.collection),
|
|
125
|
+
key: (route2) => route2.fullPath
|
|
126
|
+
});
|
|
127
|
+
const FORM_ID = "cms-single-form";
|
|
128
|
+
const route = useRoute();
|
|
129
|
+
const toast = useToast();
|
|
130
|
+
const { t } = useI18n();
|
|
131
|
+
const name = route.params.collection;
|
|
132
|
+
const config = cmsConfig[name];
|
|
133
|
+
if (!config) {
|
|
134
|
+
throw createError({ statusCode: 404, statusMessage: "Unknown collection", fatal: true });
|
|
135
|
+
}
|
|
136
|
+
const fieldKeys = Object.keys(config.fields);
|
|
137
|
+
const drafts = config.kind === "collection" && !!config.drafts;
|
|
138
|
+
const formKeys = drafts ? [...fieldKeys, "status"] : fieldKeys;
|
|
139
|
+
const endpoint = `/api/cms/admin/${name}`;
|
|
140
|
+
const { data, refresh } = await useFetch(endpoint);
|
|
141
|
+
const rows = computed(() => Array.isArray(data.value) ? data.value : []);
|
|
142
|
+
const tableUi = {
|
|
143
|
+
thead: "border-b border-(--cms-line)",
|
|
144
|
+
th: "cms-kicker px-5 py-3 font-normal",
|
|
145
|
+
td: "px-5 py-3.5 text-sm text-(--ui-text)",
|
|
146
|
+
tr: "__clickable hover:bg-(--cms-paper)/50"
|
|
147
|
+
};
|
|
148
|
+
const saving = ref(false);
|
|
149
|
+
const formState = ref({});
|
|
150
|
+
function emptyState() {
|
|
151
|
+
return Object.fromEntries(formKeys.map((k) => [k, null]));
|
|
152
|
+
}
|
|
153
|
+
function pickFields(row) {
|
|
154
|
+
return Object.fromEntries(formKeys.map((k) => [k, row[k] ?? null]));
|
|
155
|
+
}
|
|
156
|
+
async function submit(action) {
|
|
157
|
+
saving.value = true;
|
|
158
|
+
try {
|
|
159
|
+
await action();
|
|
160
|
+
await refresh();
|
|
161
|
+
return true;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
toast.add({
|
|
164
|
+
title: t("cms.toast.saveFailed"),
|
|
165
|
+
description: errorMessage(error),
|
|
166
|
+
color: "error"
|
|
167
|
+
});
|
|
168
|
+
return false;
|
|
169
|
+
} finally {
|
|
170
|
+
saving.value = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (config.kind === "single") {
|
|
174
|
+
formState.value = data.value && !Array.isArray(data.value) ? pickFields(data.value) : emptyState();
|
|
175
|
+
}
|
|
176
|
+
async function saveSingle() {
|
|
177
|
+
const ok = await submit(() => cmsApi(endpoint, { method: "PUT", body: formState.value }));
|
|
178
|
+
if (ok) toast.add({ title: t("cms.toast.saved"), color: "success" });
|
|
179
|
+
}
|
|
180
|
+
const { i18n: contentI18n } = useCmsRuntime();
|
|
181
|
+
function stripHtml(html) {
|
|
182
|
+
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
183
|
+
}
|
|
184
|
+
function truncate(value) {
|
|
185
|
+
return value.length > 60 ? `${value.slice(0, 57)}\u2026` : value;
|
|
186
|
+
}
|
|
187
|
+
function displayValue(field, value) {
|
|
188
|
+
if (value == null || value === "") return "";
|
|
189
|
+
if (isTranslatableField(field)) {
|
|
190
|
+
const record = value;
|
|
191
|
+
const raw = record[contentI18n.defaultLocale] ?? Object.values(record)[0] ?? "";
|
|
192
|
+
return truncate(field.type === "richtext" ? stripHtml(raw) : raw);
|
|
193
|
+
}
|
|
194
|
+
switch (field.type) {
|
|
195
|
+
case "boolean":
|
|
196
|
+
return value ? "\u2713" : "\u2014";
|
|
197
|
+
case "richtext":
|
|
198
|
+
return truncate(stripHtml(String(value)));
|
|
199
|
+
case "json":
|
|
200
|
+
return truncate(JSON.stringify(value));
|
|
201
|
+
case "blocks":
|
|
202
|
+
return `${value.length} \u25A4`;
|
|
203
|
+
default:
|
|
204
|
+
return truncate(String(value));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const columns = computed(() => [
|
|
208
|
+
...fieldKeys.map((key) => ({
|
|
209
|
+
id: key,
|
|
210
|
+
accessorFn: (row) => displayValue(config.fields[key], row[key]),
|
|
211
|
+
header: config.fields[key].label
|
|
212
|
+
})),
|
|
213
|
+
...drafts ? [{ accessorKey: "status", header: t("cms.collection.status") }] : [],
|
|
214
|
+
{ id: "actions", header: "" }
|
|
215
|
+
]);
|
|
216
|
+
const DEFAULT_VISIBLE_COLUMNS = 4;
|
|
217
|
+
const columnStorageKey = `cms:columns:${name}`;
|
|
218
|
+
const columnVisibility = ref(
|
|
219
|
+
Object.fromEntries(fieldKeys.map((key, index) => [key, index < DEFAULT_VISIBLE_COLUMNS]))
|
|
220
|
+
);
|
|
221
|
+
onMounted(() => {
|
|
222
|
+
const stored = localStorage.getItem(columnStorageKey);
|
|
223
|
+
if (stored) {
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(stored);
|
|
226
|
+
columnVisibility.value = Object.fromEntries(
|
|
227
|
+
fieldKeys.map((key) => [key, parsed[key] ?? columnVisibility.value[key] ?? false])
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
watch(
|
|
233
|
+
columnVisibility,
|
|
234
|
+
(value) => localStorage.setItem(columnStorageKey, JSON.stringify(value)),
|
|
235
|
+
{ deep: true }
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
const columnItems = computed(
|
|
239
|
+
() => fieldKeys.map((key) => ({
|
|
240
|
+
label: config.fields[key].label,
|
|
241
|
+
type: "checkbox",
|
|
242
|
+
checked: columnVisibility.value[key],
|
|
243
|
+
onUpdateChecked(checked) {
|
|
244
|
+
columnVisibility.value = { ...columnVisibility.value, [key]: checked };
|
|
245
|
+
},
|
|
246
|
+
onSelect(event) {
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
}
|
|
249
|
+
}))
|
|
250
|
+
);
|
|
251
|
+
const kicker = computed(
|
|
252
|
+
() => config.kind === "single" ? t("cms.collection.kickerSingle") : t("cms.collection.kicker", rows.value.length)
|
|
253
|
+
);
|
|
254
|
+
function onSelect(_event, row) {
|
|
255
|
+
navigateTo(`/cms/${name}/${row.original.id}`);
|
|
256
|
+
}
|
|
257
|
+
async function toggleStatus(row) {
|
|
258
|
+
const next = row.status === "published" ? "draft" : "published";
|
|
259
|
+
await submit(
|
|
260
|
+
() => cmsApi(`${endpoint}/${row.id}`, {
|
|
261
|
+
method: "PUT",
|
|
262
|
+
body: { ...pickFields(row), status: next }
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
function openCreate() {
|
|
267
|
+
navigateTo(`/cms/${name}/new`);
|
|
268
|
+
}
|
|
269
|
+
async function deleteRow(row) {
|
|
270
|
+
if (!confirm(t("cms.collection.deleteConfirm"))) return;
|
|
271
|
+
await submit(() => cmsApi(`${endpoint}/${row.id}`, { method: "DELETE" }));
|
|
272
|
+
}
|
|
273
|
+
</script>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-rise flex flex-col gap-7">
|
|
3
|
+
<CmsPageHeader
|
|
4
|
+
:kicker="config.label"
|
|
5
|
+
:title="isNew ? t('cms.collection.new') : t('cms.collection.edit')"
|
|
6
|
+
>
|
|
7
|
+
<template v-if="drafts" #badge>
|
|
8
|
+
<span
|
|
9
|
+
class="cms-badge pointer-events-none"
|
|
10
|
+
:class="published ? 'is-published' : 'is-draft'"
|
|
11
|
+
>
|
|
12
|
+
{{ published ? t("cms.status.published") : t("cms.status.draft") }}
|
|
13
|
+
</span>
|
|
14
|
+
</template>
|
|
15
|
+
<div class="flex items-center gap-3">
|
|
16
|
+
<UButton
|
|
17
|
+
:label="t('cms.collection.back')"
|
|
18
|
+
icon="i-lucide-arrow-left"
|
|
19
|
+
variant="subtle"
|
|
20
|
+
class="rounded-full px-4"
|
|
21
|
+
@click="goBack"
|
|
22
|
+
/>
|
|
23
|
+
<UButton
|
|
24
|
+
type="submit"
|
|
25
|
+
:form="FORM_ID"
|
|
26
|
+
:label="t('cms.form.save')"
|
|
27
|
+
:loading="saving"
|
|
28
|
+
class="rounded-full px-6"
|
|
29
|
+
/>
|
|
30
|
+
<UButton
|
|
31
|
+
v-if="drafts"
|
|
32
|
+
type="submit"
|
|
33
|
+
:form="FORM_ID"
|
|
34
|
+
:label="published ? t('cms.status.unpublish') : t('cms.status.publish')"
|
|
35
|
+
:loading="saving"
|
|
36
|
+
class="rounded-full px-6"
|
|
37
|
+
@click="togglePublished"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
</CmsPageHeader>
|
|
41
|
+
|
|
42
|
+
<div class="cms-card p-7">
|
|
43
|
+
<CmsEntryForm
|
|
44
|
+
v-model="formState"
|
|
45
|
+
:fields="config.fields"
|
|
46
|
+
:drafts="drafts"
|
|
47
|
+
:form-id="FORM_ID"
|
|
48
|
+
:loading="saving"
|
|
49
|
+
@submit="save"
|
|
50
|
+
@error="revertStatus"
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup>
|
|
57
|
+
import {
|
|
58
|
+
computed,
|
|
59
|
+
createError,
|
|
60
|
+
definePageMeta,
|
|
61
|
+
navigateTo,
|
|
62
|
+
onBeforeRouteLeave,
|
|
63
|
+
onBeforeUnmount,
|
|
64
|
+
ref,
|
|
65
|
+
useFetch,
|
|
66
|
+
useI18n,
|
|
67
|
+
useRoute,
|
|
68
|
+
useToast,
|
|
69
|
+
watch
|
|
70
|
+
} from "#imports";
|
|
71
|
+
import cmsConfig from "#cms-config";
|
|
72
|
+
import { cmsApi } from "../utils/api";
|
|
73
|
+
import { errorMessage } from "../utils/ui";
|
|
74
|
+
const FORM_ID = "cms-entry-form";
|
|
75
|
+
definePageMeta({
|
|
76
|
+
layout: "cms-admin",
|
|
77
|
+
middleware: "cms-auth",
|
|
78
|
+
validate: (route2) => Object.hasOwn(cmsConfig, route2.params.collection),
|
|
79
|
+
key: (route2) => route2.fullPath
|
|
80
|
+
});
|
|
81
|
+
const route = useRoute();
|
|
82
|
+
const toast = useToast();
|
|
83
|
+
const { t } = useI18n();
|
|
84
|
+
const name = route.params.collection;
|
|
85
|
+
const config = cmsConfig[name];
|
|
86
|
+
if (!config || config.kind !== "collection") {
|
|
87
|
+
throw createError({ statusCode: 404, statusMessage: "Unknown collection", fatal: true });
|
|
88
|
+
}
|
|
89
|
+
const id = route.params.id;
|
|
90
|
+
const isNew = id === void 0;
|
|
91
|
+
const drafts = !!config.drafts;
|
|
92
|
+
const fieldKeys = Object.keys(config.fields);
|
|
93
|
+
const formKeys = drafts ? [...fieldKeys, "status"] : fieldKeys;
|
|
94
|
+
const endpoint = `/api/cms/admin/${name}`;
|
|
95
|
+
function emptyState() {
|
|
96
|
+
return Object.fromEntries(formKeys.map((k) => [k, null]));
|
|
97
|
+
}
|
|
98
|
+
function pickFields(row) {
|
|
99
|
+
return Object.fromEntries(formKeys.map((k) => [k, row[k] ?? null]));
|
|
100
|
+
}
|
|
101
|
+
const formState = ref(emptyState());
|
|
102
|
+
if (!isNew) {
|
|
103
|
+
const { data } = await useFetch(`${endpoint}/${id}`);
|
|
104
|
+
if (!data.value) {
|
|
105
|
+
throw createError({ statusCode: 404, statusMessage: "Entry not found", fatal: true });
|
|
106
|
+
}
|
|
107
|
+
formState.value = pickFields(data.value);
|
|
108
|
+
}
|
|
109
|
+
const saving = ref(false);
|
|
110
|
+
const snapshot = ref(JSON.stringify(formState.value));
|
|
111
|
+
const dirty = computed(() => JSON.stringify(formState.value) !== snapshot.value);
|
|
112
|
+
function onBeforeUnload(event) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
}
|
|
115
|
+
watch(dirty, (value) => {
|
|
116
|
+
if (value) window.addEventListener("beforeunload", onBeforeUnload);
|
|
117
|
+
else window.removeEventListener("beforeunload", onBeforeUnload);
|
|
118
|
+
});
|
|
119
|
+
onBeforeUnmount(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|
120
|
+
onBeforeRouteLeave(() => {
|
|
121
|
+
if (dirty.value && !confirm(t("cms.entry.unsavedChanges"))) return false;
|
|
122
|
+
});
|
|
123
|
+
const published = computed({
|
|
124
|
+
get: () => formState.value.status === "published",
|
|
125
|
+
set: (value) => {
|
|
126
|
+
formState.value.status = value ? "published" : "draft";
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
function togglePublished() {
|
|
130
|
+
published.value = !published.value;
|
|
131
|
+
}
|
|
132
|
+
function revertStatus() {
|
|
133
|
+
if (!drafts) return;
|
|
134
|
+
formState.value.status = JSON.parse(snapshot.value).status ?? null;
|
|
135
|
+
}
|
|
136
|
+
function goBack() {
|
|
137
|
+
navigateTo(`/cms/${name}`);
|
|
138
|
+
}
|
|
139
|
+
async function save() {
|
|
140
|
+
saving.value = true;
|
|
141
|
+
try {
|
|
142
|
+
await (isNew ? cmsApi(endpoint, { method: "POST", body: formState.value }) : cmsApi(`${endpoint}/${id}`, { method: "PUT", body: formState.value }));
|
|
143
|
+
snapshot.value = JSON.stringify(formState.value);
|
|
144
|
+
toast.add({ title: t("cms.toast.saved"), color: "success" });
|
|
145
|
+
goBack();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
toast.add({
|
|
148
|
+
title: t("cms.toast.saveFailed"),
|
|
149
|
+
description: errorMessage(error),
|
|
150
|
+
color: "error"
|
|
151
|
+
});
|
|
152
|
+
} finally {
|
|
153
|
+
saving.value = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
</script>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import { definePageMeta, navigateTo } from "#imports";
|
|
7
|
+
import cmsConfig from "#cms-config";
|
|
8
|
+
definePageMeta({ layout: "cms-admin", middleware: "cms-auth" });
|
|
9
|
+
const first = Object.keys(cmsConfig)[0];
|
|
10
|
+
await navigateTo(first ? `/cms/${first}` : "/cms/media", { replace: true });
|
|
11
|
+
</script>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-scope cms-canvas cms-grain flex min-h-screen items-center justify-center p-6">
|
|
3
|
+
<div class="flex w-full max-w-sm flex-col gap-8">
|
|
4
|
+
<header class="cms-rise flex flex-col items-center gap-3 text-center">
|
|
5
|
+
<h1 class="cms-display text-(--ui-text-highlighted) text-4xl font-medium">
|
|
6
|
+
{{ t("cms.login.title") }}<span class="text-(--cms-fern)">.</span>
|
|
7
|
+
</h1>
|
|
8
|
+
<p class="text-(--ui-text-muted) text-sm">
|
|
9
|
+
{{ t("cms.login.subtitle") }}
|
|
10
|
+
</p>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<div class="cms-card cms-rise p-6 sm:p-7" style="animation-delay: 90ms">
|
|
14
|
+
<UForm :state="state" class="flex flex-col gap-5" @submit="login">
|
|
15
|
+
<UAlert
|
|
16
|
+
v-if="error"
|
|
17
|
+
color="error"
|
|
18
|
+
variant="subtle"
|
|
19
|
+
:title="error"
|
|
20
|
+
class="rounded-xl"
|
|
21
|
+
/>
|
|
22
|
+
<UFormField :label="t('cms.login.email')" name="email" required :ui="CMS_FIELD_UI">
|
|
23
|
+
<UInput
|
|
24
|
+
v-model="state.email"
|
|
25
|
+
type="email"
|
|
26
|
+
size="lg"
|
|
27
|
+
autocomplete="username"
|
|
28
|
+
placeholder="you@example.com"
|
|
29
|
+
class="w-full"
|
|
30
|
+
/>
|
|
31
|
+
</UFormField>
|
|
32
|
+
<UFormField
|
|
33
|
+
:label="t('cms.login.password')"
|
|
34
|
+
name="password"
|
|
35
|
+
required
|
|
36
|
+
:ui="CMS_FIELD_UI"
|
|
37
|
+
>
|
|
38
|
+
<UInput
|
|
39
|
+
v-model="state.password"
|
|
40
|
+
type="password"
|
|
41
|
+
size="lg"
|
|
42
|
+
autocomplete="current-password"
|
|
43
|
+
placeholder="••••••••"
|
|
44
|
+
class="w-full"
|
|
45
|
+
/>
|
|
46
|
+
</UFormField>
|
|
47
|
+
<UButton
|
|
48
|
+
type="submit"
|
|
49
|
+
:label="t('cms.login.submit')"
|
|
50
|
+
size="lg"
|
|
51
|
+
block
|
|
52
|
+
:loading="loading"
|
|
53
|
+
class="mt-1 rounded-full"
|
|
54
|
+
/>
|
|
55
|
+
</UForm>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup>
|
|
62
|
+
import { definePageMeta, navigateTo, ref, useI18n, useUserSession } from "#imports";
|
|
63
|
+
import { CMS_FIELD_UI, errorMessage } from "../utils/ui";
|
|
64
|
+
definePageMeta({ layout: false });
|
|
65
|
+
const { loggedIn, fetch: refreshSession } = useUserSession();
|
|
66
|
+
if (loggedIn.value) {
|
|
67
|
+
await navigateTo("/cms", { replace: true });
|
|
68
|
+
}
|
|
69
|
+
const { t } = useI18n();
|
|
70
|
+
const state = ref({ email: "", password: "" });
|
|
71
|
+
const error = ref(null);
|
|
72
|
+
const loading = ref(false);
|
|
73
|
+
async function login() {
|
|
74
|
+
loading.value = true;
|
|
75
|
+
error.value = null;
|
|
76
|
+
try {
|
|
77
|
+
await $fetch("/api/cms/auth/login", { method: "POST", body: state.value });
|
|
78
|
+
await refreshSession();
|
|
79
|
+
await navigateTo("/cms", { replace: true });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
const status = e.statusCode;
|
|
82
|
+
error.value = status === 401 ? t("cms.login.invalid") : errorMessage(e) ?? t("cms.login.failed");
|
|
83
|
+
} finally {
|
|
84
|
+
loading.value = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-rise flex flex-col gap-7">
|
|
3
|
+
<CmsPageHeader :kicker="t('cms.media.kicker')" :title="t('cms.media.title')" />
|
|
4
|
+
|
|
5
|
+
<CmsMediaGallery />
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
import { definePageMeta, useI18n } from "#imports";
|
|
11
|
+
definePageMeta({ layout: "cms-admin", middleware: "cms-auth" });
|
|
12
|
+
const { t } = useI18n();
|
|
13
|
+
</script>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const CMS_FIELD_UI = { label: "text-[13px] font-medium text-(--ui-text-toned)" };
|
|
2
|
+
export const CMS_MODAL_UI = {
|
|
3
|
+
content: "rounded-2xl shadow-xl sm:max-w-3xl",
|
|
4
|
+
title: "cms-display text-xl font-medium text-(--ui-text-highlighted)"
|
|
5
|
+
};
|
|
6
|
+
export function errorMessage(error) {
|
|
7
|
+
const err = error;
|
|
8
|
+
return err.data?.message ?? err.message;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&family=Hanken+Grotesk:ital,wght@0,300..800;1,300..800&family=Fragment+Mono:ital@0;1&display=swap");@import "tailwindcss";@import "@nuxt/ui";@source "../app";.cms-scope,body:has(.cms-scope){--cms-paper:#f6f4ef;--cms-surface:#fdfcfa;--cms-field:#f1ede4;--cms-ink:#262119;--cms-line:#e8e3d8;--cms-line-strong:#d8d1c1;--cms-fern:#3e6b4f;--cms-sage:#e3ebe1;--cms-sage-ink:#2e5138;--cms-shadow-soft:0 1px 2px rgba(38,33,25,.05),0 16px 40px -24px rgba(38,33,25,.22);color-scheme:light;--ui-text-dimmed:#a69d8b;--ui-text-muted:#877e6c;--ui-text-toned:#60594a;--ui-text:#403a2f;--ui-text-highlighted:var(--cms-ink);--ui-text-inverted:#fffdf8;--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-paper);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:#e7e1d3;--ui-bg-inverted:var(--cms-ink);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-ink);--ui-primary:var(--cms-fern);--ui-radius:0.4rem;color:var(--ui-text);font-family:Hanken Grotesk,ui-sans-serif,system-ui,sans-serif;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:var(--cms-sage);color:var(--cms-sage-ink)}body:has(.cms-scope) ::selection{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-display{font-family:Poppins,ui-sans-serif,system-ui,sans-serif;letter-spacing:-.02em}.cms-kicker{color:var(--ui-text-dimmed);font-family:Fragment Mono,ui-monospace,monospace;font-size:.6875rem;letter-spacing:.16em;line-height:1.4;text-transform:uppercase}body:has(.cms-scope) :is(button,a,label,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer transition-all duration-200 select-none}body:has(.cms-scope) :is(button,a,label,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]):active{@apply transition-none;scale:.99}.cms-canvas{background-attachment:fixed;background-color:var(--cms-paper);background-image:radial-gradient(50rem 34rem at 10% -10%,rgba(62,107,79,.07),transparent 62%),radial-gradient(44rem 30rem at 108% 112%,rgba(193,149,87,.09),transparent 62%)}.cms-grain:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='a'%3E%3CfeTurbulence baseFrequency='.8' numOctaves='2' stitchTiles='stitch' type='fractalNoise'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");content:"";inset:0;mix-blend-mode:multiply;opacity:.035;pointer-events:none;position:fixed;z-index:50}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft)}.cms-empty{background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem}.cms-navlink{align-items:center;border-radius:.75rem;color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color .15s ease,color .15s ease}.cms-navlink:hover{background:rgba(38,33,25,.05);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{border:1px solid transparent;border-radius:9999px;cursor:pointer;font-family:Fragment Mono,ui-monospace,monospace;font-size:.625rem;letter-spacing:.12em;padding:.25rem .625rem;text-transform:uppercase;transition:background-color .15s ease,border-color .15s ease}.cms-badge.is-published{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-badge.is-published:hover{background:#d6e2d3}.cms-badge.is-draft{background:transparent;border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-badge.is-draft:hover{background:rgba(38,33,25,.04)}.cms-dropzone{align-items:center;background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem;color:var(--ui-text-muted);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color .15s ease,background-color .15s ease,color .15s ease;width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-sage);border-color:var(--cms-fern);color:var(--cms-sage-ink)}.cms-dropzone.is-dense{border-radius:.75rem;flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;font-family:Fragment Mono,ui-monospace,monospace;font-size:.625rem;letter-spacing:.12em;padding:.3rem .8rem;transition:background-color .15s ease,color .15s ease}.cms-pill:hover{background:rgba(38,33,25,.05);color:var(--ui-text)}.cms-pill.is-active{background:var(--cms-sage);color:var(--cms-sage-ink)}@keyframes cms-rise{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .55s cubic-bezier(.22,1,.36,1) both}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{border:1px solid var(--cms-line);border-radius:.75rem;overflow:hidden}.cms-richtext-toolbar{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;flex-wrap:wrap;gap:2px;padding:4px}.cms-richtext-body{font-size:.875rem;min-height:9rem;outline:none;padding:.75rem 1rem}.cms-richtext-body h2{font-size:1.25rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body h3{font-size:1.1rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body p{margin:.35em 0}.cms-richtext-body ul{list-style:disc;padding-left:1.25rem}.cms-richtext-body ol{list-style:decimal;padding-left:1.25rem}.cms-richtext-body blockquote{border-left:3px solid var(--cms-line);color:var(--ui-text-muted);padding-left:.75rem}
|