@xleddyl/nuxt-cms 0.1.46 → 0.1.47
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/module.json +1 -1
- package/dist/module.mjs +10 -0
- package/dist/runtime/app/components/cms/BlocksField.vue +194 -76
- package/dist/runtime/app/components/cms/MediaGallery.vue +65 -43
- package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +8 -3
- package/dist/runtime/app/components/cms/PageHeader.vue +8 -1
- package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +8 -3
- package/dist/runtime/app/pages/admin-collection.vue +33 -9
- package/dist/runtime/app/pages/admin-entry.vue +17 -2
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/media-folder.delete.d.ts +4 -0
- package/dist/runtime/server/api/media-folder.delete.js +48 -0
- package/dist/runtime/server/api/media-folder.post.d.ts +4 -0
- package/dist/runtime/server/api/media-folder.post.js +37 -0
- package/dist/runtime/server/api/media.get.d.ts +1 -0
- package/dist/runtime/server/api/media.get.js +12 -3
- package/dist/runtime/shared/index.d.ts +3 -0
- package/dist/runtime/shared/index.js +7 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1371,6 +1371,16 @@ const module$1 = defineNuxtModule({
|
|
|
1371
1371
|
method: "delete",
|
|
1372
1372
|
handler: resolver.resolve("./runtime/server/api/media.delete")
|
|
1373
1373
|
});
|
|
1374
|
+
addServerHandler({
|
|
1375
|
+
route: "/api/cms/admin/media/folders",
|
|
1376
|
+
method: "post",
|
|
1377
|
+
handler: resolver.resolve("./runtime/server/api/media-folder.post")
|
|
1378
|
+
});
|
|
1379
|
+
addServerHandler({
|
|
1380
|
+
route: "/api/cms/admin/media/folders",
|
|
1381
|
+
method: "delete",
|
|
1382
|
+
handler: resolver.resolve("./runtime/server/api/media-folder.delete")
|
|
1383
|
+
});
|
|
1374
1384
|
const api = "/api/cms/admin/:collection";
|
|
1375
1385
|
addServerHandler({
|
|
1376
1386
|
route: api,
|
|
@@ -1,87 +1,142 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="flex flex-col gap-3">
|
|
3
|
-
<div
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
<div class="cms-blocks-grid">
|
|
4
|
+
<div
|
|
5
|
+
v-for="(item, index) in items"
|
|
6
|
+
:key="uidFor(item)"
|
|
7
|
+
class="cms-card cms-block-tile"
|
|
8
|
+
:class="{ 'is-muted': isHidden(item) }"
|
|
9
|
+
>
|
|
9
10
|
<button
|
|
10
11
|
type="button"
|
|
11
|
-
class="cms-block-
|
|
12
|
-
:aria-label="
|
|
13
|
-
@click="
|
|
12
|
+
class="cms-block-preview"
|
|
13
|
+
:aria-label="`Edit ${labelOf(item)}`"
|
|
14
|
+
@click="openEditor(index)"
|
|
14
15
|
>
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
<img
|
|
17
|
+
v-if="previewOf(item)?.kind === 'image'"
|
|
18
|
+
:src="previewOf(item).url"
|
|
19
|
+
alt=""
|
|
20
|
+
loading="lazy"
|
|
18
21
|
/>
|
|
19
|
-
|
|
22
|
+
<video
|
|
23
|
+
v-else-if="previewOf(item)?.kind === 'video'"
|
|
24
|
+
:src="previewOf(item).url"
|
|
25
|
+
preload="metadata"
|
|
26
|
+
muted
|
|
27
|
+
playsinline
|
|
28
|
+
/>
|
|
29
|
+
<span v-else-if="summaryOf(item)" class="cms-block-summary">
|
|
30
|
+
{{ summaryOf(item) }}
|
|
31
|
+
</span>
|
|
32
|
+
<CmsIcon v-else :name="iconOf(item)" class="size-7" />
|
|
33
|
+
|
|
34
|
+
<CmsIcon v-if="isHidden(item)" name="eye-slash" class="cms-block-hidden size-4" />
|
|
20
35
|
</button>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@click="remove(index)"
|
|
54
|
-
/>
|
|
36
|
+
|
|
37
|
+
<div class="cms-block-bar">
|
|
38
|
+
<span class="cms-block-bar-name" :title="labelOf(item)">{{ labelOf(item) }}</span>
|
|
39
|
+
<div class="cms-block-bar-actions">
|
|
40
|
+
<CmsButton
|
|
41
|
+
icon="chevron-left"
|
|
42
|
+
size="xs"
|
|
43
|
+
variant="ghost"
|
|
44
|
+
color="neutral"
|
|
45
|
+
aria-label="Move earlier"
|
|
46
|
+
:disabled="index === 0"
|
|
47
|
+
@click="move(index, -1)"
|
|
48
|
+
/>
|
|
49
|
+
<CmsButton
|
|
50
|
+
icon="chevron-right"
|
|
51
|
+
size="xs"
|
|
52
|
+
variant="ghost"
|
|
53
|
+
color="neutral"
|
|
54
|
+
aria-label="Move later"
|
|
55
|
+
:disabled="index === items.length - 1"
|
|
56
|
+
@click="move(index, 1)"
|
|
57
|
+
/>
|
|
58
|
+
<CmsButton
|
|
59
|
+
icon="trash"
|
|
60
|
+
size="xs"
|
|
61
|
+
variant="ghost"
|
|
62
|
+
color="error"
|
|
63
|
+
aria-label="Remove block"
|
|
64
|
+
@click="remove(index)"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
55
68
|
</div>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@update:model-value="(value) => updateField(index, blockKey, value)"
|
|
68
|
-
/>
|
|
69
|
-
</CmsFormField>
|
|
70
|
-
</template>
|
|
69
|
+
|
|
70
|
+
<CmsDropdownMenu v-if="addItems.length > 1" :items="addItems">
|
|
71
|
+
<button type="button" class="cms-block-add">
|
|
72
|
+
<CmsIcon name="plus" class="size-5" />
|
|
73
|
+
<span>Add</span>
|
|
74
|
+
</button>
|
|
75
|
+
</CmsDropdownMenu>
|
|
76
|
+
<button v-else type="button" class="cms-block-add" @click="addFirst">
|
|
77
|
+
<CmsIcon name="plus" class="size-5" />
|
|
78
|
+
<span>Add</span>
|
|
79
|
+
</button>
|
|
71
80
|
</div>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
|
|
82
|
+
<CmsModal v-model:open="editorOpen" :title="editorTitle">
|
|
83
|
+
<template #body>
|
|
84
|
+
<div class="flex flex-col gap-4">
|
|
85
|
+
<CmsFormField
|
|
86
|
+
v-for="(blockField, blockKey) in editorFields"
|
|
87
|
+
:key="blockKey"
|
|
88
|
+
:label="blockField.label"
|
|
89
|
+
:required="blockField.required"
|
|
90
|
+
>
|
|
91
|
+
<CmsFieldInput
|
|
92
|
+
:model-value="editorItem?.[blockKey]"
|
|
93
|
+
:field="blockField"
|
|
94
|
+
:locale="locale"
|
|
95
|
+
@update:model-value="(value) => updateEditorField(blockKey, value)"
|
|
96
|
+
/>
|
|
97
|
+
</CmsFormField>
|
|
98
|
+
|
|
99
|
+
<div class="cms-actions justify-between">
|
|
100
|
+
<CmsButton
|
|
101
|
+
label="Duplicate"
|
|
102
|
+
icon="document-duplicate"
|
|
103
|
+
variant="soft"
|
|
104
|
+
color="neutral"
|
|
105
|
+
@click="duplicateCurrent"
|
|
106
|
+
/>
|
|
107
|
+
<div class="cms-actions">
|
|
108
|
+
<CmsButton
|
|
109
|
+
label="Remove"
|
|
110
|
+
icon="trash"
|
|
111
|
+
variant="soft"
|
|
112
|
+
color="error"
|
|
113
|
+
@click="removeCurrent"
|
|
114
|
+
/>
|
|
115
|
+
<CmsButton label="Done" @click="editorOpen = false" />
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</template>
|
|
120
|
+
</CmsModal>
|
|
75
121
|
</div>
|
|
76
122
|
</template>
|
|
77
123
|
|
|
78
124
|
<script setup>
|
|
125
|
+
import {
|
|
126
|
+
isTranslatableMediaField,
|
|
127
|
+
mediaIconFor,
|
|
128
|
+
mediaPublicUrl,
|
|
129
|
+
mediaTypeForKey,
|
|
130
|
+
pickTranslatedMedia
|
|
131
|
+
} from "#nuxt-cms";
|
|
79
132
|
import { computed, ref } from "#imports";
|
|
133
|
+
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
80
134
|
const props = defineProps({
|
|
81
135
|
field: { type: Object, required: true },
|
|
82
136
|
locale: { type: String, required: false }
|
|
83
137
|
});
|
|
84
138
|
const model = defineModel({ type: [Array, null], ...{ required: true } });
|
|
139
|
+
const { mediaBaseUrl, i18n } = useCmsRuntime();
|
|
85
140
|
const blocks = computed(() => props.field.blocks ?? {});
|
|
86
141
|
const items = computed(() => Array.isArray(model.value) ? model.value : []);
|
|
87
142
|
let uidCounter = 0;
|
|
@@ -94,6 +149,46 @@ function uidFor(item) {
|
|
|
94
149
|
}
|
|
95
150
|
return uid;
|
|
96
151
|
}
|
|
152
|
+
function blockOf(item) {
|
|
153
|
+
return blocks.value[item.type];
|
|
154
|
+
}
|
|
155
|
+
function labelOf(item) {
|
|
156
|
+
return blockOf(item)?.label ?? String(item.type);
|
|
157
|
+
}
|
|
158
|
+
function mediaKeyOf(item) {
|
|
159
|
+
const locale = props.locale ?? i18n.defaultLocale;
|
|
160
|
+
for (const [key, field] of Object.entries(blockOf(item)?.fields ?? {})) {
|
|
161
|
+
if (field.type !== "media") continue;
|
|
162
|
+
const raw = item[key];
|
|
163
|
+
const value = isTranslatableMediaField(field) ? pickTranslatedMedia(raw, locale, i18n.defaultLocale) : raw;
|
|
164
|
+
if (typeof value === "string" && value) return value;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function previewOf(item) {
|
|
169
|
+
const key = mediaKeyOf(item);
|
|
170
|
+
if (!key) return null;
|
|
171
|
+
const url = mediaPublicUrl(mediaBaseUrl, key);
|
|
172
|
+
if (!url) return null;
|
|
173
|
+
return { url, kind: mediaTypeForKey(key) };
|
|
174
|
+
}
|
|
175
|
+
function iconOf(item) {
|
|
176
|
+
const key = mediaKeyOf(item);
|
|
177
|
+
return key ? mediaIconFor(mediaTypeForKey(key)) : "rectangle-stack";
|
|
178
|
+
}
|
|
179
|
+
function summaryOf(item) {
|
|
180
|
+
for (const [key, field] of Object.entries(blockOf(item)?.fields ?? {})) {
|
|
181
|
+
if (field.type !== "text" && field.type !== "richtext") continue;
|
|
182
|
+
const raw = item[key];
|
|
183
|
+
const value = typeof raw === "string" ? raw : raw?.[props.locale ?? i18n.defaultLocale] ?? "";
|
|
184
|
+
const text = value.replace(/<[^>]*>/g, " ").trim();
|
|
185
|
+
if (text) return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
|
|
186
|
+
}
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
function isHidden(item) {
|
|
190
|
+
return blockOf(item)?.fields.hidden?.type === "boolean" && item.hidden === true;
|
|
191
|
+
}
|
|
97
192
|
function updateField(index, key, value) {
|
|
98
193
|
const current = items.value[index];
|
|
99
194
|
if (!current) return;
|
|
@@ -106,6 +201,11 @@ function add(type) {
|
|
|
106
201
|
if (!block) return;
|
|
107
202
|
const empty = Object.fromEntries(Object.keys(block.fields).map((k) => [k, null]));
|
|
108
203
|
model.value = [...items.value, { type, ...empty }];
|
|
204
|
+
openEditor(items.value.length - 1);
|
|
205
|
+
}
|
|
206
|
+
function addFirst() {
|
|
207
|
+
const first = Object.keys(blocks.value)[0];
|
|
208
|
+
if (first) add(first);
|
|
109
209
|
}
|
|
110
210
|
function remove(index) {
|
|
111
211
|
const next = items.value.filter((_, i) => i !== index);
|
|
@@ -119,17 +219,6 @@ function duplicate(index) {
|
|
|
119
219
|
next.splice(index + 1, 0, copy);
|
|
120
220
|
model.value = next;
|
|
121
221
|
}
|
|
122
|
-
const collapsedUids = ref(/* @__PURE__ */ new Set());
|
|
123
|
-
function isCollapsed(item) {
|
|
124
|
-
return collapsedUids.value.has(uidFor(item));
|
|
125
|
-
}
|
|
126
|
-
function toggleCollapsed(item) {
|
|
127
|
-
const uid = uidFor(item);
|
|
128
|
-
const next = new Set(collapsedUids.value);
|
|
129
|
-
if (next.has(uid)) next.delete(uid);
|
|
130
|
-
else next.add(uid);
|
|
131
|
-
collapsedUids.value = next;
|
|
132
|
-
}
|
|
133
222
|
function move(index, delta) {
|
|
134
223
|
const next = [...items.value];
|
|
135
224
|
const [item] = next.splice(index, 1);
|
|
@@ -142,4 +231,33 @@ const addItems = computed(
|
|
|
142
231
|
onSelect: () => add(type)
|
|
143
232
|
}))
|
|
144
233
|
);
|
|
234
|
+
const editorIndex = ref(null);
|
|
235
|
+
const editorOpen = computed({
|
|
236
|
+
get: () => editorIndex.value !== null,
|
|
237
|
+
set: (value) => {
|
|
238
|
+
if (!value) editorIndex.value = null;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const editorItem = computed(
|
|
242
|
+
() => editorIndex.value === null ? void 0 : items.value[editorIndex.value]
|
|
243
|
+
);
|
|
244
|
+
const editorFields = computed(() => editorItem.value ? blockOf(editorItem.value)?.fields : {});
|
|
245
|
+
const editorTitle = computed(() => editorItem.value ? labelOf(editorItem.value) : "");
|
|
246
|
+
function openEditor(index) {
|
|
247
|
+
editorIndex.value = index;
|
|
248
|
+
}
|
|
249
|
+
function updateEditorField(key, value) {
|
|
250
|
+
if (editorIndex.value === null) return;
|
|
251
|
+
updateField(editorIndex.value, key, value);
|
|
252
|
+
}
|
|
253
|
+
function removeCurrent() {
|
|
254
|
+
if (editorIndex.value === null) return;
|
|
255
|
+
remove(editorIndex.value);
|
|
256
|
+
editorIndex.value = null;
|
|
257
|
+
}
|
|
258
|
+
function duplicateCurrent() {
|
|
259
|
+
if (editorIndex.value === null) return;
|
|
260
|
+
duplicate(editorIndex.value);
|
|
261
|
+
editorIndex.value = null;
|
|
262
|
+
}
|
|
145
263
|
</script>
|
|
@@ -39,13 +39,14 @@
|
|
|
39
39
|
@click="folder = folder === chip.name ? null : chip.name"
|
|
40
40
|
>
|
|
41
41
|
<CmsIcon name="folder" class="size-3" />{{ chip.name }}
|
|
42
|
+
<span v-if="chip.count" class="cms-pill-count">{{ chip.count }}</span>
|
|
42
43
|
</button>
|
|
43
44
|
<button
|
|
44
|
-
v-if="
|
|
45
|
+
v-if="!readOnly"
|
|
45
46
|
type="button"
|
|
46
47
|
class="cms-pill-discard"
|
|
47
|
-
aria-label="
|
|
48
|
-
@click="
|
|
48
|
+
:aria-label="`Delete folder ${chip.name}`"
|
|
49
|
+
@click="deleteFolder(chip.name)"
|
|
49
50
|
>
|
|
50
51
|
<CmsIcon name="x-mark" class="size-3" />
|
|
51
52
|
</button>
|
|
@@ -161,7 +162,7 @@
|
|
|
161
162
|
<CmsMediaFolderPicker
|
|
162
163
|
v-model="uploadFolder"
|
|
163
164
|
:folders="folderNames"
|
|
164
|
-
@create="
|
|
165
|
+
@create="onFolderCreate"
|
|
165
166
|
/>
|
|
166
167
|
</CmsFormField>
|
|
167
168
|
<CmsMediaUpload
|
|
@@ -223,7 +224,7 @@
|
|
|
223
224
|
<CmsMediaFolderPicker
|
|
224
225
|
v-model="editFolder"
|
|
225
226
|
:folders="folderNames"
|
|
226
|
-
@create="
|
|
227
|
+
@create="onFolderCreate"
|
|
227
228
|
/>
|
|
228
229
|
</CmsFormField>
|
|
229
230
|
<div class="cms-actions is-end">
|
|
@@ -273,8 +274,8 @@ const canUpload = computed(() => !isLocal.value);
|
|
|
273
274
|
const canEditAlt = computed(() => true);
|
|
274
275
|
const readOnly = computed(() => !canUpload.value);
|
|
275
276
|
const endpoint = "/api/cms/admin/media";
|
|
276
|
-
const DRAFT_FOLDERS_KEY = "nuxt-cms:media-folders";
|
|
277
277
|
const items = ref([]);
|
|
278
|
+
const serverFolders = ref([]);
|
|
278
279
|
const source = ref(null);
|
|
279
280
|
const loading = ref(true);
|
|
280
281
|
const errorCode = ref(null);
|
|
@@ -284,6 +285,7 @@ async function reload() {
|
|
|
284
285
|
try {
|
|
285
286
|
const result = await $fetch(endpoint);
|
|
286
287
|
items.value = result.items;
|
|
288
|
+
serverFolders.value = result.folders ?? [];
|
|
287
289
|
source.value = result.source;
|
|
288
290
|
} catch (err) {
|
|
289
291
|
errorCode.value = err.statusCode ?? 500;
|
|
@@ -315,50 +317,58 @@ watch(filters, (list) => {
|
|
|
315
317
|
if (!list.includes(filter.value)) filter.value = "all";
|
|
316
318
|
});
|
|
317
319
|
const search = ref("");
|
|
318
|
-
const draftFolders = ref([]);
|
|
319
|
-
onMounted(() => {
|
|
320
|
-
try {
|
|
321
|
-
const stored = JSON.parse(localStorage.getItem(DRAFT_FOLDERS_KEY) ?? "[]");
|
|
322
|
-
if (Array.isArray(stored)) draftFolders.value = stored.filter((f) => typeof f === "string");
|
|
323
|
-
} catch {
|
|
324
|
-
draftFolders.value = [];
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
function persistDraftFolders() {
|
|
328
|
-
try {
|
|
329
|
-
localStorage.setItem(DRAFT_FOLDERS_KEY, JSON.stringify(draftFolders.value));
|
|
330
|
-
} catch {
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
320
|
const usedFolders = computed(() => {
|
|
334
321
|
const set = new Set(items.value.map((item) => item.folder).filter((f) => !!f));
|
|
335
322
|
return [...set].sort();
|
|
336
323
|
});
|
|
337
324
|
const folderNames = computed(
|
|
338
|
-
() => [.../* @__PURE__ */ new Set([...
|
|
325
|
+
() => [.../* @__PURE__ */ new Set([...serverFolders.value, ...usedFolders.value])].sort()
|
|
339
326
|
);
|
|
327
|
+
const folderCounts = computed(() => {
|
|
328
|
+
const counts = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const item of items.value) {
|
|
330
|
+
if (!item.folder) continue;
|
|
331
|
+
counts.set(item.folder, (counts.get(item.folder) ?? 0) + 1);
|
|
332
|
+
}
|
|
333
|
+
return counts;
|
|
334
|
+
});
|
|
340
335
|
const folderChips = computed(
|
|
341
|
-
() => folderNames.value.map((name) => ({
|
|
336
|
+
() => folderNames.value.map((name) => ({
|
|
337
|
+
name,
|
|
338
|
+
count: folderCounts.value.get(name) ?? 0,
|
|
339
|
+
empty: !folderCounts.value.get(name)
|
|
340
|
+
}))
|
|
342
341
|
);
|
|
343
342
|
const folder = ref(null);
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
343
|
+
function onFolderCreate(name) {
|
|
344
|
+
registerFolder(name).catch(
|
|
345
|
+
(err) => toast.add({
|
|
346
|
+
title: "Could not create the folder",
|
|
347
|
+
description: errorMessage(err),
|
|
348
|
+
color: "error"
|
|
349
|
+
})
|
|
350
|
+
);
|
|
349
351
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
352
|
+
async function registerFolder(name) {
|
|
353
|
+
if (folderNames.value.includes(name)) return;
|
|
354
|
+
await $fetch(`${endpoint}/folders`, { method: "POST", body: { name } });
|
|
355
|
+
await reload();
|
|
354
356
|
}
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
357
|
+
async function deleteFolder(name) {
|
|
358
|
+
const count = folderCounts.value.get(name) ?? 0;
|
|
359
|
+
const question = count ? `Delete the folder "${name}" and its ${count} file${count > 1 ? "s" : ""}?` : `Delete the empty folder "${name}"?`;
|
|
360
|
+
if (!await confirmAction(question)) return;
|
|
361
|
+
try {
|
|
362
|
+
await $fetch(`${endpoint}/folders`, {
|
|
363
|
+
method: "DELETE",
|
|
364
|
+
query: { name, recursive: count ? "true" : "false" }
|
|
365
|
+
});
|
|
366
|
+
if (folder.value === name) folder.value = null;
|
|
367
|
+
await reload();
|
|
368
|
+
} catch (error) {
|
|
369
|
+
toast.add({ title: "Delete failed", description: errorMessage(error), color: "error" });
|
|
360
370
|
}
|
|
361
|
-
}
|
|
371
|
+
}
|
|
362
372
|
const visible = computed(() => {
|
|
363
373
|
const allowed = allowedTypes.value;
|
|
364
374
|
let list = allowed ? items.value.filter((item) => allowed.includes(item.type)) : items.value;
|
|
@@ -404,12 +414,24 @@ function openNewFolder() {
|
|
|
404
414
|
newFolderName.value = "";
|
|
405
415
|
newFolderOpen.value = true;
|
|
406
416
|
}
|
|
407
|
-
|
|
417
|
+
const creatingFolder = ref(false);
|
|
418
|
+
async function createFolder() {
|
|
408
419
|
const name = newFolderSlug.value;
|
|
409
|
-
if (!name) return;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
420
|
+
if (!name || creatingFolder.value) return;
|
|
421
|
+
creatingFolder.value = true;
|
|
422
|
+
try {
|
|
423
|
+
await registerFolder(name);
|
|
424
|
+
folder.value = name;
|
|
425
|
+
newFolderOpen.value = false;
|
|
426
|
+
} catch (err) {
|
|
427
|
+
toast.add({
|
|
428
|
+
title: "Could not create the folder",
|
|
429
|
+
description: errorMessage(err),
|
|
430
|
+
color: "error"
|
|
431
|
+
});
|
|
432
|
+
} finally {
|
|
433
|
+
creatingFolder.value = false;
|
|
434
|
+
}
|
|
413
435
|
}
|
|
414
436
|
const editing = ref(null);
|
|
415
437
|
const editAlt = ref("");
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
export interface CmsBreadcrumbLink {
|
|
2
|
+
label: string;
|
|
3
|
+
to: string;
|
|
4
|
+
}
|
|
1
5
|
type __VLS_Props = {
|
|
2
6
|
title: string;
|
|
7
|
+
links?: CmsBreadcrumbLink[];
|
|
3
8
|
};
|
|
4
|
-
declare var
|
|
9
|
+
declare var __VLS_12: {}, __VLS_14: {};
|
|
5
10
|
type __VLS_Slots = {} & {
|
|
6
|
-
badge?: (props: typeof
|
|
11
|
+
badge?: (props: typeof __VLS_12) => any;
|
|
7
12
|
} & {
|
|
8
|
-
default?: (props: typeof
|
|
13
|
+
default?: (props: typeof __VLS_14) => any;
|
|
9
14
|
};
|
|
10
15
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
16
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<header class="cms-page-header">
|
|
3
3
|
<div class="cms-page-heading">
|
|
4
|
+
<nav v-if="links?.length" class="cms-breadcrumb">
|
|
5
|
+
<template v-for="link in links" :key="link.to">
|
|
6
|
+
<NuxtLink :to="link.to" class="cms-breadcrumb-link">{{ link.label }}</NuxtLink>
|
|
7
|
+
<CmsIcon name="chevron-right" class="cms-breadcrumb-separator size-3" />
|
|
8
|
+
</template>
|
|
9
|
+
</nav>
|
|
4
10
|
<div class="cms-actions">
|
|
5
11
|
<h1 class="cms-title cms-title-md">
|
|
6
12
|
{{ title }}
|
|
@@ -14,6 +20,7 @@
|
|
|
14
20
|
|
|
15
21
|
<script setup>
|
|
16
22
|
defineProps({
|
|
17
|
-
title: { type: String, required: true }
|
|
23
|
+
title: { type: String, required: true },
|
|
24
|
+
links: { type: Array, required: false }
|
|
18
25
|
});
|
|
19
26
|
</script>
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
export interface CmsBreadcrumbLink {
|
|
2
|
+
label: string;
|
|
3
|
+
to: string;
|
|
4
|
+
}
|
|
1
5
|
type __VLS_Props = {
|
|
2
6
|
title: string;
|
|
7
|
+
links?: CmsBreadcrumbLink[];
|
|
3
8
|
};
|
|
4
|
-
declare var
|
|
9
|
+
declare var __VLS_12: {}, __VLS_14: {};
|
|
5
10
|
type __VLS_Slots = {} & {
|
|
6
|
-
badge?: (props: typeof
|
|
11
|
+
badge?: (props: typeof __VLS_12) => any;
|
|
7
12
|
} & {
|
|
8
|
-
default?: (props: typeof
|
|
13
|
+
default?: (props: typeof __VLS_14) => any;
|
|
9
14
|
};
|
|
10
15
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
16
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -41,20 +41,26 @@
|
|
|
41
41
|
/>
|
|
42
42
|
</div>
|
|
43
43
|
|
|
44
|
-
<div v-if="
|
|
44
|
+
<div v-if="pageTree.length" class="cms-card divide-y divide-(--cms-line)">
|
|
45
45
|
<NuxtLink
|
|
46
|
-
v-for="
|
|
47
|
-
:key="String(row.id)"
|
|
48
|
-
:to="`/cms/${name}/${row.id}`"
|
|
49
|
-
class="
|
|
46
|
+
v-for="node in pageTree"
|
|
47
|
+
:key="String(node.row.id)"
|
|
48
|
+
:to="`/cms/${name}/${node.row.id}`"
|
|
49
|
+
class="cms-tree-row"
|
|
50
|
+
:style="{ paddingLeft: `${1 + node.depth * 1.5}rem` }"
|
|
50
51
|
>
|
|
52
|
+
<CmsIcon
|
|
53
|
+
v-if="node.depth"
|
|
54
|
+
name="arrow-turn-left-up"
|
|
55
|
+
class="cms-tree-branch size-3.5 rotate-90"
|
|
56
|
+
/>
|
|
51
57
|
<div class="min-w-0 flex-1">
|
|
52
58
|
<div class="truncate font-medium text-(--ui-text-highlighted)">
|
|
53
|
-
{{ row.label }}
|
|
59
|
+
{{ node.row.label }}
|
|
54
60
|
</div>
|
|
55
|
-
<div class="cms-label truncate">{{ row.path }}</div>
|
|
61
|
+
<div class="cms-label truncate">{{ node.row.path }}</div>
|
|
56
62
|
</div>
|
|
57
|
-
<span class="cms-label shrink-0">{{ row.updatedAt ? "edited" : "empty" }}</span>
|
|
63
|
+
<span class="cms-label shrink-0">{{ node.row.updatedAt ? "edited" : "empty" }}</span>
|
|
58
64
|
<CmsIcon name="chevron-right" class="size-4 shrink-0 text-(--ui-text-dimmed)" />
|
|
59
65
|
</NuxtLink>
|
|
60
66
|
</div>
|
|
@@ -152,7 +158,12 @@
|
|
|
152
158
|
</template>
|
|
153
159
|
|
|
154
160
|
<script setup>
|
|
155
|
-
import {
|
|
161
|
+
import {
|
|
162
|
+
isTranslatableField,
|
|
163
|
+
isTranslatableMediaField,
|
|
164
|
+
pageParentPath,
|
|
165
|
+
pickTranslatedMedia
|
|
166
|
+
} from "#nuxt-cms";
|
|
156
167
|
import {
|
|
157
168
|
computed,
|
|
158
169
|
createError,
|
|
@@ -217,6 +228,19 @@ function isList(value) {
|
|
|
217
228
|
return !!value && Array.isArray(value.items);
|
|
218
229
|
}
|
|
219
230
|
const rows = computed(() => isList(data.value) ? data.value.items : []);
|
|
231
|
+
const pageTree = computed(() => {
|
|
232
|
+
const byPath = new Set(rows.value.map((row) => String(row.path)));
|
|
233
|
+
const depthOf = (path) => {
|
|
234
|
+
let depth = 0;
|
|
235
|
+
let parent = pageParentPath(path);
|
|
236
|
+
while (parent) {
|
|
237
|
+
if (byPath.has(parent)) depth++;
|
|
238
|
+
parent = pageParentPath(parent);
|
|
239
|
+
}
|
|
240
|
+
return depth;
|
|
241
|
+
};
|
|
242
|
+
return rows.value.map((row) => ({ row, depth: depthOf(String(row.path)) }));
|
|
243
|
+
});
|
|
220
244
|
const total = computed(() => isList(data.value) ? data.value.total : 0);
|
|
221
245
|
const relations = computed(() => isList(data.value) ? data.value.relations ?? {} : {});
|
|
222
246
|
const reload = async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="cms-page">
|
|
3
|
-
<CmsPageHeader :title="headerTitle">
|
|
3
|
+
<CmsPageHeader :title="headerTitle" :links="breadcrumb">
|
|
4
4
|
<template v-if="drafts" #badge>
|
|
5
5
|
<CmsStatusBadge :published="published" />
|
|
6
6
|
</template>
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</template>
|
|
34
34
|
|
|
35
35
|
<script setup>
|
|
36
|
-
import { pageFields, pageRouteOf } from "#nuxt-cms";
|
|
36
|
+
import { pageFields, pageParentPath, pageRouteOf, pageRoutes } from "#nuxt-cms";
|
|
37
37
|
import {
|
|
38
38
|
computed,
|
|
39
39
|
createError,
|
|
@@ -71,6 +71,21 @@ const pageRoute = isPage && id ? pageRouteOf(config, id) : void 0;
|
|
|
71
71
|
if (isPage && !pageRoute) {
|
|
72
72
|
throw createError({ statusCode: 404, statusMessage: "Unknown page", fatal: true });
|
|
73
73
|
}
|
|
74
|
+
const breadcrumb = (() => {
|
|
75
|
+
const links = [{ label: config.label, to: `/cms/${name}` }];
|
|
76
|
+
if (!pageRoute) return links;
|
|
77
|
+
const ancestors = [];
|
|
78
|
+
let parent = pageParentPath(pageRoute.path);
|
|
79
|
+
while (parent) {
|
|
80
|
+
ancestors.unshift(parent);
|
|
81
|
+
parent = pageParentPath(parent);
|
|
82
|
+
}
|
|
83
|
+
for (const path of ancestors) {
|
|
84
|
+
const route2 = pageRoutes(config).find((candidate) => candidate.path === path);
|
|
85
|
+
if (route2) links.push({ label: route2.label, to: `/cms/${name}/${route2.key}` });
|
|
86
|
+
}
|
|
87
|
+
return links;
|
|
88
|
+
})();
|
|
74
89
|
const isNew = !isPage && id === void 0;
|
|
75
90
|
const drafts = !isPage && !!config.drafts;
|
|
76
91
|
const fields = pageRoute ? pageFields(config, pageRoute.path) : config.fields;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer theme, base, components, utilities;@import "tailwindcss/theme.css" layer(theme) theme(inline);.cms-scope{@import "tailwindcss/preflight.css" layer(base);@import "tailwindcss/utilities.css" layer(utilities) source(none)}@source "../app";.cms-scope,body:has(.cms-scope){--cms-canvas:#1b1b1e;--cms-surface:#222226;--cms-field:#2a2a2f;--cms-elevated:#2f2f35;--cms-line:#35353b;--cms-line-strong:#46464e;--cms-text:#ededef;--cms-text-toned:#c2c2c8;--cms-text-muted:#9a9aa3;--cms-text-dimmed:#6e6e78;--cms-fern:#8fae8b;--cms-error:#f0857d;--cms-success:#7fb083;--cms-warning:#d4ad76;--cms-font-ui:"Hanken Grotesk Variable","Hanken Grotesk",ui-sans-serif,system-ui,sans-serif;--cms-font-mono:"Fragment Mono",ui-monospace,SFMono-Regular,Menlo,monospace;--cms-radius-sm:6px;--cms-radius-md:8px;--cms-radius-lg:12px;--cms-shadow-overlay:0 8px 24px rgba(0,0,0,.35);--cms-scrim:rgba(0,0,0,.5);--cms-duration:120ms;--cms-duration-lg:250ms;--cms-ease:cubic-bezier(0.2,0,0,1);color-scheme:dark;--ui-text-dimmed:var(--cms-text-dimmed);--ui-text-muted:var(--cms-text-muted);--ui-text-toned:var(--cms-text-toned);--ui-text:var(--cms-text-toned);--ui-text-highlighted:var(--cms-text);--ui-text-inverted:var(--cms-canvas);--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-canvas);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:var(--cms-elevated);--ui-bg-inverted:var(--cms-text);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-text);--ui-primary:var(--cms-text);--ui-accent:var(--cms-fern);--ui-error:var(--cms-error);--ui-success:var(--cms-success);--ui-warning:var(--cms-warning);--ui-radius:var(--cms-radius-md);color:var(--ui-text);font-family:var(--cms-font-ui);margin:0;padding:0;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:color-mix(in oklab,var(--cms-fern) 35%,transparent);color:var(--ui-text-highlighted)}body:has(.cms-scope) ::selection{background:color-mix(in oklab,var(--cms-fern) 35%,transparent);color:var(--ui-text-highlighted)}body:has(.cms-scope) :is(.cms-scope,.cms-scope *){scrollbar-color:var(--cms-line-strong) transparent;scrollbar-width:thin}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar{height:10px;width:10px}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-track{background:transparent}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-thumb{background:var(--cms-line-strong);background-clip:content-box;border:3px solid transparent;border-radius:9999px}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-thumb:hover{background:color-mix(in oklab,var(--cms-line-strong) 70%,var(--cms-text));background-clip:content-box}.cms-display,.cms-title{font-family:var(--cms-font-ui);font-weight:650;letter-spacing:-.01em}.cms-title{color:var(--ui-text-highlighted)}.cms-title-sm{font-size:1.125rem;line-height:1.4}.cms-title-md{font-size:1.5rem;line-height:1.25}.cms-title-lg{font-size:1.875rem;line-height:1.2}.cms-subtitle{color:var(--ui-text-muted);font-size:.875rem}.cms-accent{color:var(--cms-fern)}.cms-label{color:var(--ui-text-muted);font-size:.75rem;font-weight:500;line-height:1.4}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer select-none}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option]):focus-visible{outline:2px solid var(--cms-fern);outline-offset:2px}.cms-canvas{background-color:var(--cms-canvas)}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg)}.cms-panel{padding:1.75rem}body:has(.cms-scope),html:has(.cms-scope){overscroll-behavior-y:none}.cms-shell{display:flex;min-height:100vh}.cms-sidebar{background:var(--cms-surface);border-right:1px solid var(--cms-line);display:flex;flex-direction:column;flex-shrink:0;gap:1.75rem;height:100vh;padding:1.75rem 1rem;position:sticky;top:0;width:16rem}.cms-sidebar-brand{color:var(--ui-text-highlighted);font-family:var(--cms-font-ui);font-size:1.25rem;font-weight:650;letter-spacing:-.01em;padding-inline:.75rem}.cms-sidebar-nav{display:flex;flex:1;flex-direction:column;gap:1.5rem;overflow-y:auto;overscroll-behavior-y:none}.cms-sidebar-group{display:flex;flex-direction:column;gap:.25rem}.cms-sidebar-group>.cms-label{margin-bottom:.5rem;padding-inline:.75rem}.cms-sidebar-footer{border-top:1px solid var(--cms-line);display:flex;flex-direction:column;gap:.75rem;padding-top:1rem}.cms-sidebar-footer .cms-btn.is-block{color:var(--ui-text-muted);justify-content:space-between;padding-inline:.75rem}.cms-sidebar-footer .cms-btn.is-block:hover{background:var(--cms-field);color:var(--ui-text)}.cms-main{flex:1;height:100vh;min-width:0;overflow-y:auto;overscroll-behavior-y:none;padding:2.5rem}.cms-main-inner{margin-inline:auto;max-width:64rem;min-height:100%;width:100%}.cms-main-inner,.cms-page{display:flex;flex-direction:column}.cms-page{gap:1.75rem}.cms-page.is-fill{flex:1;min-height:0}.cms-page-header{align-items:flex-end;display:flex;gap:1rem;justify-content:space-between}.cms-page-heading{display:flex;flex-direction:column;gap:.5rem}.cms-toolbar{align-items:center;display:flex;gap:1.5rem}.cms-toolbar-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem;margin-left:auto}.cms-actions{align-items:center;display:flex;gap:.75rem}.cms-actions.is-end{justify-content:flex-end}.cms-spinner{color:var(--ui-text-dimmed);flex:1;justify-content:center;padding-block:2.5rem}.cms-empty,.cms-spinner{align-items:center;display:flex}.cms-empty{background:color-mix(in oklab,var(--cms-surface) 55%,transparent);border:1px dashed var(--cms-line-strong);border-radius:var(--cms-radius-lg);flex-direction:column;gap:.5rem;padding:3.5rem;text-align:center}.cms-empty.is-fill{flex:1;justify-content:center}.cms-empty-icon{color:var(--cms-fern);margin-bottom:.25rem}.cms-empty>.cms-btn{margin-top:.75rem}.cms-auth{align-items:center;display:flex;justify-content:center;min-height:100vh;padding:1.5rem}.cms-auth-panel{gap:2rem;max-width:24rem;width:100%}.cms-auth-header,.cms-auth-panel{display:flex;flex-direction:column}.cms-auth-header{align-items:center;gap:.75rem;text-align:center}.cms-auth-card{padding:1.75rem}.cms-navlink{align-items:center;border-radius:var(--cms-radius-md);color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-navlink:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-field);color:var(--cms-fern)}.cms-navlink-icon{color:var(--ui-text-dimmed)}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{align-items:center;border:1px solid transparent;border-radius:var(--cms-radius-md);display:inline-flex;font-family:var(--cms-font-mono);font-size:.6875rem;letter-spacing:.08em;line-height:1.4;padding:.1875rem .5rem;pointer-events:none;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-badge.is-published{background:color-mix(in oklab,var(--cms-success) 18%,transparent);border-color:color-mix(in oklab,var(--cms-success) 32%,transparent);color:var(--cms-success)}.cms-badge.is-muted{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-muted)}.cms-badge.is-draft{background:transparent}.cms-badge.is-draft,.cms-dropzone{border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-dropzone{align-items:center;background:color-mix(in oklab,var(--cms-surface) 55%,transparent);border-radius:var(--cms-radius-lg);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color var(--cms-duration) var(--cms-ease),background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-field);border-color:var(--cms-fern);color:var(--ui-text-highlighted)}.cms-dropzone.is-dense{flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{align-items:center;background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-muted);cursor:pointer;display:inline-flex;flex:none;font-size:.8125rem;gap:.35rem;line-height:1.4;padding:.3rem .8rem;transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-pill:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-pill.is-active{background:var(--cms-field);border-color:color-mix(in oklab,var(--cms-fern) 45%,transparent);color:var(--cms-fern)}.cms-pill.is-empty{border-style:dashed}.cms-pill-group{align-items:center;display:inline-flex;gap:.125rem}.cms-pill-group .cms-pill{padding-right:.6rem}.cms-pill-discard{align-items:center;border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;display:inline-flex;height:1.25rem;justify-content:center;transition:color var(--cms-duration) var(--cms-ease);width:1.25rem}.cms-pill-discard:hover{color:var(--ui-error)}.cms-folder-picker{align-items:center;display:flex;flex-wrap:wrap;gap:.375rem}.cms-folder-picker-input{width:11rem}.cms-form-hint{color:var(--ui-text-dimmed);font-size:.8125rem}.cms-dropzone-hint{align-items:center;color:var(--ui-text-dimmed);display:inline-flex;font-size:.75rem;gap:.3rem}.cms-block-label{color:var(--ui-text-toned);font-size:.875rem;font-weight:550;line-height:1.4;transition:color var(--cms-duration) var(--cms-ease)}.cms-block-label:hover{color:var(--ui-text-highlighted)}@keyframes cms-rise{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .3s var(--cms-ease) both}.cms-rise.is-delayed{animation-delay:90ms}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);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{color:var(--ui-text-highlighted);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-strong);color:var(--ui-text-muted);padding-left:.75rem}.cms-btn{align-items:center;border:1px solid transparent;border-radius:var(--cms-radius-md);display:inline-flex;font-weight:500;gap:.375rem;justify-content:center;line-height:1;min-height:var(--cms-btn-size);transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease),opacity var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-btn:active:not(:disabled,[aria-disabled=true]){scale:.99}.cms-btn:disabled,.cms-btn[aria-disabled=true]{opacity:.5;pointer-events:none}.cms-btn.is-block{width:100%}.cms-btn.is-icon{justify-content:center;padding-inline:0;width:var(--cms-btn-size)}.cms-btn-xs{--cms-btn-size:1.75rem;font-size:.875rem;padding:.25rem .625rem}.cms-btn-sm{--cms-btn-size:2rem;font-size:.875rem;padding:.25rem .75rem}.cms-btn-md{--cms-btn-size:2.25rem;font-size:.875rem;padding:.25rem .875rem}.cms-btn-lg{--cms-btn-size:2.625rem;font-size:.9375rem;padding:.375rem 1.125rem}.cms-btn-primary.cms-btn-solid{background:var(--ui-primary);color:var(--ui-text-inverted)}.cms-btn-primary.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-primary) 82%,#fff)}.cms-btn-primary.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-primary) 88%,#000)}.cms-btn-primary.cms-btn-soft{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-highlighted)}.cms-btn-primary.cms-btn-soft:hover{background:var(--cms-elevated);border-color:var(--cms-line-strong)}.cms-btn-primary.cms-btn-soft:active{background:var(--cms-field)}.cms-btn-primary.cms-btn-ghost{background:transparent;color:var(--ui-text-toned)}.cms-btn-primary.cms-btn-ghost:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-btn-primary.cms-btn-ghost:active{background:var(--cms-elevated)}.cms-btn-neutral.cms-btn-solid{background:var(--cms-line-strong);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-solid:hover{background:color-mix(in oklab,var(--cms-line-strong) 80%,var(--cms-text))}.cms-btn-neutral.cms-btn-solid:active{background:color-mix(in oklab,var(--cms-line-strong) 90%,#000)}.cms-btn-neutral.cms-btn-soft{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-soft:hover{background:var(--cms-elevated);border-color:var(--cms-line-strong)}.cms-btn-neutral.cms-btn-soft:active{background:var(--cms-field)}.cms-btn-neutral.cms-btn-ghost{background:transparent;color:var(--ui-text-toned)}.cms-btn-neutral.cms-btn-ghost:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-ghost:active{background:var(--cms-elevated)}.cms-btn-error.cms-btn-solid{background:var(--ui-error);color:var(--cms-canvas)}.cms-btn-error.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-error) 85%,#fff)}.cms-btn-error.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-error) 88%,#000)}.cms-btn-error.cms-btn-soft{background:color-mix(in oklab,var(--ui-error) 16%,transparent);border-color:color-mix(in oklab,var(--ui-error) 28%,transparent);color:var(--ui-error)}.cms-btn-error.cms-btn-soft:hover{background:color-mix(in oklab,var(--ui-error) 24%,transparent);border-color:color-mix(in oklab,var(--ui-error) 40%,transparent)}.cms-btn-error.cms-btn-soft:active{background:color-mix(in oklab,var(--ui-error) 16%,transparent)}.cms-btn-error.cms-btn-ghost{background:transparent;color:var(--ui-error)}.cms-btn-error.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-error) 14%,transparent)}.cms-btn-error.cms-btn-ghost:active{background:color-mix(in oklab,var(--ui-error) 22%,transparent)}.cms-btn-success.cms-btn-solid{background:var(--ui-success);color:var(--cms-canvas)}.cms-btn-success.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-success) 85%,#fff)}.cms-btn-success.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-success) 88%,#000)}.cms-btn-success.cms-btn-soft{background:color-mix(in oklab,var(--ui-success) 16%,transparent);border-color:color-mix(in oklab,var(--ui-success) 28%,transparent);color:var(--ui-success)}.cms-btn-success.cms-btn-soft:hover{background:color-mix(in oklab,var(--ui-success) 24%,transparent);border-color:color-mix(in oklab,var(--ui-success) 40%,transparent)}.cms-btn-success.cms-btn-soft:active{background:color-mix(in oklab,var(--ui-success) 16%,transparent)}.cms-btn-success.cms-btn-ghost{background:transparent;color:var(--ui-success)}.cms-btn-success.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-success) 14%,transparent)}.cms-btn-success.cms-btn-ghost:active{background:color-mix(in oklab,var(--ui-success) 22%,transparent)}.cms-form{gap:1.25rem}.cms-form,.cms-form-field{display:flex;flex-direction:column}.cms-form-field{gap:.375rem}.cms-form-label-row{align-items:center;display:flex;gap:.75rem;justify-content:space-between;min-height:1.75rem}.cms-form-label{color:var(--ui-text-toned);font-size:.875rem;font-weight:500}.cms-form-error,.cms-form-required{color:var(--ui-error)}.cms-form-error{font-size:.875rem}.cms-form-actions{align-items:center;display:flex;gap:.75rem;justify-content:flex-end;padding-top:.25rem}.cms-field{background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--ui-radius);color:var(--ui-text-highlighted);line-height:1.25;transition:border-color var(--cms-duration) var(--cms-ease),box-shadow var(--cms-duration) var(--cms-ease);width:100%}textarea.cms-field{line-height:1.5;min-height:0;padding-bottom:.625rem;padding-top:.625rem;resize:vertical}.cms-field::-moz-placeholder{color:var(--ui-text-dimmed)}.cms-field::placeholder{color:var(--ui-text-dimmed)}.cms-field:hover:not(:focus,:focus-within){border-color:var(--cms-line-strong)}.cms-field:focus,.cms-field:focus-within{border-color:var(--cms-fern);box-shadow:0 0 0 3px color-mix(in oklab,var(--cms-fern) 25%,transparent);outline:none}.cms-field.is-error{border-color:var(--ui-error)}.cms-field.is-error:focus,.cms-field.is-error:focus-within{border-color:var(--ui-error);box-shadow:0 0 0 3px color-mix(in oklab,var(--ui-error) 25%,transparent)}.cms-field-sm{font-size:.875rem;min-height:2rem;padding:.25rem .625rem}.cms-field-md{font-size:.875rem;min-height:2.25rem;padding:.25rem .75rem}.cms-field-lg{font-size:.9375rem;min-height:2.625rem;padding:.375rem .875rem}.cms-select-chevron{align-items:center;cursor:pointer;display:inline-flex;transition:transform var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-select-chevron:hover{color:var(--ui-text-muted)}.cms-select-chevron.is-open{transform:rotate(180deg)}.cms-locale-switch{align-items:center;background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--cms-radius-md);display:inline-flex;gap:2px;padding:2px}.cms-locale-option{border-radius:var(--cms-radius-sm);color:var(--ui-text-muted);cursor:pointer;font-family:var(--cms-font-mono);font-size:.6875rem;letter-spacing:.08em;line-height:1.4;padding:.125rem .5rem;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-locale-option:hover:not(.is-active){color:var(--ui-text-highlighted)}.cms-locale-option.is-empty{color:var(--ui-text-dimmed)}.cms-locale-option.is-active{background:var(--cms-elevated);color:var(--cms-fern)}.cms-input-wrap{align-items:center;display:flex;position:relative;width:100%}.cms-input-wrap .cms-field{width:100%}.cms-input-lead,.cms-input-trail{align-items:center;color:var(--ui-text-dimmed);display:flex;position:absolute}.cms-input-lead{left:.625rem}.cms-input-trail{right:.375rem}.cms-input-wrap.has-lead .cms-field{padding-left:2rem}.cms-switch{align-items:center;background:var(--cms-line-strong);border-radius:9999px;display:inline-flex;flex-shrink:0;height:1.25rem;position:relative;transition:background-color var(--cms-duration) var(--cms-ease);width:2.25rem}.cms-switch.is-on{background:var(--cms-fern)}.cms-switch:after{background:var(--cms-text);border-radius:9999px;content:"";height:.875rem;left:.1875rem;position:absolute;transition:transform var(--cms-duration) var(--cms-ease);width:.875rem}.cms-switch.is-on:after{background:var(--cms-canvas);transform:translateX(1rem)}.cms-overlay{align-items:center;background:var(--cms-scrim);display:flex;inset:0;justify-content:center;overflow-y:auto;padding:2rem 1rem;position:fixed;z-index:60}.cms-modal{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);display:flex;flex-direction:column;max-height:calc(100dvh - 4rem);max-width:32rem;width:100%}.cms-modal.is-sm{max-width:28rem}.cms-modal.is-lg{max-width:48rem}.cms-modal-header{padding:1.25rem 1.5rem 0}.cms-modal-body{flex:1;min-height:0;overflow-y:auto;padding:1.25rem 1.5rem 1.5rem}.cms-modal-enter-active,.cms-modal-leave-active{transition:opacity var(--cms-duration-lg) var(--cms-ease)}.cms-modal-enter-active .cms-modal,.cms-modal-leave-active .cms-modal{transition:transform var(--cms-duration-lg) var(--cms-ease),opacity var(--cms-duration-lg) var(--cms-ease);will-change:transform}.cms-modal-enter-from,.cms-modal-leave-to{opacity:0}.cms-modal-enter-from .cms-modal,.cms-modal-leave-to .cms-modal{opacity:0;transform:translateY(8px)}.cms-overlay-drawer{align-items:stretch;justify-content:flex-end;overflow:hidden;padding:0;z-index:55}.cms-drawer{background:var(--cms-surface);border-left:1px solid var(--cms-line);border-radius:var(--cms-radius-lg) 0 0 var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);display:flex;flex-direction:column;height:100%;max-width:46rem;width:100%}.cms-drawer-header{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;gap:1rem;justify-content:space-between;padding:1.25rem 1.75rem}.cms-drawer-body{flex:1;min-height:0;overflow-y:auto;padding:1.75rem}.cms-drawer-footer{align-items:center;border-top:1px solid var(--cms-line);display:flex;gap:1rem;justify-content:space-between;padding:1.25rem 1.75rem}.cms-drawer-enter-active,.cms-drawer-leave-active{transition:opacity var(--cms-duration-lg) var(--cms-ease)}.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active .cms-drawer{transition:transform var(--cms-duration-lg) var(--cms-ease);will-change:transform}.cms-drawer-enter-from,.cms-drawer-leave-to{opacity:0}.cms-drawer-enter-from .cms-drawer,.cms-drawer-leave-to .cms-drawer{transform:translateX(100%)}@media (prefers-reduced-motion:reduce){.cms-drawer-enter-active,.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active,.cms-drawer-leave-active .cms-drawer,.cms-modal-enter-active,.cms-modal-enter-active .cms-modal,.cms-modal-leave-active,.cms-modal-leave-active .cms-modal{transition-duration:.01ms}}.cms-menu{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);margin-top:.375rem;min-width:12rem;padding:.25rem;position:absolute;z-index:50}.cms-menu.align-end{right:0}.cms-menu-item{align-items:center;border-radius:var(--cms-radius-sm);color:var(--ui-text);cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;padding:.4rem .6rem;text-align:left;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);width:100%}.cms-menu-item:hover{background:color-mix(in oklab,var(--cms-text) 8%,transparent);color:var(--ui-text-highlighted)}.cms-menu-check{color:var(--cms-fern);margin-left:auto}.cms-popover{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);margin-top:.375rem;position:absolute;z-index:50}.cms-selectmenu-input{background:transparent;color:var(--ui-text-highlighted);flex:1;min-width:4rem;outline:none}.cms-selectmenu-affix{color:var(--ui-text-dimmed);flex-shrink:0}.cms-selectmenu-panel{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);left:0;margin-top:.375rem;max-height:15rem;overflow-y:auto;padding:.25rem;position:absolute;right:0;z-index:50}.cms-tag{align-items:center;background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-highlighted);display:inline-flex;font-size:.8125rem;gap:.25rem;padding:.125rem .5rem}.cms-media-gallery{display:flex;flex-direction:column;gap:1.25rem}.cms-media-source{color:var(--ui-text-muted);font-size:.8125rem;margin-top:-.75rem}.cms-media-filters{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.cms-media-filters-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem;margin-left:auto}.cms-media-filter-separator{background:var(--cms-line);flex:none;height:1rem;margin-inline:.25rem;width:1px}.cms-media-grid{display:grid;gap:1rem;grid-template-columns:repeat(2,minmax(0,1fr))}@media (min-width:640px){.cms-media-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.cms-media-grid:not(.is-compact){grid-template-columns:repeat(4,minmax(0,1fr))}}.cms-media-tile{overflow:hidden;text-align:left;transition:border-color var(--cms-duration) var(--cms-ease)}.cms-media-tile.is-selectable{cursor:pointer}.cms-media-tile.is-selectable:hover{border-color:var(--cms-line-strong)}.cms-media-preview{align-items:center;aspect-ratio:1;background:var(--cms-field);color:var(--ui-text-dimmed);display:flex;justify-content:center;position:relative}.cms-media-preview>img,.cms-media-preview>video{height:100%;inset:0;-o-object-fit:cover;object-fit:cover;position:absolute;width:100%}.cms-media-actions{display:flex;gap:.25rem;opacity:0;position:absolute;right:.5rem;top:.5rem;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-media-actions:focus-within,.cms-media-tile:hover .cms-media-actions{opacity:1}.cms-media-info{background:linear-gradient(0deg,rgba(0,0,0,.75),transparent);bottom:0;color:var(--cms-text);font-family:var(--cms-font-mono);font-size:.75rem;left:0;letter-spacing:.08em;line-height:1.4;opacity:0;padding:1.5rem .75rem .5rem;pointer-events:none;position:absolute;right:0;text-transform:uppercase;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-media-tile:focus-visible .cms-media-info,.cms-media-tile:hover .cms-media-info{opacity:1}.cms-media-meta{display:flex;flex-direction:column;gap:.25rem;padding:.625rem .875rem}.cms-media-name{color:var(--ui-text-highlighted);font-size:.875rem;font-weight:500}.cms-media-folder,.cms-media-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-media-folder{align-items:center;color:var(--ui-text-dimmed);display:inline-flex;font-size:.75rem;gap:.25rem}.cms-media-field{overflow:hidden}.cms-media-field-preview{align-items:center;background-color:var(--cms-canvas);background-image:repeating-conic-gradient(from 0deg,hsla(0,0%,100%,.045) 0 25%,hsla(0,0%,100%,0) 0 50%);background-position:50%;background-size:16px 16px;color:var(--ui-text-dimmed);display:flex;justify-content:center;max-height:16rem;min-height:8rem;padding:.75rem}.cms-media-field-preview>img,.cms-media-field-preview>video{max-height:14.5rem;max-width:100%;-o-object-fit:contain;object-fit:contain}.cms-media-field-bar{align-items:center;background:var(--cms-surface);border-top:1px solid var(--cms-line);display:flex;gap:.5rem;padding:.625rem .75rem}.cms-media-bar-icon{color:var(--ui-text-dimmed);flex-shrink:0}.cms-media-bar-name{color:var(--ui-text-highlighted);flex:1;font-size:.875rem;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-media-thumb{background:var(--cms-field);border-radius:var(--cms-radius-sm);height:2.25rem;-o-object-fit:cover;object-fit:cover;width:2.25rem}.cms-media-thumb.is-fallback{align-items:center;color:var(--ui-text-dimmed);display:flex;justify-content:center}.cms-media-thumb-empty{color:var(--ui-text-dimmed)}.cms-table-shell{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden}.cms-table-scroll{flex:1;min-height:0;overflow:auto}.cms-table{border-collapse:collapse;min-width:-moz-max-content;min-width:max-content;text-align:left;width:100%}.cms-table td,.cms-table th{white-space:nowrap}.cms-table thead{background:var(--cms-surface);position:sticky;top:0;z-index:10}.cms-table th{background:var(--cms-surface);box-shadow:inset 0 -1px 0 var(--cms-line);color:var(--ui-text-muted);font-family:var(--cms-font-mono);font-size:.6875rem;font-weight:400;letter-spacing:.08em;line-height:1.4;padding:.75rem 1.25rem;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-table td{color:var(--ui-text);font-size:.875rem;padding:.875rem 1.25rem}.cms-table tbody tr{border-bottom:1px solid var(--cms-line);transition:background-color var(--cms-duration) var(--cms-ease)}.cms-table tbody tr:last-child{border-bottom:0}.cms-table tbody tr:hover{background:var(--cms-field)}.cms-table-cell-actions{display:flex;justify-content:flex-end}.cms-table th[draggable=true]{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.cms-table th[draggable=true]:active{cursor:grabbing}.cms-table th.is-dragging{opacity:.4}.cms-table th.is-drop-target{background:var(--cms-field);box-shadow:inset 0 -1px 0 var(--cms-line),inset 0 0 0 2px color-mix(in oklab,var(--cms-fern) 55%,transparent)}.cms-th-label{align-items:center;display:inline-flex;gap:.35rem}.cms-table th.is-sortable{cursor:pointer}.cms-table th.is-sortable:hover,.cms-table th.is-sorted{color:var(--ui-text-highlighted)}.cms-table th.is-sorted{background:var(--cms-field)}.cms-th-sort{flex:none;opacity:0;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-table th.is-sortable:hover .cms-th-sort,.cms-table th.is-sorted .cms-th-sort{opacity:1}.cms-alert{border:1px solid transparent;border-radius:var(--cms-radius-md);display:flex;flex-direction:column;font-size:.875rem;gap:.125rem;padding:.75rem 1rem}.cms-alert-title{font-weight:550}.cms-alert-error{background:color-mix(in oklab,var(--ui-error) 14%,transparent);border-color:color-mix(in oklab,var(--ui-error) 26%,transparent);color:var(--ui-error)}.cms-alert-success{background:color-mix(in oklab,var(--ui-success) 14%,transparent);border-color:color-mix(in oklab,var(--ui-success) 26%,transparent);color:var(--ui-success)}.cms-alert-warning{background:color-mix(in oklab,var(--ui-warning) 14%,transparent);border-color:color-mix(in oklab,var(--ui-warning) 26%,transparent);color:var(--ui-warning)}.cms-alert-neutral{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-toned)}.cms-alert-neutral .cms-alert-title{color:var(--ui-text-highlighted)}.cms-toaster{bottom:1rem;display:flex;flex-direction:column;gap:.5rem;max-width:calc(100vw - 2rem);position:fixed;right:1rem;width:20rem;z-index:80}.cms-toast{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);padding:.75rem .875rem}.cms-toast-row{align-items:flex-start;display:flex;gap:.625rem}.cms-toast-icon{color:var(--ui-text-muted);flex-shrink:0;margin-top:.0625rem}.cms-toast.is-success .cms-toast-icon{color:var(--ui-success)}.cms-toast.is-error .cms-toast-icon{color:var(--ui-error)}.cms-toast-title{color:var(--ui-text-highlighted);font-size:.875rem;font-weight:500}.cms-toast-description{color:var(--ui-text-muted);font-size:.875rem;margin-top:.125rem}.cms-toast-dismiss{color:var(--ui-text-dimmed);flex-shrink:0;transition:color var(--cms-duration) var(--cms-ease)}.cms-toast-dismiss:hover{color:var(--ui-text-highlighted)}.cms-pagination{align-items:center;display:flex;gap:.25rem;justify-content:center}.cms-page-btn{border-radius:var(--cms-radius-sm);color:var(--ui-text-toned);font-size:.875rem;height:2rem;min-width:2rem;padding:0 .5rem;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-page-btn:hover:not(:disabled){background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-page-btn.is-active{background:var(--cms-field);color:var(--cms-fern);font-weight:600}.cms-page-btn:disabled{opacity:.4}
|
|
1
|
+
@layer theme, base, components, utilities;@import "tailwindcss/theme.css" layer(theme) theme(inline);.cms-scope{@import "tailwindcss/preflight.css" layer(base);@import "tailwindcss/utilities.css" layer(utilities) source(none)}@source "../app";.cms-scope,body:has(.cms-scope){--cms-canvas:#1b1b1e;--cms-surface:#222226;--cms-field:#2a2a2f;--cms-elevated:#2f2f35;--cms-line:#35353b;--cms-line-strong:#46464e;--cms-text:#ededef;--cms-text-toned:#c2c2c8;--cms-text-muted:#9a9aa3;--cms-text-dimmed:#6e6e78;--cms-fern:#8fae8b;--cms-error:#f0857d;--cms-success:#7fb083;--cms-warning:#d4ad76;--cms-font-ui:"Hanken Grotesk Variable","Hanken Grotesk",ui-sans-serif,system-ui,sans-serif;--cms-font-mono:"Fragment Mono",ui-monospace,SFMono-Regular,Menlo,monospace;--cms-radius-sm:6px;--cms-radius-md:8px;--cms-radius-lg:12px;--cms-shadow-overlay:0 8px 24px rgba(0,0,0,.35);--cms-scrim:rgba(0,0,0,.5);--cms-duration:120ms;--cms-duration-lg:250ms;--cms-ease:cubic-bezier(0.2,0,0,1);color-scheme:dark;--ui-text-dimmed:var(--cms-text-dimmed);--ui-text-muted:var(--cms-text-muted);--ui-text-toned:var(--cms-text-toned);--ui-text:var(--cms-text-toned);--ui-text-highlighted:var(--cms-text);--ui-text-inverted:var(--cms-canvas);--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-canvas);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:var(--cms-elevated);--ui-bg-inverted:var(--cms-text);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-text);--ui-primary:var(--cms-text);--ui-accent:var(--cms-fern);--ui-error:var(--cms-error);--ui-success:var(--cms-success);--ui-warning:var(--cms-warning);--ui-radius:var(--cms-radius-md);color:var(--ui-text);font-family:var(--cms-font-ui);margin:0;padding:0;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:color-mix(in oklab,var(--cms-fern) 35%,transparent);color:var(--ui-text-highlighted)}body:has(.cms-scope) ::selection{background:color-mix(in oklab,var(--cms-fern) 35%,transparent);color:var(--ui-text-highlighted)}body:has(.cms-scope) :is(.cms-scope,.cms-scope *){scrollbar-color:var(--cms-line-strong) transparent;scrollbar-width:thin}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar{height:10px;width:10px}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-track{background:transparent}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-thumb{background:var(--cms-line-strong);background-clip:content-box;border:3px solid transparent;border-radius:9999px}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-thumb:hover{background:color-mix(in oklab,var(--cms-line-strong) 70%,var(--cms-text));background-clip:content-box}.cms-display,.cms-title{font-family:var(--cms-font-ui);font-weight:650;letter-spacing:-.01em}.cms-title{color:var(--ui-text-highlighted)}.cms-title-sm{font-size:1.125rem;line-height:1.4}.cms-title-md{font-size:1.5rem;line-height:1.25}.cms-title-lg{font-size:1.875rem;line-height:1.2}.cms-subtitle{color:var(--ui-text-muted);font-size:.875rem}.cms-accent{color:var(--cms-fern)}.cms-label{color:var(--ui-text-muted);font-size:.75rem;font-weight:500;line-height:1.4}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer select-none}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option]):focus-visible{outline:2px solid var(--cms-fern);outline-offset:2px}.cms-canvas{background-color:var(--cms-canvas)}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg)}.cms-panel{padding:1.75rem}body:has(.cms-scope),html:has(.cms-scope){overscroll-behavior-y:none}.cms-shell{display:flex;min-height:100vh}.cms-sidebar{background:var(--cms-surface);border-right:1px solid var(--cms-line);display:flex;flex-direction:column;flex-shrink:0;gap:1.75rem;height:100vh;padding:1.75rem 1rem;position:sticky;top:0;width:16rem}.cms-sidebar-brand{color:var(--ui-text-highlighted);font-family:var(--cms-font-ui);font-size:1.25rem;font-weight:650;letter-spacing:-.01em;padding-inline:.75rem}.cms-sidebar-nav{display:flex;flex:1;flex-direction:column;gap:1.5rem;overflow-y:auto;overscroll-behavior-y:none}.cms-sidebar-group{display:flex;flex-direction:column;gap:.25rem}.cms-sidebar-group>.cms-label{margin-bottom:.5rem;padding-inline:.75rem}.cms-sidebar-footer{border-top:1px solid var(--cms-line);display:flex;flex-direction:column;gap:.75rem;padding-top:1rem}.cms-sidebar-footer .cms-btn.is-block{color:var(--ui-text-muted);justify-content:space-between;padding-inline:.75rem}.cms-sidebar-footer .cms-btn.is-block:hover{background:var(--cms-field);color:var(--ui-text)}.cms-main{flex:1;height:100vh;min-width:0;overflow-y:auto;overscroll-behavior-y:none;padding:2.5rem}.cms-main-inner{margin-inline:auto;max-width:64rem;min-height:100%;width:100%}.cms-main-inner,.cms-page{display:flex;flex-direction:column}.cms-page{gap:1.75rem}.cms-page.is-fill{flex:1;min-height:0}.cms-page-header{align-items:flex-end;display:flex;gap:1rem;justify-content:space-between}.cms-page-heading{display:flex;flex-direction:column;gap:.5rem}.cms-breadcrumb{align-items:center;color:var(--ui-text-muted);display:flex;font-size:.75rem;font-weight:500;gap:.375rem;line-height:1.4}.cms-breadcrumb-link{border-radius:.25rem;padding:.0625rem .125rem;transition:color .15s ease}.cms-breadcrumb-link:hover{color:var(--ui-text-highlighted)}.cms-breadcrumb-separator{color:var(--ui-text-dimmed)}.cms-tree-row{align-items:center;display:flex;gap:.75rem;padding:.625rem 1rem;transition:background-color .15s ease}.cms-tree-row:hover{background-color:var(--ui-bg-elevated)}.cms-tree-branch{color:var(--ui-text-dimmed);flex-shrink:0;width:1rem}.cms-toolbar{align-items:center;display:flex;gap:1.5rem}.cms-toolbar-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem;margin-left:auto}.cms-actions{align-items:center;display:flex;gap:.75rem}.cms-actions.is-end{justify-content:flex-end}.cms-spinner{color:var(--ui-text-dimmed);flex:1;justify-content:center;padding-block:2.5rem}.cms-empty,.cms-spinner{align-items:center;display:flex}.cms-empty{background:color-mix(in oklab,var(--cms-surface) 55%,transparent);border:1px dashed var(--cms-line-strong);border-radius:var(--cms-radius-lg);flex-direction:column;gap:.5rem;padding:3.5rem;text-align:center}.cms-empty.is-fill{flex:1;justify-content:center}.cms-empty-icon{color:var(--cms-fern);margin-bottom:.25rem}.cms-empty>.cms-btn{margin-top:.75rem}.cms-auth{align-items:center;display:flex;justify-content:center;min-height:100vh;padding:1.5rem}.cms-auth-panel{gap:2rem;max-width:24rem;width:100%}.cms-auth-header,.cms-auth-panel{display:flex;flex-direction:column}.cms-auth-header{align-items:center;gap:.75rem;text-align:center}.cms-auth-card{padding:1.75rem}.cms-navlink{align-items:center;border-radius:var(--cms-radius-md);color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-navlink:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-field);color:var(--cms-fern)}.cms-navlink-icon{color:var(--ui-text-dimmed)}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{align-items:center;border:1px solid transparent;border-radius:var(--cms-radius-md);display:inline-flex;font-family:var(--cms-font-mono);font-size:.6875rem;letter-spacing:.08em;line-height:1.4;padding:.1875rem .5rem;pointer-events:none;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-badge.is-published{background:color-mix(in oklab,var(--cms-success) 18%,transparent);border-color:color-mix(in oklab,var(--cms-success) 32%,transparent);color:var(--cms-success)}.cms-badge.is-muted{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-muted)}.cms-badge.is-draft{background:transparent}.cms-badge.is-draft,.cms-dropzone{border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-dropzone{align-items:center;background:color-mix(in oklab,var(--cms-surface) 55%,transparent);border-radius:var(--cms-radius-lg);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color var(--cms-duration) var(--cms-ease),background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-field);border-color:var(--cms-fern);color:var(--ui-text-highlighted)}.cms-dropzone.is-dense{flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{align-items:center;background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-muted);cursor:pointer;display:inline-flex;flex:none;font-size:.8125rem;gap:.35rem;line-height:1.4;padding:.3rem .8rem;transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-pill:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-pill.is-active{background:var(--cms-field);border-color:color-mix(in oklab,var(--cms-fern) 45%,transparent);color:var(--cms-fern)}.cms-pill.is-empty{border-style:dashed}.cms-pill-group{align-items:center;display:inline-flex;gap:.125rem}.cms-pill-group .cms-pill{padding-right:.6rem}.cms-pill-discard{align-items:center;border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;display:inline-flex;height:1.25rem;justify-content:center;transition:color var(--cms-duration) var(--cms-ease);width:1.25rem}.cms-pill-discard:hover{color:var(--ui-error)}.cms-folder-picker{align-items:center;display:flex;flex-wrap:wrap;gap:.375rem}.cms-folder-picker-input{width:11rem}.cms-form-hint{color:var(--ui-text-dimmed);font-size:.8125rem}.cms-dropzone-hint{align-items:center;color:var(--ui-text-dimmed);display:inline-flex;font-size:.75rem;gap:.3rem}.cms-block-label{color:var(--ui-text-toned);font-size:.875rem;font-weight:550;line-height:1.4;transition:color var(--cms-duration) var(--cms-ease)}.cms-block-label:hover{color:var(--ui-text-highlighted)}@keyframes cms-rise{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .3s var(--cms-ease) both}.cms-rise.is-delayed{animation-delay:90ms}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);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{color:var(--ui-text-highlighted);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-strong);color:var(--ui-text-muted);padding-left:.75rem}.cms-btn{align-items:center;border:1px solid transparent;border-radius:var(--cms-radius-md);display:inline-flex;font-weight:500;gap:.375rem;justify-content:center;line-height:1;min-height:var(--cms-btn-size);transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease),opacity var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-btn:active:not(:disabled,[aria-disabled=true]){scale:.99}.cms-btn:disabled,.cms-btn[aria-disabled=true]{opacity:.5;pointer-events:none}.cms-btn.is-block{width:100%}.cms-btn.is-icon{justify-content:center;padding-inline:0;width:var(--cms-btn-size)}.cms-btn-xs{--cms-btn-size:1.75rem;font-size:.875rem;padding:.25rem .625rem}.cms-btn-sm{--cms-btn-size:2rem;font-size:.875rem;padding:.25rem .75rem}.cms-btn-md{--cms-btn-size:2.25rem;font-size:.875rem;padding:.25rem .875rem}.cms-btn-lg{--cms-btn-size:2.625rem;font-size:.9375rem;padding:.375rem 1.125rem}.cms-btn-primary.cms-btn-solid{background:var(--ui-primary);color:var(--ui-text-inverted)}.cms-btn-primary.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-primary) 82%,#fff)}.cms-btn-primary.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-primary) 88%,#000)}.cms-btn-primary.cms-btn-soft{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-highlighted)}.cms-btn-primary.cms-btn-soft:hover{background:var(--cms-elevated);border-color:var(--cms-line-strong)}.cms-btn-primary.cms-btn-soft:active{background:var(--cms-field)}.cms-btn-primary.cms-btn-ghost{background:transparent;color:var(--ui-text-toned)}.cms-btn-primary.cms-btn-ghost:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-btn-primary.cms-btn-ghost:active{background:var(--cms-elevated)}.cms-btn-neutral.cms-btn-solid{background:var(--cms-line-strong);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-solid:hover{background:color-mix(in oklab,var(--cms-line-strong) 80%,var(--cms-text))}.cms-btn-neutral.cms-btn-solid:active{background:color-mix(in oklab,var(--cms-line-strong) 90%,#000)}.cms-btn-neutral.cms-btn-soft{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-soft:hover{background:var(--cms-elevated);border-color:var(--cms-line-strong)}.cms-btn-neutral.cms-btn-soft:active{background:var(--cms-field)}.cms-btn-neutral.cms-btn-ghost{background:transparent;color:var(--ui-text-toned)}.cms-btn-neutral.cms-btn-ghost:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-ghost:active{background:var(--cms-elevated)}.cms-btn-error.cms-btn-solid{background:var(--ui-error);color:var(--cms-canvas)}.cms-btn-error.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-error) 85%,#fff)}.cms-btn-error.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-error) 88%,#000)}.cms-btn-error.cms-btn-soft{background:color-mix(in oklab,var(--ui-error) 16%,transparent);border-color:color-mix(in oklab,var(--ui-error) 28%,transparent);color:var(--ui-error)}.cms-btn-error.cms-btn-soft:hover{background:color-mix(in oklab,var(--ui-error) 24%,transparent);border-color:color-mix(in oklab,var(--ui-error) 40%,transparent)}.cms-btn-error.cms-btn-soft:active{background:color-mix(in oklab,var(--ui-error) 16%,transparent)}.cms-btn-error.cms-btn-ghost{background:transparent;color:var(--ui-error)}.cms-btn-error.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-error) 14%,transparent)}.cms-btn-error.cms-btn-ghost:active{background:color-mix(in oklab,var(--ui-error) 22%,transparent)}.cms-btn-success.cms-btn-solid{background:var(--ui-success);color:var(--cms-canvas)}.cms-btn-success.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-success) 85%,#fff)}.cms-btn-success.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-success) 88%,#000)}.cms-btn-success.cms-btn-soft{background:color-mix(in oklab,var(--ui-success) 16%,transparent);border-color:color-mix(in oklab,var(--ui-success) 28%,transparent);color:var(--ui-success)}.cms-btn-success.cms-btn-soft:hover{background:color-mix(in oklab,var(--ui-success) 24%,transparent);border-color:color-mix(in oklab,var(--ui-success) 40%,transparent)}.cms-btn-success.cms-btn-soft:active{background:color-mix(in oklab,var(--ui-success) 16%,transparent)}.cms-btn-success.cms-btn-ghost{background:transparent;color:var(--ui-success)}.cms-btn-success.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-success) 14%,transparent)}.cms-btn-success.cms-btn-ghost:active{background:color-mix(in oklab,var(--ui-success) 22%,transparent)}.cms-form{gap:1.25rem}.cms-form,.cms-form-field{display:flex;flex-direction:column}.cms-form-field{gap:.375rem}.cms-form-label-row{align-items:center;display:flex;gap:.75rem;justify-content:space-between;min-height:1.75rem}.cms-form-label{color:var(--ui-text-toned);font-size:.875rem;font-weight:500}.cms-form-error,.cms-form-required{color:var(--ui-error)}.cms-form-error{font-size:.875rem}.cms-form-actions{align-items:center;display:flex;gap:.75rem;justify-content:flex-end;padding-top:.25rem}.cms-field{background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--ui-radius);color:var(--ui-text-highlighted);line-height:1.25;transition:border-color var(--cms-duration) var(--cms-ease),box-shadow var(--cms-duration) var(--cms-ease);width:100%}textarea.cms-field{line-height:1.5;min-height:0;padding-bottom:.625rem;padding-top:.625rem;resize:vertical}.cms-field::-moz-placeholder{color:var(--ui-text-dimmed)}.cms-field::placeholder{color:var(--ui-text-dimmed)}.cms-field:hover:not(:focus,:focus-within){border-color:var(--cms-line-strong)}.cms-field:focus,.cms-field:focus-within{border-color:var(--cms-fern);box-shadow:0 0 0 3px color-mix(in oklab,var(--cms-fern) 25%,transparent);outline:none}.cms-field.is-error{border-color:var(--ui-error)}.cms-field.is-error:focus,.cms-field.is-error:focus-within{border-color:var(--ui-error);box-shadow:0 0 0 3px color-mix(in oklab,var(--ui-error) 25%,transparent)}.cms-field-sm{font-size:.875rem;min-height:2rem;padding:.25rem .625rem}.cms-field-md{font-size:.875rem;min-height:2.25rem;padding:.25rem .75rem}.cms-field-lg{font-size:.9375rem;min-height:2.625rem;padding:.375rem .875rem}.cms-select-chevron{align-items:center;cursor:pointer;display:inline-flex;transition:transform var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-select-chevron:hover{color:var(--ui-text-muted)}.cms-select-chevron.is-open{transform:rotate(180deg)}.cms-locale-switch{align-items:center;background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--cms-radius-md);display:inline-flex;gap:2px;padding:2px}.cms-locale-option{border-radius:var(--cms-radius-sm);color:var(--ui-text-muted);cursor:pointer;font-family:var(--cms-font-mono);font-size:.6875rem;letter-spacing:.08em;line-height:1.4;padding:.125rem .5rem;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-locale-option:hover:not(.is-active){color:var(--ui-text-highlighted)}.cms-locale-option.is-empty{color:var(--ui-text-dimmed)}.cms-locale-option.is-active{background:var(--cms-elevated);color:var(--cms-fern)}.cms-input-wrap{align-items:center;display:flex;position:relative;width:100%}.cms-input-wrap .cms-field{width:100%}.cms-input-lead,.cms-input-trail{align-items:center;color:var(--ui-text-dimmed);display:flex;position:absolute}.cms-input-lead{left:.625rem}.cms-input-trail{right:.375rem}.cms-input-wrap.has-lead .cms-field{padding-left:2rem}.cms-switch{align-items:center;background:var(--cms-line-strong);border-radius:9999px;display:inline-flex;flex-shrink:0;height:1.25rem;position:relative;transition:background-color var(--cms-duration) var(--cms-ease);width:2.25rem}.cms-switch.is-on{background:var(--cms-fern)}.cms-switch:after{background:var(--cms-text);border-radius:9999px;content:"";height:.875rem;left:.1875rem;position:absolute;transition:transform var(--cms-duration) var(--cms-ease);width:.875rem}.cms-switch.is-on:after{background:var(--cms-canvas);transform:translateX(1rem)}.cms-overlay{align-items:center;background:var(--cms-scrim);display:flex;inset:0;justify-content:center;overflow-y:auto;padding:2rem 1rem;position:fixed;z-index:60}.cms-modal{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);display:flex;flex-direction:column;max-height:calc(100dvh - 4rem);max-width:32rem;width:100%}.cms-modal.is-sm{max-width:28rem}.cms-modal.is-lg{max-width:48rem}.cms-modal-header{padding:1.25rem 1.5rem 0}.cms-modal-body{flex:1;min-height:0;overflow-y:auto;padding:1.25rem 1.5rem 1.5rem}.cms-modal-enter-active,.cms-modal-leave-active{transition:opacity var(--cms-duration-lg) var(--cms-ease)}.cms-modal-enter-active .cms-modal,.cms-modal-leave-active .cms-modal{transition:transform var(--cms-duration-lg) var(--cms-ease),opacity var(--cms-duration-lg) var(--cms-ease);will-change:transform}.cms-modal-enter-from,.cms-modal-leave-to{opacity:0}.cms-modal-enter-from .cms-modal,.cms-modal-leave-to .cms-modal{opacity:0;transform:translateY(8px)}.cms-overlay-drawer{align-items:stretch;justify-content:flex-end;overflow:hidden;padding:0;z-index:55}.cms-drawer{background:var(--cms-surface);border-left:1px solid var(--cms-line);border-radius:var(--cms-radius-lg) 0 0 var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);display:flex;flex-direction:column;height:100%;max-width:46rem;width:100%}.cms-drawer-header{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;gap:1rem;justify-content:space-between;padding:1.25rem 1.75rem}.cms-drawer-body{flex:1;min-height:0;overflow-y:auto;padding:1.75rem}.cms-drawer-footer{align-items:center;border-top:1px solid var(--cms-line);display:flex;gap:1rem;justify-content:space-between;padding:1.25rem 1.75rem}.cms-drawer-enter-active,.cms-drawer-leave-active{transition:opacity var(--cms-duration-lg) var(--cms-ease)}.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active .cms-drawer{transition:transform var(--cms-duration-lg) var(--cms-ease);will-change:transform}.cms-drawer-enter-from,.cms-drawer-leave-to{opacity:0}.cms-drawer-enter-from .cms-drawer,.cms-drawer-leave-to .cms-drawer{transform:translateX(100%)}@media (prefers-reduced-motion:reduce){.cms-drawer-enter-active,.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active,.cms-drawer-leave-active .cms-drawer,.cms-modal-enter-active,.cms-modal-enter-active .cms-modal,.cms-modal-leave-active,.cms-modal-leave-active .cms-modal{transition-duration:.01ms}}.cms-menu{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);margin-top:.375rem;min-width:12rem;padding:.25rem;position:absolute;z-index:50}.cms-menu.align-end{right:0}.cms-menu-item{align-items:center;border-radius:var(--cms-radius-sm);color:var(--ui-text);cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;padding:.4rem .6rem;text-align:left;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);width:100%}.cms-menu-item:hover{background:color-mix(in oklab,var(--cms-text) 8%,transparent);color:var(--ui-text-highlighted)}.cms-menu-check{color:var(--cms-fern);margin-left:auto}.cms-popover{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);margin-top:.375rem;position:absolute;z-index:50}.cms-selectmenu-input{background:transparent;color:var(--ui-text-highlighted);flex:1;min-width:4rem;outline:none}.cms-selectmenu-affix{color:var(--ui-text-dimmed);flex-shrink:0}.cms-selectmenu-panel{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);left:0;margin-top:.375rem;max-height:15rem;overflow-y:auto;padding:.25rem;position:absolute;right:0;z-index:50}.cms-tag{align-items:center;background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-highlighted);display:inline-flex;font-size:.8125rem;gap:.25rem;padding:.125rem .5rem}.cms-media-gallery{display:flex;flex-direction:column;gap:1.25rem}.cms-media-source{color:var(--ui-text-muted);font-size:.8125rem;margin-top:-.75rem}.cms-media-filters{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.cms-media-filters-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem;margin-left:auto}.cms-media-filter-separator{background:var(--cms-line);flex:none;height:1rem;margin-inline:.25rem;width:1px}.cms-pill-count{color:var(--ui-text-dimmed);font-family:var(--cms-font-mono);font-size:.6875rem;margin-left:.25rem}.cms-blocks-grid{display:grid;gap:.75rem;grid-template-columns:repeat(2,minmax(0,1fr))}@media (min-width:640px){.cms-blocks-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.cms-blocks-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}.cms-block-tile{overflow:hidden;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-block-tile.is-muted{opacity:.45}.cms-block-preview{align-items:center;aspect-ratio:4/3;background:var(--cms-field);color:var(--ui-text-dimmed);display:flex;justify-content:center;position:relative;width:100%}.cms-block-preview>img,.cms-block-preview>video{height:100%;inset:0;-o-object-fit:cover;object-fit:cover;position:absolute;width:100%}.cms-block-summary{color:var(--ui-text-muted);font-size:.75rem;line-height:1.5;padding:.75rem;text-align:left}.cms-block-hidden{color:var(--cms-text);filter:drop-shadow(0 1px 2px rgb(0 0 0/.6));left:.5rem;position:absolute;top:.5rem}.cms-block-bar{align-items:center;display:flex;gap:.25rem;padding:.375rem .375rem .375rem .625rem}.cms-block-bar-name{color:var(--ui-text-muted);flex:1;font-size:.75rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-block-bar-actions{align-items:center;display:flex;gap:.125rem}.cms-block-add{align-items:center;border:1px dashed var(--cms-line-strong);border-radius:var(--cms-radius-md);color:var(--ui-text-muted);display:flex;flex-direction:column;font-size:.8125rem;font-weight:500;gap:.375rem;justify-content:center;min-height:6rem;transition:border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-block-add:hover{border-color:var(--cms-fern);color:var(--ui-text-highlighted)}.cms-media-grid{display:grid;gap:1rem;grid-template-columns:repeat(2,minmax(0,1fr))}@media (min-width:640px){.cms-media-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.cms-media-grid:not(.is-compact){grid-template-columns:repeat(4,minmax(0,1fr))}}.cms-media-tile{overflow:hidden;text-align:left;transition:border-color var(--cms-duration) var(--cms-ease)}.cms-media-tile.is-selectable{cursor:pointer}.cms-media-tile.is-selectable:hover{border-color:var(--cms-line-strong)}.cms-media-preview{align-items:center;aspect-ratio:1;background:var(--cms-field);color:var(--ui-text-dimmed);display:flex;justify-content:center;position:relative}.cms-media-preview>img,.cms-media-preview>video{height:100%;inset:0;-o-object-fit:cover;object-fit:cover;position:absolute;width:100%}.cms-media-actions{display:flex;gap:.25rem;opacity:0;position:absolute;right:.5rem;top:.5rem;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-media-actions:focus-within,.cms-media-tile:hover .cms-media-actions{opacity:1}.cms-media-info{background:linear-gradient(0deg,rgba(0,0,0,.75),transparent);bottom:0;color:var(--cms-text);font-family:var(--cms-font-mono);font-size:.75rem;left:0;letter-spacing:.08em;line-height:1.4;opacity:0;padding:1.5rem .75rem .5rem;pointer-events:none;position:absolute;right:0;text-transform:uppercase;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-media-tile:focus-visible .cms-media-info,.cms-media-tile:hover .cms-media-info{opacity:1}.cms-media-meta{display:flex;flex-direction:column;gap:.25rem;padding:.625rem .875rem}.cms-media-name{color:var(--ui-text-highlighted);font-size:.875rem;font-weight:500}.cms-media-folder,.cms-media-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-media-folder{align-items:center;color:var(--ui-text-dimmed);display:inline-flex;font-size:.75rem;gap:.25rem}.cms-media-field{overflow:hidden}.cms-media-field-preview{align-items:center;background-color:var(--cms-canvas);background-image:repeating-conic-gradient(from 0deg,hsla(0,0%,100%,.045) 0 25%,hsla(0,0%,100%,0) 0 50%);background-position:50%;background-size:16px 16px;color:var(--ui-text-dimmed);display:flex;justify-content:center;max-height:16rem;min-height:8rem;padding:.75rem}.cms-media-field-preview>img,.cms-media-field-preview>video{max-height:14.5rem;max-width:100%;-o-object-fit:contain;object-fit:contain}.cms-media-field-bar{align-items:center;background:var(--cms-surface);border-top:1px solid var(--cms-line);display:flex;gap:.5rem;padding:.625rem .75rem}.cms-media-bar-icon{color:var(--ui-text-dimmed);flex-shrink:0}.cms-media-bar-name{color:var(--ui-text-highlighted);flex:1;font-size:.875rem;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-media-thumb{background:var(--cms-field);border-radius:var(--cms-radius-sm);height:2.25rem;-o-object-fit:cover;object-fit:cover;width:2.25rem}.cms-media-thumb.is-fallback{align-items:center;color:var(--ui-text-dimmed);display:flex;justify-content:center}.cms-media-thumb-empty{color:var(--ui-text-dimmed)}.cms-table-shell{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden}.cms-table-scroll{flex:1;min-height:0;overflow:auto}.cms-table{border-collapse:collapse;min-width:-moz-max-content;min-width:max-content;text-align:left;width:100%}.cms-table td,.cms-table th{white-space:nowrap}.cms-table thead{background:var(--cms-surface);position:sticky;top:0;z-index:10}.cms-table th{background:var(--cms-surface);box-shadow:inset 0 -1px 0 var(--cms-line);color:var(--ui-text-muted);font-family:var(--cms-font-mono);font-size:.6875rem;font-weight:400;letter-spacing:.08em;line-height:1.4;padding:.75rem 1.25rem;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-table td{color:var(--ui-text);font-size:.875rem;padding:.875rem 1.25rem}.cms-table tbody tr{border-bottom:1px solid var(--cms-line);transition:background-color var(--cms-duration) var(--cms-ease)}.cms-table tbody tr:last-child{border-bottom:0}.cms-table tbody tr:hover{background:var(--cms-field)}.cms-table-cell-actions{display:flex;justify-content:flex-end}.cms-table th[draggable=true]{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.cms-table th[draggable=true]:active{cursor:grabbing}.cms-table th.is-dragging{opacity:.4}.cms-table th.is-drop-target{background:var(--cms-field);box-shadow:inset 0 -1px 0 var(--cms-line),inset 0 0 0 2px color-mix(in oklab,var(--cms-fern) 55%,transparent)}.cms-th-label{align-items:center;display:inline-flex;gap:.35rem}.cms-table th.is-sortable{cursor:pointer}.cms-table th.is-sortable:hover,.cms-table th.is-sorted{color:var(--ui-text-highlighted)}.cms-table th.is-sorted{background:var(--cms-field)}.cms-th-sort{flex:none;opacity:0;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-table th.is-sortable:hover .cms-th-sort,.cms-table th.is-sorted .cms-th-sort{opacity:1}.cms-alert{border:1px solid transparent;border-radius:var(--cms-radius-md);display:flex;flex-direction:column;font-size:.875rem;gap:.125rem;padding:.75rem 1rem}.cms-alert-title{font-weight:550}.cms-alert-error{background:color-mix(in oklab,var(--ui-error) 14%,transparent);border-color:color-mix(in oklab,var(--ui-error) 26%,transparent);color:var(--ui-error)}.cms-alert-success{background:color-mix(in oklab,var(--ui-success) 14%,transparent);border-color:color-mix(in oklab,var(--ui-success) 26%,transparent);color:var(--ui-success)}.cms-alert-warning{background:color-mix(in oklab,var(--ui-warning) 14%,transparent);border-color:color-mix(in oklab,var(--ui-warning) 26%,transparent);color:var(--ui-warning)}.cms-alert-neutral{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-toned)}.cms-alert-neutral .cms-alert-title{color:var(--ui-text-highlighted)}.cms-toaster{bottom:1rem;display:flex;flex-direction:column;gap:.5rem;max-width:calc(100vw - 2rem);position:fixed;right:1rem;width:20rem;z-index:80}.cms-toast{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);padding:.75rem .875rem}.cms-toast-row{align-items:flex-start;display:flex;gap:.625rem}.cms-toast-icon{color:var(--ui-text-muted);flex-shrink:0;margin-top:.0625rem}.cms-toast.is-success .cms-toast-icon{color:var(--ui-success)}.cms-toast.is-error .cms-toast-icon{color:var(--ui-error)}.cms-toast-title{color:var(--ui-text-highlighted);font-size:.875rem;font-weight:500}.cms-toast-description{color:var(--ui-text-muted);font-size:.875rem;margin-top:.125rem}.cms-toast-dismiss{color:var(--ui-text-dimmed);flex-shrink:0;transition:color var(--cms-duration) var(--cms-ease)}.cms-toast-dismiss:hover{color:var(--ui-text-highlighted)}.cms-pagination{align-items:center;display:flex;gap:.25rem;justify-content:center}.cms-page-btn{border-radius:var(--cms-radius-sm);color:var(--ui-text-toned);font-size:.875rem;height:2rem;min-width:2rem;padding:0 .5rem;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-page-btn:hover:not(:disabled){background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-page-btn.is-active{background:var(--cms-field);color:var(--cms-fern);font-weight:600}.cms-page-btn:disabled{opacity:.4}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { inArray, like, or } from "drizzle-orm";
|
|
2
|
+
import { createError, defineEventHandler, getValidatedQuery } from "h3";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { useDb } from "#cms-db";
|
|
5
|
+
import { cms_media } from "#cms-tables";
|
|
6
|
+
import { isMediaFolderMarker, normalizeMediaFolder } from "../../shared/index.js";
|
|
7
|
+
import { encodeKey, useMediaStorage } from "../utils/media.js";
|
|
8
|
+
import { requireAdmin } from "../utils/require-admin.js";
|
|
9
|
+
const querySchema = z.object({
|
|
10
|
+
name: z.string().min(1).max(255),
|
|
11
|
+
recursive: z.stringbool().default(false)
|
|
12
|
+
});
|
|
13
|
+
export default defineEventHandler(async (event) => {
|
|
14
|
+
await requireAdmin(event);
|
|
15
|
+
const { client, bucketUrl } = useMediaStorage(event);
|
|
16
|
+
const { name, recursive } = await getValidatedQuery(event, querySchema.parse);
|
|
17
|
+
const folder = normalizeMediaFolder(name);
|
|
18
|
+
if (!folder) {
|
|
19
|
+
throw createError({ statusCode: 400, statusMessage: "Invalid folder name" });
|
|
20
|
+
}
|
|
21
|
+
const db = useDb();
|
|
22
|
+
const rows = await db.select({ key: cms_media.key }).from(cms_media).where(or(like(cms_media.folder, `${folder}/%`), like(cms_media.folder, folder)));
|
|
23
|
+
const files = rows.filter((row) => !isMediaFolderMarker(row.key));
|
|
24
|
+
if (files.length && !recursive) {
|
|
25
|
+
throw createError({
|
|
26
|
+
statusCode: 409,
|
|
27
|
+
statusMessage: `The folder holds ${files.length} file${files.length > 1 ? "s" : ""}`
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
const res = await client.fetch(`${bucketUrl}/${encodeKey(row.key)}`, { method: "DELETE" });
|
|
32
|
+
if (!res.ok && res.status !== 404) {
|
|
33
|
+
throw createError({
|
|
34
|
+
statusCode: 502,
|
|
35
|
+
statusMessage: `Bucket delete failed (${res.status})`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (rows.length) {
|
|
40
|
+
await db.delete(cms_media).where(
|
|
41
|
+
inArray(
|
|
42
|
+
cms_media.key,
|
|
43
|
+
rows.map((row) => row.key)
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return { deleted: rows.length };
|
|
48
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createError, defineEventHandler, readValidatedBody } from "h3";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { useDb } from "#cms-db";
|
|
4
|
+
import { cms_media } from "#cms-tables";
|
|
5
|
+
import { mediaFolderMarkerKey, normalizeMediaFolder } from "../../shared/index.js";
|
|
6
|
+
import { encodeKey, useMediaStorage } from "../utils/media.js";
|
|
7
|
+
import { requireAdmin } from "../utils/require-admin.js";
|
|
8
|
+
const bodySchema = z.object({ name: z.string().min(1).max(255) });
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
await requireAdmin(event);
|
|
11
|
+
const { client, bucketUrl } = useMediaStorage(event);
|
|
12
|
+
const { name } = await readValidatedBody(event, bodySchema.parse);
|
|
13
|
+
const folder = normalizeMediaFolder(name);
|
|
14
|
+
if (!folder) {
|
|
15
|
+
throw createError({ statusCode: 400, statusMessage: "Invalid folder name" });
|
|
16
|
+
}
|
|
17
|
+
const key = mediaFolderMarkerKey(folder);
|
|
18
|
+
const res = await client.fetch(`${bucketUrl}/${encodeKey(key)}`, {
|
|
19
|
+
method: "PUT",
|
|
20
|
+
body: new Uint8Array(),
|
|
21
|
+
headers: { "content-type": "application/x-empty", "content-length": "0" }
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw createError({ statusCode: 502, statusMessage: `Bucket write failed (${res.status})` });
|
|
25
|
+
}
|
|
26
|
+
const values = {
|
|
27
|
+
key,
|
|
28
|
+
mime: "application/x-empty",
|
|
29
|
+
size: 0,
|
|
30
|
+
width: null,
|
|
31
|
+
height: null,
|
|
32
|
+
alt: null,
|
|
33
|
+
folder
|
|
34
|
+
};
|
|
35
|
+
await useDb().insert(cms_media).values(values).onConflictDoUpdate({ target: cms_media.key, set: values });
|
|
36
|
+
return { folder };
|
|
37
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MediaItem, MediaSourceInfo } from '../../shared/index.js';
|
|
2
2
|
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
3
3
|
items: MediaItem[];
|
|
4
|
+
folders: string[];
|
|
4
5
|
source: MediaSourceInfo;
|
|
5
6
|
}>>;
|
|
6
7
|
export default _default;
|
|
@@ -2,9 +2,17 @@ import { desc } from "drizzle-orm";
|
|
|
2
2
|
import { defineEventHandler } from "h3";
|
|
3
3
|
import { useDb } from "#cms-db";
|
|
4
4
|
import { cms_media } from "#cms-tables";
|
|
5
|
+
import { isMediaFolderMarker } from "../../shared/index.js";
|
|
5
6
|
import { useMediaIndex } from "../utils/media-index.js";
|
|
6
7
|
import { toMediaItem, useMediaConfig } from "../utils/media.js";
|
|
7
8
|
import { requireAdmin } from "../utils/require-admin.js";
|
|
9
|
+
function foldersOf(items) {
|
|
10
|
+
const folders = /* @__PURE__ */ new Set();
|
|
11
|
+
for (const item of items) {
|
|
12
|
+
if (item.folder) folders.add(item.folder);
|
|
13
|
+
}
|
|
14
|
+
return [...folders].sort();
|
|
15
|
+
}
|
|
8
16
|
export default defineEventHandler(async (event) => {
|
|
9
17
|
await requireAdmin(event);
|
|
10
18
|
const { media, publicUrl } = useMediaConfig(event);
|
|
@@ -15,10 +23,11 @@ export default defineEventHandler(async (event) => {
|
|
|
15
23
|
const items2 = index.files.map(
|
|
16
24
|
(file) => toMediaItem({ ...file, alt: altByKey.get(file.key) ?? null, createdAt: null }, publicUrl)
|
|
17
25
|
);
|
|
18
|
-
return { items: items2, source: index.source };
|
|
26
|
+
return { items: items2, folders: foldersOf(items2), source: index.source };
|
|
19
27
|
}
|
|
20
28
|
const rows = await useDb().select().from(cms_media).orderBy(desc(cms_media.createdAt), desc(cms_media.id));
|
|
21
|
-
const
|
|
29
|
+
const all = rows.map((row) => toMediaItem(row, publicUrl));
|
|
30
|
+
const items = all.filter((item) => !isMediaFolderMarker(item.key));
|
|
22
31
|
const source = { kind: "database", root: "", builtAt: null };
|
|
23
|
-
return { items, source };
|
|
32
|
+
return { items, folders: foldersOf(all), source };
|
|
24
33
|
});
|
|
@@ -20,6 +20,9 @@ export declare const DEFAULT_MEDIA_MAX_FILE_SIZE: number;
|
|
|
20
20
|
export declare function formatFileSize(bytes: number): string;
|
|
21
21
|
export declare function slugify(value: string): string;
|
|
22
22
|
export declare const MEDIA_FOLDER_MAX_DEPTH = 4;
|
|
23
|
+
export declare const MEDIA_FOLDER_MARKER = ".keep";
|
|
24
|
+
export declare function mediaFolderMarkerKey(folder: string): string;
|
|
25
|
+
export declare function isMediaFolderMarker(key: string): boolean;
|
|
23
26
|
export declare function normalizeMediaFolder(value: string | null | undefined): string | null;
|
|
24
27
|
export interface MediaItem {
|
|
25
28
|
id?: number;
|
|
@@ -66,6 +66,13 @@ export function slugify(value) {
|
|
|
66
66
|
return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036F]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
67
67
|
}
|
|
68
68
|
export const MEDIA_FOLDER_MAX_DEPTH = 4;
|
|
69
|
+
export const MEDIA_FOLDER_MARKER = ".keep";
|
|
70
|
+
export function mediaFolderMarkerKey(folder) {
|
|
71
|
+
return `${folder}/${MEDIA_FOLDER_MARKER}`;
|
|
72
|
+
}
|
|
73
|
+
export function isMediaFolderMarker(key) {
|
|
74
|
+
return key.endsWith(`/${MEDIA_FOLDER_MARKER}`);
|
|
75
|
+
}
|
|
69
76
|
export function normalizeMediaFolder(value) {
|
|
70
77
|
if (!value) return null;
|
|
71
78
|
const segments = value.split("/").map(slugify).filter(Boolean).slice(0, MEDIA_FOLDER_MAX_DEPTH);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
4
4
|
"description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Edoardo Alberti (https://github.com/xleddyl)",
|