@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
3
  "configKey": "cms",
4
- "version": "0.1.46",
4
+ "version": "0.1.47",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
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
- v-for="(item, index) in items"
5
- :key="uidFor(item)"
6
- class="cms-card flex flex-col gap-4 p-4"
7
- >
8
- <div class="flex items-center gap-1">
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-label flex grow items-center gap-1.5 text-left"
12
- :aria-label="isCollapsed(item) ? 'Expand block' : 'Collapse block'"
13
- @click="toggleCollapsed(item)"
12
+ class="cms-block-preview"
13
+ :aria-label="`Edit ${labelOf(item)}`"
14
+ @click="openEditor(index)"
14
15
  >
15
- <CmsIcon
16
- :name="isCollapsed(item) ? 'chevron-right' : 'chevron-down'"
17
- class="size-3.5"
16
+ <img
17
+ v-if="previewOf(item)?.kind === 'image'"
18
+ :src="previewOf(item).url"
19
+ alt=""
20
+ loading="lazy"
18
21
  />
19
- {{ blocks[item.type]?.label ?? item.type }}
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
- <CmsButton
22
- icon="chevron-up"
23
- size="xs"
24
- variant="ghost"
25
- color="neutral"
26
- aria-label="Move up"
27
- :disabled="index === 0"
28
- @click="move(index, -1)"
29
- />
30
- <CmsButton
31
- icon="chevron-down"
32
- size="xs"
33
- variant="ghost"
34
- color="neutral"
35
- aria-label="Move down"
36
- :disabled="index === items.length - 1"
37
- @click="move(index, 1)"
38
- />
39
- <CmsButton
40
- icon="document-duplicate"
41
- size="xs"
42
- variant="ghost"
43
- color="neutral"
44
- aria-label="Duplicate block"
45
- @click="duplicate(index)"
46
- />
47
- <CmsButton
48
- icon="trash"
49
- size="xs"
50
- variant="ghost"
51
- color="error"
52
- aria-label="Remove block"
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
- <template v-if="!isCollapsed(item)">
57
- <CmsFormField
58
- v-for="(blockField, blockKey) in blocks[item.type]?.fields"
59
- :key="blockKey"
60
- :label="blockField.label"
61
- :required="blockField.required"
62
- >
63
- <CmsFieldInput
64
- :model-value="item[blockKey]"
65
- :field="blockField"
66
- :locale="locale"
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
- <CmsDropdownMenu :items="addItems" class="self-start">
73
- <CmsButton label="Add block" icon="plus" variant="soft" />
74
- </CmsDropdownMenu>
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="chip.empty && !readOnly"
45
+ v-if="!readOnly"
45
46
  type="button"
46
47
  class="cms-pill-discard"
47
- aria-label="Discard empty folder"
48
- @click="discardFolder(chip.name)"
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="registerFolder"
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="registerFolder"
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([...usedFolders.value, ...draftFolders.value])].sort()
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) => ({ name, empty: !usedFolders.value.includes(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 registerFolder(name) {
345
- if (!draftFolders.value.includes(name) && !usedFolders.value.includes(name)) {
346
- draftFolders.value = [...draftFolders.value, name];
347
- persistDraftFolders();
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 discardFolder(name) {
351
- draftFolders.value = draftFolders.value.filter((f) => f !== name);
352
- persistDraftFolders();
353
- if (folder.value === name) folder.value = null;
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
- watch(usedFolders, (used) => {
356
- const next = draftFolders.value.filter((f) => !used.includes(f));
357
- if (next.length !== draftFolders.value.length) {
358
- draftFolders.value = next;
359
- persistDraftFolders();
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
- function createFolder() {
417
+ const creatingFolder = ref(false);
418
+ async function createFolder() {
408
419
  const name = newFolderSlug.value;
409
- if (!name) return;
410
- registerFolder(name);
411
- folder.value = name;
412
- newFolderOpen.value = false;
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 __VLS_1: {}, __VLS_3: {};
9
+ declare var __VLS_12: {}, __VLS_14: {};
5
10
  type __VLS_Slots = {} & {
6
- badge?: (props: typeof __VLS_1) => any;
11
+ badge?: (props: typeof __VLS_12) => any;
7
12
  } & {
8
- default?: (props: typeof __VLS_3) => any;
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 __VLS_1: {}, __VLS_3: {};
9
+ declare var __VLS_12: {}, __VLS_14: {};
5
10
  type __VLS_Slots = {} & {
6
- badge?: (props: typeof __VLS_1) => any;
11
+ badge?: (props: typeof __VLS_12) => any;
7
12
  } & {
8
- default?: (props: typeof __VLS_3) => any;
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="rows.length" class="cms-card divide-y divide-(--cms-line)">
44
+ <div v-if="pageTree.length" class="cms-card divide-y divide-(--cms-line)">
45
45
  <NuxtLink
46
- v-for="row in rows"
47
- :key="String(row.id)"
48
- :to="`/cms/${name}/${row.id}`"
49
- class="flex items-center gap-4 px-4 py-3 transition-colors hover:bg-(--ui-bg-elevated)"
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 { isTranslatableField, isTranslatableMediaField, pickTranslatedMedia } from "#nuxt-cms";
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,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ deleted: number;
3
+ }>>;
4
+ export default _default;
@@ -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,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ folder: string;
3
+ }>>;
4
+ export default _default;
@@ -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 items = rows.map((row) => toMediaItem(row, publicUrl));
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.46",
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)",