@xleddyl/nuxt-cms 0.1.23 → 0.1.25

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.
Files changed (69) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +16 -3
  3. package/dist/runtime/app/components/cms/BlocksField.vue +2 -9
  4. package/dist/runtime/app/components/cms/Button.vue +4 -2
  5. package/dist/runtime/app/components/cms/ConfirmModal.vue +5 -14
  6. package/dist/runtime/app/components/cms/Drawer.d.vue.ts +30 -0
  7. package/dist/runtime/app/components/cms/Drawer.vue +65 -0
  8. package/dist/runtime/app/components/cms/Drawer.vue.d.ts +30 -0
  9. package/dist/runtime/app/components/cms/DropdownMenu.d.vue.ts +0 -3
  10. package/dist/runtime/app/components/cms/DropdownMenu.vue +2 -7
  11. package/dist/runtime/app/components/cms/DropdownMenu.vue.d.ts +0 -3
  12. package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +1 -0
  13. package/dist/runtime/app/components/cms/EmptyState.vue +6 -7
  14. package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +1 -0
  15. package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +21 -0
  16. package/dist/runtime/app/components/cms/EntryDrawer.vue +193 -0
  17. package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +21 -0
  18. package/dist/runtime/app/components/cms/EntryForm.vue +19 -7
  19. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +1 -0
  20. package/dist/runtime/app/components/cms/FieldInput.vue +13 -27
  21. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +1 -0
  22. package/dist/runtime/app/components/cms/Form.vue +1 -1
  23. package/dist/runtime/app/components/cms/FormField.d.vue.ts +4 -5
  24. package/dist/runtime/app/components/cms/FormField.vue +9 -7
  25. package/dist/runtime/app/components/cms/FormField.vue.d.ts +4 -5
  26. package/dist/runtime/app/components/cms/JsonField.vue +3 -4
  27. package/dist/runtime/app/components/cms/LocaleSwitch.d.vue.ts +14 -0
  28. package/dist/runtime/app/components/cms/LocaleSwitch.vue +29 -0
  29. package/dist/runtime/app/components/cms/LocaleSwitch.vue.d.ts +14 -0
  30. package/dist/runtime/app/components/cms/MediaField.vue +7 -17
  31. package/dist/runtime/app/components/cms/MediaGallery.vue +48 -78
  32. package/dist/runtime/app/components/cms/MediaThumb.d.vue.ts +6 -0
  33. package/dist/runtime/app/components/cms/MediaThumb.vue +40 -0
  34. package/dist/runtime/app/components/cms/MediaThumb.vue.d.ts +6 -0
  35. package/dist/runtime/app/components/cms/Modal.d.vue.ts +1 -4
  36. package/dist/runtime/app/components/cms/Modal.vue +8 -7
  37. package/dist/runtime/app/components/cms/Modal.vue.d.ts +1 -4
  38. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +1 -1
  39. package/dist/runtime/app/components/cms/PageHeader.vue +6 -6
  40. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +1 -1
  41. package/dist/runtime/app/components/cms/RelationField.vue +0 -4
  42. package/dist/runtime/app/components/cms/SelectMenu.vue +23 -3
  43. package/dist/runtime/app/components/cms/SlugField.vue +1 -2
  44. package/dist/runtime/app/components/cms/Spinner.d.vue.ts +3 -0
  45. package/dist/runtime/app/components/cms/Spinner.vue +5 -0
  46. package/dist/runtime/app/components/cms/Spinner.vue.d.ts +3 -0
  47. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +6 -0
  48. package/dist/runtime/app/components/cms/StatusBadge.vue +11 -0
  49. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +6 -0
  50. package/dist/runtime/app/components/cms/Table.d.vue.ts +3 -6
  51. package/dist/runtime/app/components/cms/Table.vue +72 -27
  52. package/dist/runtime/app/components/cms/Table.vue.d.ts +3 -6
  53. package/dist/runtime/app/components/cms/Toaster.vue +4 -4
  54. package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +1 -0
  55. package/dist/runtime/app/components/cms/TranslatableField.vue +7 -44
  56. package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +1 -0
  57. package/dist/runtime/app/layouts/cms-admin.vue +10 -20
  58. package/dist/runtime/app/pages/admin-collection.vue +130 -103
  59. package/dist/runtime/app/pages/admin-entry.vue +6 -24
  60. package/dist/runtime/app/pages/admin-login.vue +12 -31
  61. package/dist/runtime/app/pages/admin-media.vue +8 -2
  62. package/dist/runtime/app/utils/ui.d.ts +0 -7
  63. package/dist/runtime/app/utils/ui.js +0 -5
  64. package/dist/runtime/assets/main.css +1 -1
  65. package/dist/runtime/server/api/collection.get.js +8 -3
  66. package/dist/runtime/server/utils/relations.d.ts +1 -0
  67. package/dist/runtime/server/utils/relations.js +25 -0
  68. package/dist/runtime/shared/index.d.ts +10 -3
  69. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="cms-rise flex flex-col gap-7">
2
+ <div class="cms-page" :class="{ 'is-fill': config.kind === 'collection' && !error }">
3
3
  <CmsPageHeader :kicker="kicker" :title="config.label">
4
4
  <CmsButton
5
5
  v-if="config.kind === 'single'"
@@ -7,29 +7,7 @@
7
7
  :form="FORM_ID"
8
8
  label="Save"
9
9
  :loading="saving"
10
- class="rounded-full px-6"
11
10
  />
12
- <div v-if="config.kind === 'collection'" class="flex items-center gap-2">
13
- <CmsDropdownMenu
14
- v-if="rows.length"
15
- :items="columnItems"
16
- :content="{ align: 'end' }"
17
- :ui="{ content: 'w-48' }"
18
- >
19
- <CmsButton
20
- label="Columns"
21
- icon="view-columns"
22
- variant="subtle"
23
- class="rounded-full px-4"
24
- />
25
- </CmsDropdownMenu>
26
- <CmsButton
27
- label="New entry"
28
- icon="plus"
29
- class="rounded-full px-4"
30
- @click="openCreate"
31
- />
32
- </div>
33
11
  </CmsPageHeader>
34
12
 
35
13
  <CmsEmptyState v-if="error" icon="exclamation-triangle" title="Could not load entries">
@@ -37,13 +15,12 @@
37
15
  label="Retry"
