@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,22 +1,15 @@
1
1
  <template>
2
- <CmsTranslatableField v-if="isTranslatableField(field)" v-model="translatable" :field="field" />
2
+ <CmsTranslatableField
3
+ v-if="isTranslatableField(field)"
4
+ v-model="translatable"
5
+ :field="field"
6
+ :locale="locale"
7
+ />
3
8
  <CmsBlocksField v-else-if="field.type === 'blocks'" v-model="blocksValue" :field="field" />
4
9
  <CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
5
- <CmsTextarea
6
- v-else-if="field.type === 'text' && field.textarea"
7
- v-model="str"
8
- :rows="4"
9
- size="lg"
10
- class="w-full"
11
- />
12
- <CmsInput v-else-if="field.type === 'text'" v-model="str" size="lg" class="w-full" />
13
- <CmsInput
14
- v-else-if="field.type === 'email'"
15
- v-model="str"
16
- type="email"
17
- size="lg"
18
- class="w-full"
19
- />
10
+ <CmsTextarea v-else-if="field.type === 'text' && field.textarea" v-model="str" :rows="8" />
11
+ <CmsInput v-else-if="field.type === 'text'" v-model="str" />
12
+ <CmsInput v-else-if="field.type === 'email'" v-model="str" type="email" />
20
13
  <CmsSlugField v-else-if="field.type === 'slug'" v-model="strOrNull" :source="slugSource" />
21
14
  <CmsSelectMenu
22
15
  v-else-if="field.type === 'select'"
@@ -24,8 +17,6 @@
24
17
  :multiple="field.multiple"
25
18
  :items="selectItems"
26
19
  placeholder="Select…"
27
- size="lg"
28
- class="w-full"
29
20
  />
30
21
  <CmsSwitch
31
22
  v-else-if="field.type === 'boolean'"
@@ -37,14 +28,8 @@
37
28
  "
38
29
  />
39
30
  <CmsJsonField v-else-if="field.type === 'json'" v-model="jsonValue" />
40
- <CmsInput
41
- v-else-if="field.type === 'number'"
42
- v-model.number="num"
43
- type="number"
44
- size="lg"
45
- class="w-full"
46
- />
47
- <CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" size="lg" class="w-full" />
31
+ <CmsInput v-else-if="field.type === 'number'" v-model.number="num" type="number" />
32
+ <CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" />
48
33
  <CmsRelationField
49
34
  v-else-if="field.type === 'relation'"
50
35
  v-model="relationValue"
@@ -60,7 +45,8 @@ import { isTranslatableField } from "#nuxt-cms";
60
45
  import { computed } from "#imports";
61
46
  const props = defineProps({
62
47
  field: { type: Object, required: true },
63
- slugSource: { type: [String, null], required: false }
48
+ slugSource: { type: [String, null], required: false },
49
+ locale: { type: String, required: false }
64
50
  });
65
51
  const model = defineModel({ type: null, ...{ required: true } });