38
16
  icon="arrow-path"
39
17
  variant="subtle"
40
- class="mt-3 rounded-full px-4"
41
18
  :loading="status === 'pending'"
42
19
  @click="reload"
43
20
  />
44
21
  </CmsEmptyState>
45
22
 
46
- <div v-else-if="config.kind === 'single'" class="cms-card p-7">
23
+ <div v-else-if="config.kind === 'single'" class="cms-card cms-panel">
47
24
  <CmsEntryForm
48
25
  v-model="formState"
49
26
  :fields="config.fields"
@@ -54,53 +31,56 @@
54
31
  </div>
55
32
 
56
33
  <template v-else>
57
- <CmsInput
58
- v-if="total || searchTerm"
59
- v-model="search"
60
- icon="magnifying-glass"
61
- placeholder="Search…"
62
- class="max-w-xs"
63
- />
64
-
65
- <div v-if="rows.length" class="cms-card overflow-hidden">
66
- <CmsTable
67
- v-model:column-visibility="columnVisibility"
68
- :data="rows"
69
- :columns="columns"
70
- :ui="tableUi"
71
- @select="onSelect"
72
- >
73
- <template v-if="drafts" #status-cell="{ row }">
74
- <span
75
- class="cms-badge"
76
- :class="row.original.status === 'published' ? 'is-published' : 'is-draft'"
77
- >
78
- {{ row.original.status === "published" ? "Published" : "Draft" }}
79
- </span>
80
- </template>
81
- <template #actions-cell="{ row }">
82
- <div class="flex justify-end">
83
- <CmsButton
84
- icon="trash"
85
- variant="ghost"
86
- color="error"
87
- size="xs"
88
- class="rounded-full"
89
- @click.stop="deleteRow(row.original)"
90
- />
91
- </div>
92
- </template>
93
- </CmsTable>
34
+ <div class="cms-toolbar">
35
+ <CmsInput
36
+ v-if="total || searchTerm"
37
+ v-model="search"
38
+ icon="magnifying-glass"
39
+ placeholder="Search…"
40
+ class="flex-1"
41
+ />
42
+ <div class="cms-toolbar-actions">
43
+ <CmsDropdownMenu v-if="rows.length" :items="columnItems" :content="{ align: 'end' }">
44
+ <CmsButton label="Columns" icon="view-columns" variant="subtle" />
45
+ </CmsDropdownMenu>
46
+ <CmsButton label="New entry" icon="plus" @click="openCreate" />
47
+ </div>
94
48
  </div>
95
49
 
96
- <div v-else-if="status === 'pending'" class="flex justify-center py-10">
97
- <CmsIcon name="arrow-path" class="size-6 animate-spin text-(--ui-text-dimmed)" />
98
- </div>
50
+ <CmsTable
51
+ v-if="rows.length"
52
+ v-model:column-visibility="columnVisibility"
53
+ :data="rows"
54
+ :columns="columns"
55
+ @select="onSelect"
56
+ @reorder="reorderColumns"
57
+ >
58
+ <template v-for="key in mediaKeys" #[`${key}-cell`]="{ row }" :key="key">
59
+ <CmsMediaThumb :value="row.original[key]" />
60
+ </template>
61
+ <template v-if="drafts" #status-cell="{ row }">
62
+ <CmsStatusBadge :published="row.original.status === 'published'" />
63
+ </template>
64
+ <template #actions-cell="{ row }">
65
+ <div class="cms-table-cell-actions">
66
+ <CmsButton
67
+ icon="trash"
68
+ variant="ghost"
69
+ color="error"
70
+ size="xs"
71
+ @click.stop="deleteRow(row.original)"
72
+ />
73
+ </div>
74
+ </template>
75
+ </CmsTable>
76
+
77
+ <CmsSpinner v-else-if="status === 'pending'" />
99
78
 
100
79
  <CmsEmptyState
101
80
  v-else-if="searchTerm"
102
81
  icon="magnifying-glass"
103
82
  title="No matching entries"
83
+ fill
104
84
  />
105
85
 
106
86
  <CmsEmptyState
@@ -108,19 +88,21 @@
108
88
  icon="sparkles"
109
89
  title="Nothing here yet"
110
90
  body="Entries you create will show up here."
91
+ fill
111
92
  >
112
- <CmsButton
113
- label="New entry"
114
- icon="plus"
115
- variant="subtle"
116
- class="mt-3 rounded-full px-4"
117
- @click="openCreate"
118
- />
93
+ <CmsButton label="New entry" icon="plus" variant="subtle" @click="openCreate" />
119
94
  </CmsEmptyState>
120
95
 
121
- <div v-if="total > PAGE_SIZE" class="flex justify-center">
122
- <CmsPagination v-model:page="page" :total="total" :items-per-page="PAGE_SIZE" />
123
- </div>
96
+ <CmsPagination v-model:page="page" :total="total" :items-per-page="PAGE_SIZE" />
97
+
98
+ <CmsEntryDrawer
99
+ v-model:open="drawerOpen"
100
+ :collection="name"
101
+ :config="config"
102
+ :entry-id="drawerEntryId"
103
+ @saved="refresh"
104
+ @deleted="onDrawerDeleted"
105
+ />
124
106
  </template>
125
107
  </div>
126
108
  </template>