66
52
  const selectItems = computed(() => [
@@ -2,6 +2,7 @@ import type { FieldConfig } from '#nuxt-cms';
2
2
  type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  slugSource?: string | null;
5
+ locale?: string;
5
6
  };
6
7
  type __VLS_ModelProps = {
7
8
  modelValue: unknown;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <form :id="id" @submit.prevent="onSubmit">
2
+ <form :id="id" class="cms-form" @submit.prevent="onSubmit">
3
3
  <slot />
4
4
  </form>
5
5
  </template>
@@ -2,13 +2,12 @@ type __VLS_Props = {
2
2
  label?: string;
3
3
  name?: string;
4
4
  required?: boolean;
5
- ui?: {
6
- label?: string;
7
- };
8
5
  };
9
- declare var __VLS_1: {};
6
+ declare var __VLS_1: {}, __VLS_3: {};
10
7
  type __VLS_Slots = {} & {
11
- default?: (props: typeof __VLS_1) => any;
8
+ 'label-actions'?: (props: typeof __VLS_1) => any;
9
+ } & {
10
+ default?: (props: typeof __VLS_3) => any;
12
11
  };
13
12
  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>;
14
13
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,10 +1,13 @@
1
1
  <template>
2
- <div class="flex flex-col gap-1.5">
3
- <label v-if="label" :class="ui?.label ?? 'text-sm font-medium text-(--ui-text-toned)'">
4
- {{ label }}<span v-if="required" class="text-(--ui-error)"> *</span>
5
- </label>
2
+ <div class="cms-form-field">
3
+ <div v-if="label || $slots['label-actions']" class="cms-form-label-row">
4
+ <label v-if="label" class="cms-form-label">
5
+ {{ label }}<span v-if="required" class="cms-form-required"> *</span>
6
+ </label>
7
+ <slot name="label-actions" />
8
+ </div>
6
9
  <slot />
7
- <p v-if="error" class="text-sm text-(--ui-error)">{{ error }}</p>
10
+ <p v-if="error" class="cms-form-error">{{ error }}</p>
8
11
  </div>
9
12
  </template>
10
13
 
@@ -14,8 +17,7 @@ import { CMS_FORM_ERRORS } from "../../utils/ui";
14
17
  const props = defineProps({
15
18
  label: { type: String, required: false },
16
19
  name: { type: String, required: false },
17
- required: { type: Boolean, required: false },
18
- ui: { type: Object, required: false }
20
+ required: { type: Boolean, required: false }
19
21
  });
20
22
  const errors = inject(CMS_FORM_ERRORS, null);
21
23
  const error = computed(() => props.name && errors ? errors[props.name] : void 0);
@@ -2,13 +2,12 @@ type __VLS_Props = {
2
2
  label?: string;
3
3
  name?: string;
4
4
  required?: boolean;
5
- ui?: {
6
- label?: string;
7
- };
8
5
  };
9
- declare var __VLS_1: {};
6
+ declare var __VLS_1: {}, __VLS_3: {};
10
7
  type __VLS_Slots = {} & {
11
- default?: (props: typeof __VLS_1) => any;
8
+ 'label-actions'?: (props: typeof __VLS_1) => any;
9
+ } & {
10
+ default?: (props: typeof __VLS_3) => any;
12
11
  };
13
12
  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>;
14
13
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,15 +1,14 @@
1
1
  <template>
2
- <div class="flex flex-col gap-1">
2
+ <div class="cms-form-field">
3
3
  <CmsTextarea
4
4
  v-model="text"
5
5
  :rows="6"
6
- size="lg"
7
- class="w-full font-mono"
6
+ class="font-mono"
8
7
  :color="invalid ? 'error' : void 0"
9
8
  @blur="commit"
10
9
  @update:model-value="invalid = false"
11
10
  />
12
- <p v-if="invalid" class="text-sm text-(--ui-error)">Invalid JSON</p>
11
+ <p v-if="invalid" class="cms-form-error">Invalid JSON</p>
13
12
  </div>
14
13
  </template>
15
14
 
@@ -0,0 +1,14 @@
1
+ type __VLS_Props = {
2
+ value?: Record<string, string> | null;
3
+ };
4
+ type __VLS_ModelProps = {
5
+ modelValue: string;
6
+ };
7
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:modelValue": (value: string) => any;
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
11
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div class="cms-locale-switch" role="tablist" aria-label="Content language">
3
+ <button
4
+ v-for="locale in i18n.locales"
5
+ :key="locale"
6
+ type="button"
7
+ role="tab"
8
+ :aria-selected="model === locale"
9
+ :title="filled(locale) ? locale : `${locale} \u2014 empty`"
10
+ class="cms-locale-option"
11
+ :class="{ 'is-active': model === locale, 'is-empty': !filled(locale) }"
12
+ @click="model = locale"
13
+ >
14
+ {{ locale }}
15
+ </button>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup>
20
+ import { useCmsRuntime } from "../../composables/cms-runtime";
21
+ const props = defineProps({
22
+ value: { type: [Object, null], required: false }
23
+ });
24
+ const model = defineModel({ type: String, ...{ required: true } });
25
+ const { i18n } = useCmsRuntime();
26
+ function filled(locale) {
27
+ return !!props.value?.[locale]?.trim();
28
+ }
29
+ </script>
@@ -0,0 +1,14 @@
1
+ type __VLS_Props = {
2
+ value?: Record<string, string> | null;
3
+ };
4
+ type __VLS_ModelProps = {
5
+ modelValue: string;
6
+ };
7
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:modelValue": (value: string) => any;
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
11
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -1,30 +1,22 @@
1
1
  <template>
2
- <div v-if="model" class="cms-card overflow-hidden">
3
- <img
4
- v-if="kind === 'image' && url"
5
- :src="url"
6
- alt=""
7
- class="max-h-56 w-full bg-(--cms-field) object-cover"
8
- />
2
+ <div v-if="model" class="cms-card cms-media-tile">
3
+ <img v-if="kind === 'image' && url" :src="url" alt="" class="cms-media-preview-large" />
9
4
  <video
10
5
  v-else-if="kind === 'video' && url"
11
6
  :src="url"
12
7
  controls
13
8
  preload="metadata"
14
9
  playsinline
15
- class="max-h-56 w-full bg-(--cms-field)"
10
+ class="cms-media-preview-large"
16
11
  />
17
- <div class="flex items-center gap-2.5 px-4 py-3">
18
- <CmsIcon :name="icon" class="size-4 shrink-0 text-(--ui-text-dimmed)" />
19
- <span class="min-w-0 flex-1 truncate text-sm font-medium" :title="model">{{
20
- mediaFilename(model)
21
- }}</span>
12
+ <div class="cms-media-bar">
13
+ <CmsIcon :name="icon" class="cms-media-bar-icon size-4" />
14
+ <span class="cms-media-bar-name" :title="model">{{ mediaFilename(model) }}</span>
22
15
  <CmsButton
23
16
  icon="photo"
24
17
  variant="ghost"
25
18
  color="neutral"
26
19
  size="xs"
27
- class="rounded-full"
28
20
  aria-label="Change"
29
21
  @click="openGallery"
30
22
  />
@@ -33,7 +25,6 @@
33
25
  variant="ghost"
34
26
  color="neutral"
35
27
  size="xs"
36
- class="rounded-full"
37
28
  aria-label="Remove"
38
29
  @click="clear"
39
30
  />
@@ -45,7 +36,7 @@
45
36
  <span class="text-sm font-medium">Choose from the gallery</span>
46
37
  </button>
47
38
 
48
- <CmsModal v-model:open="galleryOpen" title="Media" :ui="CMS_MODAL_UI">
39
+ <CmsModal v-model:open="galleryOpen" title="Media" size="lg">
49
40
  <template #body>
50
41
  <CmsMediaGallery selectable :media-type="mediaType" :accept="accept" @select="onSelect" />
51
42
  </template>
@@ -56,7 +47,6 @@
56
47
  import { computed, ref } from "#imports";
57
48
  import { mediaFilename, mediaIconFor, mediaPublicUrl, mediaTypeForKey } from "#nuxt-cms";
58
49
  import { useCmsRuntime } from "../../composables/cms-runtime";
59
- import { CMS_MODAL_UI } from "../../utils/ui";
60
50
  defineProps({
61
51
  mediaType: { type: null, required: false },
62
52
  accept: { type: Array, required: false }
@@ -6,7 +6,7 @@
6
6
  body="Set cms.media in nuxt.config to enable uploads."
7
7
  />
8
8
 
9
- <div v-else class="flex flex-col gap-5">
9
+ <div v-else class="cms-media-gallery">
10
10
  <CmsMediaUpload
11
11
  v-if="!readOnly"
12
12
  :multiple="!selectable"
@@ -16,7 +16,7 @@
16
16
  @uploaded="onUploaded"
17
17
  />
18
18
 
19
- <div v-if="!restricted && items.length" class="flex flex-wrap items-center gap-1">
19
+ <div v-if="!restricted && items.length" class="cms-media-filters">
20
20
  <button
21
21
  v-for="f in filters"
22
22
  :key="f"
@@ -28,7 +28,7 @@
28
28
  {{ filterLabels[f] }}
29
29
  </button>
30
30
  <template v-if="folders.length">
31
- <span class="mx-1 h-4 w-px bg-(--cms-line)" />
31
+ <span class="cms-media-filter-separator" />
32
32
  <button
33
33
  v-for="f in folders"
34
34
  :key="f"
@@ -37,40 +37,29 @@
37
37
  :class="{ 'is-active': folder === f }"
38
38
  @click="folder = folder === f ? null : f"
39
39
  >
40
- <CmsIcon name="folder" class="mr-1 size-3" />{{ f }}
40
+ <CmsIcon name="folder" class="size-3" />{{ f }}
41
41
  </button>
42
42
  </template>
43
43
  </div>
44
44
 
45
- <div v-if="loading" class="flex justify-center py-10">
46
- <CmsIcon name="arrow-path" class="size-6 animate-spin text-(--ui-text-dimmed)" />
47
- </div>
45
+ <CmsSpinner v-if="loading" />
48
46
 
49
- <div
50
- v-else-if="visible.length"
51
- class="grid gap-4"
52
- :class="
53
- selectable ? 'grid-cols-2 sm:grid-cols-3' : 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
54
- "
55
- >
47
+ <div v-else-if="visible.length" class="cms-media-grid" :class="{ 'is-compact': selectable }">
56
48
  <component
57
49
  :is="selectable ? 'button' : 'div'"
58
- v-for="item in visible"
50
+ v-for="item in paged"
59
51
  :key="item.key"
60
52
  :type="selectable ? 'button' : void 0"
61
- class="cms-card group overflow-hidden text-left"
62
- :class="{
63
- 'hover:border-(--cms-line-strong)': selectable
64
- }"
53
+ class="cms-card cms-media-tile"
54
+ :class="{ 'is-selectable': selectable }"
65
55
  @click="selectable && emit('select', { key: item.key, url: item.url })"
66
56
  >
67
- <div class="relative flex aspect-square items-center justify-center bg-(--cms-field)">
57
+ <div class="cms-media-preview">
68
58
  <img
69
59
  v-if="item.type === 'image' && item.url"
70
60
  :src="item.url"
71
61
  alt=""
72
62
  loading="lazy"
73
- class="absolute inset-0 size-full object-cover"
74
63
  />
75
64
  <video
76
65
  v-else-if="item.type === 'video' && item.url"
@@ -78,70 +67,45 @@
78
67
  preload="metadata"
79
68
  muted
80
69
  playsinline
81
- class="absolute inset-0 size-full object-cover"
82
- />
83
- <CmsIcon
84
- v-else
85
- :name="mediaIconFor(item.type)"
86
- class="size-8 text-(--ui-text-dimmed)"
87
70
  />
88
- <div
89
- v-if="!selectable"
90
- class="absolute top-2 right-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
91
- >
71
+ <CmsIcon v-else :name="mediaIconFor(item.type)" class="size-8" />
72
+ <div v-if="!selectable" class="cms-media-actions">
92
73
  <CmsButton
93
74
  v-if="!readOnly"
94
75
  icon="pencil-square"
95
76
  size="xs"
96
77
  color="neutral"
97
78
  variant="solid"
98
- class="rounded-full"
99
79
  aria-label="Edit details"
100
80
  @click="openEdit(item)"
101
81
  />
102
- <CmsButton
103
- icon="document-duplicate"
104
- size="xs"
105
- color="neutral"
106
- variant="solid"
107
- class="rounded-full"
108
- aria-label="Copy URL"
109
- @click="copy(item)"
110
- />
111
82
  <CmsButton
112
83
  v-if="!readOnly"
113
84
  icon="trash"
114
85
  size="xs"
115
86
  color="error"
116
87
  variant="solid"
117
- class="rounded-full"
118
88
  aria-label="Delete"
119
89
  @click="remove(item)"
120
90
  />
121
91
  </div>
122
- </div>
123
- <div class="flex flex-col gap-1 px-3.5 py-2.5">
124
- <div class="truncate text-sm font-medium" :title="item.key">
125
- {{ mediaFilename(item.key) }}
126
- </div>
127
- <div class="cms-kicker">
92
+ <div class="cms-media-info">
128
93
  {{ extension(item.key) }} · {{ formatSize(item.size)
129
94
  }}<template v-if="item.width && item.height">
130
95
  · {{ item.width }}×{{ item.height }}
131
96
  </template>
132
97
  </div>
133
98
  </div>
99
+ <div class="cms-media-meta">
100
+ <div class="cms-media-name" :title="item.key">
101
+ {{ mediaFilename(item.key) }}
102
+ </div>
103
+ </div>
134
104
  </component>
135
105
  </div>
136
106
 
137
107
  <CmsEmptyState v-else-if="loadError" icon="exclamation-triangle" title="Could not load media">
138
- <CmsButton
139
- label="Retry"
140
- icon="arrow-path"
141
- variant="subtle"
142
- class="mt-3 rounded-full px-4"
143
- @click="reload"
144
- />
108
+ <CmsButton label="Retry" icon="arrow-path" variant="subtle" @click="reload" />
145
109
  </CmsEmptyState>
146
110
 
147
111
  <CmsEmptyState
@@ -151,11 +115,18 @@
151
115
  :body="readOnly ? 'No media registered.' : 'Files you upload will show up here.'"
152
116
  />
153
117
 
118
+ <CmsPagination
119
+ v-if="!loading && visible.length"
120
+ v-model:page="page"
121
+ :total="visible.length"
122
+ :items-per-page="PAGE_SIZE"
123
+ />
124
+
154
125
  <CmsModal
155
126
  v-if="!readOnly"
156
127
  :open="!!editing"
157
128
  :title="editing ? mediaFilename(editing.key) : ''"
158
- :ui="CMS_MODAL_UI"
129
+ size="lg"
159
130
  @update:open="
160
131
  (open) => {
161
132
  if (!open) editing = null;
@@ -163,20 +134,15 @@
163
134
  "
164
135
  >
165
136
  <template #body>
166
- <div class="flex flex-col gap-4">
137
+ <div class="cms-form">
167
138
  <CmsFormField label="Alt text">
168
- <CmsInput v-model="editAlt" size="lg" class="w-full" />
139
+ <CmsInput v-model="editAlt" />
169
140
  </CmsFormField>
170
141
  <CmsFormField label="Folder">
171
- <CmsInput v-model="editFolder" size="lg" class="w-full" />
142
+ <CmsInput v-model="editFolder" />
172
143
  </CmsFormField>
173
- <div class="flex justify-end">
174
- <CmsButton
175
- label="Save"
176
- :loading="editSaving"
177
- class="rounded-full px-6"
178
- @click="saveEdit"
179
- />
144
+ <div class="cms-actions is-end">
145
+ <CmsButton label="Save" :loading="editSaving" @click="saveEdit" />
180
146
  </div>
181
147
  </div>
182
148
  </template>
@@ -185,12 +151,12 @@
185
151
  </template>
186
152
 
187
153
  <script setup>
188
- import { computed, onMounted, ref } from "#imports";
154
+ import { computed, onMounted, ref, watch } from "#imports";
189
155
  import { MEDIA_TYPES, mediaFilename, mediaIconFor } from "#nuxt-cms";
190
156
  import { useCmsConfirm } from "../../composables/cms-confirm";
191
157
  import { useCmsRuntime } from "../../composables/cms-runtime";
192
158
  import { useCmsToast } from "../../composables/cms-toast";
193
- import { CMS_MODAL_UI, errorMessage } from "../../utils/ui";
159
+ import { errorMessage } from "../../utils/ui";
194
160
  const filterLabels = {
195
161
  all: "All",
196
162
  image: "Images",
@@ -214,8 +180,8 @@ async function reload() {
214
180
  loading.value = true;
215
181
  errorCode.value = null;
216
182
  try {
217
- const page = await $fetch(endpoint);
218
- items.value = page.items;
183
+ const result = await $fetch(endpoint);
184
+ items.value = result.items;
219
185
  } catch (err) {
220
186
  errorCode.value = err.statusCode ?? 500;
221
187
  } finally {
@@ -241,6 +207,18 @@ const visible = computed(() => {
241
207
  if (folder.value) list = list.filter((item) => item.folder === folder.value);
242
208
  return list;
243
209
  });
210
+ const PAGE_SIZE = 24;
211
+ const page = ref(1);
212
+ watch([filter, folder], () => {
213
+ page.value = 1;
214
+ });
215
+ watch(visible, (list) => {
216
+ const pageCount = Math.max(1, Math.ceil(list.length / PAGE_SIZE));
217
+ if (page.value > pageCount) page.value = pageCount;
218
+ });
219
+ const paged = computed(
220
+ () => visible.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE)
221
+ );
244
222
  function onUploaded(uploaded) {
245
223
  if (props.selectable && uploaded[0]) {
246
224
  emit("select", uploaded[0]);
@@ -295,14 +273,6 @@ function formatSize(bytes) {
295
273
  }
296
274
  return `${Math.round(value / 1024)} TB`;
297
275
  }
298
- async function copy(item) {
299
- try {
300
- await navigator.clipboard.writeText(item.url ?? item.key);
301
- toast.add({ title: "Copied", color: "success" });
302
- } catch {
303
- toast.add({ title: "Copy failed", color: "error" });
304
- }
305
- }
306
276
  const confirmAction = useCmsConfirm();
307
277
  const removing = ref(false);
308
278
  async function remove(item) {
@@ -0,0 +1,6 @@
1
+ type __VLS_Props = {
2
+ value?: string | null;
3
+ };
4
+ declare const __VLS_export: 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>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <span v-if="!value" class="cms-media-thumb-empty">—</span>
3
+
4
+ <img
5
+ v-else-if="kind === 'image' && url"
6
+ :src="url"
7
+ :alt="filename"
8
+ :title="filename"
9
+ loading="lazy"
10
+ class="cms-media-thumb"
11
+ />
12
+
13
+ <video
14
+ v-else-if="kind === 'video' && url"
15
+ :src="url"
16
+ :title="filename"
17
+ muted
18
+ playsinline
19
+ preload="metadata"
20
+ class="cms-media-thumb"
21
+ />
22
+
23
+ <span v-else :title="filename" class="cms-media-thumb is-fallback">
24
+ <CmsIcon :name="icon" class="size-4" />
25
+ </span>
26
+ </template>
27
+
28
+ <script setup>
29
+ import { computed } from "#imports";
30
+ import { mediaFilename, mediaIconFor, mediaPublicUrl, mediaTypeForKey } from "#nuxt-cms";
31
+ import { useCmsRuntime } from "../../composables/cms-runtime";
32
+ const props = defineProps({
33
+ value: { type: [String, null], required: false }
34
+ });
35
+ const { mediaBaseUrl } = useCmsRuntime();
36
+ const kind = computed(() => props.value ? mediaTypeForKey(props.value) : "file");
37
+ const url = computed(() => props.value ? mediaPublicUrl(mediaBaseUrl, props.value) : null);
38
+ const filename = computed(() => props.value ? mediaFilename(props.value) : "");
39
+ const icon = computed(() => mediaIconFor(kind.value));
40
+ </script>
@@ -0,0 +1,6 @@
1
+ type __VLS_Props = {
2
+ value?: string | null;
3
+ };
4
+ declare const __VLS_export: 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>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -1,9 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  title?: string;
3
- ui?: {
4
- content?: string;
5
- title?: string;
6
- };
3
+ size?: 'sm' | 'lg';
7
4
  };
8
5
  type __VLS_ModelProps = {
9
6
  'open'?: boolean;
@@ -4,14 +4,15 @@
4
4
  once teleported outside the admin layout root. -->
5
5
  <div v-if="open" class="cms-scope">
6
6
  <div class="cms-overlay" @click.self="close">
7
- <div class="cms-modal cms-rise" :class="ui?.content" role="dialog" aria-modal="true">
7
+ <div
8
+ class="cms-modal cms-rise"
9
+ :class="size ? `is-${size}` : void 0"
10
+ role="dialog"
11
+ aria-modal="true"
12
+ >
8
13
  <div v-if="title || $slots.header" class="cms-modal-header">
9
14
  <slot name="header">
10
- <h2
11
- :class="
12
- ui?.title ?? 'cms-display text-xl font-medium text-(--ui-text-highlighted)'
13
- "
14
- >
15
+ <h2 class="cms-title cms-title-sm">
15
16
  {{ title }}
16
17
  </h2>
17
18
  </slot>
@@ -29,7 +30,7 @@
29
30
  import { onBeforeUnmount, watch } from "#imports";
30
31
  defineProps({
31
32
  title: { type: String, required: false },
32
- ui: { type: Object, required: false }
33
+ size: { type: String, required: false }
33
34
  });
34
35
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
35
36
  const emit = defineEmits(["after:leave"]);