@@ -131,7 +113,6 @@ import {
131
113
  computed,
132
114
  createError,
133
115
  definePageMeta,
134
- navigateTo,
135
116
  onMounted,
136
117
  ref,
137
118
  useFetch,
@@ -159,6 +140,7 @@ if (!config) {
159
140
  throw createError({ statusCode: 404, statusMessage: "Unknown collection", fatal: true });
160
141
  }
161
142
  const fieldKeys = Object.keys(config.fields);
143
+ const mediaKeys = fieldKeys.filter((key) => config.fields[key].type === "media");
162
144
  const drafts = config.kind === "collection" && !!config.drafts;
163
145
  const formKeys = drafts ? [...fieldKeys, "status"] : fieldKeys;
164
146
  const endpoint = `/api/cms/admin/${name}`;
@@ -187,18 +169,13 @@ function isList(value) {
187
169
  }
188
170
  const rows = computed(() => isList(data.value) ? data.value.items : []);
189
171
  const total = computed(() => isList(data.value) ? data.value.total : 0);
172
+ const relations = computed(() => isList(data.value) ? data.value.relations ?? {} : {});
190
173
  const reload = async () => {
191
174
  await refresh();
192
175
  if (config.kind === "single" && !error.value) {
193
176
  formState.value = data.value && !isList(data.value) ? pickFields(data.value) : emptyState();
194
177
  }
195
178
  };
196
- const tableUi = {
197
- thead: "border-b border-(--cms-line)",
198
- th: "cms-kicker px-5 py-3 font-normal",
199
- td: "px-5 py-3.5 text-sm text-(--ui-text)",
200
- tr: "__clickable hover:bg-(--cms-paper)/50"
201
- };
202
179
  const saving = ref(false);
203
180
  const formState = ref({});
204
181
  function emptyState() {
@@ -239,11 +216,22 @@ function stripHtml(html) {
239
216
  function truncate(value) {
240
217
  return value.length > 60 ? `${value.slice(0, 57)}\u2026` : value;
241
218
  }
242
- function displayValue(field, value) {
219
+ function localized(value) {
220
+ const record = value;
221
+ return record[contentI18n.defaultLocale] ?? Object.values(record)[0] ?? "";
222
+ }
223
+ function relationLabel(field, key, id) {
224
+ const title = relations.value[key]?.[String(id)];
225
+ if (title == null || title === "") return `#${id}`;
226
+ const target = cmsConfig[field.to];
227
+ const titleField = target?.titleField ? target.fields[target.titleField] : void 0;
228
+ const label = titleField && isTranslatableField(titleField) ? localized(title) : String(title);
229
+ return label || `#${id}`;
230
+ }
231
+ function displayValue(field, key, value) {
243
232
  if (value == null || value === "") return "";
244
233
  if (isTranslatableField(field)) {
245
- const record = value;
246
- const raw = record[contentI18n.defaultLocale] ?? Object.values(record)[0] ?? "";
234
+ const raw = localized(value);
247
235
  return truncate(field.type === "richtext" ? stripHtml(raw) : raw);
248
236
  }
249
237
  switch (field.type) {
@@ -255,43 +243,76 @@ function displayValue(field, value) {
255
243
  return truncate(JSON.stringify(value));
256
244
  case "blocks":
257
245
  return `${value.length} \u25A4`;
246
+ case "relation": {
247
+ const ids = Array.isArray(value) ? value : [value];
248
+ if (!ids.length) return "";
249
+ return truncate(ids.map((id) => relationLabel(field, key, id)).join(", "));
250
+ }
258
251
  default:
259
252
  return truncate(String(value));
260
253
  }
261
254
  }
255
+ const columnOrder = ref([...fieldKeys]);
256
+ const orderedKeys = computed(() => {
257
+ const known = columnOrder.value.filter((key) => fieldKeys.includes(key));
258
+ return [...known, ...fieldKeys.filter((key) => !known.includes(key))];
259
+ });
262
260
  const columns = computed(() => [
263
- ...fieldKeys.map((key) => ({
261
+ ...orderedKeys.value.map((key) => ({
264
262
  id: key,
265
- accessorFn: (row) => displayValue(config.fields[key], row[key]),
266
- header: config.fields[key].label
263
+ accessorFn: (row) => displayValue(config.fields[key], key, row[key]),
264
+ header: config.fields[key].label,
265
+ reorderable: true
267
266
  })),
268
267
  ...drafts ? [{ accessorKey: "status", header: "Status" }] : [],
269
268
  { id: "actions", header: "" }
270
269
  ]);
270
+ function reorderColumns(from, to) {
271
+ const next = [...orderedKeys.value];
272
+ const fromIndex = next.indexOf(from);
273
+ const toIndex = next.indexOf(to);
274
+ if (fromIndex === -1 || toIndex === -1) return;
275
+ next.splice(toIndex, 0, ...next.splice(fromIndex, 1));
276
+ columnOrder.value = next;
277
+ }
271
278
  const DEFAULT_VISIBLE_COLUMNS = 4;
272
279
  const columnStorageKey = `cms:columns:${name}`;
280
+ const columnOrderStorageKey = `cms:column-order:${name}`;
273
281
  const columnVisibility = ref(
274
282
  Object.fromEntries(fieldKeys.map((key, index) => [key, index < DEFAULT_VISIBLE_COLUMNS]))
275
283
  );
284
+ function readStored(key) {
285
+ const stored = localStorage.getItem(key);
286
+ if (!stored) return void 0;
287
+ try {
288
+ return JSON.parse(stored);
289
+ } catch {
290
+ return void 0;
291
+ }
292
+ }
276
293
  onMounted(() => {
277
- const stored = localStorage.getItem(columnStorageKey);
278
- if (stored) {
279
- try {
280
- const parsed = JSON.parse(stored);
281
- columnVisibility.value = Object.fromEntries(
282
- fieldKeys.map((key) => [key, parsed[key] ?? columnVisibility.value[key] ?? false])
283
- );
284
- } catch {
285
- }
294
+ const storedVisibility = readStored(columnStorageKey);
295
+ if (storedVisibility) {
296
+ columnVisibility.value = Object.fromEntries(
297
+ fieldKeys.map((key) => [
298
+ key,
299
+ storedVisibility[key] ?? columnVisibility.value[key] ?? false
300
+ ])
301
+ );
302
+ }
303
+ const storedOrder = readStored(columnOrderStorageKey);
304
+ if (Array.isArray(storedOrder)) {
305
+ columnOrder.value = storedOrder.filter((key) => typeof key === "string");
286
306
  }
287
307
  watch(
288
308
  columnVisibility,
289
309
  (value) => localStorage.setItem(columnStorageKey, JSON.stringify(value)),
290
310
  { deep: true }
291
311
  );
312
+ watch(orderedKeys, (value) => localStorage.setItem(columnOrderStorageKey, JSON.stringify(value)));
292
313
  });
293
314
  const columnItems = computed(
294
- () => fieldKeys.map((key) => ({
315
+ () => orderedKeys.value.map((key) => ({
295
316
  label: config.fields[key].label,
296
317
  type: "checkbox",
297
318
  checked: columnVisibility.value[key],
@@ -303,14 +324,20 @@ const columnItems = computed(
303
324
  }
304
325
  }))
305
326
  );
306
- const kicker = computed(
307
- () => config.kind === "single" ? "single document" : `collection \xB7 ${total.value} ${total.value === 1 ? "entry" : "entries"}`
308
- );
327
+ const kicker = config.kind === "single" ? "single document" : void 0;
328
+ const drawerOpen = ref(false);
329
+ const drawerEntryId = ref(null);
309
330
  function onSelect(_event, row) {
310
- navigateTo(`/cms/${name}/${row.original.id}`);
331
+ drawerEntryId.value = String(row.original.id);
332
+ drawerOpen.value = true;
311
333
  }
312
334
  function openCreate() {
313
- navigateTo(`/cms/${name}/new`);
335
+ drawerEntryId.value = null;
336
+ drawerOpen.value = true;
337
+ }
338
+ async function onDrawerDeleted() {
339
+ if (rows.value.length === 1 && page.value > 1) page.value -= 1;
340
+ else await refresh();
314
341
  }
315
342
  const confirmAction = useCmsConfirm();
316
343
  async function deleteRow(row) {
@@ -1,42 +1,24 @@
1
1
  <template>
2
- <div class="cms-rise flex flex-col gap-7">
2
+ <div class="cms-page">
3
3
  <CmsPageHeader :kicker="config.label" :title="isNew ? 'New entry' : 'Edit entry'">
4
4
  <template v-if="drafts" #badge>
5
- <span
6
- class="cms-badge pointer-events-none"
7
- :class="published ? 'is-published' : 'is-draft'"
8
- >
9
- {{ published ? "Published" : "Draft" }}
10
- </span>
5
+ <CmsStatusBadge :published="published" />
11
6
  </template>
12
- <div class="flex items-center gap-3">
13
- <CmsButton
14
- label="Back"
15
- icon="arrow-left"
16
- variant="subtle"
17
- class="rounded-full px-4"
18
- @click="goBack"
19
- />
20
- <CmsButton
21
- type="submit"
22
- :form="FORM_ID"
23
- label="Save"
24
- :loading="saving"
25
- class="rounded-full px-6"
26
- />
7
+ <div class="cms-actions">
8
+ <CmsButton label="Back" icon="arrow-left" variant="subtle" @click="goBack" />
9
+ <CmsButton type="submit" :form="FORM_ID" label="Save" :loading="saving" />
27
10
  <CmsButton
28
11
  v-if="drafts"
29
12
  type="submit"
30
13
  :form="FORM_ID"
31
14
  :label="published ? 'Make draft' : 'Publish'"
32
15
  :loading="saving"
33
- class="rounded-full px-6"
34
16
  @click="togglePublished"
35
17
  />
36
18
  </div>
37
19
  </CmsPageHeader>
38
20
 
39
- <div class="cms-card p-7">
21
+ <div class="cms-card cms-panel">
40
22
  <CmsEntryForm
41
23
  v-model="formState"
42
24
  :fields="config.fields"
@@ -1,50 +1,31 @@
1
1
  <template>
2
- <div class="cms-scope cms-canvas cms-grain flex min-h-screen items-center justify-center p-6">
3
- <div class="flex w-full max-w-sm flex-col gap-8">
4
- <header class="cms-rise flex flex-col items-center gap-3 text-center">
5
- <h1 class="cms-display text-4xl font-medium text-(--ui-text-highlighted)">
6
- Welcome back<span class="text-(--cms-fern)">.</span>
7
- </h1>
8
- <p class="text-sm text-(--ui-text-muted)">Sign in to manage your content.</p>
2
+ <div class="cms-scope cms-canvas cms-grain cms-auth">
3
+ <div class="cms-auth-panel">
4
+ <header class="cms-auth-header cms-rise">
5
+ <h1 class="cms-title cms-title-lg">Welcome back<span class="cms-accent">.</span></h1>
6
+ <p class="cms-subtitle">Sign in to manage your content.</p>
9
7
  </header>
10
8
 
11
- <div class="cms-card cms-rise p-6 sm:p-7" style="animation-delay: 90ms">
12
- <CmsForm :state="state" class="flex flex-col gap-5" @submit="login">
13
- <CmsAlert
14
- v-if="error"
15
- color="error"
16
- variant="subtle"
17
- :title="error"
18
- class="rounded-xl"
19
- />
20
- <CmsFormField label="Email" name="email" required :ui="CMS_FIELD_UI">
9
+ <div class="cms-card cms-auth-card cms-rise is-delayed">
10
+ <CmsForm :state="state" @submit="login">
11
+ <CmsAlert v-if="error" color="error" variant="subtle" :title="error" />
12
+ <CmsFormField label="Email" name="email" required>
21
13
  <CmsInput
22
14
  v-model="state.email"
23
15
  type="email"
24
- size="lg"
25
16
  autocomplete="username"
26
17
  placeholder="you@example.com"
27
- class="w-full"
28
18
  />
29
19
  </CmsFormField>
30
- <CmsFormField label="Password" name="password" required :ui="CMS_FIELD_UI">
20
+ <CmsFormField label="Password" name="password" required>
31
21
  <CmsInput
32
22
  v-model="state.password"
33
23
  type="password"
34
- size="lg"
35
24
  autocomplete="current-password"
36
25
  placeholder="••••••••"
37
- class="w-full"
38
26
  />
39
27
  </CmsFormField>
40
- <CmsButton
41
- type="submit"
42
- label="Sign in"
43
- size="lg"
44
- block
45
- :loading="loading"
46
- class="mt-1 rounded-full"
47
- />
28
+ <CmsButton type="submit" label="Sign in" block :loading="loading" />
48
29
  </CmsForm>
49
30
  </div>
50
31
  </div>
@@ -53,7 +34,7 @@
53
34
 
54
35
  <script setup>
55
36
  import { definePageMeta, navigateTo, ref, useUserSession } from "#imports";
56
- import { CMS_FIELD_UI, errorMessage } from "../utils/ui";
37
+ import { errorMessage } from "../utils/ui";
57
38
  definePageMeta({ layout: false });
58
39
  const { loggedIn } = useUserSession();
59
40
  if (loggedIn.value) {
@@ -1,6 +1,10 @@
1
1
  <template>
2
- <div class="cms-rise flex flex-col gap-7">
3
- <CmsPageHeader kicker="media library" title="Media" />
2
+ <div class="cms-page">
3
+ <CmsPageHeader title="Media">
4
+ <template #badge>
5
+ <span class="cms-badge is-muted">{{ runtime.mediaStorage }}</span>
6
+ </template>
7
+ </CmsPageHeader>
4
8
 
5
9
  <CmsMediaGallery />
6
10
  </div>
@@ -8,5 +12,7 @@
8
12
 
9
13
  <script setup>
10
14
  import { definePageMeta } from "#imports";
15
+ import { useCmsRuntime } from "../composables/cms-runtime";
11
16
  definePageMeta({ layout: "cms-admin", middleware: "cms-auth" });
17
+ const runtime = useCmsRuntime();
12
18
  </script>
@@ -1,11 +1,4 @@
1
1
  import type { InjectionKey, Reactive } from 'vue';
2
- export declare const CMS_FIELD_UI: {
3
- label: string;
4
- };
5
- export declare const CMS_MODAL_UI: {
6
- content: string;
7
- title: string;
8
- };
9
2
  /** Shared reactive map of field-name -> error message, provided by CmsForm and
10
3
  * read by CmsFormField to render inline validation errors. */
11
4
  export declare const CMS_FORM_ERRORS: InjectionKey<Reactive<Record<string, string>>>;
@@ -1,8 +1,3 @@
1
- export const CMS_FIELD_UI = { label: "text-sm font-medium text-(--ui-text-toned)" };
2
- export const CMS_MODAL_UI = {
3
- content: "rounded-2xl sm:max-w-3xl",
4
- title: "cms-display text-xl font-medium text-(--ui-text-highlighted)"
5
- };
6
1
  export const CMS_FORM_ERRORS = Symbol("cms-form-errors");
7
2
  export function errorMessage(error) {
8
3
  const err = error;
@@ -1 +1 @@
1
- @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&family=Hanken+Grotesk:ital,wght@0,300..800;1,300..800&family=Fragment+Mono:ital@0;1&display=swap");@layer theme, base, components, utilities;@import "tailwindcss/theme.css" layer(theme);.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-paper:#f5f5f5;--cms-surface:#fff;--cms-field:#f0f0f0;--cms-ink:#171717;--cms-line:#e5e5e5;--cms-line-strong:#d4d4d4;--cms-fern:#171717;--cms-sage:#ebebeb;--cms-sage-ink:#171717;--cms-shadow-soft:none;color-scheme:light;--ui-text-dimmed:#a3a3a3;--ui-text-muted:#737373;--ui-text-toned:#525252;--ui-text:#404040;--ui-text-highlighted:var(--cms-ink);--ui-text-inverted:#fff;--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-paper);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:#e0e0e0;--ui-bg-inverted:var(--cms-ink);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-ink);--ui-primary:var(--cms-ink);--ui-error:#dc2626;--ui-radius:0.4rem;color:var(--ui-text);font-family:Hanken Grotesk,ui-sans-serif,system-ui,sans-serif;margin:0;padding:0;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:var(--cms-sage);color:var(--cms-sage-ink)}body:has(.cms-scope) ::selection{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-display{font-family:Poppins,ui-sans-serif,system-ui,sans-serif;letter-spacing:-.02em}.cms-kicker{color:var(--ui-text-dimmed);font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;letter-spacing:.16em;line-height:1.4;text-transform:uppercase}.cms-scope thead .cms-kicker{font-size:.6875rem;letter-spacing:.1em}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer transition-all duration-200 select-none}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]):active{@apply transition-none;scale:.99}.cms-canvas{background-color:var(--cms-paper)}.cms-grain:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='a'%3E%3CfeTurbulence baseFrequency='.8' numOctaves='2' stitchTiles='stitch' type='fractalNoise'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");content:"";inset:0;mix-blend-mode:multiply;opacity:.035;pointer-events:none;position:fixed;z-index:50}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft)}.cms-empty{background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem}.cms-navlink{align-items:center;border-radius:.75rem;color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color .15s ease,color .15s ease}.cms-navlink:hover{background:rgba(38,33,25,.05);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{border:1px solid transparent;border-radius:9999px;font-family:Fragment Mono,ui-monospace,monospace;font-size:.6875rem;letter-spacing:.1em;padding:.1875rem .5rem;text-transform:uppercase;transition:background-color .15s ease,border-color .15s ease}.cms-badge.is-published{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-badge.is-draft{background:transparent;border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-dropzone{align-items:center;background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem;color:var(--ui-text-muted);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color .15s ease,background-color .15s ease,color .15s ease;width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-sage);border-color:var(--cms-fern);color:var(--cms-sage-ink)}.cms-dropzone.is-dense{border-radius:.75rem;flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;letter-spacing:.12em;padding:.3rem .8rem;transition:background-color .15s ease,color .15s ease}.cms-pill:hover{background:rgba(38,33,25,.05);color:var(--ui-text)}.cms-pill.is-active{background:var(--cms-sage);color:var(--cms-sage-ink)}@keyframes cms-rise{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .55s cubic-bezier(.22,1,.36,1) both}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{border:1px solid var(--cms-line);border-radius:.75rem;overflow:hidden}.cms-richtext-toolbar{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;flex-wrap:wrap;gap:2px;padding:4px}.cms-richtext-body{font-size:.875rem;min-height:9rem;outline:none;padding:.75rem 1rem}.cms-richtext-body h2{font-size:1.25rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body h3{font-size:1.1rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body p{margin:.35em 0}.cms-richtext-body ul{list-style:disc;padding-left:1.25rem}.cms-richtext-body ol{list-style:decimal;padding-left:1.25rem}.cms-richtext-body blockquote{border-left:3px solid var(--cms-line);color:var(--ui-text-muted);padding-left:.75rem}.cms-btn{align-items:center;border:1px solid transparent;border-radius:var(--ui-radius);display:inline-flex;font-weight:500;gap:.375rem;justify-content:space-between;line-height:1;transition:background-color .15s ease,border-color .15s ease,color .15s ease,opacity .15s ease;white-space:nowrap}.cms-btn:disabled,.cms-btn[aria-disabled=true]{opacity:.5;pointer-events:none}.cms-btn.is-block{width:100%}.cms-btn-xs{font-size:.875rem;padding:.25rem .5rem}.cms-btn-sm{font-size:.875rem;padding:.375rem .625rem}.cms-btn-md{font-size:.875rem;padding:.5rem .875rem}.cms-btn-lg{font-size:.9375rem;padding:.625rem 1rem}.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) 88%,#000)}.cms-btn-primary.cms-btn-subtle{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-btn-primary.cms-btn-subtle:hover{background:#e0e0e0}.cms-btn-primary.cms-btn-soft{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-btn-neutral.cms-btn-solid{background:var(--ui-bg-inverted);color:var(--ui-text-inverted)}.cms-btn-neutral.cms-btn-subtle{background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-subtle:hover{background:var(--ui-bg-accented)}.cms-btn-neutral.cms-btn-soft{background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-ghost{color:var(--ui-text-toned)}.cms-btn-neutral.cms-btn-ghost:hover{background:rgba(38,33,25,.06);color:var(--ui-text-highlighted)}.cms-btn-error.cms-btn-solid{background:var(--ui-error);color:#fff}.cms-btn-error.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-error) 88%,#000)}.cms-btn-error.cms-btn-subtle{background:color-mix(in oklab,var(--ui-error) 12%,transparent);color:var(--ui-error)}.cms-btn-error.cms-btn-ghost{color:var(--ui-error)}.cms-btn-error.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-error) 10%,transparent)}.cms-btn-success.cms-btn-solid{background:var(--cms-fern);color:var(--ui-text-inverted)}.cms-field{background:var(--ui-bg);border:1px solid var(--cms-line-strong);border-radius:var(--ui-radius);color:var(--ui-text-highlighted);transition:border-color .15s ease,box-shadow .15s ease;width:100%}.cms-field::-moz-placeholder{color:var(--ui-text-dimmed)}.cms-field::placeholder{color:var(--ui-text-dimmed)}.cms-field:focus,.cms-field:focus-within{border-color:var(--ui-primary);box-shadow:0 0 0 3px color-mix(in oklab,var(--ui-primary) 18%,transparent);outline:none}.cms-field.is-error{border-color:var(--ui-error)}.cms-field-sm{font-size:.875rem;padding:.375rem .625rem}.cms-field-md{font-size:.875rem;padding:.5rem .75rem}.cms-field-lg{font-size:.9375rem;padding:.625rem .875rem}.cms-input-wrap{align-items:center;display:flex;position:relative}.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 .15s ease;width:2.25rem}.cms-switch.is-on{background:var(--ui-primary)}.cms-switch:after{background:#fff;border-radius:9999px;content:"";height:.875rem;left:.1875rem;position:absolute;transition:transform .15s ease;width:.875rem}.cms-switch.is-on:after{transform:translateX(1rem)}.cms-overlay{align-items:center;backdrop-filter:blur(2px);background:rgba(38,33,25,.32);display:flex;inset:0;justify-content:center;overflow-y:auto;padding:2rem 1rem;position:fixed;z-index:60}.cms-modal{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft);max-width:32rem;width:100%}.cms-modal-header{padding:1.25rem 1.5rem 0}.cms-modal-body{padding:1.25rem 1.5rem 1.5rem}.cms-menu{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);margin-top:.375rem;min-width:11rem;padding:.25rem;position:absolute;z-index:50}.cms-menu.align-end{right:0}.cms-menu-item{align-items:center;border-radius:.5rem;color:var(--ui-text);cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;padding:.4rem .6rem;text-align:left;width:100%}.cms-menu-item:hover{background:var(--ui-bg-elevated)}.cms-menu-check{color:var(--ui-primary);margin-left:auto}.cms-popover,.cms-selectmenu-panel{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);margin-top:.375rem;position:absolute;z-index:50}.cms-selectmenu-panel{left:0;max-height:15rem;overflow-y:auto;padding:.25rem;right:0}.cms-tag{align-items:center;background:var(--ui-bg-elevated);border-radius:9999px;display:inline-flex;font-size:.875rem;gap:.25rem;padding:.125rem .5rem}.cms-table{border-collapse:collapse;text-align:left;width:100%}.cms-alert{border-radius:.75rem;font-size:.875rem;padding:.75rem 1rem}.cms-alert-error{background:color-mix(in oklab,var(--ui-error) 10%,transparent);color:var(--ui-error)}.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(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);padding:.75rem .875rem}.cms-toast.is-success{border-left:3px solid var(--cms-fern)}.cms-toast.is-error{border-left:3px solid var(--ui-error)}.cms-pagination{align-items:center;display:flex;gap:.25rem}.cms-page-btn{border-radius:.5rem;color:var(--ui-text-toned);font-size:.875rem;height:2rem;min-width:2rem;padding:0 .5rem}.cms-page-btn:hover:not(:disabled){background:var(--ui-bg-elevated)}.cms-page-btn.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-page-btn:disabled{opacity:.4}
1
+ @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&family=Hanken+Grotesk:ital,wght@0,300..800;1,300..800&family=Fragment+Mono:ital@0;1&display=swap");@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-paper:#f5f5f5;--cms-surface:#fff;--cms-field:#f0f0f0;--cms-ink:#171717;--cms-line:#e5e5e5;--cms-line-strong:#d4d4d4;--cms-fern:#171717;--cms-sage:#ebebeb;--cms-sage-ink:#171717;--cms-shadow-soft:none;color-scheme:light;--ui-text-dimmed:#a3a3a3;--ui-text-muted:#737373;--ui-text-toned:#525252;--ui-text:#404040;--ui-text-highlighted:var(--cms-ink);--ui-text-inverted:#fff;--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-paper);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:#e0e0e0;--ui-bg-inverted:var(--cms-ink);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-ink);--ui-primary:var(--cms-ink);--ui-error:#dc2626;--ui-radius:0.4rem;color:var(--ui-text);font-family:Hanken Grotesk,ui-sans-serif,system-ui,sans-serif;margin:0;padding:0;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:var(--cms-sage);color:var(--cms-sage-ink)}body:has(.cms-scope) ::selection{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-display,.cms-title{font-family:Poppins,ui-sans-serif,system-ui,sans-serif;letter-spacing:-.02em}.cms-title{color:var(--ui-text-highlighted);font-weight:500}.cms-title-sm{font-size:1.25rem;line-height:1.4}.cms-title-md{font-size:2rem;line-height:1}.cms-title-lg{font-size:2.25rem;line-height:1.2}.cms-subtitle{color:var(--ui-text-muted);font-size:.875rem}.cms-accent{color:var(--cms-fern)}.cms-kicker{color:var(--ui-text-dimmed);font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;letter-spacing:.16em;line-height:1.4;text-transform:uppercase}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer transition-all duration-200 select-none}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]):active{@apply transition-none;scale:.99}.cms-canvas{background-color:var(--cms-paper)}.cms-grain:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='a'%3E%3CfeTurbulence baseFrequency='.8' numOctaves='2' stitchTiles='stitch' type='fractalNoise'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");content:"";inset:0;mix-blend-mode:multiply;opacity:.035;pointer-events:none;position:fixed;z-index:50}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft)}.cms-panel{padding:1.75rem}.cms-shell{display:flex;min-height:100vh}.cms-sidebar{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:Poppins,ui-sans-serif,system-ui,sans-serif;font-size:1.375rem;font-weight:600;letter-spacing:-.02em;padding-inline:.75rem}.cms-sidebar-nav{display:flex;flex:1;flex-direction:column;gap:1.5rem;overflow-y:auto}.cms-sidebar-group{display:flex;flex-direction:column;gap:.25rem}.cms-sidebar-group>.cms-kicker{margin-bottom:.5rem;padding-inline:.75rem}.cms-sidebar-footer{border-top:1px solid var(--cms-line);display:flex;flex-direction:column;gap:.75rem;padding:1.25rem .75rem 0}.cms-main{flex:1;height:100vh;min-width:0;overflow-y:auto;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:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem;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:.75rem;color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color .15s ease,color .15s ease}.cms-navlink:hover{background:rgba(38,33,25,.05);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-navlink-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:9999px;display:inline-flex;font-family:Fragment Mono,ui-monospace,monospace;font-size:.6875rem;letter-spacing:.1em;padding:.1875rem .5rem;pointer-events:none;text-transform:uppercase;transition:background-color .15s ease,border-color .15s ease;white-space:nowrap}.cms-badge.is-published{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-badge.is-muted{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-muted)}.cms-badge.is-draft{background:transparent;border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-dropzone{align-items:center;background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem;color:var(--ui-text-muted);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color .15s ease,background-color .15s ease,color .15s ease;width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-sage);border-color:var(--cms-fern);color:var(--cms-sage-ink)}.cms-dropzone.is-dense{border-radius:.75rem;flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{align-items:center;background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;display:inline-flex;flex:none;font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;gap:.35rem;letter-spacing:.12em;padding:.3rem .8rem;transition:background-color .15s ease,color .15s ease;white-space:nowrap}.cms-pill:hover{background:rgba(38,33,25,.05);color:var(--ui-text)}.cms-pill.is-active{background:var(--cms-sage);color:var(--cms-sage-ink)}@keyframes cms-rise{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .55s cubic-bezier(.22,1,.36,1) both}.cms-rise.is-delayed{animation-delay:90ms}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{border:1px solid var(--cms-line);border-radius:.75rem;overflow:hidden}.cms-richtext-toolbar{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;flex-wrap:wrap;gap:2px;padding:4px}.cms-richtext-body{font-size:.875rem;min-height:9rem;outline:none;padding:.75rem 1rem}.cms-richtext-body h2{font-size:1.25rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body h3{font-size:1.1rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body p{margin:.35em 0}.cms-richtext-body ul{list-style:disc;padding-left:1.25rem}.cms-richtext-body ol{list-style:decimal;padding-left:1.25rem}.cms-richtext-body blockquote{border-left:3px solid var(--cms-line);color:var(--ui-text-muted);padding-left:.75rem}.cms-btn{align-items:center;border:1px solid transparent;border-radius:9999px;display:inline-flex;font-weight:500;gap:.375rem;justify-content:space-between;line-height:1;min-height:var(--cms-btn-size);transition:background-color .15s ease,border-color .15s ease,color .15s ease,opacity .15s ease;white-space:nowrap}.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 .75rem}.cms-btn-sm{--cms-btn-size:2rem;font-size:.875rem;padding:.25rem .875rem}.cms-btn-md{--cms-btn-size:2.25rem;font-size:.875rem;padding:.25rem 1.125rem}.cms-btn-lg{--cms-btn-size:2.625rem;font-size:.9375rem;padding:.375rem 1.375rem}.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) 88%,#000)}.cms-btn-primary.cms-btn-subtle{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-btn-primary.cms-btn-subtle:hover{background:#e0e0e0}.cms-btn-primary.cms-btn-soft{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-btn-neutral.cms-btn-solid{background:var(--ui-bg-inverted);color:var(--ui-text-inverted)}.cms-btn-neutral.cms-btn-subtle{background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-subtle:hover{background:var(--ui-bg-accented)}.cms-btn-neutral.cms-btn-soft{background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-ghost{color:var(--ui-text-toned)}.cms-btn-neutral.cms-btn-ghost:hover{background:rgba(38,33,25,.06);color:var(--ui-text-highlighted)}.cms-btn-error.cms-btn-solid{background:var(--ui-error);color:#fff}.cms-btn-error.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-error) 88%,#000)}.cms-btn-error.cms-btn-subtle{background:color-mix(in oklab,var(--ui-error) 12%,transparent);color:var(--ui-error)}.cms-btn-error.cms-btn-ghost{color:var(--ui-error)}.cms-btn-error.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-error) 10%,transparent)}.cms-btn-success.cms-btn-solid{background:var(--cms-fern);color:var(--ui-text-inverted)}.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(--ui-bg);border:1px solid var(--cms-line-strong);border-radius:var(--ui-radius);color:var(--ui-text-highlighted);line-height:1.25;transition:border-color .15s ease,box-shadow .15s 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:focus,.cms-field:focus-within{border-color:var(--ui-primary);box-shadow:0 0 0 3px color-mix(in oklab,var(--ui-primary) 18%,transparent);outline:none}.cms-field.is-error{border-color:var(--ui-error)}.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 .18s ease,color .15s 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-paper);border:1px solid var(--cms-line);border-radius:999px;display:inline-flex;gap:2px;padding:2px}.cms-locale-option{border-radius:999px;color:var(--ui-text-muted);cursor:pointer;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.6875rem;letter-spacing:.06em;line-height:1.4;padding:.125rem .5rem;text-transform:uppercase;transition:background-color .15s ease,color .15s 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(--ui-bg);box-shadow:0 1px 2px rgba(38,33,25,.1);color:var(--ui-text-highlighted)}.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 .15s ease;width:2.25rem}.cms-switch.is-on{background:var(--ui-primary)}.cms-switch:after{background:#fff;border-radius:9999px;content:"";height:.875rem;left:.1875rem;position:absolute;transition:transform .15s ease;width:.875rem}.cms-switch.is-on:after{transform:translateX(1rem)}.cms-overlay{align-items:center;backdrop-filter:blur(2px);background:rgba(38,33,25,.32);display:flex;inset:0;justify-content:center;overflow-y:auto;padding:2rem 1rem;position:fixed;z-index:60}.cms-modal{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft);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{padding:1.25rem 1.5rem 1.5rem}.cms-overlay-drawer{align-items:stretch;justify-content:flex-end;overflow:hidden;padding:0;z-index:55}.cms-drawer{background:var(--ui-bg);border-left:1px solid var(--cms-line);box-shadow:-18px 0 48px rgba(38,33,25,.12);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 .3s ease}.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active .cms-drawer{transition:transform .36s cubic-bezier(.32,.72,0,1);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{transition-duration:.01ms}}.cms-menu{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);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:.5rem;color:var(--ui-text);cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;padding:.4rem .6rem;text-align:left;width:100%}.cms-menu-item:hover{background:var(--ui-bg-elevated)}.cms-menu-check{color:var(--ui-primary);margin-left:auto}.cms-popover{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);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(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);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(--ui-bg-elevated);border-radius:9999px;display:inline-flex;font-size:.875rem;gap:.25rem;padding:.125rem .5rem}.cms-media-gallery{display:flex;flex-direction:column;gap:1.25rem}.cms-media-filters{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.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}.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 .15s 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,.7),transparent);bottom:0;color:#fff;font-family:Fragment Mono,ui-monospace,monospace;font-size:.75rem;left:0;letter-spacing:.1em;line-height:1.4;opacity:0;padding:1.5rem .75rem .5rem;pointer-events:none;position:absolute;right:0;text-transform:uppercase;transition:opacity .15s 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{font-size:.875rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-media-preview-large{background:var(--cms-field);max-height:14rem;-o-object-fit:cover;object-fit:cover;width:100%}.cms-media-bar{align-items:center;display:flex;gap:.625rem;padding:.75rem 1rem}.cms-media-bar-icon{color:var(--ui-text-dimmed);flex-shrink:0}.cms-media-bar-name{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:.375rem;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{box-shadow:inset 0 -1px 0 var(--cms-line);color:var(--ui-text-dimmed);font-family:Fragment Mono,ui-monospace,monospace;font-size:.6875rem;font-weight:400;letter-spacing:.1em;line-height:1.4;padding:.75rem 1.25rem;text-transform:uppercase}.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)}.cms-table tbody tr:last-child{border-bottom:0}.cms-table tbody tr:hover{background:color-mix(in oklab,var(--cms-paper) 50%,transparent)}.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-paper);box-shadow:inset 0 -1px 0 var(--cms-line),inset 0 0 0 2px var(--cms-line-strong)}.cms-alert{border-radius:.75rem;font-size:.875rem;padding:.75rem 1rem}.cms-alert-error{background:color-mix(in oklab,var(--ui-error) 10%,transparent);color:var(--ui-error)}.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(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);padding:.75rem .875rem}.cms-toast-row{align-items:flex-start;display:flex;gap:.75rem}.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}.cms-toast-dismiss:hover{color:var(--ui-text)}.cms-toast.is-success{border-left:3px solid var(--cms-fern)}.cms-toast.is-error{border-left:3px solid var(--ui-error)}.cms-pagination{align-items:center;display:flex;gap:.25rem;justify-content:center}.cms-page-btn{border-radius:.5rem;color:var(--ui-text-toned);font-size:.875rem;height:2rem;min-width:2rem;padding:0 .5rem}.cms-page-btn:hover:not(:disabled){background:var(--ui-bg-elevated)}.cms-page-btn.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-page-btn:disabled{opacity:.4}
@@ -3,7 +3,7 @@ import { defineEventHandler, getValidatedQuery } from "h3";
3
3
  import { z } from "zod";
4
4
  import { useDb } from "#cms-db";
5
5
  import { getRegistryEntry, idColumn, tableColumns } from "../utils/registry.js";
6
- import { attachManyToMany } from "../utils/relations.js";
6
+ import { attachManyToMany, relationTitles } from "../utils/relations.js";
7
7
  import { requireAdmin } from "../utils/require-admin.js";
8
8
  const querySchema = z.object({
9
9
  limit: z.coerce.number().int().min(1).max(100).default(50),
@@ -40,6 +40,11 @@ export default defineEventHandler(async (event) => {
40
40
  const base = selection ? db.select(selection).from(table) : db.select().from(table);
41
41
  const items = await (where ? base.where(where) : base).orderBy(desc(orderBy)).limit(limit).offset(offset);
42
42
  const [counted] = await (where ? db.select({ total: count() }).from(table).where(where) : db.select({ total: count() }).from(table));
43
- if (!light) await attachManyToMany(db, name, entry, items);
44
- return { items, total: counted?.total ?? 0 };
43
+ if (light) return { items, total: counted?.total ?? 0, relations: {} };
44
+ await attachManyToMany(db, name, entry, items);
45
+ return {
46
+ items,
47
+ total: counted?.total ?? 0,
48
+ relations: await relationTitles(db, entry, items)
49
+ };
45
50
  });
@@ -9,5 +9,6 @@ export declare function splitRelationValues(entry: CmsEntry, body: Row): {
9
9
  };
10
10
  export declare function assertRelationTargets(entry: CmsEntry, lists: Record<string, string[]>): Promise<void>;
11
11
  export declare function saveManyToMany(db: CmsDb, name: string, sourceId: string, lists: Record<string, string[]>): Promise<void>;
12
+ export declare function relationTitles(db: CmsDb, entry: CmsEntry, rows: Row[]): Promise<Record<string, Record<string, unknown>>>;
12
13
  export declare function attachManyToMany<T extends Row>(db: CmsDb, name: string, entry: CmsEntry, rows: T[]): Promise<T[]>;
13
14
  export {};